From 771c34b8c80e50c742dce2210e5f93527b0a6551 Mon Sep 17 00:00:00 2001 From: sup39 Date: Sun, 24 May 2020 15:15:53 +0900 Subject: [PATCH] implement $ref support for responses, parameters, requestBody --- README.md | 2 ++ dist/OpenAPI.d.ts | 31 ++++++++++----------- dist/OpenAPI.js | 56 +++++++++++++++++++++++++++++--------- dist/codegen.js | 5 +++- lib/OpenAPI.ts | 68 ++++++++++++++++++++++++++++++++++------------- lib/codegen.ts | 11 +++++--- package.json | 2 +- 7 files changed, 123 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index bea1032..9b51041 100644 --- a/README.md +++ b/README.md @@ -519,6 +519,8 @@ This tool only supports `application/json` type for request and response body. A Other $ref like requestBody, responseBody are not supported currently. ## Versions +#### 2.0.5 +- implement \$ref support for responses, parameters, requestBody #### 2.0.4 - fix FullDate stringify in Axios params - use local timezone instead of UTC in FullDate diff --git a/dist/OpenAPI.d.ts b/dist/OpenAPI.d.ts index 8dccd9c..f1c7b1a 100644 --- a/dist/OpenAPI.d.ts +++ b/dist/OpenAPI.d.ts @@ -1,3 +1,6 @@ +declare type Dict = { + [_: string]: T; +}; export interface OpenAPI { paths: Paths; components?: Components; @@ -14,14 +17,11 @@ interface PathItem { [_: string]: any; } interface Operation { - responses: Responses; - parameters?: Parameter[]; - requestBody?: RequestBody; + responses: Dict; + parameters?: Array; + requestBody?: RequestBody | Reference; operationId?: string; } -interface Responses { - [status: string]: Response; -} interface Response { content?: TMediaTypes; } @@ -48,20 +48,16 @@ declare type EParameterIn = 'query' | 'header' | 'path' | 'cookie'; export declare const ELParameterIn: Array; interface RequestBody { description: string; - content: { - [contentType: string]: MediaType; - }; + content: Dict; required?: boolean; } interface Components { - schemas: { - [_: string]: Schema | Reference; - }; + schemas: Dict; + responses: Dict; + parameters: Dict; + requestBodies: Dict; } -export declare type Schemas = { - [_: string]: Schema | Reference; -}; -interface Schema { +export interface Schema { type: string; format?: string; nullable?: boolean; @@ -79,7 +75,7 @@ interface ObjectSchema extends Schema { }; } export declare function isObjectSchema(x: any): x is ObjectSchema; -interface Reference { +export interface Reference { $ref: string; maxSize?: string | number; } @@ -108,6 +104,7 @@ declare type TReqTypes = { declare type TResTypes = { [status: string]: SchemaType; }; +export declare function resolveRef(obj: T | Reference, dict: Dict | undefined, prefix: string): T | undefined; export declare class SchemaType { private _required; private _typeName?; diff --git a/dist/OpenAPI.js b/dist/OpenAPI.js index ace2967..fab7f50 100644 --- a/dist/OpenAPI.js +++ b/dist/OpenAPI.js @@ -28,6 +28,28 @@ var APIFunction = /** @class */ (function () { return APIFunction; }()); /* ==== ==== */ +// Reference +function resolveRef(obj, dict, prefix) { + do { + if (!isReference(obj)) + return obj; + var ref = obj.$ref; + if (ref.startsWith(prefix)) { + var name_1 = ref.substring(prefix.length + 1); // $prefix/ + var obj0 = dict === null || dict === void 0 ? void 0 : dict[name_1]; + if (obj0 === undefined) { + console.error("ref not found: " + ref); + return; + } + obj = obj0; + } + else { + console.error("Invalid ref: " + ref + ", expect prefix " + prefix); + return; + } + } while (true); +} +exports.resolveRef = resolveRef; function mediaTypes2type(content, required) { var media = content === null || content === void 0 ? void 0 : content['application/json']; // TODO if (media == null) { @@ -94,8 +116,8 @@ var SchemaType = /** @class */ (function () { else if (isObjectSchema(schema)) { sType = '{'; for (var _i = 0, _b = Object.entries(schema.properties); _i < _b.length; _i++) { - var _c = _b[_i], name_1 = _c[0], sub = _c[1]; - sType += name_1 + ": " + SchemaType.typeNameOf(sub) + ", "; + var _c = _b[_i], name_2 = _c[0], sub = _c[1]; + sType += name_2 + ": " + SchemaType.typeNameOf(sub) + ", "; } sType += '}'; } @@ -137,8 +159,8 @@ var SchemaType = /** @class */ (function () { else if (isObjectSchema(schema)) { sStp = '()=>({'; for (var _i = 0, _a = Object.entries(schema.properties); _i < _a.length; _i++) { - var _b = _a[_i], name_2 = _b[0], sub = _b[1]; - sStp += name_2 + ": " + SchemaType.gcStp(para + '.' + name_2, sub, label + '.' + name_2, false) + ", "; + var _b = _a[_i], name_3 = _b[0], sub = _b[1]; + sStp += name_3 + ": " + SchemaType.gcStp(para + '.' + name_3, sub, label + '.' + name_3, false) + ", "; } sStp += '})'; } @@ -188,7 +210,8 @@ var SchemaType = /** @class */ (function () { }()); exports.SchemaType = SchemaType; function apiFunctionsOf(openAPI) { - var paths = openAPI.paths; + var paths = openAPI.paths, comps = openAPI.components; + var compPrefix = '#/components/'; var functions = {}; for (var _i = 0, _a = Object.entries(paths); _i < _a.length; _i++) { var _b = _a[_i], url = _b[0], pathItem = _b[1]; @@ -204,32 +227,41 @@ function apiFunctionsOf(openAPI) { 'operationId should be given'); continue; } - var name_3 = operationId; + var name_4 = operationId; var reqTypes = {}; var resTypes = {}; // reqParas if (parameters != null) { for (var _d = 0, parameters_1 = parameters; _d < parameters_1.length; _d++) { - var para = parameters_1[_d]; - var name_4 = para.name, _in = para.in, required = para.required, schema = para.schema; + var rPara = parameters_1[_d]; + var para = resolveRef(rPara, comps === null || comps === void 0 ? void 0 : comps.parameters, compPrefix + 'parameters'); + if (para == null) + continue; + var name_5 = para.name, _in = para.in, required = para.required, schema = para.schema; // add if (reqTypes[_in] == null) reqTypes[_in] = {}; - reqTypes[_in][name_4] = new SchemaType(schema !== null && schema !== void 0 ? schema : 'any', required !== null && required !== void 0 ? required : false); + reqTypes[_in][name_5] = new SchemaType(schema !== null && schema !== void 0 ? schema : 'any', required !== null && required !== void 0 ? required : false); } } // requestBody if (requestBody != null) { - reqTypes.body = mediaTypes2type(requestBody.content, requestBody.required); + var requestBodyO = resolveRef(requestBody, comps === null || comps === void 0 ? void 0 : comps.requestBodies, compPrefix + 'requestBodies'); + if (requestBodyO == null) + continue; + reqTypes.body = mediaTypes2type(requestBodyO.content, requestBodyO.required); } // responses for (var _e = 0, _f = Object.entries(responses); _e < _f.length; _e++) { - var _g = _f[_e], status_1 = _g[0], res = _g[1]; + var _g = _f[_e], status_1 = _g[0], rRes = _g[1]; + var res = resolveRef(rRes, comps === null || comps === void 0 ? void 0 : comps.responses, compPrefix + 'responses'); + if (res == null) + continue; resTypes[status_1] = mediaTypes2type(res.content, true); } // add to group var saf = new APIFunction(method, url, reqTypes, resTypes); - functions[name_3] = saf; + functions[name_4] = saf; } } return functions; diff --git a/dist/codegen.js b/dist/codegen.js index c070cc2..5427f71 100644 --- a/dist/codegen.js +++ b/dist/codegen.js @@ -255,7 +255,10 @@ function codegenSchemas(schemas, config, cp) { cp.writeln("import {FullDate, StrictTypeParser as STP} from '" + utilsTSPath + "'"); // schema for (var _i = 0, _b = Object.entries(schemas); _i < _b.length; _i++) { - var _c = _b[_i], typeName = _c[0], schema = _c[1]; + var _c = _b[_i], typeName = _c[0], rSchema = _c[1]; + var schema = OpenAPI_1.resolveRef(rSchema, schemas, '#/components/schemas'); + if (schema == null) + continue; cp.writeln(); if (OpenAPI_1.isObjectSchema(schema)) { // interface diff --git a/lib/OpenAPI.ts b/lib/OpenAPI.ts index e439630..9e5fff2 100644 --- a/lib/OpenAPI.ts +++ b/lib/OpenAPI.ts @@ -1,5 +1,6 @@ import {StrictTypeParser as STP} from './utils/StrictTypeParser'; const warn = (x: any) => console.warn('\x1b[1;33mWarning: '+x+'\x1b[0m'); +type Dict = {[_: string]: T}; /* ==== type declaration ==== */ export interface OpenAPI { @@ -22,16 +23,13 @@ interface PathItem { type EMethod = 'get' | 'put' | 'post' | 'delete' | 'patch'; const ELMethod: Array = ['get', 'put', 'post', 'delete', 'patch']; interface Operation { - responses: Responses; - parameters?: Parameter[]; - requestBody?: RequestBody; + responses: Dict; + parameters?: Array; + requestBody?: RequestBody | Reference; operationId?: string; } // response -interface Responses { - [status: string]: Response // | Reference; -} interface Response { // headers?: Header; content?: TMediaTypes; @@ -60,18 +58,20 @@ export const ELParameterIn: Array = [ // request body interface RequestBody { description: string; - content: {[contentType: string]: MediaType}; + content: Dict; required?: boolean; } // components interface Components { - schemas: {[_: string]: Schema | Reference}; + schemas: Dict; + responses: Dict; + parameters: Dict; + requestBodies: Dict; } // schemeType -export type Schemas = {[_: string]: Schema | Reference}; -interface Schema { +export interface Schema { type: string; format?: string; nullable?: boolean; @@ -91,7 +91,7 @@ interface ObjectSchema extends Schema { export function isObjectSchema(x: any): x is ObjectSchema { return x.type === 'object'; } -interface Reference { +export interface Reference { $ref: string; maxSize?: string | number; } @@ -118,8 +118,31 @@ type TReqTypes = { type TResTypes = {[status: string]: SchemaType}; /* ==== ==== */ -function mediaTypes2type(content?: TMediaTypes, required?: boolean): - SchemaType { +// Reference +export function resolveRef( + obj: T|Reference, dict: Dict|undefined, prefix: string, +): T | undefined { + do { + if (!isReference(obj)) return obj; + const ref = obj.$ref; + if (ref.startsWith(prefix)) { + const name = ref.substring(prefix.length+1); // $prefix/ + const obj0 = dict?.[name]; + if (obj0 === undefined) { + console.error(`ref not found: ${ref}`); + return; + } + obj = obj0; + } else { + console.error(`Invalid ref: ${ref}, expect prefix ${prefix}`); + return; + } + } while (true); +} + +function mediaTypes2type( + content?: TMediaTypes, required?: boolean, +): SchemaType { const media = content?.['application/json']; // TODO if (media == null) { if (Object.keys(content ?? {}).length > 0) { @@ -250,7 +273,8 @@ export class SchemaType { export type APIFunctions = {[_: string]: APIFunction}; export function apiFunctionsOf(openAPI: OpenAPI): APIFunctions { - const {paths} = openAPI; + const {paths, components: comps} = openAPI; + const compPrefix = '#/components/'; const functions: APIFunctions = {}; for (const [url, pathItem] of Object.entries(paths)) { for (const method of ELMethod) { @@ -270,7 +294,10 @@ export function apiFunctionsOf(openAPI: OpenAPI): APIFunctions { const resTypes: TResTypes = {}; // reqParas if (parameters != null) { - for (const para of parameters) { + for (const rPara of parameters) { + const para = resolveRef( + rPara, comps?.parameters, compPrefix+'parameters'); + if (para == null) continue; const { name, in: _in, required, schema, } = para; @@ -282,13 +309,18 @@ export function apiFunctionsOf(openAPI: OpenAPI): APIFunctions { } // requestBody if (requestBody != null) { + const requestBodyO = resolveRef( + requestBody, comps?.requestBodies, compPrefix+'requestBodies'); + if (requestBodyO == null) continue; reqTypes.body = mediaTypes2type( - requestBody.content, - requestBody.required, + requestBodyO.content, + requestBodyO.required, ); } // responses - for (const [status, res] of Object.entries(responses)) { + for (const [status, rRes] of Object.entries(responses)) { + const res = resolveRef(rRes, comps?.responses, compPrefix+'responses'); + if (res == null) continue; resTypes[status] = mediaTypes2type(res.content, true); } // add to group diff --git a/lib/codegen.ts b/lib/codegen.ts index e71c4a1..5dfbc7e 100644 --- a/lib/codegen.ts +++ b/lib/codegen.ts @@ -3,9 +3,10 @@ import * as path from 'path'; import {Config, ConfigUser, configDefault} from './Config'; import { apiFunctionsOf, OpenAPI, APIFunctions as APIFuncs, - ELParameterIn, SchemaType, Schemas, isObjectSchema, + ELParameterIn, SchemaType, Schema, isObjectSchema, Reference, resolveRef, } from './OpenAPI'; import {CodePrinter} from './CodePrinter'; +type Dict = {[_: string]: T}; function codegenIHandler(funcs: APIFuncs, config: Config, cp: CodePrinter) { const { @@ -239,13 +240,17 @@ function codegenClientAPI(funcs: APIFuncs, config: Config, cp: CodePrinter) { return cp.end(); } -function codegenSchemas(schemas: Schemas, config: Config, cp: CodePrinter) { +function codegenSchemas( + schemas: Dict, config: Config, cp: CodePrinter, +) { const {utilsTSPath} = config; // import cp.writeln( `import {FullDate, StrictTypeParser as STP} from '${utilsTSPath}'`); // schema - for (const [typeName, schema] of Object.entries(schemas)) { + for (const [typeName, rSchema] of Object.entries(schemas)) { + const schema = resolveRef(rSchema, schemas, '#/components/schemas'); + if (schema == null) continue; cp.writeln(); if (isObjectSchema(schema)) { // interface diff --git a/package.json b/package.json index bb2fe81..3af2012 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sup39/api-ts-gen", - "version": "2.0.4-a", + "version": "2.0.5", "description": "OpenAPI code generator for TypeScript", "main": "dist/index.js", "types": "dist/index.d.ts",