Skip to content

Commit bd70d56

Browse files
authored
feat: support property access via reference object (#59)
* test: add property access test via reference object * feat: support property reference
1 parent 2361e5b commit bd70d56

File tree

8 files changed

+315
-10
lines changed

8 files changed

+315
-10
lines changed

scripts/testCodeGen.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,16 @@ const main = () => {
118118
sync: false,
119119
});
120120

121+
generateTypedefWithTemplateCode("test/ref.access/index.yml", "test/code/typedef-with-template/ref-access.ts", false, {
122+
sync: false,
123+
});
124+
121125
generateSplitCode("test/api.test.domain/index.yml", "test/code/split");
122126

123127
generateParameter("test/api.test.domain/index.yml", "test/code/parameter/api.test.domain.json");
124128
generateParameter("test/infer.domain/index.yml", "test/code/parameter/infer.domain.json");
129+
130+
125131
};
126132

127133
main();

src/internal/OpenApiTools/Parser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class Parser {
1818
private convertContext: ConvertContext.Types;
1919
private store: Store;
2020
private factory: TypeScriptCodeGenerator.Factory.Type;
21-
constructor(private entryPoint: string, private rootSchema: OpenApi.Document, noReferenceOpenApiSchema: OpenApi.Document) {
21+
constructor(private entryPoint: string, private readonly rootSchema: OpenApi.Document, noReferenceOpenApiSchema: OpenApi.Document) {
2222
this.currentPoint = entryPoint;
2323
this.convertContext = ConvertContext.create();
2424
this.factory = TypeScriptCodeGenerator.Factory.create();
@@ -27,7 +27,7 @@ export class Parser {
2727
}
2828

2929
private initialize(): void {
30-
const toTypeNodeContext = TypeNodeContext.create(this.entryPoint, this.store, this.factory, this.convertContext);
30+
const toTypeNodeContext = TypeNodeContext.create(this.entryPoint, this.rootSchema, this.store, this.factory, this.convertContext);
3131
const rootSchema = this.rootSchema;
3232
if (rootSchema.components) {
3333
if (rootSchema.components.schemas) {

src/internal/OpenApiTools/TypeNodeContext.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Path from "path";
22

33
import ts from "typescript";
44

5+
import type { OpenApi } from "../../types";
56
import { DevelopmentError } from "../Exception";
67
import * as TypeScriptCodeGenerator from "../TsGenerator";
78
import * as ConverterContext from "./ConverterContext";
@@ -66,15 +67,19 @@ const calculateReferencePath = (store: Walker.Store, base: string, pathArray: st
6667
if (names.length === 0) {
6768
throw new DevelopmentError("Local Reference Error \n" + JSON.stringify({ pathArray, names, base }, null, 2));
6869
}
70+
const maybeResolvedNameFragments = names.concat(unresolvedPaths).map(converterContext.escapeDeclarationText);
6971
return {
7072
name: names.map(converterContext.escapeDeclarationText).join("."),
71-
maybeResolvedName: names.concat(unresolvedPaths).map(converterContext.escapeDeclarationText).join("."),
73+
maybeResolvedName: maybeResolvedNameFragments.join("."),
7274
unresolvedPaths,
75+
depth: maybeResolvedNameFragments.length,
76+
pathArray,
7377
};
7478
};
7579

7680
export const create = (
7781
entryPoint: string,
82+
rootSchema: OpenApi.Document,
7883
store: Walker.Store,
7984
factory: TypeScriptCodeGenerator.Factory.Type,
8085
converterContext: ConverterContext.Types,
@@ -94,6 +99,7 @@ export const create = (
9499
factory,
95100
reference.data,
96101
{
102+
rootSchema,
97103
setReferenceHandler,
98104
resolveReferencePath,
99105
},
@@ -119,6 +125,7 @@ export const create = (
119125
factory,
120126
reference.data,
121127
{
128+
rootSchema,
122129
setReferenceHandler,
123130
resolveReferencePath,
124131
},
@@ -149,5 +156,5 @@ export const create = (
149156
}
150157
}
151158
};
152-
return { setReferenceHandler: setReferenceHandler, resolveReferencePath: resolveReferencePath };
159+
return { rootSchema, setReferenceHandler: setReferenceHandler, resolveReferencePath: resolveReferencePath };
153160
};

src/internal/OpenApiTools/components/Schemas.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import DotProp from "dot-prop";
2+
13
import type { OpenApi } from "../../../types";
24
import { UnSupportError } from "../../Exception";
35
import { Factory } from "../../TsGenerator";
@@ -45,16 +47,27 @@ export const generateNamespace = (
4547
const schema = targetSchema;
4648
const reference = Reference.generate<OpenApi.Schema>(entryPoint, currentPoint, schema);
4749
if (reference.type === "local") {
48-
const { maybeResolvedName } = context.resolveReferencePath(currentPoint, reference.path);
50+
const { maybeResolvedName, depth, pathArray } = context.resolveReferencePath(currentPoint, reference.path);
51+
// console.log({
52+
// depth,
53+
// pathArray,
54+
// });
55+
const createTypeNode = () => {
56+
if (depth === 2) {
57+
return factory.TypeReferenceNode.create({
58+
name: convertContext.escapeReferenceDeclarationText(maybeResolvedName),
59+
});
60+
}
61+
const schema = DotProp.get(context.rootSchema, pathArray.join(".")) as any;
62+
return ToTypeNode.convert(entryPoint, currentPoint, factory, schema, context, convertContext, { parent: schema });
63+
};
4964
store.addStatement(`${basePath}/${name}`, {
5065
kind: "typeAlias",
5166
name: convertContext.escapeDeclarationText(name),
5267
value: factory.TypeAliasDeclaration.create({
5368
export: true,
5469
name: convertContext.escapeDeclarationText(name),
55-
type: factory.TypeReferenceNode.create({
56-
name: convertContext.escapeReferenceDeclarationText(maybeResolvedName),
57-
}),
70+
type: createTypeNode(),
5871
}),
5972
});
6073
return;

src/internal/OpenApiTools/toTypeNode.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import DotProp from "dot-prop";
12
import ts from "typescript";
23

34
import type { OpenApi } from "../../types";
@@ -15,9 +16,16 @@ export interface ResolveReferencePath {
1516
name: string;
1617
maybeResolvedName: string;
1718
unresolvedPaths: string[];
19+
/**
20+
* @example components.a.b.c.dの場合 ["a", "b", "c", "d"].length = 4
21+
**/
22+
depth: number;
23+
/** 入力$refを分解したモノ(#は除く) */
24+
pathArray: string[];
1825
}
1926

2027
export interface Context {
28+
readonly rootSchema: OpenApi.Document;
2129
setReferenceHandler: (currentPoint: string, reference: Reference.Type<OpenApi.Schema | OpenApi.JSONSchemaDefinition>) => void;
2230
resolveReferencePath: (currentPoint: string, referencePath: string) => ResolveReferencePath;
2331
}
@@ -96,8 +104,13 @@ export const convert: Convert = (
96104
if (reference.type === "local") {
97105
// Type Aliasを作成 (or すでにある場合は作成しない)
98106
context.setReferenceHandler(currentPoint, reference);
99-
const { maybeResolvedName } = context.resolveReferencePath(currentPoint, reference.path);
100-
return factory.TypeReferenceNode.create({ name: converterContext.escapeReferenceDeclarationText(maybeResolvedName) });
107+
const { maybeResolvedName, depth, pathArray } = context.resolveReferencePath(currentPoint, reference.path);
108+
if (depth === 2) {
109+
return factory.TypeReferenceNode.create({ name: converterContext.escapeReferenceDeclarationText(maybeResolvedName) });
110+
} else {
111+
const resolveSchema = DotProp.get(context.rootSchema, pathArray.join(".")) as any;
112+
return convert(entryPoint, currentPoint, factory, resolveSchema, context, converterContext, { parent: schema });
113+
}
101114
}
102115
// サポートしているディレクトリに対して存在する場合
103116
if (reference.componentName) {

test/__tests__/__snapshots__/typedef-with-template-test.ts.snap

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2986,3 +2986,140 @@ export class Client<RequestOption> {
29862986
}
29872987
"
29882988
`;
2989+
2990+
exports[`Typedef with template ref-access 1`] = `
2991+
"//
2992+
// Generated by @himenon/openapi-typescript-code-generator
2993+
//
2994+
// OpenApi : 3.1.0
2995+
//
2996+
// License : MIT
2997+
//
2998+
2999+
3000+
export namespace Schemas {
3001+
export interface Book {
3002+
author?: {
3003+
name?: string;
3004+
age?: string;
3005+
};
3006+
publisher?: {
3007+
name?: any;
3008+
address?: string;
3009+
};
3010+
metadata: {
3011+
description: string;
3012+
};
3013+
}
3014+
export type Author = {
3015+
name?: string;
3016+
age?: string;
3017+
};
3018+
export type Publisher = {
3019+
name?: any;
3020+
address?: string;
3021+
};
3022+
}
3023+
export interface Parameter$getBook {
3024+
/** Book ID */
3025+
id: string;
3026+
}
3027+
export interface Response$getBook$Status$200 {
3028+
\\"application/json\\": Schemas.Book;
3029+
}
3030+
export interface Parameter$getDescription {
3031+
/** Book ID */
3032+
id: string;
3033+
}
3034+
export interface Response$getDescription$Status$200 {
3035+
\\"application/json\\": string;
3036+
}
3037+
export interface Parameter$getAuthor {
3038+
/** Author Id */
3039+
id: string;
3040+
}
3041+
export interface Response$getAuthor$Status$200 {
3042+
\\"application/json\\": {
3043+
name?: string;
3044+
age?: string;
3045+
};
3046+
}
3047+
export interface Parameter$getPublisher {
3048+
/** Publisher ID */
3049+
id: string;
3050+
}
3051+
export interface Response$getPublisher$Status$200 {
3052+
\\"application/json\\": Schemas.Publisher;
3053+
}
3054+
export type ResponseContentType$getBook = keyof Response$getBook$Status$200;
3055+
export interface Params$getBook {
3056+
parameter: Parameter$getBook;
3057+
}
3058+
export type ResponseContentType$getDescription = keyof Response$getDescription$Status$200;
3059+
export interface Params$getDescription {
3060+
parameter: Parameter$getDescription;
3061+
}
3062+
export type ResponseContentType$getAuthor = keyof Response$getAuthor$Status$200;
3063+
export interface Params$getAuthor {
3064+
parameter: Parameter$getAuthor;
3065+
}
3066+
export type ResponseContentType$getPublisher = keyof Response$getPublisher$Status$200;
3067+
export interface Params$getPublisher {
3068+
parameter: Parameter$getPublisher;
3069+
}
3070+
export type HttpMethod = \\"GET\\" | \\"PUT\\" | \\"POST\\" | \\"DELETE\\" | \\"OPTIONS\\" | \\"HEAD\\" | \\"PATCH\\" | \\"TRACE\\";
3071+
export interface ObjectLike {
3072+
[key: string]: any;
3073+
}
3074+
export interface QueryParameter {
3075+
value: any;
3076+
style?: \\"form\\" | \\"spaceDelimited\\" | \\"pipeDelimited\\" | \\"deepObject\\";
3077+
explode: boolean;
3078+
}
3079+
export interface QueryParameters {
3080+
[key: string]: QueryParameter;
3081+
}
3082+
export type SuccessResponses = Response$getBook$Status$200 | Response$getDescription$Status$200 | Response$getAuthor$Status$200 | Response$getPublisher$Status$200;
3083+
export namespace ErrorResponse {
3084+
export type getBook = void;
3085+
export type getDescription = void;
3086+
export type getAuthor = void;
3087+
export type getPublisher = void;
3088+
}
3089+
export interface ApiClient<RequestOption> {
3090+
request: <T = SuccessResponses>(httpMethod: HttpMethod, url: string, headers: ObjectLike | any, requestBody: ObjectLike | any, queryParameters: QueryParameters | undefined, options?: RequestOption) => Promise<T>;
3091+
}
3092+
export class Client<RequestOption> {
3093+
private baseUrl: string;
3094+
constructor(private apiClient: ApiClient<RequestOption>, baseUrl: string) { this.baseUrl = baseUrl.replace(/\\\\/$/, \\"\\"); }
3095+
public async getBook(params: Params$getBook, option?: RequestOption): Promise<Response$getBook$Status$200[\\"application/json\\"]> {
3096+
const url = this.baseUrl + \`/get/book/\${params.parameter.id}\`;
3097+
const headers = {
3098+
Accept: \\"application/json\\"
3099+
};
3100+
return this.apiClient.request(\\"GET\\", url, headers, undefined, undefined, option);
3101+
}
3102+
public async getDescription(params: Params$getDescription, option?: RequestOption): Promise<Response$getDescription$Status$200[\\"application/json\\"]> {
3103+
const url = this.baseUrl + \`/get/book/\${params.parameter.id}/description\`;
3104+
const headers = {
3105+
Accept: \\"application/json\\"
3106+
};
3107+
return this.apiClient.request(\\"GET\\", url, headers, undefined, undefined, option);
3108+
}
3109+
public async getAuthor(params: Params$getAuthor, option?: RequestOption): Promise<Response$getAuthor$Status$200[\\"application/json\\"]> {
3110+
const url = this.baseUrl + \`/get/author/\${params.parameter.id}\`;
3111+
const headers = {
3112+
Accept: \\"application/json\\"
3113+
};
3114+
return this.apiClient.request(\\"GET\\", url, headers, undefined, undefined, option);
3115+
}
3116+
public async getPublisher(params: Params$getPublisher, option?: RequestOption): Promise<Response$getPublisher$Status$200[\\"application/json\\"]> {
3117+
const url = this.baseUrl + \`/get/publisher/\${params.parameter.id}\`;
3118+
const headers = {
3119+
Accept: \\"application/json\\"
3120+
};
3121+
return this.apiClient.request(\\"GET\\", url, headers, undefined, undefined, option);
3122+
}
3123+
}
3124+
"
3125+
`;

test/__tests__/typedef-with-template-test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,9 @@ describe("Typedef with template", () => {
2929
const text = Utils.replaceVersionInfo(generateCode);
3030
expect(text).toMatchSnapshot();
3131
});
32+
test("ref-access", () => {
33+
const generateCode = fs.readFileSync(path.join(__dirname, "../code/typedef-with-template/ref-access.ts"), { encoding: "utf-8" });
34+
const text = Utils.replaceVersionInfo(generateCode);
35+
expect(text).toMatchSnapshot();
36+
});
3237
});

0 commit comments

Comments
 (0)