Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 0 additions & 170 deletions src/__tests__/generateValidRootSchema.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,85 +5,89 @@ import type { CodeGenerator } from "../../../../types";
import * as Utils from "../../utils";
import * as PathParameter from "../PathParameter";

// テンプレートリテラル式の文字列表現を組み立てるヘルパー。
// "$" を分離して結合することで noTemplateCurlyInString を回避している。
// biome-ignore lint/style/useTemplate: "$" + template is intentional to prevent noTemplateCurlyInString
const p = (name: string): string => "$" + `{encodeURIComponent(params.parameter.${name})}`;
const tpl = (...parts: string[]): string => `\`${parts.join("")}\``;

describe("PathParameter Test", () => {
const factory = TsGenerator.Factory.create();
const generate = (url: string, pathParameter: CodeGenerator.PickedParameter[]): string => {
const urlTemplates = PathParameter.generateUrlTemplateExpression(factory, url, pathParameter);
return Utils.generateTemplateExpression(factory, urlTemplates);
};
test("generateUrlTemplateExpression", () => {
expect(generate("/{a}", [{ in: "path", name: "a", required: true }])).toEqual("`/${encodeURIComponent(params.parameter.a)}`");
expect(generate("/{a}/", [{ in: "path", name: "a", required: true }])).toEqual("`/${encodeURIComponent(params.parameter.a)}/`");
expect(generate("/a/{b}", [{ in: "path", name: "b", required: true }])).toEqual("`/a/${encodeURIComponent(params.parameter.b)}`");
expect(generate("/a/{b}/", [{ in: "path", name: "b", required: true }])).toEqual("`/a/${encodeURIComponent(params.parameter.b)}/`");
expect(generate("/a/{b}/c", [{ in: "path", name: "b", required: true }])).toEqual("`/a/${encodeURIComponent(params.parameter.b)}/c`");
expect(generate("/a/{b}/c/", [{ in: "path", name: "b", required: true }])).toEqual("`/a/${encodeURIComponent(params.parameter.b)}/c/`");
expect(generate("/a/b/{c}", [{ in: "path", name: "c", required: true }])).toEqual("`/a/b/${encodeURIComponent(params.parameter.c)}`");
expect(generate("/a/b/{c}", [{ in: "path", name: "c", required: true }])).toEqual("`/a/b/${encodeURIComponent(params.parameter.c)}`");
expect(generate("/a/b/{c}/", [{ in: "path", name: "c", required: true }])).toEqual("`/a/b/${encodeURIComponent(params.parameter.c)}/`");
expect(generate("/a/b/{c}.json", [{ in: "path", name: "c", required: true }])).toEqual(
"`/a/b/${encodeURIComponent(params.parameter.c)}.json`",
);
expect(generate("/{a}", [{ in: "path", name: "a", required: true }])).toEqual(tpl("/", p("a")));
expect(generate("/{a}/", [{ in: "path", name: "a", required: true }])).toEqual(tpl("/", p("a"), "/"));
expect(generate("/a/{b}", [{ in: "path", name: "b", required: true }])).toEqual(tpl("/a/", p("b")));
expect(generate("/a/{b}/", [{ in: "path", name: "b", required: true }])).toEqual(tpl("/a/", p("b"), "/"));
expect(generate("/a/{b}/c", [{ in: "path", name: "b", required: true }])).toEqual(tpl("/a/", p("b"), "/c"));
expect(generate("/a/{b}/c/", [{ in: "path", name: "b", required: true }])).toEqual(tpl("/a/", p("b"), "/c/"));
expect(generate("/a/b/{c}", [{ in: "path", name: "c", required: true }])).toEqual(tpl("/a/b/", p("c")));
expect(generate("/a/b/{c}", [{ in: "path", name: "c", required: true }])).toEqual(tpl("/a/b/", p("c")));
expect(generate("/a/b/{c}/", [{ in: "path", name: "c", required: true }])).toEqual(tpl("/a/b/", p("c"), "/"));
expect(generate("/a/b/{c}.json", [{ in: "path", name: "c", required: true }])).toEqual(tpl("/a/b/", p("c"), ".json"));
expect(generate("/{a}.json/{a}.json/{a}.json", [{ in: "path", name: "a", required: true }])).toEqual(
"`/${encodeURIComponent(params.parameter.a)}.json/${encodeURIComponent(params.parameter.a)}.json/${encodeURIComponent(params.parameter.a)}.json`",
tpl("/", p("a"), ".json/", p("a"), ".json/", p("a"), ".json"),
);
expect(generate("/.json.{a}.json/{a}.json.{a}", [{ in: "path", name: "a", required: true }])).toEqual(
"`/.json.${encodeURIComponent(params.parameter.a)}.json/${encodeURIComponent(params.parameter.a)}.json.${encodeURIComponent(params.parameter.a)}`",
tpl("/.json.", p("a"), ".json/", p("a"), ".json.", p("a")),
);

expect(
generate("/{a}/{b}", [
{ in: "path", name: "a", required: true },
{ in: "path", name: "b", required: true },
]),
).toBe("`/${encodeURIComponent(params.parameter.a)}/${encodeURIComponent(params.parameter.b)}`");
).toBe(tpl("/", p("a"), "/", p("b")));
expect(
generate("/{a}/{b}/", [
{ in: "path", name: "a", required: true },
{ in: "path", name: "b", required: true },
]),
).toBe("`/${encodeURIComponent(params.parameter.a)}/${encodeURIComponent(params.parameter.b)}/`");
).toBe(tpl("/", p("a"), "/", p("b"), "/"));
expect(
generate("/{a}/{b}/c", [
{ in: "path", name: "a", required: true },
{ in: "path", name: "b", required: true },
]),
).toBe("`/${encodeURIComponent(params.parameter.a)}/${encodeURIComponent(params.parameter.b)}/c`");
).toBe(tpl("/", p("a"), "/", p("b"), "/c"));
expect(
generate("/{a}/{b}/c/", [
{ in: "path", name: "a", required: true },
{ in: "path", name: "b", required: true },
]),
).toBe("`/${encodeURIComponent(params.parameter.a)}/${encodeURIComponent(params.parameter.b)}/c/`");
).toBe(tpl("/", p("a"), "/", p("b"), "/c/"));
expect(
generate("/{a}/b/{c}", [
{ in: "path", name: "a", required: true },
{ in: "path", name: "c", required: true },
]),
).toBe("`/${encodeURIComponent(params.parameter.a)}/b/${encodeURIComponent(params.parameter.c)}`");
).toBe(tpl("/", p("a"), "/b/", p("c")));
expect(
generate("/{a}/b/{c}/", [
{ in: "path", name: "a", required: true },
{ in: "path", name: "c", required: true },
]),
).toBe("`/${encodeURIComponent(params.parameter.a)}/b/${encodeURIComponent(params.parameter.c)}/`");
).toBe(tpl("/", p("a"), "/b/", p("c"), "/"));
expect(
generate("/a/{b}/{c}", [
{ in: "path", name: "b", required: true },
{ in: "path", name: "c", required: true },
]),
).toBe("`/a/${encodeURIComponent(params.parameter.b)}/${encodeURIComponent(params.parameter.c)}`");
).toBe(tpl("/a/", p("b"), "/", p("c")));
expect(
generate("/a/{b}/{c}/", [
{ in: "path", name: "b", required: true },
{ in: "path", name: "c", required: true },
]),
).toBe("`/a/${encodeURIComponent(params.parameter.b)}/${encodeURIComponent(params.parameter.c)}/`");
).toBe(tpl("/a/", p("b"), "/", p("c"), "/"));
expect(
generate("/a/{b}...{c}/", [
{ in: "path", name: "b", required: true },
{ in: "path", name: "c", required: true },
]),
).toBe("`/a/${encodeURIComponent(params.parameter.b)}...${encodeURIComponent(params.parameter.c)}/`");
).toBe(tpl("/a/", p("b"), "...", p("c"), "/"));
});
});
42 changes: 13 additions & 29 deletions src/generateValidRootSchema.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,22 @@
import type * as Types from "./types";

