diff --git a/packages/openapi-ts/rollup.config.ts b/packages/openapi-ts/rollup.config.ts index 31f149289..9cf57b0ac 100644 --- a/packages/openapi-ts/rollup.config.ts +++ b/packages/openapi-ts/rollup.config.ts @@ -25,13 +25,12 @@ export function handlebarsPlugin(): Plugin { knownHelpers: { camelCase: true, dataDestructure: true, - dataParameters: true, equals: true, - escapeDescription: true, ifdef: true, nameOperationDataType: true, notEquals: true, toOperationComment: true, + toRequestOptions: true, useDateType: true, }, knownHelpersOnly: true, diff --git a/packages/openapi-ts/src/compiler/types.ts b/packages/openapi-ts/src/compiler/types.ts index f0a61719c..416c37f69 100644 --- a/packages/openapi-ts/src/compiler/types.ts +++ b/packages/openapi-ts/src/compiler/types.ts @@ -5,15 +5,28 @@ import { addLeadingComment, type Comments, isType, ots } from './utils'; /** * Convert an unknown value to an expression. * @param value - the unknown value. + * @param unescape - if string should be unescaped. + * @param shorthand - if shorthand syntax is allowed. + * @param indentifier - list of keys that are treated as indentifiers. * @returns ts.Expression */ -export const toExpression = (value: unknown, unescape = false): ts.Expression | undefined => { +const toExpression = ({ + value, + unescape = false, + shorthand = false, + identifiers = [], +}: { + value: T; + unescape?: boolean; + shorthand?: boolean; + identifiers?: string[]; +}): ts.Expression | undefined => { if (Array.isArray(value)) { return createArrayType({ arr: value }); } if (typeof value === 'object' && value !== null) { - return createObjectType({ obj: value }); + return createObjectType({ identifiers, obj: value, shorthand }); } if (typeof value === 'number') { @@ -47,46 +60,67 @@ export const createArrayType = ({ multiLine?: boolean; }): ts.ArrayLiteralExpression => ts.factory.createArrayLiteralExpression( - arr.map(v => toExpression(v)).filter(isType), + arr.map(value => toExpression({ value })).filter(isType), // Multiline if the array contains objects, or if specified by the user. (!Array.isArray(arr[0]) && typeof arr[0] === 'object') || multiLine ); /** * Create Object type expression. - * @param options - options to use when creating type. + * @param comments - comments to add to each property. + * @param identifier - keys that should be treated as identifiers. + * @param multiLine - if the object should be multiline. + * @param obj - the object to create expression with. + * @param shorthand - if shorthand syntax should be used. + * @param unescape - if properties strings should be unescaped. * @returns ts.ObjectLiteralExpression */ export const createObjectType = ({ comments = {}, + identifiers = [], multiLine = true, obj, + shorthand = false, unescape = false, }: { obj: T; + comments?: Record; + identifiers?: string[]; multiLine?: boolean; + shorthand?: boolean; unescape?: boolean; - comments?: Record; }): ts.ObjectLiteralExpression => { const properties = Object.entries(obj) .map(([key, value]) => { - const initializer = toExpression(value, unescape); + // Pass all object properties as identifiers if the whole object is a indentifier + let initializer: ts.Expression | undefined = toExpression({ + identifiers: identifiers.includes(key) ? Object.keys(value) : [], + shorthand, + unescape, + value, + }); if (!initializer) { return undefined; } + // Create a identifier if the current key is one and it is not an object + if (identifiers.includes(key) && !ts.isObjectLiteralExpression(initializer)) { + initializer = ts.factory.createIdentifier(value as string); + } if (key.match(/\W/g) && !key.startsWith("'") && !key.endsWith("'")) { key = `'${key}'`; } - const assignment = ts.factory.createPropertyAssignment(key, initializer); + const assignment = + shorthand && key === value + ? ts.factory.createShorthandPropertyAssignment(key) + : ts.factory.createPropertyAssignment(key, initializer); const c = comments?.[key]; if (c?.length) { addLeadingComment(assignment, c); } return assignment; }) - .filter(isType); - const expression = ts.factory.createObjectLiteralExpression(properties, multiLine); - return expression; + .filter(isType); + return ts.factory.createObjectLiteralExpression(properties as any[], multiLine); }; /** @@ -112,7 +146,7 @@ export const createEnumDeclaration = ({ [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(name), Object.entries(obj).map(([key, value]) => { - const initializer = toExpression(value, true); + const initializer = toExpression({ unescape: true, value }); const assignment = ts.factory.createEnumMember(key, initializer); const c = comments?.[key]; if (c) { diff --git a/packages/openapi-ts/src/templates/exportService.hbs b/packages/openapi-ts/src/templates/exportService.hbs index fad3a6b66..522c978e0 100644 --- a/packages/openapi-ts/src/templates/exportService.hbs +++ b/packages/openapi-ts/src/templates/exportService.hbs @@ -19,73 +19,27 @@ export class {{{name}}}{{{@root.$config.postfixServices}}} { {{#equals @root.$config.client 'angular'}} public {{{name}}}({{{nameOperationDataType 'req' this}}}): Observable<{{{nameOperationDataType 'res' this}}}> { {{{dataDestructure this}}} - return this.httpRequest.request({ + return this.httpRequest.request({{{toRequestOptions this}}}); + } {{else}} public {{{name}}}({{{nameOperationDataType 'req' this}}}): CancelablePromise<{{{nameOperationDataType 'res' this}}}> { {{{dataDestructure this}}} - return this.httpRequest.request({ + return this.httpRequest.request({{{toRequestOptions this}}}); + } {{/equals}} {{else}} {{#equals @root.$config.client 'angular'}} public {{{name}}}({{{nameOperationDataType 'req' this}}}): Observable<{{{nameOperationDataType 'res' this}}}> { {{{dataDestructure this}}} - return __request(OpenAPI, this.http, { + return __request(OpenAPI, this.http, {{{toRequestOptions this}}}); + } {{else}} public static {{{name}}}({{{nameOperationDataType 'req' this}}}): CancelablePromise<{{{nameOperationDataType 'res' this}}}> { {{{dataDestructure this}}} - return __request(OpenAPI, { + return __request(OpenAPI, {{{toRequestOptions this}}}); + } {{/equals}} {{/if}} - method: '{{{method}}}', - url: '{{{path}}}', - {{#if parametersPath}} - path: { - {{{dataParameters parametersPath}}} - }, - {{/if}} - {{#if parametersCookie}} - cookies: { - {{{dataParameters parametersCookie}}} - }, - {{/if}} - {{#if parametersHeader}} - headers: { - {{{dataParameters parametersHeader}}} - }, - {{/if}} - {{#if parametersQuery}} - query: { - {{{dataParameters parametersQuery}}} - }, - {{/if}} - {{#if parametersForm}} - formData: { - {{{dataParameters parametersForm}}} - }, - {{/if}} - {{#if parametersBody}} - {{#equals parametersBody.in 'formData'}} - formData: {{{parametersBody.name}}}, - {{/equals}} - {{#equals parametersBody.in 'body'}} - body: {{{parametersBody.name}}}, - {{/equals}} - {{#if parametersBody.mediaType}} - mediaType: '{{{parametersBody.mediaType}}}', - {{/if}} - {{/if}} - {{#if responseHeader}} - responseHeader: '{{{responseHeader}}}', - {{/if}} - {{#if errors}} - errors: { - {{#each errors}} - {{{code}}}: `{{{escapeDescription description}}}`, - {{/each}} - }, - {{/if}} - }); - } {{/each}} } diff --git a/packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts b/packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts index 203d03874..bad7855fb 100644 --- a/packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts +++ b/packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts @@ -29,9 +29,7 @@ describe('registerHandlebarHelpers', () => { const helpers = Object.keys(Handlebars.helpers); expect(helpers).toContain('camelCase'); expect(helpers).toContain('dataDestructure'); - expect(helpers).toContain('dataParameters'); expect(helpers).toContain('equals'); - expect(helpers).toContain('escapeDescription'); expect(helpers).toContain('ifdef'); expect(helpers).toContain('nameOperationDataType'); expect(helpers).toContain('notEquals'); diff --git a/packages/openapi-ts/src/utils/handlebars.ts b/packages/openapi-ts/src/utils/handlebars.ts index 91e5f256e..1873d36fb 100644 --- a/packages/openapi-ts/src/utils/handlebars.ts +++ b/packages/openapi-ts/src/utils/handlebars.ts @@ -77,21 +77,6 @@ const dataDestructure = (operation: Operation) => { return ''; }; -const dataParameters = (parameters: OperationParameter[]) => { - const output = parameters.map(parameter => { - const key = parameter.prop; - const value = parameter.name; - if (key === value) { - return key; - } - if (escapeName(key) === key) { - return `${key}: ${value}`; - } - return `'${key}': ${value}`; - }); - return output.join(', '); -}; - export const serviceExportedNamespace = () => '$OpenApiTs'; export const nameOperationDataType = (namespace: 'req' | 'res', operation: Service['operations'][number]) => { @@ -135,7 +120,6 @@ export const nameOperationDataType = (namespace: 'req' | 'res', operation: Servi export const registerHandlebarHelpers = (): void => { Handlebars.registerHelper('camelCase', camelCase); Handlebars.registerHelper('dataDestructure', dataDestructure); - Handlebars.registerHelper('dataParameters', dataParameters); Handlebars.registerHelper( 'equals', @@ -144,7 +128,72 @@ export const registerHandlebarHelpers = (): void => { } ); - Handlebars.registerHelper('escapeDescription', escapeDescription); + Handlebars.registerHelper('toRequestOptions', (operation: Operation) => { + const toObj = (parameters: OperationParameter[]) => + parameters.reduce( + (prev, curr) => { + const key = curr.prop; + const value = curr.name; + if (key === value) { + prev[key] = key; + } else if (escapeName(key) === key) { + prev[key] = value; + } else { + prev[`'${key}'`] = value; + } + return prev; + }, + {} as Record + ); + + const obj: Record = { + method: operation.method, + url: operation.path, + }; + if (operation.parametersPath.length) { + obj.path = toObj(operation.parametersPath); + } + if (operation.parametersCookie.length) { + obj.cookies = toObj(operation.parametersCookie); + } + if (operation.parametersHeader.length) { + obj.headers = toObj(operation.parametersHeader); + } + if (operation.parametersQuery.length) { + obj.query = toObj(operation.parametersQuery); + } + if (operation.parametersForm.length) { + obj.formData = toObj(operation.parametersForm); + } + if (operation.parametersBody) { + if (operation.parametersBody.in === 'formData') { + obj.formData = operation.parametersBody.name; + } + if (operation.parametersBody.in === 'body') { + obj.body = operation.parametersBody.name; + } + } + if (operation.parametersBody?.mediaType) { + obj.mediaType = operation.parametersBody?.mediaType; + } + if (operation.responseHeader) { + obj.responseHeader = operation.responseHeader; + } + if (operation.errors.length) { + const errors: Record = {}; + operation.errors.forEach(err => { + errors[err.code] = escapeDescription(err.description); + }); + obj.errors = errors; + } + return compiler.utils.toString( + compiler.types.object({ + identifiers: ['body', 'headers', 'formData', 'cookies', 'path', 'query'], + obj, + shorthand: true, + }) + ); + }); Handlebars.registerHelper('toOperationComment', (operation: Operation) => { const config = getConfig(); diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v2/services.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v2/services.gen.ts.snap index 26882d5fd..d10fa0fad 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v2/services.gen.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v2/services.gen.ts.snap @@ -369,9 +369,9 @@ export class ResponseService { method: 'POST', url: '/api/v{api-version}/response', errors: { - 500: `Message for 500 error`, - 501: `Message for 501 error`, - 502: `Message for 502 error`, + 500: 'Message for 500 error', + 501: 'Message for 501 error', + 502: 'Message for 502 error', }, }); } @@ -393,9 +393,9 @@ export class ResponseService { method: 'PUT', url: '/api/v{api-version}/response', errors: { - 500: `Message for 500 error`, - 501: `Message for 501 error`, - 502: `Message for 502 error`, + 500: 'Message for 500 error', + 501: 'Message for 501 error', + 502: 'Message for 502 error', }, }); } @@ -547,8 +547,8 @@ export class ComplexService { parameterReference, }, errors: { - 400: `400 server error`, - 500: `500 server error`, + 400: '400 server error', + 500: '500 server error', }, }); } @@ -567,8 +567,8 @@ export class HeaderService { url: '/api/v{api-version}/header', responseHeader: 'operation-location', errors: { - 400: `400 server error`, - 500: `500 server error`, + 400: '400 server error', + 500: '500 server error', }, }); } @@ -590,10 +590,10 @@ export class ErrorService { status, }, errors: { - 500: `Custom message: Internal Server Error`, - 501: `Custom message: Not Implemented`, - 502: `Custom message: Bad Gateway`, - 503: `Custom message: Service Unavailable`, + 500: 'Custom message: Internal Server Error', + 501: 'Custom message: Not Implemented', + 502: 'Custom message: Bad Gateway', + 503: 'Custom message: Service Unavailable', }, }); } diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3/services.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3/services.gen.ts.snap index 16eb117b0..0fc3d79f9 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3/services.gen.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3/services.gen.ts.snap @@ -340,7 +340,7 @@ export class FormDataService { query: { parameter, }, - formData: formData, + formData, mediaType: 'multipart/form-data', }); } @@ -530,9 +530,9 @@ export class ResponseService { method: 'POST', url: '/api/v{api-version}/response', errors: { - 500: `Message for 500 error`, - 501: `Message for 501 error`, - 502: `Message for 502 error`, + 500: 'Message for 500 error', + 501: 'Message for 501 error', + 502: 'Message for 502 error', }, }); } @@ -554,9 +554,9 @@ export class ResponseService { method: 'PUT', url: '/api/v{api-version}/response', errors: { - 500: `Message for 500 error`, - 501: `Message for 501 error`, - 502: `Message for 502 error`, + 500: 'Message for 500 error', + 501: 'Message for 501 error', + 502: 'Message for 502 error', }, }); } @@ -746,8 +746,8 @@ export class ComplexService { parameterReference, }, errors: { - 400: `400 server error`, - 500: `500 server error`, + 400: '400 server error', + 500: '500 server error', }, }); } @@ -783,7 +783,7 @@ export class MultipartService { return __request(OpenAPI, { method: 'POST', url: '/api/v{api-version}/multipart', - formData: formData, + formData, mediaType: 'multipart/form-data', }); } @@ -815,8 +815,8 @@ export class HeaderService { url: '/api/v{api-version}/header', responseHeader: 'operation-location', errors: { - 400: `400 server error`, - 500: `500 server error`, + 400: '400 server error', + 500: '500 server error', }, }); } @@ -838,10 +838,10 @@ export class ErrorService { status, }, errors: { - 500: `Custom message: Internal Server Error`, - 501: `Custom message: Not Implemented`, - 502: `Custom message: Bad Gateway`, - 503: `Custom message: Service Unavailable`, + 500: 'Custom message: Internal Server Error', + 501: 'Custom message: Not Implemented', + 502: 'Custom message: Bad Gateway', + 503: 'Custom message: Service Unavailable', }, }); } diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_angular/services.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_angular/services.gen.ts.snap index a690702fc..46c40cee1 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3_angular/services.gen.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_angular/services.gen.ts.snap @@ -373,7 +373,7 @@ export class FormDataService { query: { parameter, }, - formData: formData, + formData, mediaType: 'multipart/form-data', }); } @@ -577,9 +577,9 @@ export class ResponseService { method: 'POST', url: '/api/v{api-version}/response', errors: { - 500: `Message for 500 error`, - 501: `Message for 501 error`, - 502: `Message for 502 error`, + 500: 'Message for 500 error', + 501: 'Message for 501 error', + 502: 'Message for 502 error', }, }); } @@ -601,9 +601,9 @@ export class ResponseService { method: 'PUT', url: '/api/v{api-version}/response', errors: { - 500: `Message for 500 error`, - 501: `Message for 501 error`, - 502: `Message for 502 error`, + 500: 'Message for 500 error', + 501: 'Message for 501 error', + 502: 'Message for 502 error', }, }); } @@ -831,8 +831,8 @@ export class ComplexService { parameterReference, }, errors: { - 400: `400 server error`, - 500: `500 server error`, + 400: '400 server error', + 500: '500 server error', }, }); } @@ -871,7 +871,7 @@ export class MultipartService { return __request(OpenAPI, this.http, { method: 'POST', url: '/api/v{api-version}/multipart', - formData: formData, + formData, mediaType: 'multipart/form-data', }); } @@ -904,8 +904,8 @@ export class HeaderService { url: '/api/v{api-version}/header', responseHeader: 'operation-location', errors: { - 400: `400 server error`, - 500: `500 server error`, + 400: '400 server error', + 500: '500 server error', }, }); } @@ -932,10 +932,10 @@ export class ErrorService { status, }, errors: { - 500: `Custom message: Internal Server Error`, - 501: `Custom message: Not Implemented`, - 502: `Custom message: Bad Gateway`, - 503: `Custom message: Service Unavailable`, + 500: 'Custom message: Internal Server Error', + 501: 'Custom message: Not Implemented', + 502: 'Custom message: Bad Gateway', + 503: 'Custom message: Service Unavailable', }, }); } diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_client/services.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_client/services.gen.ts.snap index fdbdd55d8..d0805d4a5 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3_client/services.gen.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_client/services.gen.ts.snap @@ -353,7 +353,7 @@ export class FormDataService { query: { parameter, }, - formData: formData, + formData, mediaType: 'multipart/form-data', }); } @@ -551,9 +551,9 @@ export class ResponseService { method: 'POST', url: '/api/v{api-version}/response', errors: { - 500: `Message for 500 error`, - 501: `Message for 501 error`, - 502: `Message for 502 error`, + 500: 'Message for 500 error', + 501: 'Message for 501 error', + 502: 'Message for 502 error', }, }); } @@ -575,9 +575,9 @@ export class ResponseService { method: 'PUT', url: '/api/v{api-version}/response', errors: { - 500: `Message for 500 error`, - 501: `Message for 501 error`, - 502: `Message for 502 error`, + 500: 'Message for 500 error', + 501: 'Message for 501 error', + 502: 'Message for 502 error', }, }); } @@ -783,8 +783,8 @@ export class ComplexService { parameterReference, }, errors: { - 400: `400 server error`, - 500: `500 server error`, + 400: '400 server error', + 500: '500 server error', }, }); } @@ -822,7 +822,7 @@ export class MultipartService { return this.httpRequest.request({ method: 'POST', url: '/api/v{api-version}/multipart', - formData: formData, + formData, mediaType: 'multipart/form-data', }); } @@ -852,8 +852,8 @@ export class HeaderService { url: '/api/v{api-version}/header', responseHeader: 'operation-location', errors: { - 400: `400 server error`, - 500: `500 server error`, + 400: '400 server error', + 500: '500 server error', }, }); } @@ -877,10 +877,10 @@ export class ErrorService { status, }, errors: { - 500: `Custom message: Internal Server Error`, - 501: `Custom message: Not Implemented`, - 502: `Custom message: Bad Gateway`, - 503: `Custom message: Service Unavailable`, + 500: 'Custom message: Internal Server Error', + 501: 'Custom message: Not Implemented', + 502: 'Custom message: Bad Gateway', + 503: 'Custom message: Service Unavailable', }, }); } diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_enums_typescript/services.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_enums_typescript/services.gen.ts.snap index 16eb117b0..0fc3d79f9 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3_enums_typescript/services.gen.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_enums_typescript/services.gen.ts.snap @@ -340,7 +340,7 @@ export class FormDataService { query: { parameter, }, - formData: formData, + formData, mediaType: 'multipart/form-data', }); } @@ -530,9 +530,9 @@ export class ResponseService { method: 'POST', url: '/api/v{api-version}/response', errors: { - 500: `Message for 500 error`, - 501: `Message for 501 error`, - 502: `Message for 502 error`, + 500: 'Message for 500 error', + 501: 'Message for 501 error', + 502: 'Message for 502 error', }, }); } @@ -554,9 +554,9 @@ export class ResponseService { method: 'PUT', url: '/api/v{api-version}/response', errors: { - 500: `Message for 500 error`, - 501: `Message for 501 error`, - 502: `Message for 502 error`, + 500: 'Message for 500 error', + 501: 'Message for 501 error', + 502: 'Message for 502 error', }, }); } @@ -746,8 +746,8 @@ export class ComplexService { parameterReference, }, errors: { - 400: `400 server error`, - 500: `500 server error`, + 400: '400 server error', + 500: '500 server error', }, }); } @@ -783,7 +783,7 @@ export class MultipartService { return __request(OpenAPI, { method: 'POST', url: '/api/v{api-version}/multipart', - formData: formData, + formData, mediaType: 'multipart/form-data', }); } @@ -815,8 +815,8 @@ export class HeaderService { url: '/api/v{api-version}/header', responseHeader: 'operation-location', errors: { - 400: `400 server error`, - 500: `500 server error`, + 400: '400 server error', + 500: '500 server error', }, }); } @@ -838,10 +838,10 @@ export class ErrorService { status, }, errors: { - 500: `Custom message: Internal Server Error`, - 501: `Custom message: Not Implemented`, - 502: `Custom message: Bad Gateway`, - 503: `Custom message: Service Unavailable`, + 500: 'Custom message: Internal Server Error', + 501: 'Custom message: Not Implemented', + 502: 'Custom message: Bad Gateway', + 503: 'Custom message: Service Unavailable', }, }); }