diff --git a/README.md b/README.md index b175a947c..915e9f7ac 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Options: -p, --path path/url to swagger scheme -o, --output output path of typescript api file (default: "./") -n, --name name of output typescript api file (default: "Api.ts") + -t, --templates path to folder containing templates (default: "./src/templates") -d, --default-as-success use "default" response status code as success response too. some swagger schemas use "default" response status code as success response type by default. (default: false) diff --git a/index.d.ts b/index.d.ts index 6b4498aec..e4eae725b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,6 +1,4 @@ - interface GenerateApiParams { - /** * path to swagger schema */ @@ -20,7 +18,12 @@ interface GenerateApiParams { * path to folder where will been located the created api module */ output?: string; - + + /** + * path to folder containing templates (default: ./scr/templates) + */ + templates?: string; + /** * generate type definitions for API routes (default: false) */ @@ -32,7 +35,7 @@ interface GenerateApiParams { generateClient?: boolean; /** - * use "default" response status code as success response too. + * use "default" response status code as success response too. * some swagger schemas use "default" response status code as success response type by default. */ defaultResponseAsSuccess?: boolean; @@ -44,5 +47,5 @@ interface GenerateApiParams { generateResponses?: boolean; } -export declare function generateApi(params: Omit): Promise -export declare function generateApi(params: Omit): Promise \ No newline at end of file +export declare function generateApi(params: Omit): Promise; +export declare function generateApi(params: Omit): Promise; diff --git a/index.js b/index.js old mode 100644 new mode 100755 index d27d6d0eb..78c006976 --- a/index.js +++ b/index.js @@ -6,43 +6,36 @@ // License text available at https://opensource.org/licenses/MIT // Repository https://github.com/acacode/swagger-typescript-api -const program = require('commander'); -const { resolve } = require('path'); -const { generateApi } = require('./src'); -const { version } = require('./package.json'); +const program = require("commander"); +const { resolve } = require("path"); +const { generateApi } = require("./src"); +const { version } = require("./package.json"); program - .version(version, '-v, --version', 'output the current version') + .version(version, "-v, --version", "output the current version") .description("Generate api via swagger scheme.\nSupports OA 3.0, 2.0, JSON, yaml.") - .requiredOption('-p, --path ', 'path/url to swagger scheme') - .option('-o, --output ', 'output path of typescript api file', './') - .option('-n, --name ', 'name of output typescript api file', 'Api.ts') + .requiredOption("-p, --path ", "path/url to swagger scheme") + .option("-o, --output ", "output path of typescript api file", "./") + .option("-n, --name ", "name of output typescript api file", "Api.ts") + .option("-t, --templates ", "path to folder containing templates") .option( - '-d, --default-as-success', + "-d, --default-as-success", 'use "default" response status code as success response too.\n' + - 'some swagger schemas use "default" response status code as success response type by default.', - false + 'some swagger schemas use "default" response status code as success response type by default.', + false, ) .option( - '-r, --responses', - 'generate additional information about request responses\n' + - 'also add typings for bad responses', + "-r, --responses", + "generate additional information about request responses\n" + + "also add typings for bad responses", false, ) - .option('--route-types', 'generate type definitions for API routes', false) - .option('--no-client', 'do not generate an API class', false); - + .option("--route-types", "generate type definitions for API routes", false) + .option("--no-client", "do not generate an API class", false); + program.parse(process.argv); -const { - path, - output, - name, - routeTypes, - client, - defaultAsSuccess, - responses, -} = program; +const { path, output, name, templates, routeTypes, client, defaultAsSuccess, responses } = program; generateApi({ name, @@ -52,5 +45,6 @@ generateApi({ defaultResponseAsSuccess: defaultAsSuccess, generateResponses: responses, input: resolve(process.cwd(), path), - output: resolve(process.cwd(), output || '.') -}) \ No newline at end of file + output: resolve(process.cwd(), output || "."), + templates: resolve(templates ? process.cwd() : __dirname, templates || "./src/templates"), +}); diff --git a/package.json b/package.json index 79c81715a..e70a0a908 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "cli:debug:json": "node --nolazy --inspect-brk=9229 index.js -p ./swagger-test-cli.json -n swagger-test-cli.ts", "cli:debug:yaml": "node --nolazy --inspect-brk=9229 index.js -p ./swagger-test-cli.yaml -n swagger-test-cli.ts", "cli:help": "node index.js -h", - "test:all": "npm-run-all generate validate test:routeTypes test:noClient test:defaultAsSuccess test:responses --continue-on-error", + "test:all": "npm-run-all generate validate test:routeTypes test:noClient test:defaultAsSuccess test:responses test:templates --continue-on-error", "generate": "node tests/generate.js", "generate:debug": "node --nolazy --inspect-brk=9229 tests/generate.js", "validate": "node tests/validate.js", @@ -16,6 +16,7 @@ "test:routeTypes": "node tests/spec/routeTypes/test.js", "test:noClient": "node tests/spec/noClient/test.js", "test:defaultAsSuccess": "node tests/spec/defaultAsSuccess/test.js", + "test:templates": "node tests/spec/templates/test.js", "test:responses": "node tests/spec/responses/test.js" }, "author": "acacode", diff --git a/src/config.js b/src/config.js index b2de67cfe..0c5c130bb 100644 --- a/src/config.js +++ b/src/config.js @@ -1,4 +1,6 @@ const config = { + /** CLI flag */ + templates: "./templates", /** CLI flag */ generateResponses: false, /** CLI flag */ diff --git a/src/files.js b/src/files.js index e1a64f076..f20161a95 100644 --- a/src/files.js +++ b/src/files.js @@ -2,21 +2,15 @@ const _ = require("lodash"); const fs = require("fs"); const { resolve } = require("path"); -const getFileContent = path => - fs.readFileSync(path, { encoding: 'UTF-8' }) +const getFileContent = (path) => fs.readFileSync(path, { encoding: "UTF-8" }); -const pathIsExist = path => - path && fs.existsSync(path) +const pathIsExist = (path) => path && fs.existsSync(path); const createFile = (pathTo, fileName, content) => - fs.writeFileSync(resolve(__dirname, pathTo, `./${fileName}`), content, _.noop) - -const getTemplate = templateName => - getFileContent(resolve(__dirname, `./templates/${templateName}.mustache`)) + fs.writeFileSync(resolve(__dirname, pathTo, `./${fileName}`), content, _.noop); module.exports = { - getTemplate, createFile, pathIsExist, getFileContent, -} \ No newline at end of file +}; diff --git a/src/index.js b/src/index.js index 0e9e40d68..a29bbb5dc 100644 --- a/src/index.js +++ b/src/index.js @@ -9,14 +9,16 @@ const mustache = require("mustache"); const prettier = require("prettier"); const _ = require("lodash"); +const { resolve } = require("path"); const { parseSchemas } = require("./schema"); const { parseRoutes, groupRoutes } = require("./routes"); const { createApiConfig } = require("./apiConfig"); const { getModelType } = require("./modelTypes"); const { getSwaggerObject, fixSwaggerScheme } = require("./swagger"); const { createComponentsMap, filterComponentsMap } = require("./components"); -const { getTemplate, createFile, pathIsExist } = require("./files"); +const { createFile, pathIsExist } = require("./files"); const { addToConfig, config } = require("./config"); +const { getTemplates } = require("./templates"); mustache.escape = (value) => value; @@ -33,6 +35,7 @@ module.exports = { output, url, name, + templates = resolve(__dirname, config.templates), generateResponses = config.generateResponses, defaultResponseAsSuccess = config.defaultResponseAsSuccess, generateRouteTypes = config.generateRouteTypes, @@ -44,9 +47,12 @@ module.exports = { generateRouteTypes, generateClient, generateResponses, + templates, }); getSwaggerObject(input, url) .then(({ usageSchema, originalSchema }) => { + const { apiTemplate, clientTemplate, routeTypesTemplate } = getTemplates(); + console.log("☄️ start generating your typescript api"); fixSwaggerScheme(usageSchema, originalSchema); @@ -58,10 +64,6 @@ module.exports = { const { info, paths, servers, components } = usageSchema; - const apiTemplate = getTemplate("api"); - const clientTemplate = getTemplate("client"); - const routeTypesTemplate = getTemplate("route-types"); - const componentsMap = createComponentsMap(components); const schemasMap = filterComponentsMap(componentsMap, "schemas"); diff --git a/src/templates.js b/src/templates.js new file mode 100644 index 000000000..7383d5b9b --- /dev/null +++ b/src/templates.js @@ -0,0 +1,20 @@ +const { getFileContent } = require("./files"); +const { config } = require("./config"); +const { resolve } = require("path"); + +const getTemplates = () => { + console.log(`✨ try to read templates from directory "${config.templates}"`); + + return { + apiTemplate: getTemplate("api"), + clientTemplate: config.generateClient ? getTemplate("client") : null, + routeTypesTemplate: config.generateRouteTypes ? getTemplate("route-types") : null, + }; +}; + +const getTemplate = (templateName) => + getFileContent(resolve(config.templates, `./${templateName}.mustache`)); + +module.exports = { + getTemplates, +}; diff --git a/tests/spec/templates/schema.json b/tests/spec/templates/schema.json new file mode 100644 index 000000000..66d6de404 --- /dev/null +++ b/tests/spec/templates/schema.json @@ -0,0 +1,60 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Swagger API Team" + }, + "license": { + "name": "MIT" + } + }, + "host": "petstore.swagger.io", + "basePath": "/api", + "schemes": ["http"], + "consumes": ["application/json"], + "produces": ["application/json"], + "paths": { + "/pets": { + "get": { + "description": "Returns all pets from the system that the user has access to", + "produces": ["application/json"], + "responses": { + "200": { + "description": "A list of pets.", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + } + } + } + } + }, + "definitions": { + "Pet": { + "type": "object", + "required": ["id", "name"], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "multiple": { + "type": ["string", "number"] + } + } + } + } +} diff --git a/tests/spec/templates/schema.ts b/tests/spec/templates/schema.ts new file mode 100644 index 000000000..e0267a91f --- /dev/null +++ b/tests/spec/templates/schema.ts @@ -0,0 +1,96 @@ +/* tslint:disable */ +/* eslint-disable */ + +export interface Pet { + id: number; + name: string; + tag?: string; + multiple?: string | number; +} + +export type RequestParams = Omit & { + secure?: boolean; +}; + +type ApiConfig = { + baseUrl?: string; + baseApiParams?: RequestParams; + securityWorker?: (securityData: SecurityDataType) => RequestParams; +}; + +const enum BodyType { + Json, +} + +class HttpClient { + public baseUrl: string = "http://petstore.swagger.io/api"; + private securityData: SecurityDataType = null as any; + private securityWorker: ApiConfig["securityWorker"] = (() => {}) as any; + + private baseApiParams: RequestParams = { + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + }, + redirect: "follow", + referrerPolicy: "no-referrer", + }; + + constructor({ baseUrl, baseApiParams, securityWorker }: ApiConfig = {}) { + this.baseUrl = baseUrl || this.baseUrl; + this.baseApiParams = baseApiParams || this.baseApiParams; + this.securityWorker = securityWorker || this.securityWorker; + } + + public setSecurityData = (data: SecurityDataType) => { + this.securityData = data; + }; + + private bodyFormatters: Record any> = { + [BodyType.Json]: JSON.stringify, + }; + + private mergeRequestOptions(params: RequestParams, securityParams?: RequestParams): RequestParams { + return { + ...this.baseApiParams, + ...params, + ...(securityParams || {}), + headers: { + ...(this.baseApiParams.headers || {}), + ...(params.headers || {}), + ...((securityParams && securityParams.headers) || {}), + }, + }; + } + + private safeParseResponse = (response: Response): Promise => + response + .json() + .then((data) => data) + .catch((e) => response.text); + + public request = ( + path: string, + method: string, + { secure, ...params }: RequestParams = {}, + body?: any, + bodyType?: BodyType, + secureByDefault?: boolean, + ): Promise => + fetch(`${this.baseUrl}${path}`, { + // @ts-ignore + ...this.mergeRequestOptions(params, (secureByDefault || secure) && this.securityWorker(this.securityData)), + method, + body: body ? this.bodyFormatters[bodyType || BodyType.Json](body) : null, + }).then(async (response) => { + const data = await this.safeParseResponse(response); + if (!response.ok) throw data; + return data; + }); +} + +export class Api extends HttpClient { + pets = { + petsList: (params?: RequestParams) => this.request(`/pets`, "GET", params), + }; +} diff --git a/tests/spec/templates/spec_templates/api.mustache b/tests/spec/templates/spec_templates/api.mustache new file mode 100644 index 000000000..35af05bf9 --- /dev/null +++ b/tests/spec/templates/spec_templates/api.mustache @@ -0,0 +1,6 @@ +/* tslint:disable */ +/* eslint-disable */ + +{{#modelTypes}} +export {{typeIdentifier}} {{name}} {{content}} +{{/modelTypes}} diff --git a/tests/spec/templates/spec_templates/client.mustache b/tests/spec/templates/spec_templates/client.mustache new file mode 100644 index 000000000..01fadaa3e --- /dev/null +++ b/tests/spec/templates/spec_templates/client.mustache @@ -0,0 +1,132 @@ + +export type RequestParams = Omit & { + secure?: boolean; +} + +{{#hasQueryRoutes}} +export type RequestQueryParamsType = Record; +{{/hasQueryRoutes}} + +type ApiConfig<{{#apiConfig.generic}}{{name}},{{/apiConfig.generic}}> = { +{{#apiConfig.props}} + {{name}}{{#optional}}?{{/optional}}: {{type}}, +{{/apiConfig.props}} +} + +const enum BodyType { + Json, + {{#hasFormDataRoutes}} + FormData, + {{/hasFormDataRoutes}} +} + +class HttpClient<{{#apiConfig.generic}}{{name}},{{/apiConfig.generic}}> { + public baseUrl: string = "{{apiConfig.baseUrl}}"; + private securityData: SecurityDataType = (null as any); + private securityWorker: ApiConfig<{{#apiConfig.generic}}{{name}},{{/apiConfig.generic}}>["securityWorker"] = (() => {}) as any + + private baseApiParams: RequestParams = { + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json' + }, + redirect: 'follow', + referrerPolicy: 'no-referrer', + } + + constructor({ {{#apiConfig.props}}{{name}},{{/apiConfig.props}} }: ApiConfig<{{#apiConfig.generic}}{{name}},{{/apiConfig.generic}}> = {}) { + {{#apiConfig.props}} + this.{{name}} = {{name}} || this.{{name}}; + {{/apiConfig.props}} + } + + public setSecurityData = (data: SecurityDataType) => { + this.securityData = data + } + + {{#hasQueryRoutes}} + private addQueryParam(query: RequestQueryParamsType, key: string) { + return encodeURIComponent(key) + "=" + encodeURIComponent(Array.isArray(query[key]) ? query[key].join(",") : query[key]) + } + + protected addQueryParams(rawQuery?: RequestQueryParamsType): string { + const query = rawQuery || {}; + const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]); + return keys.length ? `?${keys.map(key => + typeof query[key] === "object" && !Array.isArray(query[key]) ? + this.addQueryParams(query[key] as object).substring(1) : + this.addQueryParam(query, key)).join("&") + }` : ""; + } + {{/hasQueryRoutes}} + + private bodyFormatters: Record any> = { + [BodyType.Json]: JSON.stringify, + {{#hasFormDataRoutes}} + [BodyType.FormData]: (input: any) => + Object.keys(input).reduce((data, key) => { + data.append(key, input[key]); + return data; + }, new FormData()), + {{/hasFormDataRoutes}} + } + + private mergeRequestOptions(params: RequestParams, securityParams?: RequestParams): RequestParams { + return { + ...this.baseApiParams, + ...params, + ...(securityParams || {}), + headers: { + ...(this.baseApiParams.headers || {}), + ...(params.headers || {}), + ...((securityParams && securityParams.headers) || {}) + } + } + } + + private safeParseResponse = (response: Response): {{#generateResponses}}TPromise{{/generateResponses}}{{^generateResponses}}Promise{{/generateResponses}} => + response.json() + .then(data => data) + .catch(e => response.text); + + public request = ( + path: string, + method: string, + { secure, ...params }: RequestParams = {}, + body?: any, + bodyType?: BodyType, + secureByDefault?: boolean, + ): {{#generateResponses}}TPromise{{/generateResponses}}{{^generateResponses}}Promise{{/generateResponses}} => + fetch(`${this.baseUrl}${path}`, { + // @ts-ignore + ...this.mergeRequestOptions(params, (secureByDefault || secure) && this.securityWorker(this.securityData)), + method, + body: body ? this.bodyFormatters[bodyType || BodyType.Json](body) : null, + }).then(async response => { + const data = await this.safeParseResponse(response); + if (!response.ok) throw data + return data + }) +} + +export class Api<{{#apiConfig.generic}}{{name}}{{#defaultValue}} = {{.}}{{/defaultValue}},{{/apiConfig.generic}}> extends HttpClient<{{#apiConfig.generic}}{{name}},{{/apiConfig.generic}}>{ +{{#routes}} + + {{#outOfModule}} + + {{name}} = ({{#routeArgs}}{{name}}{{#optional}}?{{/optional}}: {{type}}, {{/routeArgs}}) => + this.request<{{returnType}}, {{errorReturnType}}>({{requestMethodContent}}) + {{/outOfModule}} + + {{#combined}} + {{moduleName}} = { + {{#routes}} + + {{name}}: ({{#routeArgs}}{{name}}{{#optional}}?{{/optional}}: {{type}}, {{/routeArgs}}) => + this.request<{{returnType}}, {{errorReturnType}}>({{requestMethodContent}}), + {{/routes}} + } + {{/combined}} +{{/routes}} + +} diff --git a/tests/spec/templates/spec_templates/route-types.mustache b/tests/spec/templates/spec_templates/route-types.mustache new file mode 100644 index 000000000..21988a504 --- /dev/null +++ b/tests/spec/templates/spec_templates/route-types.mustache @@ -0,0 +1,35 @@ +{{#routes}} +{{#outOfModule}} +{{#routes}} + +/** +{{#comments}} + * {{.}} +{{/comments}} + */ +export namespace {{pascalName}} { + export type RequestQuery = {{queryType}}; + export type RequestBody = {{bodyType}}; + export type ResponseBody = {{returnType}}; +} +{{/routes}} +{{/outOfModule}} + +{{#combined}} +export namespace {{moduleName}} { + {{#routes}} + + /** + {{#comments}} + * {{.}} + {{/comments}} + */ + export namespace {{pascalName}} { + export type RequestQuery = {{queryType}}; + export type RequestBody = {{bodyType}}; + export type ResponseBody = {{returnType}}; + } + {{/routes}} +} +{{/combined}} +{{/routes}} diff --git a/tests/spec/templates/test.js b/tests/spec/templates/test.js new file mode 100644 index 000000000..863e3af83 --- /dev/null +++ b/tests/spec/templates/test.js @@ -0,0 +1,26 @@ +const { generateApi } = require("../../../src"); +const { resolve } = require("path"); +const validateGeneratedModule = require("../../helpers/validateGeneratedModule"); +const createSchemasInfos = require("../../helpers/createSchemaInfos"); + +const schemas = createSchemasInfos({ absolutePathToSchemas: resolve(__dirname, "./") }); + +schemas.forEach(({ absolutePath, apiFileName }) => { + generateApi({ + name: apiFileName, + input: absolutePath, + output: resolve(__dirname, "./"), + // because this script was called from package.json folder + templates: "./tests/spec/templates/spec_templates", + }) + .then(() => { + const diagnostics = validateGeneratedModule({ + pathToFile: resolve(__dirname, `./${apiFileName}`), + }); + if (diagnostics.length) throw "Failed"; + }) + .catch((e) => { + console.error("templates option test failed."); + throw e; + }); +});