const normalizePathParameters = (parameters: (Types.OpenApi.Parameter | Types.OpenApi.Reference)[] | undefined): void => {
if (!parameters) {
return;
}
for (const parameter of parameters) {
if ("$ref" in parameter) {
continue;
}
// OpenAPI 3.x spec §3.3.2: path パラメータは常に required: true
if (parameter.in === "path") {
parameter.required = true;
}
}
};

export const generateValidRootSchema = (input: Types.OpenApi.Document): Types.OpenApi.Document => {
if (input.components?.parameters) {
normalizePathParameters(Object.values(input.components.parameters));
}

if (!input.paths) {
return input;
}

const httpMethods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as const;

for (const [path, pathItem] of Object.entries(input.paths)) {
normalizePathParameters(pathItem.parameters);

for (const method of httpMethods) {
const operation = pathItem[method];
/** update undefined operation id */
for (const [path, methods] of Object.entries(input.paths || {})) {
const targets = {
get: methods.get,
put: methods.put,
post: methods.post,
delete: methods.delete,
options: methods.options,
head: methods.head,
patch: methods.patch,
trace: methods.trace,
} satisfies Record<string, Types.OpenApi.Operation | undefined>;
for (const [method, operation] of Object.entries(targets)) {
if (!operation) {
continue;
}
Expand All @@ -41,9 +27,7 @@ export const generateValidRootSchema = (input: Types.OpenApi.Document): Types.Op
if (!operation.operationId) {
operation.operationId = `${method.toLowerCase()}${path.charAt(0).toUpperCase() + path.slice(1)}`;
}
normalizePathParameters(operation.parameters);
}
}

return input;
};
11 changes: 10 additions & 1 deletion src/internal/OpenApiTools/components/Parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,16 @@ export const generatePropertySignatures = (
context: ToTypeNode.Context,
converterContext: ConverterContext.Types,
): string[] => {
const typeElementMap = parameters.reduce<Record<string, string>>((all, parameter) => {
// Path parameters must be processed last so they win over same-named query/header
// parameters when building the TypeScript interface (path params are always required).
const sorted = [...parameters].sort((a, b): number => {
const aIsPath = !Guard.isReference(a) && a.in === "path";
const bIsPath = !Guard.isReference(b) && b.in === "path";
if (aIsPath && !bIsPath) return 1;
if (!aIsPath && bIsPath) return -1;
return 0;
});
const typeElementMap = sorted.reduce<Record<string, string>>((all, parameter) => {
const { name, typeElement } = generatePropertySignatureObject(
entryPoint,
currentPoint,
Expand Down
1 change: 1 addition & 0 deletions src/internal/TsGenerator/__tests__/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe("TsGenerator Factory Helpers", () => {
// バックスラッシュ、バッククォート、${ をエスケープする
expect(Factory.escapeTemplateText("path\\to\\file")).toBe("path\\\\to\\\\file");
expect(Factory.escapeTemplateText("`quoted`")).toBe("\\`quoted\\`");
// biome-ignore lint/suspicious/noTemplateCurlyInString: intentional - testing escaping of template literal placeholders
expect(Factory.escapeTemplateText("${variable}")).toBe("\\${variable}");
});
});
Expand Down
Loading
Loading