From 63eff084f73ef6c3fadbc4887348c488c4a09104 Mon Sep 17 00:00:00 2001 From: Himenon Date: Fri, 15 Jan 2021 14:43:20 +0900 Subject: [PATCH] fix: unresolved reference close #7 --- src/Converter/v3/Context.ts | 53 ++++++++++++++----- src/Converter/v3/components/Header.ts | 6 +-- src/Converter/v3/components/Operation.ts | 12 ++--- src/Converter/v3/components/Parameter.ts | 4 +- src/Converter/v3/components/Parameters.ts | 2 +- src/Converter/v3/components/Response.ts | 2 +- src/Converter/v3/components/Responses.ts | 7 +-- src/Converter/v3/components/Schemas.ts | 6 +-- src/Converter/v3/store/Store.ts | 10 +++- src/Converter/v3/toTypeNode.ts | 18 ++++--- .../__snapshots__/snapshot-test.ts.snap | 12 +++++ test/api.test.domain/index.yml | 22 ++++++++ 12 files changed, 113 insertions(+), 41 deletions(-) diff --git a/src/Converter/v3/Context.ts b/src/Converter/v3/Context.ts index 4968b352..24a8a8dc 100644 --- a/src/Converter/v3/Context.ts +++ b/src/Converter/v3/Context.ts @@ -3,7 +3,7 @@ import * as Path from "path"; import ts from "typescript"; import * as TypeScriptCodeGenerator from "../../CodeGenerator"; -import { DevelopmentError, NotFoundReference } from "../../Exception"; +import { DevelopmentError } from "../../Exception"; import { Store } from "./store"; import * as ToTypeNode from "./toTypeNode"; @@ -24,8 +24,9 @@ const generatePath = (entryPoint: string, currentPoint: string, referencePath: s }; }; -const generateName = (store: Store.Type, base: string, pathArray: string[]): string => { +const calculateReferencePath = (store: Store.Type, base: string, pathArray: string[]): ToTypeNode.ResolveReferencePath => { let names: string[] = []; + let unresolvedPaths: string[] = []; pathArray.reduce((previous, lastPath, index) => { const current = Path.join(previous, lastPath); // ディレクトリが深い場合は相対パスが`..`を繰り返す可能性があり、 @@ -33,22 +34,30 @@ const generateName = (store: Store.Type, base: string, pathArray: string[]): str if (lastPath === ".." && names.length > 0) { names = names.slice(0, names.length - 1); } - const isLast = index === pathArray.length - 1; - if (isLast) { + const isFinalPath = index === pathArray.length - 1; + if (isFinalPath) { const statement = store.getStatement(current, "interface"); const statement2 = store.getStatement(current, "typeAlias"); const statement3 = store.getStatement(current, "namespace"); if (statement) { names.push(statement.name); + return current; } else if (statement2) { names.push(statement2.name); + return current; } else if (statement3) { names.push(statement3.name); + return current; + } else { + unresolvedPaths.push(lastPath); } } else { const statement = store.getStatement(current, "namespace"); if (statement) { - names.push(statement.value.name.text); + unresolvedPaths = unresolvedPaths.slice(0, unresolvedPaths.length - 1); + names.push(statement.name); + } else { + unresolvedPaths.push(lastPath); } } return current; @@ -56,22 +65,26 @@ const generateName = (store: Store.Type, base: string, pathArray: string[]): str if (names.length === 0) { throw new DevelopmentError("Local Reference Error \n" + JSON.stringify({ pathArray, names, base }, null, 2)); } - return names.join("."); + return { + name: names.join("."), + maybeResolvedName: names.concat(unresolvedPaths).join("."), + unresolvedPaths, + }; }; export const create = (entryPoint: string, store: Store.Type, factory: TypeScriptCodeGenerator.Factory.Type): ToTypeNode.Context => { - const getReferenceName: ToTypeNode.Context["getReferenceName"] = (currentPoint, referencePath): string => { + const resolveReferencePath: ToTypeNode.Context["resolveReferencePath"] = (currentPoint, referencePath) => { const { pathArray, base } = generatePath(entryPoint, currentPoint, referencePath); - return generateName(store, base, pathArray); + return calculateReferencePath(store, base, pathArray); }; - const setReferenceHandler: ToTypeNode.Context["setReferenceHandler"] = reference => { + const setReferenceHandler: ToTypeNode.Context["setReferenceHandler"] = (currentPoint, reference) => { if (store.hasStatement(reference.path, ["interface", "typeAlias"])) { return; } if (reference.type === "remote") { const typeNode = ToTypeNode.convert(entryPoint, reference.referencePoint, factory, reference.data, { setReferenceHandler, - getReferenceName, + resolveReferencePath, }); if (ts.isTypeLiteralNode(typeNode)) { store.addStatement(reference.path, { @@ -89,7 +102,7 @@ export const create = (entryPoint: string, store: Store.Type, factory: TypeScrip name: reference.name, type: ToTypeNode.convert(entryPoint, reference.referencePoint, factory, reference.data, { setReferenceHandler, - getReferenceName, + resolveReferencePath, }), }); store.addStatement(reference.path, { @@ -99,10 +112,22 @@ export const create = (entryPoint: string, store: Store.Type, factory: TypeScrip }); } } else if (reference.type === "local") { - if (!store.hasStatement(reference.path, ["namespace", "interface", "typeAlias"])) { - throw new NotFoundReference(`The schema ${reference.name} is undefined in "${reference.path}".`); + if (!store.isAfterDefined(reference.path)) { + const { maybeResolvedName } = resolveReferencePath(currentPoint, reference.path); + const value = factory.TypeAliasDeclaration.create({ + export: true, + name: reference.name, + type: factory.TypeReferenceNode.create({ + name: maybeResolvedName, + }), + }); + store.addStatement(reference.path, { + name: reference.name, + type: "typeAlias", + value, + }); } } }; - return { setReferenceHandler: setReferenceHandler, getReferenceName }; + return { setReferenceHandler: setReferenceHandler, resolveReferencePath: resolveReferencePath }; }; diff --git a/src/Converter/v3/components/Header.ts b/src/Converter/v3/components/Header.ts index 187e354e..816494db 100644 --- a/src/Converter/v3/components/Header.ts +++ b/src/Converter/v3/components/Header.ts @@ -33,12 +33,12 @@ export const generatePropertySignature = ( if (Guard.isReference(header)) { const reference = Reference.generate(entryPoint, currentPoint, header); if (reference.type === "local") { - context.setReferenceHandler(reference); + context.setReferenceHandler(currentPoint, reference); return factory.PropertySignature.create({ name, optional: false, type: factory.TypeReferenceNode.create({ - name: context.getReferenceName(currentPoint, reference.path), + name: context.resolveReferencePath(currentPoint, reference.path).name, }), }); } @@ -46,7 +46,7 @@ export const generatePropertySignature = ( name, optional: false, type: factory.TypeReferenceNode.create({ - name: context.getReferenceName(currentPoint, reference.path), + name: context.resolveReferencePath(currentPoint, reference.path).name, }), }); } diff --git a/src/Converter/v3/components/Operation.ts b/src/Converter/v3/components/Operation.ts index 026cd913..039e655f 100644 --- a/src/Converter/v3/components/Operation.ts +++ b/src/Converter/v3/components/Operation.ts @@ -72,9 +72,9 @@ export const generateNamespace = ( if (Guard.isReference(operation.requestBody)) { const reference = Reference.generate(entryPoint, currentPoint, operation.requestBody); if (reference.type === "local") { - context.setReferenceHandler(reference); + context.setReferenceHandler(currentPoint, reference); // TODO (not-use) 追加する必要がある(このメソッドを使わない可能性あり) - factory.TypeReferenceNode.create({ name: context.getReferenceName(currentPoint, reference.path) }); + factory.TypeReferenceNode.create({ name: context.resolveReferencePath(currentPoint, reference.path).name }); } else if (reference.type === "remote" && reference.componentName) { const contentPath = path.join(reference.path, "Content"); // requestBodyはNamespaceを形成するため const name = "Content"; @@ -83,7 +83,7 @@ export const generateNamespace = ( name: name, value: RequestBody.generateInterface(entryPoint, reference.referencePoint, factory, name, reference.data, context), }); - const typeAliasName = context.getReferenceName(currentPoint, contentPath); + const typeAliasName = context.resolveReferencePath(currentPoint, contentPath).name; store.addStatement(`${basePath}/RequestBody`, { type: "typeAlias", name: typeAliasName, @@ -129,13 +129,13 @@ export const generateStatements = ( if (Guard.isReference(operation.requestBody)) { const reference = Reference.generate(entryPoint, currentPoint, operation.requestBody); if (reference.type === "local") { - context.setReferenceHandler(reference); + context.setReferenceHandler(currentPoint, reference); statements.push( factory.TypeAliasDeclaration.create({ export: true, name: Name.requestBodyName(operationId), type: factory.TypeReferenceNode.create({ - name: context.getReferenceName(currentPoint, `${reference.path}`) + "." + Name.ComponentChild.Content, // TODO Contextから作成? + name: context.resolveReferencePath(currentPoint, `${reference.path}`) + "." + Name.ComponentChild.Content, // TODO Contextから作成? }), }), ); @@ -151,7 +151,7 @@ export const generateStatements = ( factory.TypeAliasDeclaration.create({ export: true, name: requestBodyName, - type: factory.TypeReferenceNode.create({ name: context.getReferenceName(currentPoint, contentPath) }), + type: factory.TypeReferenceNode.create({ name: context.resolveReferencePath(currentPoint, contentPath).name }), }), ); diff --git a/src/Converter/v3/components/Parameter.ts b/src/Converter/v3/components/Parameter.ts index bbb5ce9f..9e71be3a 100644 --- a/src/Converter/v3/components/Parameter.ts +++ b/src/Converter/v3/components/Parameter.ts @@ -43,12 +43,12 @@ export const generatePropertySignature = ( if (Guard.isReference(parameter)) { const reference = Reference.generate(entryPoint, currentPoint, parameter); if (reference.type === "local") { - context.setReferenceHandler(reference); + context.setReferenceHandler(currentPoint, reference); return factory.PropertySignature.create({ name: reference.name, optional: false, type: factory.TypeReferenceNode.create({ - name: context.getReferenceName(currentPoint, reference.path), + name: context.resolveReferencePath(currentPoint, reference.path).name, }), }); } diff --git a/src/Converter/v3/components/Parameters.ts b/src/Converter/v3/components/Parameters.ts index 8a4a7b25..5e1e4bbf 100644 --- a/src/Converter/v3/components/Parameters.ts +++ b/src/Converter/v3/components/Parameters.ts @@ -47,7 +47,7 @@ export const generateNamespace = ( export: true, name: name, type: factory.TypeReferenceNode.create({ - name: context.getReferenceName(currentPoint, reference.path), + name: context.resolveReferencePath(currentPoint, reference.path).name, }), }), }); diff --git a/src/Converter/v3/components/Response.ts b/src/Converter/v3/components/Response.ts index 1e74690a..99d3132b 100644 --- a/src/Converter/v3/components/Response.ts +++ b/src/Converter/v3/components/Response.ts @@ -59,7 +59,7 @@ export const generateReferenceNamespace = ( context: ToTypeNode.Context, ): void => { const basePath = `${parentPath}/${nameWithStatusCode}`; - const referenceNamespaceName = context.getReferenceName(currentPoint, responseReference.path); + const referenceNamespaceName = context.resolveReferencePath(currentPoint, responseReference.path).name; store.addStatement(basePath, { type: "namespace", name: nameWithStatusCode, diff --git a/src/Converter/v3/components/Responses.ts b/src/Converter/v3/components/Responses.ts index 69f3f4eb..773dfded 100644 --- a/src/Converter/v3/components/Responses.ts +++ b/src/Converter/v3/components/Responses.ts @@ -85,7 +85,7 @@ export const generateNamespaceWithStatusCode = ( if (Guard.isReference(response)) { const reference = Reference.generate(entryPoint, currentPoint, response); if (reference.type === "local") { - context.setReferenceHandler(reference); + context.setReferenceHandler(currentPoint, reference); Response.generateReferenceNamespace(entryPoint, currentPoint, store, factory, basePath, nameWithStatusCode, reference, context); } else if (reference.componentName) { // reference先に定義を作成 @@ -125,13 +125,14 @@ export const generateInterfacesWithStatusCode = ( if (Guard.isReference(response)) { const reference = Reference.generate(entryPoint, currentPoint, response); if (reference.type === "local") { - context.setReferenceHandler(reference); + context.setReferenceHandler(currentPoint, reference); + const name = context.resolveReferencePath(currentPoint, `${reference.path}/Content`).maybeResolvedName; statements.push( factory.TypeAliasDeclaration.create({ export: true, name: Name.responseName(operationId, statusCode), type: factory.TypeReferenceNode.create({ - name: context.getReferenceName(currentPoint, `${reference.path}/Content`), + name: name, }), }), ); diff --git a/src/Converter/v3/components/Schemas.ts b/src/Converter/v3/components/Schemas.ts index 9c6b0ec3..fba3e4d8 100644 --- a/src/Converter/v3/components/Schemas.ts +++ b/src/Converter/v3/components/Schemas.ts @@ -32,7 +32,7 @@ export const generateNamespace = ( if (Guard.isReference(schema)) { const reference = Reference.generate(entryPoint, currentPoint, schema); if (reference.type === "local") { - const referenceName = context.getReferenceName(currentPoint, reference.path); + const { maybeResolvedName } = context.resolveReferencePath(currentPoint, reference.path); store.addStatement(`${basePath}/${name}`, { type: "typeAlias", name: name, @@ -40,7 +40,7 @@ export const generateNamespace = ( export: true, name: name, type: factory.TypeReferenceNode.create({ - name: referenceName, + name: maybeResolvedName, }), }), }); @@ -58,7 +58,7 @@ export const generateNamespace = ( name: name, comment: reference.data.description, type: factory.TypeReferenceNode.create({ - name: context.getReferenceName(currentPoint, reference.path), + name: context.resolveReferencePath(currentPoint, reference.path).name, }), }), }); diff --git a/src/Converter/v3/store/Store.ts b/src/Converter/v3/store/Store.ts index 911d5b72..c0624c47 100644 --- a/src/Converter/v3/store/Store.ts +++ b/src/Converter/v3/store/Store.ts @@ -37,6 +37,7 @@ export interface Type { dumpOperationState: (filename: string) => void; getNoReferenceOperationState: () => Operation.State; getPathItem: (localPath: string) => OpenApi.PathItem; + isAfterDefined: (referencePath: string) => boolean; } export const create = (factory: Factory.Type, rootDocument: OpenApi.Document): Type => { @@ -79,8 +80,12 @@ export const create = (factory: Factory.Type, rootDocument: OpenApi.Document): T }; const hasStatement = (path: string, types: Def.Statement["type"][]): boolean => { - const targetPath = relative("components", path); - return types.some(type => !!PropAccess.get(state.components, type, targetPath)); + const alreadyRegistered = types.some(type => !!getStatement(path, type)); + return alreadyRegistered; + }; + + const isAfterDefined = (referencePath: string) => { + return !!Dot.get(state.document, referencePath.replace(/\//g, ".")); }; const addStatement = (path: string, statement: Def.Statement): void => { @@ -191,5 +196,6 @@ export const create = (factory: Factory.Type, rootDocument: OpenApi.Document): T addAdditionalStatement, dumpOperationState, getPathItem, + isAfterDefined, }; }; diff --git a/src/Converter/v3/toTypeNode.ts b/src/Converter/v3/toTypeNode.ts index 1e015f8b..e7f1eb32 100644 --- a/src/Converter/v3/toTypeNode.ts +++ b/src/Converter/v3/toTypeNode.ts @@ -9,9 +9,15 @@ import * as Guard from "./Guard"; import { OpenApi } from "./types"; import { ObjectSchemaWithAdditionalProperties } from "./types"; +export interface ResolveReferencePath { + name: string; + maybeResolvedName: string; + unresolvedPaths: string[]; +} + export interface Context { - setReferenceHandler: (reference: Reference.Type) => void; - getReferenceName: (currentPoint: string, referencePath: string) => string; + setReferenceHandler: (currentPoint: string, reference: Reference.Type) => void; + resolveReferencePath: (currentPoint: string, referencePath: string) => ResolveReferencePath; } export type Convert = ( @@ -84,15 +90,15 @@ export const convert: Convert = ( const reference = Reference.generate(entryPoint, currentPoint, schema); if (reference.type === "local") { // Type Aliasを作成 (or すでにある場合は作成しない) - context.setReferenceHandler(reference); - return factory.TypeReferenceNode.create({ name: context.getReferenceName(currentPoint, reference.path) }); + context.setReferenceHandler(currentPoint, reference); + return factory.TypeReferenceNode.create({ name: context.resolveReferencePath(currentPoint, reference.path).maybeResolvedName }); } // サポートしているディレクトリに対して存在する場合 if (reference.componentName) { // Type AliasもしくはInterfaceを作成 - context.setReferenceHandler(reference); + context.setReferenceHandler(currentPoint, reference); // Aliasを貼る - return factory.TypeReferenceNode.create({ name: context.getReferenceName(currentPoint, reference.path) }); + return factory.TypeReferenceNode.create({ name: context.resolveReferencePath(currentPoint, reference.path).name }); } // サポートしていないディレクトリに存在する場合、直接Interface、もしくはTypeAliasを作成 return convert(entryPoint, reference.referencePoint, factory, reference.data, context, { parent: schema }); diff --git a/test/__tests__/__snapshots__/snapshot-test.ts.snap b/test/__tests__/__snapshots__/snapshot-test.ts.snap index 8f4829ae..4d97c546 100644 --- a/test/__tests__/__snapshots__/snapshot-test.ts.snap +++ b/test/__tests__/__snapshots__/snapshot-test.ts.snap @@ -64,6 +64,18 @@ export namespace Schemas { } export type LocalRefOneOfType = Schemas.StringType | Schemas.NumberType | Schemas.ObjectHasPropertiesType | Schemas.LocalRefObjectProperties; export type LocalRefAllOfType = Schemas.StringType & Schemas.NumberType & Schemas.ObjectHasPropertiesType & Schemas.LocalRefObjectProperties; + export type LocalReferenceBeforeResolvedSchema1 = Schemas.UnresolvedTarget1; + export type UnresolvedTarget1 = boolean; + export type LocalReferenceBeforeResolvedSchema2 = Schemas.UnresolvedTarget2; + export type UnresolvedTarget2 = Schemas.UnresolvedTarget3; + export type UnresolvedTarget3 = number; + export interface LocalReferenceBeforeResolvedSchema3 { + unresolvedTarget4?: Schemas.UnresolvedTarget4; + } + export interface UnresolvedTarget4 { + unresolvedTarget5?: Schemas.UnresolvedTarget5; + } + export type UnresolvedTarget5 = string; export type RemoteString = string; export type RemoteRefString = Schemas.RemoteString; export namespace Level1 { diff --git a/test/api.test.domain/index.yml b/test/api.test.domain/index.yml index 04638691..9c901409 100644 --- a/test/api.test.domain/index.yml +++ b/test/api.test.domain/index.yml @@ -150,6 +150,28 @@ components: - $ref: "#/components/schemas/NumberType" - $ref: "#/components/schemas/ObjectHasPropertiesType" - $ref: "#/components/schemas/LocalRefObjectProperties" + LocalReferenceBeforeResolvedSchema1: + $ref: "#/components/schemas/UnresolvedTarget1" + UnresolvedTarget1: + type: boolean + LocalReferenceBeforeResolvedSchema2: + $ref: "#/components/schemas/UnresolvedTarget2" + UnresolvedTarget2: + $ref: "#/components/schemas/UnresolvedTarget3" + UnresolvedTarget3: + type: number + LocalReferenceBeforeResolvedSchema3: + type: object + properties: + unresolvedTarget4: + $ref: "#/components/schemas/UnresolvedTarget4" + UnresolvedTarget4: + type: object + properties: + unresolvedTarget5: + $ref: "#/components/schemas/UnresolvedTarget5" + UnresolvedTarget5: + type: string RemoteRefString: $ref: "./components/schemas/RemoteString.yml" RemoteRefBoolean: