diff --git a/src/common.js b/src/common.js index 62243b83..4203cd18 100644 --- a/src/common.js +++ b/src/common.js @@ -6,7 +6,7 @@ module.exports = { let prettified = description; - prettified = _.replace(prettified, /\*\//g, "*/"); + prettified = _.replace(prettified, /\*\//g, "*/"); const hasMultipleLines = _.includes(prettified, "\n"); diff --git a/src/typeFormatters.js b/src/typeFormatters.js index 950c876e..01f94e15 100644 --- a/src/typeFormatters.js +++ b/src/typeFormatters.js @@ -1,6 +1,7 @@ const _ = require("lodash"); const { config } = require("./config"); const { TS_KEYWORDS, SCHEMA_TYPES } = require("./constants"); +const { formatDescription } = require("./common"); const formatters = { [SCHEMA_TYPES.ENUM]: (content) => { @@ -18,10 +19,12 @@ const formatters = { const extraSpace = " "; const result = `${extraSpace}${part.field};\n`; - const comments = _.uniq(_.compact([part.title, part.description]).reduce( - (acc, comment) => [...acc, ...comment.split(/\n/g)], - [], - )); + const comments = _.uniq( + _.compact([part.title, formatDescription(part.description)]).reduce( + (acc, comment) => [...acc, ...comment.split(/\n/g)], + [], + ), + ); const commonText = comments.length ? [ diff --git a/tests/generated/v3.0/description-escape.ts b/tests/generated/v3.0/description-escape.ts new file mode 100644 index 00000000..d036fb7b --- /dev/null +++ b/tests/generated/v3.0/description-escape.ts @@ -0,0 +1,257 @@ +/* eslint-disable */ +/* tslint:disable */ +/* + * ------------------------------------------------------------------ + * # THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API-NEXTGEN # + * # AUTHORS: acacode & grandsilence # + * # https://github.com/grandsilence/swagger-typescript-api-nextgen # + * ------------------------------------------------------------------ + */ + +/** + * Ask Foo or see */api/foo + */ +export interface FooBarBaz { + /** Ask Foo or see */api/foo */ + id?: string; + + /** Ask Foo or see */api/foo */ + name?: string; +} + +export type QueryParamsType = Record; +export type ResponseFormat = keyof Omit; + +export interface FullRequestParams extends Omit { + /** set parameter to `true` for call `securityWorker` for this request */ + secure?: boolean; + /** request path */ + path: string; + /** content type of request body */ + type?: ContentType; + /** query params */ + query?: QueryParamsType; + /** format of response (i.e. response.json() -> format: "json") */ + format?: ResponseFormat; + /** request body */ + body?: unknown; + /** base url */ + baseUrl?: string; + /** request cancellation token */ + cancelToken?: CancelToken; +} + +export type RequestParams = Omit; + +export interface ApiConfig { + baseUrl?: string; + baseApiParams?: Omit; + securityWorker?: (securityData: SecurityDataType | null) => Promise | RequestParams | void; + customFetch?: typeof fetch; +} + +export interface HttpResponse extends Response { + data: D; + error: E; +} + +type CancelToken = Symbol | string | number; + +export enum ContentType { + Json = "application/json", + FormData = "multipart/form-data", + UrlEncoded = "application/x-www-form-urlencoded", +} + +export class HttpClient { + public baseUrl: string = "https://your-domain.com"; + private securityData: SecurityDataType | null = null; + private securityWorker?: ApiConfig["securityWorker"]; + private abortControllers = new Map(); + private customFetch = (...fetchParams: Parameters) => fetch(...fetchParams); + + private baseApiParams: RequestParams = { + credentials: "same-origin", + headers: {}, + redirect: "follow", + referrerPolicy: "no-referrer", + }; + + constructor(apiConfig: ApiConfig = {}) { + Object.assign(this, apiConfig); + } + + public setSecurityData = (data: SecurityDataType | null) => { + this.securityData = data; + }; + + private encodeQueryParam(key: string, value: any) { + const encodedKey = encodeURIComponent(key); + return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`; + } + + private addQueryParam(query: QueryParamsType, key: string) { + return this.encodeQueryParam(key, query[key]); + } + + private addArrayQueryParam(query: QueryParamsType, key: string) { + const value = query[key]; + return value.map((v: any) => this.encodeQueryParam(key, v)).join("&"); + } + + protected toQueryString(rawQuery?: QueryParamsType): string { + const query = rawQuery || {}; + const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]); + return keys + .map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key))) + .join("&"); + } + + protected addQueryParams(rawQuery?: QueryParamsType): string { + const queryString = this.toQueryString(rawQuery); + return queryString ? `?${queryString}` : ""; + } + + private contentFormatters: Record any> = { + [ContentType.Json]: (input: any) => + input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input, + [ContentType.FormData]: (input: any) => + Object.keys(input || {}).reduce((formData, key) => { + const property = input[key]; + formData.append( + key, + property instanceof Blob + ? property + : typeof property === "object" && property !== null + ? JSON.stringify(property) + : `${property}`, + ); + return formData; + }, new FormData()), + [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input), + }; + + private mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams { + return { + ...this.baseApiParams, + ...params1, + ...(params2 || {}), + headers: { + ...(this.baseApiParams.headers || {}), + ...(params1.headers || {}), + ...((params2 && params2.headers) || {}), + }, + }; + } + + private createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => { + if (this.abortControllers.has(cancelToken)) { + const abortController = this.abortControllers.get(cancelToken); + if (abortController) { + return abortController.signal; + } + return void 0; + } + + const abortController = new AbortController(); + this.abortControllers.set(cancelToken, abortController); + return abortController.signal; + }; + + public abortRequest = (cancelToken: CancelToken) => { + const abortController = this.abortControllers.get(cancelToken); + + if (abortController) { + abortController.abort(); + this.abortControllers.delete(cancelToken); + } + }; + + public request = async ({ + body, + secure, + path, + type, + query, + format, + baseUrl, + cancelToken, + ...params + }: FullRequestParams): Promise> => { + const secureParams = + ((typeof secure === "boolean" ? secure : this.baseApiParams.secure) && + this.securityWorker && + (await this.securityWorker(this.securityData))) || + {}; + const requestParams = this.mergeRequestParams(params, secureParams); + const queryString = query && this.toQueryString(query); + const payloadFormatter = this.contentFormatters[type || ContentType.Json]; + const responseFormat = format || requestParams.format; + + return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, { + ...requestParams, + headers: { + ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}), + ...(requestParams.headers || {}), + }, + signal: cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal, + body: typeof body === "undefined" || body === null ? null : payloadFormatter(body), + }).then(async (response) => { + const r = response as HttpResponse; + r.data = null as unknown as T; + r.error = null as unknown as E; + + const data = !responseFormat + ? r + : await response[responseFormat]() + .then((data) => { + if (r.ok) { + r.data = data; + } else { + r.error = data; + } + return r; + }) + .catch((e) => { + r.error = e; + return r; + }); + + if (cancelToken) { + this.abortControllers.delete(cancelToken); + } + + if (!response.ok) throw data; + return data; + }); + }; +} + +/** + * @title Comment Escape Test + * @version 1.0.0 + * @baseUrl https://your-domain.com + * + * Ask Foo or see */api/foo + */ +export class Api extends HttpClient { + foobar = { + /** + * @description Ask Foo or see */api/foo + * + * @tags FooBar + * @name FooBar + * @request POST:/foobar + */ + fooBar: (query: { q: string }, data: { id?: string; name?: string }, params: RequestParams = {}) => + this.request<{ id?: string; name?: string }, any>({ + path: `/foobar`, + method: "POST", + query: query, + body: data, + type: ContentType.Json, + format: "json", + ...params, + }), + }; +} diff --git a/tests/schemas/v3.0/description-escape.json b/tests/schemas/v3.0/description-escape.json new file mode 100644 index 00000000..d69bc5ef --- /dev/null +++ b/tests/schemas/v3.0/description-escape.json @@ -0,0 +1,95 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Comment Escape Test", + "description": "Ask Foo or see */api/foo", + "version": "1.0.0" + }, + "servers": [ + { + "description": "Ask Foo or see */api/foo", + "url": "https://your-domain.com" + } + ], + "tags": [], + "components": { + "schemas": { + "FooBarBaz": { + "description": "Ask Foo or see */api/foo", + "properties": { + "id": { + "type": "string", + "description": "Ask Foo or see */api/foo" + }, + "name": { + "description": "Ask Foo or see */api/foo", + "type": "string" + } + }, + "type": "object" + } + } + }, + "paths": { + "/foobar": { + "post": { + "operationId": "FooBar", + "description": "Ask Foo or see */api/foo", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "description": "Ask Foo or see */api/foo", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Ask Foo or see */api/foo" + } + }, + "tags": ["FooBar"], + "security": [], + "parameters": [ + { + "name": "q", + "description": "Ask Foo or see */api/foo", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "description": "Ask Foo or see */api/foo", + "properties": { + "id": { + "type": "string", + "description": "Ask Foo or see */api/foo" + }, + "name": { + "type": "string" + } + }, + "type": "object" + } + } + } + } + } + } + } +}