diff --git a/README.md b/README.md index a1a812950..94609f477 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ $ openapi --help --name Custom client class name --useOptions Use options instead of arguments --useUnionTypes Use union types instead of enums + --exposeHeadersAndBody Return response headers and body --exportCore Write core files to disk (default: true) --exportServices Write services to disk (default: true) --exportModels Write models to disk (default: true) @@ -64,6 +65,7 @@ Documentation - [Client instances](docs/client-instances.md) `--name` - [Argument vs. Object style](docs/arguments-vs-object-style.md) `--useOptions` - [Enums vs. Union types](docs/enum-vs-union-types.md) `--useUnionTypes` +- [Body vs. Headers and Body style](docs/body-vs-headers-and-body-style.md) `--exposeHeadersAndBody` - [Runtime schemas](docs/runtime-schemas.md) `--exportSchemas` - [Enum with custom names and descriptions](docs/custom-enums.md) - [Nullable props (OpenAPI v2)](docs/nullable-props.md) diff --git a/bin/index.js b/bin/index.js index decf79420..b5daac2ed 100755 --- a/bin/index.js +++ b/bin/index.js @@ -16,6 +16,7 @@ const params = program .option('--name ', 'Custom client class name') .option('--useOptions', 'Use options instead of arguments') .option('--useUnionTypes', 'Use union types instead of enums') + .option('--exposeHeadersAndBody', 'Return response headers and body (default is body only') .option('--exportCore ', 'Write core files to disk', true) .option('--exportServices ', 'Write services to disk', true) .option('--exportModels ', 'Write models to disk', true) @@ -36,6 +37,7 @@ if (OpenAPI) { clientName: params.name, useOptions: params.useOptions, useUnionTypes: params.useUnionTypes, + exposeHeadersAndBody: params.exposeHeadersAndBody, exportCore: JSON.parse(params.exportCore) === true, exportServices: JSON.parse(params.exportServices) === true, exportModels: JSON.parse(params.exportModels) === true, diff --git a/docs/basic-usage.md b/docs/basic-usage.md index 5b110f2ac..dfb5a7207 100644 --- a/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -13,6 +13,7 @@ $ openapi --help --name Custom client class name --useOptions Use options instead of arguments --useUnionTypes Use union types instead of enums + --exposeHeadersAndBody Return response headers and body --exportCore Write core files to disk (default: true) --exportServices Write services to disk (default: true) --exportModels Write models to disk (default: true) diff --git a/docs/body-vs-headers-and-body-style.md b/docs/body-vs-headers-and-body-style.md new file mode 100644 index 000000000..571c72d24 --- /dev/null +++ b/docs/body-vs-headers-and-body-style.md @@ -0,0 +1,24 @@ +# Body vs. Headers and Body style + +**Flag:** `--exposeHeadersAndBody` + +By default, the OpenAPI generator creates service functions that return the response body (or a single response header). + +```typescript +public static createUser( + requestBody?: User, +): CancelablePromise { + // ... +} +``` + +Alternatively, use the flag `--exposeHeadersAndBody` to generate service functions that return the response headers and +the response body. + +```typescript +public static createUser( + requestBody?: User, +): CancelablePromise<{ headers: Record; body: User; }> { + // ... +} +``` diff --git a/src/index.ts b/src/index.ts index ef7a8b1bf..f0ecc7f8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ export type Options = { clientName?: string; useOptions?: boolean; useUnionTypes?: boolean; + exposeHeadersAndBody?: boolean; exportCore?: boolean; exportServices?: boolean; exportModels?: boolean; @@ -39,6 +40,7 @@ export type Options = { * @param clientName Custom client class name * @param useOptions Use options or arguments functions * @param useUnionTypes Use union types instead of enums + * @param exposeHeadersAndBody Return response headers and body (default is body only) * @param exportCore Generate core client classes * @param exportServices Generate services * @param exportModels Generate models @@ -55,6 +57,7 @@ export const generate = async ({ clientName, useOptions = false, useUnionTypes = false, + exposeHeadersAndBody = false, exportCore = true, exportServices = true, exportModels = true, @@ -84,6 +87,7 @@ export const generate = async ({ httpClient, useOptions, useUnionTypes, + exposeHeadersAndBody, exportCore, exportServices, exportModels, @@ -107,6 +111,7 @@ export const generate = async ({ httpClient, useOptions, useUnionTypes, + exposeHeadersAndBody, exportCore, exportServices, exportModels, diff --git a/src/templates/core/BaseHttpRequest.hbs b/src/templates/core/BaseHttpRequest.hbs index 032b9fff9..ae1f3e674 100644 --- a/src/templates/core/BaseHttpRequest.hbs +++ b/src/templates/core/BaseHttpRequest.hbs @@ -28,7 +28,11 @@ export class BaseHttpRequest { throw new Error('Not Implemented'); } {{else}} + {{#if @root.exposeHeadersAndBody}} + public request(options: ApiRequestOptions): CancelablePromise<{ headers: Record; body: T; }> { + {{else}} public request(options: ApiRequestOptions): CancelablePromise { + {{/if}} throw new Error('Not Implemented'); } {{/equals}} diff --git a/src/templates/core/HttpRequest.hbs b/src/templates/core/HttpRequest.hbs index e1620a3c0..110d01bd0 100644 --- a/src/templates/core/HttpRequest.hbs +++ b/src/templates/core/HttpRequest.hbs @@ -51,10 +51,18 @@ export class {{httpRequest}} extends BaseHttpRequest { /** * Request method * @param options The request options from the service + {{#if @root.exposeHeadersAndBody}} + * @returns CancelablePromise<{ headers: Record; body: T; }> + {{else}} * @returns CancelablePromise + {{/if}} * @throws ApiError */ + {{#if @root.exposeHeadersAndBody}} + public override request(options: ApiRequestOptions): CancelablePromise<{ headers: Record; body: T; }> { + {{else}} public override request(options: ApiRequestOptions): CancelablePromise { + {{/if}} return __request(this.config, options); } {{/equals}} diff --git a/src/templates/core/fetch/request.hbs b/src/templates/core/fetch/request.hbs index 4af6f9440..f5bb12d09 100644 --- a/src/templates/core/fetch/request.hbs +++ b/src/templates/core/fetch/request.hbs @@ -59,10 +59,21 @@ import type { OpenAPIConfig } from './OpenAPI'; * Request method * @param config The OpenAPI configuration object * @param options The request options from the service + {{#if @root.exposeHeadersAndBody}} + * @returns CancelablePromise<{ headers: Record; body: T; }> + {{else}} * @returns CancelablePromise + {{/if}} * @throws ApiError */ +{{#if @root.exposeHeadersAndBody}} +export const request = (config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<{ + headers: Record; + body: T; +}> => { +{{else}} export const request = (config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise => { +{{/if}} return new CancelablePromise(async (resolve, reject, onCancel) => { try { const url = getUrl(config, options); @@ -73,19 +84,34 @@ export const request = (config: OpenAPIConfig, options: ApiRequestOptions): C if (!onCancel.isCancelled) { const response = await sendRequest(config, options, url, body, formData, headers, onCancel); const responseBody = await getResponseBody(response); + {{#if @root.exposeHeadersAndBody}} + const responseHeaders: Record = {}; + for (const header of Array.from(response.headers.keys())) { + responseHeaders[header] = response.headers.get(header) ?? ''; + } + {{else}} const responseHeader = getResponseHeader(response, options.responseHeader); + {{/if}} const result: ApiResult = { url, ok: response.ok, status: response.status, statusText: response.statusText, + {{#if @root.exposeHeadersAndBody}} + body: responseBody, + {{else}} body: responseHeader ?? responseBody, + {{/if}} }; catchErrorCodes(options, result); + {{#if @root.exposeHeadersAndBody}} + resolve({ headers: responseHeaders, body: result.body }); + {{else}} resolve(result.body); + {{/if}} } } catch (error) { reject(error); diff --git a/src/templates/core/node/request.hbs b/src/templates/core/node/request.hbs index 8405467d5..b58a73a6c 100644 --- a/src/templates/core/node/request.hbs +++ b/src/templates/core/node/request.hbs @@ -63,10 +63,21 @@ import type { OpenAPIConfig } from './OpenAPI'; * Request method * @param config The OpenAPI configuration object * @param options The request options from the service + {{#if @root.exposeHeadersAndBody}} + * @returns CancelablePromise<{ headers: Record; body: T; }> + {{else}} * @returns CancelablePromise + {{/if}} * @throws ApiError */ +{{#if @root.exposeHeadersAndBody}} +export const request = (config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<{ + headers: Record; + body: T; +}> => { +{{else}} export const request = (config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise => { +{{/if}} return new CancelablePromise(async (resolve, reject, onCancel) => { try { const url = getUrl(config, options); @@ -77,19 +88,34 @@ export const request = (config: OpenAPIConfig, options: ApiRequestOptions): C if (!onCancel.isCancelled) { const response = await sendRequest(options, url, body, formData, headers, onCancel); const responseBody = await getResponseBody(response); + {{#if @root.exposeHeadersAndBody}} + const responseHeaders: Record = {}; + for (const header of Array.from(response.headers.keys())) { + responseHeaders[header] = response.headers.get(header) ?? ''; + } + {{else}} const responseHeader = getResponseHeader(response, options.responseHeader); + {{/if}} const result: ApiResult = { url, ok: response.ok, status: response.status, statusText: response.statusText, + {{#if @root.exposeHeadersAndBody}} + body: responseBody, + {{else}} body: responseHeader ?? responseBody, + {{/if}} }; catchErrorCodes(options, result); + {{#if @root.exposeHeadersAndBody}} + resolve({ headers: responseHeaders, body: result.body }); + {{else}} resolve(result.body); + {{/if}} } } catch (error) { reject(error); diff --git a/src/templates/core/xhr/request.hbs b/src/templates/core/xhr/request.hbs index 47f92870b..9c9924aed 100644 --- a/src/templates/core/xhr/request.hbs +++ b/src/templates/core/xhr/request.hbs @@ -62,10 +62,21 @@ import type { OpenAPIConfig } from './OpenAPI'; * Request method * @param config The OpenAPI configuration object * @param options The request options from the service + {{#if @root.exposeHeadersAndBody}} + * @returns CancelablePromise<{ headers: Record; body: T; }> + {{else}} * @returns CancelablePromise + {{/if}} * @throws ApiError */ +{{#if @root.exposeHeadersAndBody}} +export const request = (config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<{ + headers: Record; + body: T; +}> => { +{{else}} export const request = (config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise => { +{{/if}} return new CancelablePromise(async (resolve, reject, onCancel) => { try { const url = getUrl(config, options); @@ -76,19 +87,36 @@ export const request = (config: OpenAPIConfig, options: ApiRequestOptions): C if (!onCancel.isCancelled) { const response = await sendRequest(config, options, url, body, formData, headers, onCancel); const responseBody = getResponseBody(response); + {{#if @root.exposeHeadersAndBody}} + const responseHeaders: Record = {}; + const allResponseHeaders = response.getAllResponseHeaders() ?? ''; + allResponseHeaders.split(/[\r\n]+/).forEach(line => { + const nv = line.split(': ', 2); + responseHeaders[nv[0]] = nv[1]; + }); + {{else}} const responseHeader = getResponseHeader(response, options.responseHeader); + {{/if}} const result: ApiResult = { url, ok: isSuccess(response.status), status: response.status, statusText: response.statusText, + {{#if @root.exposeHeadersAndBody}} + body: responseBody, + {{else}} body: responseHeader ?? responseBody, + {{/if}} }; catchErrorCodes(options, result); + {{#if @root.exposeHeadersAndBody}} + resolve({ headers: responseHeaders, body: result.body }); + {{else}} resolve(result.body); + {{/if}} } } catch (error) { reject(error); diff --git a/src/templates/exportService.hbs b/src/templates/exportService.hbs index 2fdd9af58..86af6ef77 100644 --- a/src/templates/exportService.hbs +++ b/src/templates/exportService.hbs @@ -73,7 +73,11 @@ export class {{{name}}}{{{@root.postfix}}} { public {{{name}}}({{>parameters}}): Observable<{{>result}}> { return this.httpRequest.request({ {{else}} + {{#if @root.exposeHeadersAndBody}} + public {{{name}}}({{>parameters}}): CancelablePromise<{ headers: Record; body: {{>result}}; }> { + {{else}} public {{{name}}}({{>parameters}}): CancelablePromise<{{>result}}> { + {{/if}} return this.httpRequest.request({ {{/equals}} {{else}} @@ -81,7 +85,11 @@ export class {{{name}}}{{{@root.postfix}}} { public {{{name}}}({{>parameters}}): Observable<{{>result}}> { return __request(OpenAPI, this.http, { {{else}} + {{#if @root.exposeHeadersAndBody}} + public static {{{name}}}({{>parameters}}): CancelablePromise<{ headers: Record; body: {{>result}}; }> { + {{else}} public static {{{name}}}({{>parameters}}): CancelablePromise<{{>result}}> { + {{/if}} return __request(OpenAPI, { {{/equals}} {{/if}} diff --git a/src/utils/writeClient.spec.ts b/src/utils/writeClient.spec.ts index 3c06a95a5..2bfc3e284 100644 --- a/src/utils/writeClient.spec.ts +++ b/src/utils/writeClient.spec.ts @@ -43,6 +43,7 @@ describe('writeClient', () => { HttpClient.FETCH, false, false, + false, true, true, true, diff --git a/src/utils/writeClient.ts b/src/utils/writeClient.ts index a0ffc1821..e3e6b3e6f 100644 --- a/src/utils/writeClient.ts +++ b/src/utils/writeClient.ts @@ -22,6 +22,7 @@ import { writeClientServices } from './writeClientServices'; * @param httpClient The selected httpClient (fetch, xhr, node or axios) * @param useOptions Use options or arguments functions * @param useUnionTypes Use union types instead of enums + * @param exposeHeadersAndBody Return response headers and body (default is body only) * @param exportCore Generate core client classes * @param exportServices Generate services * @param exportModels Generate models @@ -39,6 +40,7 @@ export const writeClient = async ( httpClient: HttpClient, useOptions: boolean, useUnionTypes: boolean, + exposeHeadersAndBody: boolean, exportCore: boolean, exportServices: boolean, exportModels: boolean, @@ -61,7 +63,7 @@ export const writeClient = async ( if (exportCore) { await rmdir(outputPathCore); await mkdir(outputPathCore); - await writeClientCore(client, templates, outputPathCore, httpClient, indent, clientName, request); + await writeClientCore(client, templates, outputPathCore, httpClient, exposeHeadersAndBody, indent, clientName, request); } if (exportServices) { @@ -74,6 +76,7 @@ export const writeClient = async ( httpClient, useUnionTypes, useOptions, + exposeHeadersAndBody, indent, postfix, clientName diff --git a/src/utils/writeClientCore.spec.ts b/src/utils/writeClientCore.spec.ts index cb2cc2925..02a4c7da0 100644 --- a/src/utils/writeClientCore.spec.ts +++ b/src/utils/writeClientCore.spec.ts @@ -36,7 +36,7 @@ describe('writeClientCore', () => { }, }; - await writeClientCore(client, templates, '/', HttpClient.FETCH, Indent.SPACE_4); + await writeClientCore(client, templates, '/', HttpClient.FETCH, false, Indent.SPACE_4); expect(writeFile).toBeCalledWith('/OpenAPI.ts', 'settings'); expect(writeFile).toBeCalledWith('/ApiError.ts', 'apiError'); diff --git a/src/utils/writeClientCore.ts b/src/utils/writeClientCore.ts index 6d35849d2..68c1a2707 100644 --- a/src/utils/writeClientCore.ts +++ b/src/utils/writeClientCore.ts @@ -15,6 +15,7 @@ import type { Templates } from './registerHandlebarTemplates'; * @param templates The loaded handlebar templates * @param outputPath Directory to write the generated files to * @param httpClient The selected httpClient (fetch, xhr, node or axios) + * @param exposeHeadersAndBody Return response headers and body (default is body only) * @param indent Indentation options (4, 2 or tab) * @param clientName Custom client class name * @param request Path to custom request file @@ -24,6 +25,7 @@ export const writeClientCore = async ( templates: Templates, outputPath: string, httpClient: HttpClient, + exposeHeadersAndBody: boolean, indent: Indent, clientName?: string, request?: string @@ -33,6 +35,7 @@ export const writeClientCore = async ( httpClient, clientName, httpRequest, + exposeHeadersAndBody, server: client.server, version: client.version, }; diff --git a/src/utils/writeClientServices.spec.ts b/src/utils/writeClientServices.spec.ts index 38a649483..ba1af1d12 100644 --- a/src/utils/writeClientServices.spec.ts +++ b/src/utils/writeClientServices.spec.ts @@ -37,7 +37,7 @@ describe('writeClientServices', () => { }, }; - await writeClientServices(services, templates, '/', HttpClient.FETCH, false, false, Indent.SPACE_4, 'Service'); + await writeClientServices(services, templates, '/', HttpClient.FETCH, false, false, false, Indent.SPACE_4, 'Service'); expect(writeFile).toBeCalledWith('/UserService.ts', 'service'); }); diff --git a/src/utils/writeClientServices.ts b/src/utils/writeClientServices.ts index 2f95341d2..b55a72f68 100644 --- a/src/utils/writeClientServices.ts +++ b/src/utils/writeClientServices.ts @@ -17,6 +17,7 @@ import type { Templates } from './registerHandlebarTemplates'; * @param httpClient The selected httpClient (fetch, xhr, node or axios) * @param useUnionTypes Use union types instead of enums * @param useOptions Use options or arguments functions + * @param exposeHeadersAndBody Return response headers and body (default is body only) * @param indent Indentation options (4, 2 or tab) * @param postfix Service name postfix * @param clientName Custom client class name @@ -28,6 +29,7 @@ export const writeClientServices = async ( httpClient: HttpClient, useUnionTypes: boolean, useOptions: boolean, + exposeHeadersAndBody: boolean, indent: Indent, postfix: string, clientName?: string @@ -39,6 +41,7 @@ export const writeClientServices = async ( httpClient, useUnionTypes, useOptions, + exposeHeadersAndBody, postfix, exportClient: isDefined(clientName), });