diff --git a/scripts/testCodeGen.ts b/scripts/testCodeGen.ts index 8cf8b1b5..35b0a958 100644 --- a/scripts/testCodeGen.ts +++ b/scripts/testCodeGen.ts @@ -118,10 +118,16 @@ const main = () => { sync: false, }); + generateTypedefWithTemplateCode("test/ref.access/index.yml", "test/code/typedef-with-template/ref-access.ts", false, { + sync: false, + }); + generateSplitCode("test/api.test.domain/index.yml", "test/code/split"); generateParameter("test/api.test.domain/index.yml", "test/code/parameter/api.test.domain.json"); generateParameter("test/infer.domain/index.yml", "test/code/parameter/infer.domain.json"); + + }; main(); diff --git a/src/internal/OpenApiTools/Parser.ts b/src/internal/OpenApiTools/Parser.ts index aebd3d39..0e2b92ca 100644 --- a/src/internal/OpenApiTools/Parser.ts +++ b/src/internal/OpenApiTools/Parser.ts @@ -18,7 +18,7 @@ export class Parser { private convertContext: ConvertContext.Types; private store: Store; private factory: TypeScriptCodeGenerator.Factory.Type; - constructor(private entryPoint: string, private rootSchema: OpenApi.Document, noReferenceOpenApiSchema: OpenApi.Document) { + constructor(private entryPoint: string, private readonly rootSchema: OpenApi.Document, noReferenceOpenApiSchema: OpenApi.Document) { this.currentPoint = entryPoint; this.convertContext = ConvertContext.create(); this.factory = TypeScriptCodeGenerator.Factory.create(); @@ -27,7 +27,7 @@ export class Parser { } private initialize(): void { - const toTypeNodeContext = TypeNodeContext.create(this.entryPoint, this.store, this.factory, this.convertContext); + const toTypeNodeContext = TypeNodeContext.create(this.entryPoint, this.rootSchema, this.store, this.factory, this.convertContext); const rootSchema = this.rootSchema; if (rootSchema.components) { if (rootSchema.components.schemas) { diff --git a/src/internal/OpenApiTools/TypeNodeContext.ts b/src/internal/OpenApiTools/TypeNodeContext.ts index 0f7bfdb6..f0bdaf66 100644 --- a/src/internal/OpenApiTools/TypeNodeContext.ts +++ b/src/internal/OpenApiTools/TypeNodeContext.ts @@ -2,6 +2,7 @@ import * as Path from "path"; import ts from "typescript"; +import type { OpenApi } from "../../types"; import { DevelopmentError } from "../Exception"; import * as TypeScriptCodeGenerator from "../TsGenerator"; import * as ConverterContext from "./ConverterContext"; @@ -66,15 +67,19 @@ const calculateReferencePath = (store: Walker.Store, base: string, pathArray: st if (names.length === 0) { throw new DevelopmentError("Local Reference Error \n" + JSON.stringify({ pathArray, names, base }, null, 2)); } + const maybeResolvedNameFragments = names.concat(unresolvedPaths).map(converterContext.escapeDeclarationText); return { name: names.map(converterContext.escapeDeclarationText).join("."), - maybeResolvedName: names.concat(unresolvedPaths).map(converterContext.escapeDeclarationText).join("."), + maybeResolvedName: maybeResolvedNameFragments.join("."), unresolvedPaths, + depth: maybeResolvedNameFragments.length, + pathArray, }; }; export const create = ( entryPoint: string, + rootSchema: OpenApi.Document, store: Walker.Store, factory: TypeScriptCodeGenerator.Factory.Type, converterContext: ConverterContext.Types, @@ -94,6 +99,7 @@ export const create = ( factory, reference.data, { + rootSchema, setReferenceHandler, resolveReferencePath, }, @@ -119,6 +125,7 @@ export const create = ( factory, reference.data, { + rootSchema, setReferenceHandler, resolveReferencePath, }, @@ -149,5 +156,5 @@ export const create = ( } } }; - return { setReferenceHandler: setReferenceHandler, resolveReferencePath: resolveReferencePath }; + return { rootSchema, setReferenceHandler: setReferenceHandler, resolveReferencePath: resolveReferencePath }; }; diff --git a/src/internal/OpenApiTools/components/Schemas.ts b/src/internal/OpenApiTools/components/Schemas.ts index e50e2d50..e67b4e3f 100644 --- a/src/internal/OpenApiTools/components/Schemas.ts +++ b/src/internal/OpenApiTools/components/Schemas.ts @@ -1,3 +1,5 @@ +import DotProp from "dot-prop"; + import type { OpenApi } from "../../../types"; import { UnSupportError } from "../../Exception"; import { Factory } from "../../TsGenerator"; @@ -45,16 +47,27 @@ export const generateNamespace = ( const schema = targetSchema; const reference = Reference.generate(entryPoint, currentPoint, schema); if (reference.type === "local") { - const { maybeResolvedName } = context.resolveReferencePath(currentPoint, reference.path); + const { maybeResolvedName, depth, pathArray } = context.resolveReferencePath(currentPoint, reference.path); + // console.log({ + // depth, + // pathArray, + // }); + const createTypeNode = () => { + if (depth === 2) { + return factory.TypeReferenceNode.create({ + name: convertContext.escapeReferenceDeclarationText(maybeResolvedName), + }); + } + const schema = DotProp.get(context.rootSchema, pathArray.join(".")) as any; + return ToTypeNode.convert(entryPoint, currentPoint, factory, schema, context, convertContext, { parent: schema }); + }; store.addStatement(`${basePath}/${name}`, { kind: "typeAlias", name: convertContext.escapeDeclarationText(name), value: factory.TypeAliasDeclaration.create({ export: true, name: convertContext.escapeDeclarationText(name), - type: factory.TypeReferenceNode.create({ - name: convertContext.escapeReferenceDeclarationText(maybeResolvedName), - }), + type: createTypeNode(), }), }); return; diff --git a/src/internal/OpenApiTools/toTypeNode.ts b/src/internal/OpenApiTools/toTypeNode.ts index 1b6fdfb3..9a023781 100644 --- a/src/internal/OpenApiTools/toTypeNode.ts +++ b/src/internal/OpenApiTools/toTypeNode.ts @@ -1,3 +1,4 @@ +import DotProp from "dot-prop"; import ts from "typescript"; import type { OpenApi } from "../../types"; @@ -15,9 +16,16 @@ export interface ResolveReferencePath { name: string; maybeResolvedName: string; unresolvedPaths: string[]; + /** + * @example components.a.b.c.dの場合 ["a", "b", "c", "d"].length = 4 + **/ + depth: number; + /** 入力$refを分解したモノ(#は除く) */ + pathArray: string[]; } export interface Context { + readonly rootSchema: OpenApi.Document; setReferenceHandler: (currentPoint: string, reference: Reference.Type) => void; resolveReferencePath: (currentPoint: string, referencePath: string) => ResolveReferencePath; } @@ -96,8 +104,13 @@ export const convert: Convert = ( if (reference.type === "local") { // Type Aliasを作成 (or すでにある場合は作成しない) context.setReferenceHandler(currentPoint, reference); - const { maybeResolvedName } = context.resolveReferencePath(currentPoint, reference.path); - return factory.TypeReferenceNode.create({ name: converterContext.escapeReferenceDeclarationText(maybeResolvedName) }); + const { maybeResolvedName, depth, pathArray } = context.resolveReferencePath(currentPoint, reference.path); + if (depth === 2) { + return factory.TypeReferenceNode.create({ name: converterContext.escapeReferenceDeclarationText(maybeResolvedName) }); + } else { + const resolveSchema = DotProp.get(context.rootSchema, pathArray.join(".")) as any; + return convert(entryPoint, currentPoint, factory, resolveSchema, context, converterContext, { parent: schema }); + } } // サポートしているディレクトリに対して存在する場合 if (reference.componentName) { diff --git a/test/__tests__/__snapshots__/typedef-with-template-test.ts.snap b/test/__tests__/__snapshots__/typedef-with-template-test.ts.snap index 31b8eee4..269e43fe 100644 --- a/test/__tests__/__snapshots__/typedef-with-template-test.ts.snap +++ b/test/__tests__/__snapshots__/typedef-with-template-test.ts.snap @@ -2986,3 +2986,140 @@ export class Client { } " `; + +exports[`Typedef with template ref-access 1`] = ` +"// +// Generated by @himenon/openapi-typescript-code-generator +// +// OpenApi : 3.1.0 +// +// License : MIT +// + + +export namespace Schemas { + export interface Book { + author?: { + name?: string; + age?: string; + }; + publisher?: { + name?: any; + address?: string; + }; + metadata: { + description: string; + }; + } + export type Author = { + name?: string; + age?: string; + }; + export type Publisher = { + name?: any; + address?: string; + }; +} +export interface Parameter$getBook { + /** Book ID */ + id: string; +} +export interface Response$getBook$Status$200 { + \\"application/json\\": Schemas.Book; +} +export interface Parameter$getDescription { + /** Book ID */ + id: string; +} +export interface Response$getDescription$Status$200 { + \\"application/json\\": string; +} +export interface Parameter$getAuthor { + /** Author Id */ + id: string; +} +export interface Response$getAuthor$Status$200 { + \\"application/json\\": { + name?: string; + age?: string; + }; +} +export interface Parameter$getPublisher { + /** Publisher ID */ + id: string; +} +export interface Response$getPublisher$Status$200 { + \\"application/json\\": Schemas.Publisher; +} +export type ResponseContentType$getBook = keyof Response$getBook$Status$200; +export interface Params$getBook { + parameter: Parameter$getBook; +} +export type ResponseContentType$getDescription = keyof Response$getDescription$Status$200; +export interface Params$getDescription { + parameter: Parameter$getDescription; +} +export type ResponseContentType$getAuthor = keyof Response$getAuthor$Status$200; +export interface Params$getAuthor { + parameter: Parameter$getAuthor; +} +export type ResponseContentType$getPublisher = keyof Response$getPublisher$Status$200; +export interface Params$getPublisher { + parameter: Parameter$getPublisher; +} +export type HttpMethod = \\"GET\\" | \\"PUT\\" | \\"POST\\" | \\"DELETE\\" | \\"OPTIONS\\" | \\"HEAD\\" | \\"PATCH\\" | \\"TRACE\\"; +export interface ObjectLike { + [key: string]: any; +} +export interface QueryParameter { + value: any; + style?: \\"form\\" | \\"spaceDelimited\\" | \\"pipeDelimited\\" | \\"deepObject\\"; + explode: boolean; +} +export interface QueryParameters { + [key: string]: QueryParameter; +} +export type SuccessResponses = Response$getBook$Status$200 | Response$getDescription$Status$200 | Response$getAuthor$Status$200 | Response$getPublisher$Status$200; +export namespace ErrorResponse { + export type getBook = void; + export type getDescription = void; + export type getAuthor = void; + export type getPublisher = void; +} +export interface ApiClient { + request: (httpMethod: HttpMethod, url: string, headers: ObjectLike | any, requestBody: ObjectLike | any, queryParameters: QueryParameters | undefined, options?: RequestOption) => Promise; +} +export class Client { + private baseUrl: string; + constructor(private apiClient: ApiClient, baseUrl: string) { this.baseUrl = baseUrl.replace(/\\\\/$/, \\"\\"); } + public async getBook(params: Params$getBook, option?: RequestOption): Promise { + const url = this.baseUrl + \`/get/book/\${params.parameter.id}\`; + const headers = { + Accept: \\"application/json\\" + }; + return this.apiClient.request(\\"GET\\", url, headers, undefined, undefined, option); + } + public async getDescription(params: Params$getDescription, option?: RequestOption): Promise { + const url = this.baseUrl + \`/get/book/\${params.parameter.id}/description\`; + const headers = { + Accept: \\"application/json\\" + }; + return this.apiClient.request(\\"GET\\", url, headers, undefined, undefined, option); + } + public async getAuthor(params: Params$getAuthor, option?: RequestOption): Promise { + const url = this.baseUrl + \`/get/author/\${params.parameter.id}\`; + const headers = { + Accept: \\"application/json\\" + }; + return this.apiClient.request(\\"GET\\", url, headers, undefined, undefined, option); + } + public async getPublisher(params: Params$getPublisher, option?: RequestOption): Promise { + const url = this.baseUrl + \`/get/publisher/\${params.parameter.id}\`; + const headers = { + Accept: \\"application/json\\" + }; + return this.apiClient.request(\\"GET\\", url, headers, undefined, undefined, option); + } +} +" +`; diff --git a/test/__tests__/typedef-with-template-test.ts b/test/__tests__/typedef-with-template-test.ts index dda84844..45038419 100644 --- a/test/__tests__/typedef-with-template-test.ts +++ b/test/__tests__/typedef-with-template-test.ts @@ -29,4 +29,9 @@ describe("Typedef with template", () => { const text = Utils.replaceVersionInfo(generateCode); expect(text).toMatchSnapshot(); }); + test("ref-access", () => { + const generateCode = fs.readFileSync(path.join(__dirname, "../code/typedef-with-template/ref-access.ts"), { encoding: "utf-8" }); + const text = Utils.replaceVersionInfo(generateCode); + expect(text).toMatchSnapshot(); + }); }); diff --git a/test/ref.access/index.yml b/test/ref.access/index.yml new file mode 100644 index 00000000..f8898af3 --- /dev/null +++ b/test/ref.access/index.yml @@ -0,0 +1,124 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: ref.access + description: Library test schema + license: + name: MIT + +servers: + - url: "http://dev.ref.access/" + description: Development Environment + - url: "https://ref.access/" + description: Production Environment + +tags: + - name: test + +components: + schemas: + Book: + type: object + required: + - metadata + properties: + author: + type: object + properties: + name: + type: string + age: + type: string + publisher: + type: object + properties: + name: + type: String + address: + type: string + metadata: + type: object + required: + - description + properties: + description: + type: string + Author: + $ref: "#/components/schemas/Book/properties/author" + Publisher: + $ref: "#/components/schemas/Book/properties/publisher" + +paths: + /get/book/{id}: + parameters: + - name: id + in: path + required: true + description: Book ID + schema: + type: string + format: uuid + get: + operationId: getBook + responses: + 200: + description: Get Books + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + /get/book/{id}/description: + parameters: + - name: id + in: path + required: true + description: Book ID + schema: + type: string + format: uuid + get: + operationId: getDescription + responses: + 200: + description: Get Book Description + content: + application/json: + schema: + $ref: "#/components/schemas/Book/properties/metadata/properties/description" + + /get/author/{id}: + parameters: + - name: id + in: path + required: true + description: Author Id + schema: + type: string + format: uuid + get: + operationId: getAuthor + responses: + 200: + description: Get Author + content: + application/json: + schema: + $ref: "#/components/schemas/Book/properties/author" + /get/publisher/{id}: + parameters: + - name: id + in: path + required: true + description: Publisher ID + schema: + type: string + format: uuid + get: + operationId: getPublisher + responses: + 200: + description: Get Publisher + content: + application/json: + schema: + $ref: "#/components/schemas/Publisher"