Skip to content

Commit

Permalink
fix: unresolved reference (#8)
Browse files Browse the repository at this point in the history
close #7
  • Loading branch information
Himenon authored Jan 15, 2021
1 parent 4bd3aa3 commit 07e0e6d
Show file tree
Hide file tree
Showing 12 changed files with 113 additions and 41 deletions.
53 changes: 39 additions & 14 deletions src/Converter/v3/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -24,54 +24,67 @@ 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);
// ディレクトリが深い場合は相対パスが`..`を繰り返す可能性があり、
// その場合はすでに登録されたnamesを削除する
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;
}, base);
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, {
Expand All @@ -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, {
Expand All @@ -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 };
};
6 changes: 3 additions & 3 deletions src/Converter/v3/components/Header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,20 @@ export const generatePropertySignature = (
if (Guard.isReference(header)) {
const reference = Reference.generate<OpenApi.Header>(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,
}),
});
}
return factory.PropertySignature.create({
name,
optional: false,
type: factory.TypeReferenceNode.create({
name: context.getReferenceName(currentPoint, reference.path),
name: context.resolveReferencePath(currentPoint, reference.path).name,
}),
});
}
Expand Down
12 changes: 6 additions & 6 deletions src/Converter/v3/components/Operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ export const generateNamespace = (
if (Guard.isReference(operation.requestBody)) {
const reference = Reference.generate<OpenApi.RequestBody>(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";
Expand All @@ -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,
Expand Down Expand Up @@ -129,13 +129,13 @@ export const generateStatements = (
if (Guard.isReference(operation.requestBody)) {
const reference = Reference.generate<OpenApi.RequestBody>(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から作成?
}),
}),
);
Expand All @@ -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 }),
}),
);

Expand Down
4 changes: 2 additions & 2 deletions src/Converter/v3/components/Parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ export const generatePropertySignature = (
if (Guard.isReference(parameter)) {
const reference = Reference.generate<OpenApi.Parameter>(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,
}),
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/Converter/v3/components/Parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
}),
});
Expand Down
2 changes: 1 addition & 1 deletion src/Converter/v3/components/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions src/Converter/v3/components/Responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const generateNamespaceWithStatusCode = (
if (Guard.isReference(response)) {
const reference = Reference.generate<OpenApi.Response>(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先に定義を作成
Expand Down Expand Up @@ -125,13 +125,14 @@ export const generateInterfacesWithStatusCode = (
if (Guard.isReference(response)) {
const reference = Reference.generate<OpenApi.Response>(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,
}),
}),
);
Expand Down
6 changes: 3 additions & 3 deletions src/Converter/v3/components/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ export const generateNamespace = (
if (Guard.isReference(schema)) {
const reference = Reference.generate<OpenApi.Schema>(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,
value: factory.TypeAliasDeclaration.create({
export: true,
name: name,
type: factory.TypeReferenceNode.create({
name: referenceName,
name: maybeResolvedName,
}),
}),
});
Expand All @@ -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,
}),
}),
});
Expand Down
10 changes: 8 additions & 2 deletions src/Converter/v3/store/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -79,8 +80,12 @@ export const create = (factory: Factory.Type, rootDocument: OpenApi.Document): T
};

const hasStatement = (path: string, types: Def.Statement<A, B, C>["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<A, B, C>): void => {
Expand Down Expand Up @@ -191,5 +196,6 @@ export const create = (factory: Factory.Type, rootDocument: OpenApi.Document): T
addAdditionalStatement,
dumpOperationState,
getPathItem,
isAfterDefined,
};
};
18 changes: 12 additions & 6 deletions src/Converter/v3/toTypeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenApi.Schema | OpenApi.JSONSchemaDefinition>) => void;
getReferenceName: (currentPoint: string, referencePath: string) => string;
setReferenceHandler: (currentPoint: string, reference: Reference.Type<OpenApi.Schema | OpenApi.JSONSchemaDefinition>) => void;
resolveReferencePath: (currentPoint: string, referencePath: string) => ResolveReferencePath;
}

export type Convert = (
Expand Down Expand Up @@ -84,15 +90,15 @@ export const convert: Convert = (
const reference = Reference.generate<OpenApi.Schema | OpenApi.JSONSchemaDefinition>(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 });
Expand Down
12 changes: 12 additions & 0 deletions test/__tests__/__snapshots__/snapshot-test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions test/api.test.domain/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 07e0e6d

Please sign in to comment.