Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[日本語](./docs/ja/README-ja.md)

This library provides TypeScript type definitions and extracted parameters from OpenAPI v3.0.x compliant specifications.
This library provides TypeScript type definitions and extracted parameters from OpenAPI v3.x compliant specifications.
Template literals are used to generate the code, which is accurately converted to TypeScript code.
Since the parameters extracted from OpenAPI can be used freely, it can be used for automatic generation of API Client and Server Side code, load balancer configuration files, etc.

Expand Down
2 changes: 1 addition & 1 deletion docs/ja/README-ja.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @himenon/openapi-typescript-code-generator

このライブラリは OpenAPI v3.0.x 系に準拠した仕様書から TypeScript の型定義と抽出したパラメーターを提供します。
このライブラリは OpenAPI v3.x 系に準拠した仕様書から TypeScript の型定義と抽出したパラメーターを提供します。
コードの生成にはテンプレートリテラルを利用し、正確に TypeScript のコードへ変換します。
OpenAPI から抽出したパラメーターは自由に使うことができるため、API Client や Server Side 用のコード、ロードバランサーの設定ファイルなどの自動生成に役立てることができます。

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"lint": "biome check .",
"release:github:registry": "pnpm publish --access public --no-git-checks",
"release:npm:registry": "pnpm publish --access public --no-git-checks",
"test": "run-p test:depcruise test:vitest test:code:gen:* test:snapshot",
"test": "run-p test:depcruise test:vitest test:code:gen && run-s test:snapshot",
"test:code:gen": "run-p test:code:gen:*",
"test:code:gen:class": "pnpm ts ./scripts/testCodeGenWithClass.ts",
"test:code:gen:currying-function": "pnpm ts ./scripts/testCodeGenWithCurryingFunctional.ts",
Expand Down
8 changes: 5 additions & 3 deletions src/code-templates/_shared/ApiClientInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ const createSuccessResponseTypeAlias = (typeName: string, factory: TsGenerator.F
});
};

const createHttpMethod = (factory: TsGenerator.Factory.Type) => {
const createHttpMethod = (factory: TsGenerator.Factory.Type, list: CodeGenerator.Params[]) => {
// OpenAPI 3.2 で QUERY が使われる場合だけ、生成するクライアントの HTTP メソッド型に追加します。
const methods = list.some(item => item.operationParams.httpMethod.toLowerCase() === "query") ? [...httpMethodList, "QUERY"] : httpMethodList;
return factory.TypeAliasDeclaration.create({
export: true,
name: "HttpMethod",
type: factory.TypeNode.create({ type: "string", enum: httpMethodList }),
type: factory.TypeNode.create({ type: "string", enum: methods }),
});
};

Expand Down Expand Up @@ -278,7 +280,7 @@ export const create = (factory: TsGenerator.Factory.Type, list: CodeGenerator.Pa
});

