Skip to content

Commit ccdeaac

Browse files
committed
test: add test code
1 parent 1e2705b commit ccdeaac

4 files changed

Lines changed: 399 additions & 10 deletions

File tree

src/internal/OpenApiTools/TypeNodeContext.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,19 @@ export interface ReferencePathSet {
1717
base: string;
1818
}
1919

20-
const generatePath = (entryPoint: string, currentPoint: string, referencePath: string): ReferencePathSet => {
20+
/**
21+
* エントリポイント、現在のファイルパス、参照パスから、相対的なパスの配列とベースディレクトリを生成します。
22+
*
23+
* @param entryPoint - OpenAPI定義のエントリポイント(例: "openapi.yml")
24+
* @param currentPoint - 現在処理中のファイルパス(例: "components/schemas/A.yml")
25+
* @param referencePath - 参照先のパス(例: "components/schemas/B.yml")
26+
* @returns パスの配列とベースディレクトリのセット
27+
*
28+
* @example
29+
* generatePath("openapi.yml", "components/schemas/User.yml", "components/schemas/Common.yml")
30+
* // 返り値の例: { pathArray: ["Common"], base: "components/schemas" }
31+
*/
32+
export const generatePath = (entryPoint: string, currentPoint: string, referencePath: string): ReferencePathSet => {
2133
const ext = Path.extname(currentPoint); // .yml
2234
const from = Path.relative(Path.dirname(entryPoint), currentPoint).replace(ext, ""); // components/schemas/A/B
2335
const base = Path.dirname(from).replace(Path.sep, "/");
@@ -29,7 +41,16 @@ const generatePath = (entryPoint: string, currentPoint: string, referencePath: s
2941
};
3042
};
3143

32-
const calculateReferencePath = (
44+
/**
45+
* store を参照して、参照先のパスから TypeScript の型名や名前空間の階層を計算します。
46+
*
47+
* @param store - 型定義の情報を保持するストア
48+
* @param base - 探索のベースディレクトリ
49+
* @param pathArray - 探索対象のパス配列
50+
* @param converterContext - 変換コンテキスト
51+
* @returns 解決された型名や未解決のパス、階層の深さなどの情報
52+
*/
53+
export const calculateReferencePath = (
3354
store: Walker.Store,
3455
base: string,
3556
pathArray: string[],
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { posix as Path } from "path";
2+
import { describe, it, expect, vi } from "vitest";
3+
import * as TypeNodeContext from "../TypeNodeContext";
4+
5+
describe("TypeNodeContext", () => {
6+
describe("generatePath", () => {
7+
it("同じディレクトリ内のファイル参照において正しいパス配列を生成できること", () => {
8+
const entryPoint = "src/api/openapi.yml";
9+
const currentPoint = "src/api/components/schemas/User.yml";
10+
// referencePath はエントリポイント(src/api/openapi.yml)からの相対パス(拡張子なし)
11+
const referencePath = "components/schemas/Common";
12+
13+
const result = TypeNodeContext.generatePath(entryPoint, currentPoint, referencePath);
14+
15+
// from = components/schemas/User
16+
// base = components/schemas
17+
expect(result.base).toBe("components/schemas");
18+
// components/schemas から components/schemas/Common への相対パスは "Common"
19+
expect(result.pathArray).toEqual(["Common"]);
20+
});
21+
22+
it("親ディレクトリのファイル参照において正しいパス配列を生成できること", () => {
23+
const entryPoint = "openapi.yml";
24+
const currentPoint = "components/schemas/User.yml";
25+
const referencePath = "components/Common";
26+
27+
const result = TypeNodeContext.generatePath(entryPoint, currentPoint, referencePath);
28+
29+
expect(result.base).toBe("components/schemas");
30+
// components/schemas から components/Common への相対パスは "../Common"
31+
expect(result.pathArray).toEqual(["..", "Common"]);
32+
});
33+
34+
it("パスにバックスラッシュが含まれる場合でも POSIX スタイルとして正しく処理されること", () => {
35+
// Windows スタイルの入力をシミュレートするが、内部で POSIX 変換されることを期待
36+
const entryPoint = "api/openapi.yml";
37+
const currentPoint = "api/components/schemas/User.yml";
38+
const referencePath = "components/schemas/Common";
39+
40+
const result = TypeNodeContext.generatePath(entryPoint, currentPoint, referencePath);
41+
42+
expect(result.base).toBe("components/schemas");
43+
expect(result.pathArray).toEqual(["Common"]);
44+
});
45+
});
46+
47+
describe("calculateReferencePath", () => {
48+
const mockConverterContext = {
49+
escapeDeclarationText: (text: string) => text,
50+
} as any;
51+
52+
it("ストアに登録されたインターフェースを解決できること", () => {
53+
const mockStore = {
54+
getStatement: vi.fn().mockImplementation((path, kind) => {
55+
if (path === "components/schemas/User" && kind === "interface") {
56+
return { name: "User" };
57+
}
58+
return undefined;
59+
}),
60+
} as any;
61+
62+
const base = "components/schemas";
63+
const pathArray = ["User"];
64+
const result = TypeNodeContext.calculateReferencePath(mockStore, base, pathArray, mockConverterContext);
65+
66+
expect(result.name).toBe("User");
67+
expect(result.maybeResolvedName).toBe("User");
68+
expect(result.unresolvedPaths).toEqual([]);
69+
expect(result.depth).toBe(1);
70+
});
71+
72+
it("名前空間を経由して型を解決できること", () => {
73+
const mockStore = {
74+
getStatement: vi.fn().mockImplementation((path, kind) => {
75+
if (path === "components/schemas" && kind === "namespace") {
76+
return { name: "Schemas" };
77+
}
78+
if (path === "components/schemas/User" && kind === "interface") {
79+
return { name: "User" };
80+
}
81+
return undefined;
82+
}),
83+
} as any;
84+
85+
const base = "components";
86+
const pathArray = ["schemas", "User"];
87+
const result = TypeNodeContext.calculateReferencePath(mockStore, base, pathArray, mockConverterContext);
88+
89+
expect(result.name).toBe("Schemas.User");
90+
expect(result.maybeResolvedName).toBe("Schemas.User");
91+
expect(result.depth).toBe(2);
92+
});
93+
94+
it("未解決のパスがある場合に maybeResolvedName に含まれること", () => {
95+
const mockStore = {
96+
getStatement: vi.fn().mockImplementation((path, kind) => {
97+
if (path === "components/schemas" && kind === "namespace") {
98+
return { name: "Schemas" };
99+
}
100+
// User は見つからない
101+
return undefined;
102+
}),
103+
} as any;
104+
105+
const base = "components";
106+
const pathArray = ["schemas", "User"];
107+
const result = TypeNodeContext.calculateReferencePath(mockStore, base, pathArray, mockConverterContext);
108+
109+
expect(result.name).toBe("Schemas");
110+
expect(result.maybeResolvedName).toBe("Schemas.User");
111+
expect(result.unresolvedPaths).toEqual(["User"]);
112+
});
113+
114+
it("型が全く見つからない場合にエラーを投げること", () => {
115+
const mockStore = {
116+
getStatement: vi.fn().mockReturnValue(undefined),
117+
} as any;
118+
119+
expect(() => {
120+
TypeNodeContext.calculateReferencePath(mockStore, "base", ["Unknown"], mockConverterContext);
121+
}).toThrow("Local Reference Error");
122+
});
123+
});
124+
});
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { describe, it, expect } from "vitest";
2+
import { EOL } from "os";
3+
import * as Factory from "../factory";
4+
5+
describe("TsGenerator Factory Helpers", () => {
6+
describe("escapeTemplateText", () => {
7+
it("テンプレートリテラル内の特殊文字をエスケープできること", () => {
8+
// バックスラッシュ、バッククォート、${ をエスケープする
9+
expect(Factory.escapeTemplateText("path\\to\\file")).toBe("path\\\\to\\\\file");
10+
expect(Factory.escapeTemplateText("`quoted`")).toBe("\\`quoted\\`");
11+
expect(Factory.escapeTemplateText("${variable}")).toBe("\\${variable}");
12+
});
13+
});
14+
15+
describe("escapeIdentifier", () => {
16+
it("識別子に含まれるハイフンをアンダースコアに置換できること", () => {
17+
expect(Factory.escapeIdentifier("my-variable-name")).toBe("my_variable_name");
18+
expect(Factory.escapeIdentifier("nochange")).toBe("nochange");
19+
});
20+
});
21+
22+
describe("indentLines", () => {
23+
it("各行にインデントを付与し、空行はそのままにすること", () => {
24+
const input = "line1\n\nline2";
25+
const expected = " line1\n\n line2";
26+
expect(Factory.indentLines(input, " ")).toBe(expected);
27+
});
28+
});
29+
30+
describe("hasTopLevelOp", () => {
31+
it("トップレベルに | または & がある場合は true を返すこと", () => {
32+
expect(Factory.hasTopLevelOp("A | B")).toBe(true);
33+
expect(Factory.hasTopLevelOp("A & B")).toBe(true);
34+
});
35+
36+
it("括弧や型引数の中にある演算子は無視されること", () => {
37+
expect(Factory.hasTopLevelOp("(A | B)")).toBe(false);
38+
expect(Factory.hasTopLevelOp("Array<A | B>")).toBe(false);
39+
expect(Factory.hasTopLevelOp("{ prop: A | B }")).toBe(false);
40+
expect(Factory.hasTopLevelOp("[A | B]")).toBe(false);
41+
});
42+
43+
it("ネストが深い場合の演算子も正しく無視されること", () => {
44+
expect(Factory.hasTopLevelOp("A | Array<{ p: B & C }>")).toBe(true);
45+
expect(Factory.hasTopLevelOp("Array<{ p: B & C }> | D")).toBe(true);
46+
expect(Factory.hasTopLevelOp("Map<K, V | T>")).toBe(false);
47+
});
48+
});
49+
50+
describe("buildComment", () => {
51+
it("単一ラインのコメントを生成できること", () => {
52+
expect(Factory.buildComment("hello")).toBe(`/** hello */${EOL}`);
53+
});
54+
55+
it("複数ラインのコメントを生成できること", () => {
56+
const input = "line1\nline2";
57+
const expected = `/**${EOL} * line1${EOL} * line2${EOL} */${EOL}`;
58+
expect(Factory.buildComment(input)).toBe(expected);
59+
});
60+
61+
it("deprecated フラグがある場合に @deprecated タグを付与すること", () => {
62+
const expected = `/**${EOL} * @deprecated${EOL} * old feature${EOL} */${EOL}`;
63+
expect(Factory.buildComment("old feature", true)).toBe(expected);
64+
});
65+
66+
it("コメント内の特殊な記号をエスケープすること", () => {
67+
expect(Factory.buildComment("*/")).toContain("\\*\\\\/");
68+
expect(Factory.buildComment("/*")).toContain("/\\\\*");
69+
});
70+
});
71+
72+
describe("addComment", () => {
73+
it("コメントがある場合にコードの前に付与すること", () => {
74+
const code = "const a = 1;";
75+
const comment = "my variable";
76+
const result = Factory.addComment(code, comment);
77+
expect(result).toBe(`/** my variable */${EOL}${code}`);
78+
});
79+
80+
it("コメントも deprecated もない場合は元のコードを返すこと", () => {
81+
const code = "const a = 1;";
82+
expect(Factory.addComment(code)).toBe(code);
83+
});
84+
});
85+
});
86+
87+
describe("TsGenerator Factory Create API", () => {
88+
const factory = Factory.create();
89+
90+
describe("StringLiteral", () => {
91+
it("文字列リテラルを生成できること(ダブルクォート)", () => {
92+
expect(factory.StringLiteral.create({ text: 'hello "world"' })).toBe('"hello \\"world\\""');
93+
});
94+
95+
it("文字列リテラルを生成できること(シングルクォート)", () => {
96+
expect(factory.StringLiteral.create({ text: "hello 'world'", isSingleQuote: true })).toBe("'hello \\'world\\''");
97+
});
98+
});
99+
100+
describe("TypeNode", () => {
101+
it("プリミティブ型を生成できること", () => {
102+
expect(factory.TypeNode.create({ type: "string" })).toBe("string");
103+
expect(factory.TypeNode.create({ type: "number" })).toBe("number");
104+
expect(factory.TypeNode.create({ type: "boolean" })).toBe("boolean");
105+
});
106+
107+
it("enum 文字列型を生成できること", () => {
108+
expect(factory.TypeNode.create({ type: "string", enum: ["a", "b"] })).toBe('"a" | "b"');
109+
});
110+
111+
it("配列型を生成できること", () => {
112+
expect(factory.TypeNode.create({ type: "array", value: "string" })).toBe("string[]");
113+
});
114+
115+
it("トップレベル演算子を含む型の配列は括弧で囲まれること", () => {
116+
expect(factory.TypeNode.create({ type: "array", value: "string | number" })).toBe("(string | number)[]");
117+
});
118+
119+
it("オブジェクト型(インライン)を生成できること", () => {
120+
const result = factory.TypeNode.create({ type: "object", value: ["id: string;", "name: string;"] });
121+
expect(result).toBe("{\n id: string;\n name: string;\n}");
122+
});
123+
});
124+
125+
describe("UnionTypeNode", () => {
126+
it("ユニオン型を生成できること", () => {
127+
expect(factory.UnionTypeNode.create({ typeNodes: ["string", "number"] })).toBe("string | number");
128+
});
129+
130+
it("ネストした演算子を持つ型は括弧で囲まれること", () => {
131+
expect(factory.UnionTypeNode.create({ typeNodes: ["A & B", "C"] })).toBe("(A & B) | C");
132+
});
133+
});
134+
135+
describe("InterfaceDeclaration", () => {
136+
it("インターフェース宣言を生成できること", () => {
137+
const result = factory.InterfaceDeclaration.create({
138+
name: "MyInterface",
139+
members: ["id: string;", "name?: string;"],
140+
export: true,
141+
});
142+
expect(result).toBe("export interface MyInterface {\n id: string;\n name?: string;\n}");
143+
});
144+
});
145+
146+
describe("Namespace", () => {
147+
it("名前空間を生成できること", () => {
148+
const result = factory.Namespace.create({
149+
name: "MyNamespace",
150+
statements: ["export type T = string;"],
151+
export: true,
152+
});
153+
expect(result).toBe("export namespace MyNamespace {\n export type T = string;\n}");
154+
});
155+
156+
it("ネストした名前空間を一括生成できること", () => {
157+
const result = factory.Namespace.createMultiple({
158+
names: ["A", "B", "C"],
159+
statements: ["export const v = 1;"],
160+
export: true,
161+
});
162+
expect(result).toContain("namespace A");
163+
expect(result).toContain("namespace B");
164+
expect(result).toContain("namespace C");
165+
});
166+
});
167+
});

0 commit comments

Comments
 (0)