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
5 changes: 5 additions & 0 deletions scripts/testCodeGenWithClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ const main = () => {
Writer.generateParameter("test/api.test.domain/index.yml", "test/code/class/parameter/api.test.domain.json");
Writer.generateParameter("test/infer.domain/index.yml", "test/code/class/parameter/infer.domain.json");

Writer.generateTypedefWithTemplateCode("test/path-parameter/index.yml", "test/code/class/typedef-with-template/path-parameter.ts", false, {
sync: false,
});
Writer.generateParameter("test/path-parameter/index.yml", "test/code/class/parameter/path-parameter.json");

Writer.generateFormatTypeCode("test/format.domain/index.yml", "test/code/class/format.domain/code.ts");

Writer.generateFormatTypeCode("test/cloudflare/openapi.yaml", "test/code/class/cloudflare/client.ts");
Expand Down
8 changes: 8 additions & 0 deletions scripts/testCodeGenWithCurryingFunctional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ const main = () => {
Writer.generateParameter("test/api.test.domain/index.yml", "test/code/currying-functional/parameter/api.test.domain.json");
Writer.generateParameter("test/infer.domain/index.yml", "test/code/currying-functional/parameter/infer.domain.json");

Writer.generateTypedefWithTemplateCode(
"test/path-parameter/index.yml",
"test/code/currying-functional/typedef-with-template/path-parameter.ts",
false,
{ sync: false },
);
Writer.generateParameter("test/path-parameter/index.yml", "test/code/currying-functional/parameter/path-parameter.json");

Writer.generateFormatTypeCode("test/format.domain/index.yml", "test/code/currying-functional/format.domain/code.ts");

Writer.generateFormatTypeCode("test/cloudflare/openapi.yaml", "test/code/currying-functional/cloudflare/client.ts");
Expand Down
8 changes: 8 additions & 0 deletions scripts/testCodeGenWithFunctional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ const main = () => {
Writer.generateParameter("test/api.test.domain/index.yml", "test/code/functional/parameter/api.test.domain.json");
Writer.generateParameter("test/infer.domain/index.yml", "test/code/functional/parameter/infer.domain.json");

Writer.generateTypedefWithTemplateCode(
"test/path-parameter/index.yml",
"test/code/functional/typedef-with-template/path-parameter.ts",
false,
{ sync: false },
);
Writer.generateParameter("test/path-parameter/index.yml", "test/code/functional/parameter/path-parameter.json");

Writer.generateFormatTypeCode("test/format.domain/index.yml", "test/code/functional/format.domain/code.ts");

Writer.generateFormatTypeCode("test/cloudflare/openapi.yaml", "test/code/functional/cloudflare/client.ts");
Expand Down
170 changes: 170 additions & 0 deletions src/__tests__/generateValidRootSchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import assert from "node:assert";
import { describe, expect, it } from "vitest";
import { generateValidRootSchema } from "../generateValidRootSchema";
import type * as Types from "../types";

describe("generateValidRootSchema", () => {
describe("パスパラメータの required 正規化", () => {
it("paths 直下のオペレーションに定義されたパスパラメータで required を省略した場合、required: true が設定されること", () => {
const input: Types.OpenApi.Document = {
openapi: "3.1.0",
info: { title: "test", version: "1.0.0", summary: "", description: "", termsOfService: "" },
paths: {
"/items/{id}": {
get: {
operationId: "getItemById",
parameters: [
{
in: "path",
name: "id",
required: false,
schema: { type: "string" },
},
],
responses: { default: { description: "default" } },
},
},
},
};

const result = generateValidRootSchema(input);

const paths = result.paths;
assert(paths);
const pathItem = paths["/items/{id}"];
assert(pathItem);
const parameters = pathItem.get?.parameters;
assert(parameters);
const parameter = parameters[0] as Types.OpenApi.Parameter;
expect(parameter.required).toBe(true);
});

it("paths 直下のオペレーションに定義されたパスパラメータで required を省略した場合でも required: true が設定されること", () => {
const input: Types.OpenApi.Document = {
openapi: "3.1.0",
info: { title: "test", version: "1.0.0", summary: "", description: "", termsOfService: "" },
paths: {
"/items/{id}": {
get: {
operationId: "getItemById",
// required フィールド自体を省略
parameters: [{ in: "path", name: "id", required: undefined as unknown as boolean, schema: { type: "string" } }],
responses: { default: { description: "default" } },
},
},
},
};

const result = generateValidRootSchema(input);

const paths = result.paths;
assert(paths);
const pathItem = paths["/items/{id}"];
assert(pathItem);
const parameters = pathItem.get?.parameters;
assert(parameters);
const parameter = parameters[0] as Types.OpenApi.Parameter;
expect(parameter.required).toBe(true);
});

it("PathItem レベルのパスパラメータで required を省略した場合、required: true が設定されること", () => {
const input: Types.OpenApi.Document = {
openapi: "3.1.0",
info: { title: "test", version: "1.0.0", summary: "", description: "", termsOfService: "" },
paths: {
"/items/{id}": {
parameters: [{ in: "path", name: "id", required: false, schema: { type: "string" } }],
get: {
operationId: "getItemById",
responses: { default: { description: "default" } },
},
},
},
};

const result = generateValidRootSchema(input);

const paths = result.paths;
assert(paths);
const pathItem = paths["/items/{id}"];
assert(pathItem);
const parameters = pathItem.parameters;
assert(parameters);
const parameter = parameters[0] as Types.OpenApi.Parameter;
expect(parameter.required).toBe(true);
});

it("components.parameters に定義されたパスパラメータで required を省略した場合、required: true が設定されること", () => {
const input: Types.OpenApi.Document = {
openapi: "3.1.0",
info: { title: "test", version: "1.0.0", summary: "", description: "", termsOfService: "" },
components: {
parameters: {
ItemId: { in: "path", name: "id", required: false, schema: { type: "string" } },
},
},
};

const result = generateValidRootSchema(input);

const componentsParameters = result.components?.parameters;
assert(componentsParameters);
const parameter = componentsParameters.ItemId as Types.OpenApi.Parameter;
expect(parameter.required).toBe(true);
});

it("クエリパラメータで required を省略した場合、required フィールドは変更されないこと", () => {
const input: Types.OpenApi.Document = {
openapi: "3.1.0",
info: { title: "test", version: "1.0.0", summary: "", description: "", termsOfService: "" },
paths: {
"/items": {
get: {
operationId: "getItems",
parameters: [{ in: "query", name: "filter", required: false, schema: { type: "string" } }],
responses: { default: { description: "default" } },
},
},
},
};

const result = generateValidRootSchema(input);

const paths = result.paths;
assert(paths);
const pathItem = paths["/items"];
assert(pathItem);
const parameters = pathItem.get?.parameters;
assert(parameters);
const parameter = parameters[0] as Types.OpenApi.Parameter;
expect(parameter.required).toBe(false);
});

it("パスパラメータと明示的に required: true が設定されている場合、その値が維持されること", () => {
const input: Types.OpenApi.Document = {
openapi: "3.1.0",
info: { title: "test", version: "1.0.0", summary: "", description: "", termsOfService: "" },
paths: {
"/items/{id}": {
get: {
operationId: "getItemById",
parameters: [{ in: "path", name: "id", required: true, schema: { type: "string" } }],
responses: { default: { description: "default" } },
},
},
},
};

const result = generateValidRootSchema(input);

const paths = result.paths;
assert(paths);
const pathItem = paths["/items/{id}"];
assert(pathItem);
const parameters = pathItem.get?.parameters;
assert(parameters);
const parameter = parameters[0] as Types.OpenApi.Parameter;
expect(parameter.required).toBe(true);
});
});
});
42 changes: 29 additions & 13 deletions src/generateValidRootSchema.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
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;
}
/** 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)) {

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];
if (!operation) {
continue;
}
Expand All @@ -27,7 +41,9 @@ 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;
};
Loading
Loading