diff --git a/lib/operation.ts b/lib/operation.ts index c5f1574..8f6455c 100644 --- a/lib/operation.ts +++ b/lib/operation.ts @@ -201,7 +201,13 @@ export class Operation { if (content && content.length > 0) { for (const type of content) { if (type && type.mediaType) { - map.set(this.variantMethodPart(type), type); + const part = this.variantMethodPart(type); + + if (map.has(part)) { + this.logger.warn(`Overwriting variant method part '${part}' for media type '${map.get(part)?.mediaType}' by media type '${type.mediaType}'.`); + } + + map.set(part, type); } } } @@ -234,19 +240,46 @@ export class Operation { */ private variantMethodPart(content: Content | null): string { if (content) { - let type = content.mediaType.replace(/\/\*/, ''); + const keep = this.keepFullResponseMediaType(content.mediaType); + let type = content.mediaType; + type = content.mediaType.replace(/\/\*/, ''); if (type === '*' || type === 'application/octet-stream') { return '$Any'; } - type = last(type.split('/')) as string; - const plus = type.lastIndexOf('+'); - if (plus >= 0) { - type = type.substring(plus + 1); + + if (keep !== 'full') { + type = last(type.split('/')) as string; + + if (keep !== 'tail') { + const plus = type.lastIndexOf('+'); + if (plus >= 0) { + type = type.substring(plus + 1); + } + } } + return this.options.skipJsonSuffix && type === 'json' ? '' : `$${typeName(type)}`; } else { return ''; } } + /** + * Returns hint, how the expected response type in the request method names should be abbreviated. + */ + private keepFullResponseMediaType(mediaType: string) { + if (this.options.keepFullResponseMediaType === true) { + return 'full'; + } + + if (Array.isArray(this.options.keepFullResponseMediaType)) { + for (const check of this.options.keepFullResponseMediaType) { + if (check.mediaType === undefined || new RegExp(check.mediaType).test(mediaType)) { + return check.use ?? 'short'; + } + } + } + + return 'short'; + } } diff --git a/lib/options.ts b/lib/options.ts index 74509e2..077d7a7 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -126,4 +126,16 @@ export interface Options { /** List of paths to early exclude from the processing */ excludePaths?: string[]; + + /** + * When true, the expected response type in the request method names are not abbreviated and all response variants are kept. + * Default is false. + * When array is given, `mediaType` is expected to be a RegExp string matching the response media type. The first match in the array + * will decide whether or how to shorten the media type. If no mediaType is given, it will always match. + * + * 'short': application/x-spring-data-compact+json -> getEntities$Json + * 'tail': application/x-spring-data-compact+json -> getEntities$XSpringDataCompactJson + * 'full': application/x-spring-data-compact+json -> getEntities$ApplicationXSpringDataCompactJson + */ + keepFullResponseMediaType?: boolean | Array<{ mediaType?: string; use: 'full' | 'tail' | 'short' }>; } diff --git a/ng-openapi-gen-schema.json b/ng-openapi-gen-schema.json index 9e4a51e..86f3c52 100644 --- a/ng-openapi-gen-schema.json +++ b/ng-openapi-gen-schema.json @@ -222,6 +222,36 @@ "description": "When true (default) models names will be camelized, besides having the first letter capitalized. Setting to false will prevent camelizing.", "default": "true", "type": "boolean" + }, + "keepFullResponseMediaType": { + "description": "When true, the expected response type in the request method names are not abbreviated and all response variants are kept.\\nWhen array is given, `mediaType` is expected to be a RegExp string matching the response media type. The first match in the array\\nwill decide whether or how to shorten the media type. If no mediaType is given, it will always match.\\n'short': application/x-spring-data-compact+json -> getEntities$Json\\n'tail': application/x-spring-data-compact+json -> getEntities$XSpringDataCompactJson\\n'full': application/x-spring-data-compact+json -> getEntities$ApplicationXSpringDataCompactJson", + "default": "false", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "object", + "required": [ + "use" + ], + "properties": { + "mediaType": { + "type": "string" + }, + "use": { + "enum": [ + "full", + "tail", + "short" + ] + } + } + } + } + ] } } } diff --git a/test/all-operations.config.json b/test/all-operations.config.json index ad76bbb..e9f2c83 100644 --- a/test/all-operations.config.json +++ b/test/all-operations.config.json @@ -3,5 +3,17 @@ "input": "all-operations.json", "output": "out/all-operations", "defaultTag": "noTag", - "excludeParameters": ["X-Exclude"] + "excludeParameters": [ + "X-Exclude" + ], + "keepFullResponseMediaType": [ + { + "mediaType": "spring", + "use": "full" + }, + { + "mediaType": "hal\\+json", + "use": "tail" + } + ] } diff --git a/test/all-operations.json b/test/all-operations.json index 6ad4b3a..0cb31db 100644 --- a/test/all-operations.json +++ b/test/all-operations.json @@ -408,6 +408,66 @@ } } }, + "/path8": { + "get": { + "tags": [ + "tag.tag2.tag3.tag4.tag5" + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/hal+json": { + "schema": { + "type": "string" + } + }, + "application/x-spring-data-compact+json": { + "schema": { + "type": "string" + } + }, + "text/uri-list": { + "schema": { + "type": "string" + } + } + }, + "description": "OK" + } + } + }, + "post": { + "tags": [ + "tag.tag2.tag3.tag4.tag5" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/hal+json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/duplicated1": { "get": { "operationId": "duplicated", diff --git a/test/all-operations.spec.ts b/test/all-operations.spec.ts index c85ef85..c59fed6 100644 --- a/test/all-operations.spec.ts +++ b/test/all-operations.spec.ts @@ -20,7 +20,7 @@ describe('Generation tests using all-operations.json', () => { let gen: NgOpenApiGen; beforeEach(() => { - gen = new NgOpenApiGen(allOperationsSpec as OpenAPIObject, options); + gen = new NgOpenApiGen(allOperationsSpec as OpenAPIObject, options as any); gen.generate(); }); @@ -533,4 +533,54 @@ describe('Generation tests using all-operations.json', () => { const success = operation.successResponse; expect(success?.statusCode).toEqual('204'); }); + + + it('GET /path8', () => { + const optionsWithCustomizedResponseType = { ...options } as Options; + gen = new NgOpenApiGen(allOperationsSpec as OpenAPIObject, optionsWithCustomizedResponseType); + gen.generate(); + const operation = gen.operations.get('path8Get'); + expect(operation).toBeDefined(); + + if (!operation) return; + + // Assert each variant + const vars = operation.variants; + expect(vars.length).toBe(4); + + const jsonPlain = vars[0]; + expect(jsonPlain.responseType).toBe('json'); + expect(jsonPlain.methodName).toBe('path8Get$Json'); + + const halJsonPlain = vars[1]; + expect(halJsonPlain.responseType).toBe('json'); + expect(halJsonPlain.methodName).toBe('path8Get$HalJson'); + + const compactJsonPlain = vars[2]; + expect(compactJsonPlain.responseType).toBe('json'); + expect(compactJsonPlain.methodName).toBe('path8Get$ApplicationXSpringDataCompactJson'); + + const text = vars[3]; + expect(text.responseType).toBe('text'); + expect(text.methodName).toBe('path8Get$UriList'); + }); + + + it('POST /path8', () => { + const optionsWithCustomizedResponseType = { ...options } as Options; + gen = new NgOpenApiGen(allOperationsSpec as OpenAPIObject, optionsWithCustomizedResponseType); + gen.generate(); + const operation = gen.operations.get('path8Post'); + expect(operation).toBeDefined(); + expect(operation?.variants[0].responseType).toBe('json'); + + if (!operation) return; + + // Assert each variant + const vars = operation.variants; + expect(vars.length).toBe(1); + + const jsonPlain = vars[0]; + expect(jsonPlain.methodName).toBe('path8Post'); + }); });