diff --git a/scripts/testCodeGen.ts b/scripts/testCodeGen.ts index 85254fd2..4c60c037 100644 --- a/scripts/testCodeGen.ts +++ b/scripts/testCodeGen.ts @@ -3,39 +3,57 @@ import * as fs from "fs"; import * as CodeGenerator from "../lib"; const gen = (name: string, enableValidate = true): void => { - const params: CodeGenerator.Params = { + const params: CodeGenerator.OpenApiTsCodeGen.Configuration = { entryPoint: `test/${name}/index.yml`, - enableValidate, - log: { - validator: { + codeGenerator: { + templates: { + Default: CodeGenerator.DefaultCodeTemplate.makeApiClient, + }, + }, + typeDefinitionGenerator: { + additional: { + template: "Default", + }, + }, + validator: { + openapiSchema: enableValidate, + logger: { displayLogLines: 1, }, }, }; fs.mkdirSync("test/code", { recursive: true }); - const code = CodeGenerator.generateTypeScriptCode(params); - fs.writeFileSync(`test/code/${name}.ts`, code, { encoding: "utf-8" }); + const output = CodeGenerator.make(params); + fs.writeFileSync(`test/code/${name}.ts`, output.typeDefinition.value, { encoding: "utf-8" }); console.log(`Generate Code : test/code/${name}.ts`); }; const genSyncMode = (name: string, enableValidate = true): void => { - const params: CodeGenerator.Params = { + const params: CodeGenerator.OpenApiTsCodeGen.Configuration = { entryPoint: `test/${name}/index.yml`, - enableValidate, - option: { - codeGenerator: { - sync: true, + codeGenerator: { + templates: { + Default: CodeGenerator.DefaultCodeTemplate.makeApiClient, + }, + }, + typeDefinitionGenerator: { + additional: { + template: "Default", + option: { + sync: true, + }, }, }, - log: { - validator: { + validator: { + openapiSchema: enableValidate, + logger: { displayLogLines: 1, }, }, }; fs.mkdirSync("test/code", { recursive: true }); - const code = CodeGenerator.generateTypeScriptCode(params); - fs.writeFileSync(`test/code/sync-${name}.ts`, code, { encoding: "utf-8" }); + const code = CodeGenerator.make(params); + fs.writeFileSync(`test/code/sync-${name}.ts`, code.typeDefinition.value, { encoding: "utf-8" }); console.log(`Generate Code : test/code/sync-${name}.ts`); }; diff --git a/src/CodeGenerator/index.ts b/src/CodeGenerator/index.ts index e1fda15c..04b3fa32 100644 --- a/src/CodeGenerator/index.ts +++ b/src/CodeGenerator/index.ts @@ -4,6 +4,8 @@ import { DevelopmentError } from "../Exception"; import * as Factory from "./factory"; import { CreateFunction, traverse } from "./traverse"; +export * as Utils from "./utils"; + export { CreateFunction, Factory }; export type TransformerFactory<T extends ts.Node> = ts.TransformerFactory<T>; diff --git a/src/CodeGenerator/utils.ts b/src/CodeGenerator/utils.ts new file mode 100644 index 00000000..62dfbff0 --- /dev/null +++ b/src/CodeGenerator/utils.ts @@ -0,0 +1,17 @@ +import ts from "typescript"; + +import type { CodeGenerator } from "../types"; + +export const stringToStatements = (code: string): ts.Statement[] => { + const source = ts.createSourceFile("", code, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); + return Array.from(source.statements); +}; + +export const convertIntermediateCodes = (intermediateCodes: CodeGenerator.IntermediateCode[]): ts.Statement[] => { + return intermediateCodes.reduce<ts.Statement[]>((result, intermediateCode) => { + if (typeof intermediateCode === "string") { + return [...result, ...stringToStatements(intermediateCode)]; + } + return result.concat(intermediateCode); + }, []); +}; diff --git a/src/Converter/CodeGenerator.ts b/src/Converter/CodeGenerator.ts index 75123c6a..f8481aac 100644 --- a/src/Converter/CodeGenerator.ts +++ b/src/Converter/CodeGenerator.ts @@ -2,7 +2,8 @@ import ts from "typescript"; import * as ConverterContext from "./ConverterContext"; import { Store } from "./store"; -import { CodeGeneratorParams, OpenApi, PickedParameter } from "./types"; +import type { CodeGeneratorParams, OpenApi, PickedParameter } from "./types"; +import type { CodeGenerator } from "../types"; const extractPickedParameter = (parameter: OpenApi.Parameter): PickedParameter => { return { @@ -57,7 +58,7 @@ const hasQueryParameters = (parameters?: OpenApi.Parameter[]): boolean => { return parameters.filter(parameter => parameter.in === "query").length > 0; }; -const generateCodeGeneratorParamsList = ( +export const generateCodeGeneratorParamsList = ( store: Store.Type, converterContext: ConverterContext.Types, allowOperationIds: string[] | undefined, @@ -120,25 +121,3 @@ const generateCodeGeneratorParamsList = ( return params; }; - -export interface Option { - sync: boolean; -} - -export type RewriteCodeAfterTypeDeclaration = ( - context: ts.TransformationContext, - codeGeneratorParamsList: CodeGeneratorParams[], - codeGenerateOption: Option, -) => ts.Statement[]; - -export const generateApiClientCode = ( - store: Store.Type, - context: ts.TransformationContext, - converterContext: ConverterContext.Types, - rewriteCodeAfterTypeDeclaration: RewriteCodeAfterTypeDeclaration, - allowOperationIds: string[] | undefined, - option: Option, -): void => { - const codeGeneratorParamsList = generateCodeGeneratorParamsList(store, converterContext, allowOperationIds); - store.addAdditionalStatement(rewriteCodeAfterTypeDeclaration(context, codeGeneratorParamsList, option)); -}; diff --git a/src/Converter/InferredType.ts b/src/Converter/InferredType.ts index 07570f88..052c7690 100644 --- a/src/Converter/InferredType.ts +++ b/src/Converter/InferredType.ts @@ -1,6 +1,6 @@ -import * as Types from "./types"; +import type { OpenApi } from "./types"; -export const getInferredType = (schema: Types.OpenApi.Schema): Types.OpenApi.Schema | undefined => { +export const getInferredType = (schema: OpenApi.Schema): OpenApi.Schema | undefined => { if (schema.type || schema.oneOf || schema.allOf || schema.anyOf) { return schema; } diff --git a/src/Converter/index.ts b/src/Converter/index.ts index ce83a5a1..15372bd7 100644 --- a/src/Converter/index.ts +++ b/src/Converter/index.ts @@ -1,6 +1,7 @@ import ts from "typescript"; import * as TypeScriptCodeGenerator from "../CodeGenerator"; +import { CodeGenerator as CodeGenerator2 } from "../types"; import * as CodeGenerator from "./CodeGenerator"; import * as Comment from "./Comment"; import * as Headers from "./components/Headers"; @@ -21,22 +22,18 @@ export { OpenApi, CodeGenerator, CodeGeneratorParams, PickedParameter, Name }; export interface Type { generateLeadingComment: () => string; createFunction: TypeScriptCodeGenerator.CreateFunction; - codeGeneratorOption: CodeGenerator.Option; + codeGeneratorOption: CodeGenerator2.Option; } export interface Option { - /** - * It is possible to rewrite the implementation after the type declaration. - */ - rewriteCodeAfterTypeDeclaration: CodeGenerator.RewriteCodeAfterTypeDeclaration; - /** - * - */ - codeGeneratorOption: CodeGenerator.Option; /** * List of operationId to be used */ allowOperationIds?: string[]; + + generateCodeAfterGeneratedTypeDefinition?: CodeGenerator2.GenerateFunction; + + codeGeneratorOption: CodeGenerator2.Option; } export const create = (entryPoint: string, rootSchema: OpenApi.Document, noReferenceOpenApiSchema: OpenApi.Document, option: Option): Type => { @@ -47,6 +44,7 @@ export const create = (entryPoint: string, rootSchema: OpenApi.Document, noRefer const factory = TypeScriptCodeGenerator.Factory.create(context); const store = Store.create(factory, noReferenceOpenApiSchema); const toTypeNodeContext = TypeNodeContext.create(entryPoint, store, factory, converterContext); + let extraStatements: ts.Statement[] = []; if (rootSchema.components) { if (rootSchema.components.schemas) { @@ -106,16 +104,12 @@ export const create = (entryPoint: string, rootSchema: OpenApi.Document, noRefer } if (rootSchema.paths) { Paths.generateStatements(entryPoint, currentPoint, store, factory, rootSchema.paths, toTypeNodeContext, converterContext); - CodeGenerator.generateApiClientCode( - store, - context, - converterContext, - option.rewriteCodeAfterTypeDeclaration, - option.allowOperationIds, - option.codeGeneratorOption, - ); + + const codeGeneratorParamsList = CodeGenerator.generateCodeGeneratorParamsList(store, converterContext, option.allowOperationIds); + const extraStatements2 = option.generateCodeAfterGeneratedTypeDefinition?.(context, codeGeneratorParamsList, option.codeGeneratorOption) || []; + extraStatements = extraStatements.concat(TypeScriptCodeGenerator.Utils.convertIntermediateCodes(extraStatements2)); } - return store.getRootStatements(); + return store.getRootStatements().concat(extraStatements); }; return { diff --git a/src/Converter/types/index.ts b/src/Converter/types/index.ts index ece0ca07..fdb5acac 100644 --- a/src/Converter/types/index.ts +++ b/src/Converter/types/index.ts @@ -1,5 +1,5 @@ -import { CodeGeneratorParams, PickedParameter } from "./CodeGeneratorParams"; -import * as OpenApi from "./OpenApiSchemaV3"; +import { CodeGeneratorParams, PickedParameter } from "../../types/extractSchema"; +import * as OpenApi from "../../types/OpenApi"; export { OpenApi, CodeGeneratorParams, PickedParameter }; diff --git a/src/DefaultCodeTemplate/ApiClientClass/ApiClientInterface.ts b/src/DefaultCodeTemplate/ApiClientClass/ApiClientInterface.ts index 236ddd26..21b9a6f1 100644 --- a/src/DefaultCodeTemplate/ApiClientClass/ApiClientInterface.ts +++ b/src/DefaultCodeTemplate/ApiClientClass/ApiClientInterface.ts @@ -1,7 +1,8 @@ import ts from "typescript"; import { Factory } from "../../CodeGenerator"; -import type { CodeGenerator, CodeGeneratorParams } from "../../Converter"; +import type { CodeGenerator } from "../../types"; +import type { CodeGeneratorParams } from "../../types/extractSchema"; const httpMethodList: string[] = ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"]; diff --git a/src/DefaultCodeTemplate/ApiClientClass/Method.ts b/src/DefaultCodeTemplate/ApiClientClass/Method.ts index 144d4242..03d319cf 100644 --- a/src/DefaultCodeTemplate/ApiClientClass/Method.ts +++ b/src/DefaultCodeTemplate/ApiClientClass/Method.ts @@ -1,7 +1,8 @@ import ts from "typescript"; import { Factory } from "../../CodeGenerator"; -import type { CodeGenerator, CodeGeneratorParams } from "../../Converter"; +import type { CodeGenerator } from "../../types"; +import type { CodeGeneratorParams } from "../../types/extractSchema"; import * as MethodBody from "./MethodBody"; export { MethodBody }; diff --git a/src/DefaultCodeTemplate/ApiClientClass/index.ts b/src/DefaultCodeTemplate/ApiClientClass/index.ts index 892a76a2..9504f6f0 100644 --- a/src/DefaultCodeTemplate/ApiClientClass/index.ts +++ b/src/DefaultCodeTemplate/ApiClientClass/index.ts @@ -1,7 +1,8 @@ import ts from "typescript"; import { Factory } from "../../CodeGenerator"; -import type { CodeGenerator, CodeGeneratorParams } from "../../Converter"; +import type { CodeGenerator } from "../../types"; +import type { CodeGeneratorParams } from "../../types/extractSchema"; import * as ApiClientInterface from "./ApiClientInterface"; import * as Class from "./Class"; import * as Constructor from "./Constructor"; diff --git a/src/DefaultCodeTemplate/index.ts b/src/DefaultCodeTemplate/index.ts index 7201cd02..148826fb 100644 --- a/src/DefaultCodeTemplate/index.ts +++ b/src/DefaultCodeTemplate/index.ts @@ -1,14 +1,15 @@ import ts from "typescript"; import * as TypeScriptCodeGenerator from "../CodeGenerator"; -import type * as Converter from "../Converter"; +import type { CodeGenerator } from "../types"; +import type { CodeGeneratorParams } from "../types/extractSchema"; import * as ApiClientArgument from "./ApiClientArgument"; import * as ApiClientClass from "./ApiClientClass"; -export const rewriteCodeAfterTypeDeclaration: Converter.CodeGenerator.RewriteCodeAfterTypeDeclaration = ( +export const makeApiClient: CodeGenerator.GenerateFunction = ( context: ts.TransformationContext, - codeGeneratorParamsList: Converter.CodeGeneratorParams[], - option: Converter.CodeGenerator.Option, + codeGeneratorParamsList: CodeGeneratorParams[], + option: CodeGenerator.Option, ): ts.Statement[] => { const statements: ts.Statement[] = []; const factory = TypeScriptCodeGenerator.Factory.create(context); diff --git a/src/Validator/index.ts b/src/Validator/index.ts index 4bda68b7..91efb504 100644 --- a/src/Validator/index.ts +++ b/src/Validator/index.ts @@ -6,18 +6,11 @@ import * as Ajv from "ajv"; import type { OpenApi } from "../Converter/types"; import openapiSchema from "./openapi.json"; +import type { Validator } from "../types"; -export interface LogOption { - /** - * default: undefined (all logs) - * Number of lines displayed in the latest log - */ - displayLogLines?: number; -} - -const showLogs = (logs: any[], option?: LogOption) => { - if (option && option.displayLogLines && option.displayLogLines > 0) { - const latestLogs = logs.slice(0, option.displayLogLines); +const showLogs = (logs: any[], logger?: Validator.Logger) => { + if (logger && logger.displayLogLines && logger.displayLogLines > 0) { + const latestLogs = logs.slice(0, logger.displayLogLines); const moreLogNum = logs.length - latestLogs.length; console.error("Correct the validation error before generating the code."); console.error(`There are a total of ${logs.length} errors below.`); @@ -37,12 +30,12 @@ const showLogs = (logs: any[], option?: LogOption) => { } }; -export const validate = (openapiDoc: OpenApi.Document, option?: LogOption): void => { +export const validate = (openapiDoc: OpenApi.Document, logger?: Validator.Logger): void => { const ajv = new Ajv.default({ allErrors: true }); const validate = ajv.compile(openapiSchema); validate(openapiDoc); if (validate.errors) { - showLogs(validate.errors, option); + showLogs(validate.errors, logger); throw new Error("Validation Error"); } }; diff --git a/src/index.ts b/src/index.ts index c5a744c2..592888e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,67 +1,39 @@ import { EOL } from "os"; -import * as TypeScriptCodeGenerator from "./CodeGenerator"; -import * as Converter from "./Converter"; -import * as DefaultCodeTemplate from "./DefaultCodeTemplate"; +import * as TsGenerator from "./CodeGenerator"; +import * as Transformer from "./Converter"; import { fileSystem } from "./FileSystem"; import * as ResolveReference from "./ResolveReference"; +import type { OpenApiTsCodeGen } from "./types"; import * as Validator from "./Validator"; +import * as DefaultCodeTemplate from "./DefaultCodeTemplate"; -export { Converter }; +export { Transformer, OpenApiTsCodeGen, DefaultCodeTemplate }; -export interface Params { - entryPoint: string; - option?: { - rewriteCodeAfterTypeDeclaration?: Converter.CodeGenerator.RewriteCodeAfterTypeDeclaration; - codeGenerator?: { - /** default false */ - sync?: boolean; - }; - }; - /** default: true */ - enableValidate?: boolean; - log?: { - validator?: { - /** - * default: undefined (all logs) - * Number of lines displayed in the latest log - */ - displayLogLines?: number; - }; - }; - filter?: { - allowOperationIds?: string[]; - }; -} +export const make = (config: OpenApiTsCodeGen.Configuration): OpenApiTsCodeGen.Output => { + const schema = fileSystem.loadJsonOrYaml(config.entryPoint); + const resolvedReferenceDocument = ResolveReference.resolve(config.entryPoint, config.entryPoint, JSON.parse(JSON.stringify(schema))); -const generateConvertOption = (filter: Params["filter"] = {}, option?: Params["option"]): Converter.Option => { - if (option) { - return { - rewriteCodeAfterTypeDeclaration: option.rewriteCodeAfterTypeDeclaration || DefaultCodeTemplate.rewriteCodeAfterTypeDeclaration, - allowOperationIds: filter.allowOperationIds, - codeGeneratorOption: { - sync: option.codeGenerator ? !!option.codeGenerator.sync : false, - }, - }; + if (!config.validator) { + Validator.validate(resolvedReferenceDocument); + } else { + if (config.validator.openapiSchema) { + Validator.validate(resolvedReferenceDocument, config.validator.logger); + } } - return { - rewriteCodeAfterTypeDeclaration: DefaultCodeTemplate.rewriteCodeAfterTypeDeclaration, - allowOperationIds: filter.allowOperationIds, - codeGeneratorOption: { - sync: false, - }, - }; -}; -export const generateTypeScriptCode = ({ entryPoint, option, enableValidate = true, log, filter = {} }: Params): string => { - const schema = fileSystem.loadJsonOrYaml(entryPoint); - const resolvedReferenceDocument = ResolveReference.resolve(entryPoint, entryPoint, JSON.parse(JSON.stringify(schema))); + const templateName = config.typeDefinitionGenerator?.additional?.template; - if (enableValidate) { - Validator.validate(resolvedReferenceDocument, log && log.validator); - } + const { createFunction, generateLeadingComment } = Transformer.create(config.entryPoint, schema, resolvedReferenceDocument, { + allowOperationIds: config.openApiSchemaParser?.allowOperationIds, + codeGeneratorOption: config.typeDefinitionGenerator?.additional?.option || {}, + generateCodeAfterGeneratedTypeDefinition: templateName ? config.codeGenerator?.templates?.[templateName] : undefined + }); - const convertOption = generateConvertOption(filter, option); - const { createFunction, generateLeadingComment } = Converter.create(entryPoint, schema, resolvedReferenceDocument, convertOption); - return [generateLeadingComment(), TypeScriptCodeGenerator.generate(createFunction)].join(EOL + EOL + EOL); + return { + typeDefinition: { + value: [generateLeadingComment(), TsGenerator.generate(createFunction)].join(EOL + EOL + EOL), + }, + additionalCodes: {}, + }; }; diff --git a/src/Converter/types/OpenApiSchemaV3.ts b/src/types/OpenApi.ts similarity index 100% rename from src/Converter/types/OpenApiSchemaV3.ts rename to src/types/OpenApi.ts diff --git a/src/Converter/types/CodeGeneratorParams.ts b/src/types/extractSchema.ts similarity index 97% rename from src/Converter/types/CodeGeneratorParams.ts rename to src/types/extractSchema.ts index 09b68dba..59acf585 100644 --- a/src/Converter/types/CodeGeneratorParams.ts +++ b/src/types/extractSchema.ts @@ -1,4 +1,4 @@ -import * as OpenApi from "./OpenApiSchemaV3"; +import type * as OpenApi from "./OpenApi"; export type PickedParameter = Pick<OpenApi.Parameter, "name" | "in" | "required" | "style" | "explode">; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..cbc0489b --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,90 @@ +import type ts from "typescript"; + +import type { CodeGeneratorParams } from "./extractSchema"; + +export namespace Validator { + export interface Logger { + /** + * default: undefined (all logs) + * Number of lines displayed in the latest log + */ + displayLogLines?: number; + } + export interface Configuration { + openapiSchema?: boolean; + logger?: Logger; + } +} + +export namespace OpenApiSchemaParser { + export interface Configuration { + allowOperationIds?: string[]; + } +} + +export namespace CodeGenerator { + /** + * The parameters specified here will be passed directly to the Code Generate function. + */ + export interface Option { + sync?: boolean; + } + + /** + * Used to further transform the code created by the specified Generator Template. + */ + export type IntermediateCode = string | ts.Statement; + + export type GenerateFunction = (context: ts.TransformationContext, payload: CodeGeneratorParams[], option: Option) => IntermediateCode[]; + + export interface OutputConfiguration { + /** + * Template Name + */ + template: string; + /** + * Code generatorOption + */ + option?: Option; + /** + * + */ + transform?: (params: IntermediateCode) => IntermediateCode[]; + } + + export interface Configuration { + /** + * Output files + */ + outputs?: Record<string, OutputConfiguration>; + /** + * Register template + */ + templates?: Record<string, GenerateFunction>; + } +} + +export namespace TypeDefinitionGenerator { + export interface Configuration { + additional?: CodeGenerator.OutputConfiguration; + } +} + +export namespace OpenApiTsCodeGen { + export interface Configuration { + entryPoint: string; + typeDefinitionGenerator?: TypeDefinitionGenerator.Configuration; + validator?: Validator.Configuration; + openApiSchemaParser?: OpenApiSchemaParser.Configuration; + codeGenerator?: CodeGenerator.Configuration; + } + + export interface GeneratedCode { + value: string; + } + + export interface Output { + typeDefinition: GeneratedCode; + additionalCodes: Record<string, GeneratedCode>; + } +} diff --git a/tsconfig.json b/tsconfig.json index 2927f0f8..64fdcb81 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,13 +5,13 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "jsx": "react", - "module": "esnext", + "module": "commonjs", "moduleResolution": "node", "noFallthroughCasesInSwitch": true, "noUnusedParameters": false, "skipLibCheck": true, "sourceMap": true, - "target": "esnext", + "target": "es2019", "lib": ["dom", "es2019"], "rootDir": ".", "outDir": "lib",