return [
createHttpMethod(factory),
createHttpMethod(factory, list),
createObjectLikeInterface(factory),
...createQueryParamsDeclarations(factory),
createSuccessResponseTypeAlias("SuccessResponses", factory, successResponseNames),
Expand Down
8 changes: 7 additions & 1 deletion src/code-templates/_shared/MethodBody/CallRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import * as Utils from "../utils";
import { createEncodingMap } from "./createEncodingMap";
import type { MethodType } from "./types";

const standardHttpMethods = new Set(["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE", "QUERY"]);

export interface Params {
httpMethod: string;
hasRequestBody: boolean;
Expand Down Expand Up @@ -43,12 +45,16 @@ export const create = (factory: TsGenerator.Factory.Type, params: CodeGenerator.
};
const expression = Utils.generateVariableIdentifier(factory, apiClientVariableIdentifier[methodType]);
const requestBodyEncoding = createEncodingParams(factory, params);
const httpMethod = params.operationParams.httpMethod.toUpperCase();
const httpMethodLiteral = factory.StringLiteral.create({ text: httpMethod });
// OpenAPI 3.2 の additionalOperations は任意の HTTP メソッド名を許可します。
const httpMethodInitializer = standardHttpMethods.has(httpMethod) ? httpMethodLiteral : `${httpMethodLiteral} as HttpMethod`;

const requestArgs = factory.ObjectLiteralExpression.create({
properties: [
factory.PropertyAssignment.create({
name: "httpMethod",
initializer: factory.StringLiteral.create({ text: params.operationParams.httpMethod.toUpperCase() }),
initializer: httpMethodInitializer,
}),
factory.ShorthandPropertyAssignment.create({
name: methodType === "currying-function" ? "uri" : "url",
Expand Down
3 changes: 2 additions & 1 deletion src/code-templates/_shared/MethodBody/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ export const create = (factory: TsGenerator.Factory.Type, params: CodeGenerator.

// Generate Query Parameter
if (convertedParams.hasQueryParameters) {
const queryParameter = pickedParameters.filter(item => item.in === "query");
// OpenAPI 3.2 の querystring パラメータもクエリ文字列へ渡します。
const queryParameter = pickedParameters.filter(item => item.in === "query" || item.in === "querystring");
const queryObject = Object.values(queryParameter).reduce<{ [key: string]: QueryParameter.Item }>((previous, current) => {
const { text, escaped } = escapeText(current.name);
const variableDeclareText = escaped ? `params.parameter[${text}]` : `params.parameter.${text}`;
Expand Down
48 changes: 34 additions & 14 deletions src/generateValidRootSchema.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type * as Types from "./types";

export const generateValidRootSchema = (input: Types.OpenApi.Document): Types.OpenApi.Document => {
if (!input.paths) {
return input;
}
/** update undefined operation id */
for (const [path, methods] of Object.entries(input.paths || {})) {
if ("$ref" in methods) {
continue;
}
const targets = {
get: methods.get,
put: methods.put,
Expand All @@ -15,19 +15,39 @@ export const generateValidRootSchema = (input: Types.OpenApi.Document): Types.Op
head: methods.head,
patch: methods.patch,
trace: methods.trace,
query: methods.query,
} satisfies Record<string, Types.OpenApi.Operation | undefined>;
for (const [method, operation] of Object.entries(targets)) {
if (!operation) {
continue;
}
// skip reference object
if ("$ref" in operation) {
continue;
}
if (!operation.operationId) {
operation.operationId = `${method.toLowerCase()}${path.charAt(0).toUpperCase() + path.slice(1)}`;
}
assignOperationIds(path, targets);
// OpenAPI 3.2 で追加された additionalOperations にも operationId を補完します。
assignOperationIds(path, methods.additionalOperations || {});
}
for (const [name, pathItem] of Object.entries(input.components?.pathItems || {})) {
if ("$ref" in pathItem) {
continue;
}
const targets = {
get: pathItem.get,
put: pathItem.put,
post: pathItem.post,
delete: pathItem.delete,
options: pathItem.options,
head: pathItem.head,
patch: pathItem.patch,
trace: pathItem.trace,
query: pathItem.query,
} satisfies Record<string, Types.OpenApi.Operation | undefined>;
assignOperationIds(name, targets);
// OpenAPI 3.2 で追加された additionalOperations にも operationId を補完します。
assignOperationIds(name, pathItem.additionalOperations || {});
}
return input;
};

const assignOperationIds = (path: string, operations: Record<string, Types.OpenApi.Operation | undefined>): void => {
for (const [method, operation] of Object.entries(operations)) {
if (!operation || "$ref" in operation || operation.operationId) {
continue;
}
operation.operationId = `${method.toLowerCase()}${path.charAt(0).toUpperCase() + path.slice(1)}`;
}
};
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class CodeGenerator {
JSON.parse(JSON.stringify(this.rootSchema)),
);
} else {
this.rootSchema = entryPointOrDocument;
this.rootSchema = generateValidRootSchema(entryPointOrDocument);
this.resolvedReferenceDocument = Api.ResolveReference.resolve(".", ".", JSON.parse(JSON.stringify(this.rootSchema)));
}
this.parser = this.createParser();
Expand Down
3 changes: 2 additions & 1 deletion src/internal/OpenApiTools/Extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ const hasQueryParameters = (parameters?: OpenApi.Parameter[]): boolean => {
if (!parameters) {
return false;
}
return parameters.filter(parameter => parameter.in === "query").length > 0;
// OpenAPI 3.2 で追加された querystring もクエリ文字列として扱います。
return parameters.some(parameter => parameter.in === "query" || parameter.in === "querystring");
};

export const generateCodeGeneratorParamsArray = (
Expand Down
6 changes: 4 additions & 2 deletions src/internal/OpenApiTools/Guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ export const isReference = (data: any): data is OpenApi.Reference => {
return typeof data.$ref === "string";
};

export const isUnSupportSchema = (schema: OpenApi.Schema): schema is Types.UnSupportSchema => {
export const isTypeArraySchema = (schema: OpenApi.Schema): schema is Types.TypeArraySchema => {
return Array.isArray(schema.type);
};

export const isUnSupportSchema = isTypeArraySchema;

export const isObjectSchema = (schema: OpenApi.Schema): schema is Types.ObjectSchema => {
return schema.type === "object";
};
Expand Down Expand Up @@ -67,5 +69,5 @@ export const isAnyOfSchema = (schema: OpenApi.Schema): schema is Types.AnyOfSche
};

export const isComponentName = (name: string): name is Def.ComponentName => {
return ["schemas", "headers", "responses", "parameters", "requestBodies", "securitySchemes", "pathItems"].includes(name);
return ["schemas", "headers", "responses", "parameters", "requestBodies", "securitySchemes", "pathItems", "mediaTypes"].includes(name);
};
24 changes: 24 additions & 0 deletions src/internal/OpenApiTools/InferredType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,36 @@ export const getInferredType = (schema: OpenApi.Schema): OpenApi.Schema | undefi
if (schema.type || schema.oneOf || schema.allOf || schema.anyOf) {
return schema;
}
// OpenAPI 3.1 で JSON Schema の const キーワードが利用可能になりました。
if (Object.hasOwn(schema, "const")) {
const value = schema.const;
const type = value === null ? "null" : typeof value;
if (type === "string" || type === "number" || type === "boolean" || type === "null") {
return { ...schema, type } as OpenApi.Schema;
}
}
// type: arrayを指定せずに、itemsのみを指定している場合に type array変換する
if (schema.items) {
return { ...schema, type: "array" };
}
// type: string/numberを指定せずに、enumのみを指定している場合に type array変換する
if (schema.enum) {
const enumTypes = [
...new Set(
schema.enum
.map(value => {
if (value === null) return "null";
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return typeof value;
return undefined;
})
.filter((type): type is "string" | "number" | "boolean" | "null" => !!type),
),
];
// OpenAPI 3.1 では enum の値から複数の JSON Schema 型を推論できます。
if (enumTypes.length > 1) {
return { ...schema, type: enumTypes };
}
// 単一型 enum の既存推論は維持し、OpenAPI 3.1 で追加された異種 enum のみ type 配列へ推論します。
return { ...schema, type: "string" };
}
// type: objectを指定せずに、propertiesのみを指定している場合に type object変換する
Expand Down
1 change: 1 addition & 0 deletions src/internal/OpenApiTools/Name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const Components = {
PathItems: "PathItems",
RequestBodies: "RequestBodies",
Responses: "Responses",
MediaTypes: "MediaTypes",
} as const;

export const ComponentChild = {
Expand Down
26 changes: 26 additions & 0 deletions src/internal/OpenApiTools/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import type { CodeGenerator, OpenApi } from "../../types";
import * as TypeScriptCodeGenerator from "../TsGenerator";
import * as ConvertContext from "./ConverterContext";
import * as Headers from "./components/Headers";
import * as MediaTypes from "./components/MediaTypes";
import * as Parameters from "./components/Parameters";
import * as PathItems from "./components/PathItems";
import * as RequestBodies from "./components/RequestBodies";
import * as Responses from "./components/Responses";
import * as Schemas from "./components/Schemas";
Expand Down Expand Up @@ -46,6 +48,18 @@ export class Parser {
this.convertContext,
);
}
if (rootSchema.components.mediaTypes) {
// OpenAPI 3.2 で再利用可能な Media Type が追加されました。
MediaTypes.generateNamespace(
this.entryPoint,
this.currentPoint,
this.store,
this.factory,
rootSchema.components.mediaTypes,
toTypeNodeContext,
this.convertContext,
);
}
if (rootSchema.components.headers) {
Headers.generateNamespace(
this.entryPoint,
Expand Down Expand Up @@ -90,6 +104,18 @@ export class Parser {
this.convertContext,
);
}
if (rootSchema.components.pathItems) {
// OpenAPI 3.1 で components.pathItems に再利用可能な Path Item が追加されました。
PathItems.generateNamespace(
this.entryPoint,
this.currentPoint,
this.store,
this.factory,
rootSchema.components.pathItems,
toTypeNodeContext,
this.convertContext,
);
}
}
if (rootSchema.paths) {
Paths.generateStatements(
Expand Down
11 changes: 10 additions & 1 deletion src/internal/OpenApiTools/Walker/Definition.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
export type ComponentName = "schemas" | "headers" | "responses" | "parameters" | "requestBodies" | "securitySchemes" | "pathItems";
export type ComponentName =
| "schemas"
| "headers"
| "responses"
| "parameters"
| "requestBodies"
| "securitySchemes"
| "pathItems"
| "mediaTypes";

export const componentNames: ComponentName[] = [
"schemas",
Expand All @@ -8,4 +16,5 @@ export const componentNames: ComponentName[] = [
"requestBodies",
"securitySchemes",
"pathItems",
"mediaTypes",
];
15 changes: 11 additions & 4 deletions src/internal/OpenApiTools/Walker/Operation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CodeGenerator, OpenApi } from "../../../types";

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

export interface State {
[operationId: string]: CodeGenerator.OpenApiOperation;
Expand All @@ -19,15 +19,22 @@ export const create = (rootSchema: OpenApi.Document): State => {
const paths = rootSchema.paths || {};
const state: State = {};
Object.entries(paths).forEach(([requestUri, pathItem]) => {
httpMethodList.forEach(httpMethod => {
const operation = pathItem[httpMethod];
const pathItemData = pathItem as OpenApi.PathItem;
if (pathItemData.$ref) {
return;
}
const operations: Record<string, OpenApi.Operation | undefined> = {
...Object.fromEntries(httpMethodList.map(httpMethod => [httpMethod, pathItemData[httpMethod]])),
...pathItemData.additionalOperations,
};
Object.entries(operations).forEach(([httpMethod, operation]) => {
if (!operation) {
return;
}
if (!operation.operationId) {
return;
}
const parameters = [...(pathItem.parameters || []), ...(operation.parameters || [])] as OpenApi.Parameter[];
const parameters = [...(pathItemData.parameters || []), ...(operation.parameters || [])] as OpenApi.Parameter[];

const requestBody = operation.requestBody as OpenApi.RequestBody | undefined;
const hasValidMediaType = Object.values(requestBody?.content || {}).filter(mediaType => Object.values(mediaType).length > 0).length > 0;
Expand Down
12 changes: 11 additions & 1 deletion src/internal/OpenApiTools/Walker/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class Store {
this.state.additionalStatements = this.state.additionalStatements.concat(statements);
}
public getPathItem(localPath: string): OpenApi.PathItem {
if (!localPath.startsWith("components/pathItem")) {
if (!localPath.startsWith("components/pathItems")) {
throw new Error(`Only use start with 'component/pathItems': ${localPath}`);
}
const result = DotProp.getProperty(this.state.document, localPath.replace(/\//g, "."));
Expand All @@ -153,6 +153,16 @@ class Store {
}
return result as OpenApi.Parameter;
}
public getMediaType(localPath: string): OpenApi.MediaType | OpenApi.Reference {
if (!localPath.startsWith("components/mediaTypes")) {
throw new Error(`Only use start with 'components/mediaTypes': ${localPath}`);
}
const result = DotProp.getProperty(this.state.document, localPath.replace(/\//g, "."));
if (!result) {
throw new Error(`Not found ${localPath}`);
}
return result as OpenApi.MediaType | OpenApi.Reference;
}
public isAfterDefined(referencePath: string): boolean {
return !!DotProp.getProperty(this.state.document, referencePath.replace(/\//g, "."));
}
Expand Down
4 changes: 2 additions & 2 deletions src/internal/OpenApiTools/components/Header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const generateTypeNode = (
return factory.TypeAliasDeclaration.create({
export: true,
name: converterContext.escapeDeclarationText(name),
type: ToTypeNode.convert(entryPoint, currentPoint, factory, header.schema || { type: "null" }, context, converterContext),
type: ToTypeNode.convert(entryPoint, currentPoint, factory, header.schema ?? { type: "null" }, context, converterContext),
});
};

Expand Down Expand Up @@ -56,7 +56,7 @@ export const generatePropertySignature = (
readOnly: false,
name: converterContext.escapePropertySignatureName(name),
optional: false,
type: ToTypeNode.convert(entryPoint, currentPoint, factory, header.schema || { type: "null" }, context, converterContext),
type: ToTypeNode.convert(entryPoint, currentPoint, factory, header.schema ?? { type: "null" }, context, converterContext),
});
};

Expand Down
Loading
Loading