Skip to content

Commit 1689d7b

Browse files
Typescript-Angular: Fix several query parameters serialization issues (#22459)
* Generate skeleton for new integration test * Typescript-angular: Move query param deep-object test * typescript-angular: Add query param JSON test * Typescript-angular: Add query param form test * Test for #20998 * typescript-angular: Reimplement query param serialisation This notably fixes: - JSON query param serialisation - array serialisation with style=form and explode=true As the class HttpParams from Angular is specially designed for the mimetype: `application/x-www-form-urlencoded` it does not support the range of query parameters defined by the OpenAPI specification. To workaround this issue, this patch introduces a custom `OpenAPIHttpParams` class which supports a wider range of query param styles. Note that as `HttpClient` is used afterwards, the class `OpenApiHttpParams` has a method to convert it into a `HttpParams` from Angular with a no-op HttpParameterCodec to avoid double serialisation of the query parameters. * update samples --------- Co-authored-by: Vladimir Svoboda <[email protected]>
1 parent 08858a9 commit 1689d7b

File tree

254 files changed

+19753
-1290
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

254 files changed

+19753
-1290
lines changed

.github/workflows/samples-typescript-client.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
paths:
66
- samples/client/others/typescript-angular/**
7+
- samples/client/others/typescript-angular-v20/**
78
# comment out angular released before Nov 2023
89
#- samples/client/petstore/typescript-angular-v12-provided-in-root/**
910
#- samples/client/petstore/typescript-angular-v13-provided-in-root/**
@@ -43,6 +44,7 @@ on:
4344
pull_request:
4445
paths:
4546
- samples/client/others/typescript-angular/**
47+
- samples/client/others/typescript-angular-v20/**
4648
#- samples/client/petstore/typescript-angular-v12-provided-in-root/**
4749
#- samples/client/petstore/typescript-angular-v13-provided-in-root/**
4850
#- samples/client/petstore/typescript-angular-v14-provided-in-root/**
@@ -92,6 +94,7 @@ jobs:
9294
- "20.x"
9395
sample:
9496
- samples/client/others/typescript-angular/
97+
- samples/client/others/typescript-angular-v20/
9598
#- samples/client/petstore/typescript-angular-v12-provided-in-root/
9699
#- samples/client/petstore/typescript-angular-v13-provided-in-root/
97100
#- samples/client/petstore/typescript-angular-v14-provided-in-root/

bin/configs/typescript-angular-v19-deep-object.yaml

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
generatorName: typescript-angular
2+
outputDir: samples/client/others/typescript-angular-v20/builds/query-param-deep-object
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/query-param-deep-object.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/typescript-angular
5+
additionalProperties:
6+
ngVersion: 20.0.0
7+
npmName: sample-angular-20-0-0-query-param-deep-object
8+
supportsES6: true
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
generatorName: typescript-angular
2+
outputDir: samples/client/others/typescript-angular-v20/builds/query-param-form
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/query-param-form.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/typescript-angular
5+
additionalProperties:
6+
ngVersion: 20.0.0
7+
npmName: sample-angular-20-0-0-query-param-form
8+
supportsES6: true
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
generatorName: typescript-angular
2+
outputDir: samples/client/others/typescript-angular-v20/builds/query-param-json
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/query-param-json.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/typescript-angular
5+
additionalProperties:
6+
ngVersion: 20.0.0
7+
npmName: sample-angular-20-0-0-query-param-json
8+
supportsES6: true

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptAngularClientCodegen.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ public void processOpts() {
204204
supportingFiles.add(new SupportingFile("param.mustache", getIndexDirectory(), "param.ts"));
205205
supportingFiles.add(new SupportingFile("gitignore", "", ".gitignore"));
206206
supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh"));
207+
supportingFiles.add(new SupportingFile("queryParams.mustache", getIndexDirectory(), "query.params.ts"));
207208

208209
if(ngVersionAtLeast_17) {
209210
supportingFiles.add(new SupportingFile("README.mustache", getIndexDirectory(), "README.md"));

modules/openapi-generator/src/main/resources/typescript-angular/api.base.service.mustache

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { HttpHeaders, HttpParams, HttpParameterCodec } from '@angular/common/http';
33
import { CustomHttpParameterCodec } from './encoder';
44
import { {{configurationClassName}} } from './configuration';
5+
import { OpenApiHttpParams, QueryParamStyle, concatHttpParamsObject} from './query.params';
56

67
export class BaseService {
78
protected basePath = '{{{basePath}}}';
@@ -29,47 +30,58 @@ export class BaseService {
2930
return consumes.indexOf('multipart/form-data') !== -1;
3031
}
3132

32-
protected addToHttpParams(httpParams: HttpParams, value: any, key?: string, isDeep: boolean = false): HttpParams {
33-
// If the value is an object (but not a Date), recursively add its keys.
34-
if (typeof value === 'object' && !(value instanceof Date)) {
35-
return this.addToHttpParamsRecursive(httpParams, value, isDeep ? key : undefined, isDeep);
36-
}
37-
return this.addToHttpParamsRecursive(httpParams, value, key);
38-
}
39-
40-
protected addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string, isDeep: boolean = false): HttpParams {
33+
protected addToHttpParams(httpParams: OpenApiHttpParams, key: string, value: any | null | undefined, paramStyle: QueryParamStyle, explode: boolean): OpenApiHttpParams {
4134
if (value === null || value === undefined) {
4235
return httpParams;
4336
}
44-
if (typeof value === 'object') {
45-
// If JSON format is preferred, key must be provided.
46-
if (key != null) {
47-
return isDeep
48-
? Object.keys(value as Record<string, any>).reduce(
49-
(hp, k) => hp.append(`${key}[${k}]`, value[k]),
50-
httpParams,
51-
)
52-
: httpParams.append(key, JSON.stringify(value));
37+
38+
if (paramStyle === QueryParamStyle.DeepObject) {
39+
if (typeof value !== 'object') {
40+
throw Error(`An object must be provided for key ${key} as it is a deep object`);
5341
}
54-
// Otherwise, if it's an array, add each element.
55-
if (Array.isArray(value)) {
56-
value.forEach(elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key));
42+
43+
return Object.keys(value as Record<string, any>).reduce(
44+
(hp, k) => hp.append(`${key}[${k}]`, value[k]),
45+
httpParams,
46+
);
47+
} else if (paramStyle === QueryParamStyle.Json) {
48+
return httpParams.append(key, JSON.stringify(value));
49+
} else {
50+
// Form-style, SpaceDelimited or PipeDelimited
51+
52+
if (Object(value) !== value) {
53+
// If it is a primitive type, add its string representation
54+
return httpParams.append(key, value.toString());
5755
} else if (value instanceof Date) {
58-
if (key != null) {
59-
httpParams = httpParams.append(key, value.toISOString());
56+
return httpParams.append(key, value.toISOString());
57+
} else if (Array.isArray(value)) {
58+
// Otherwise, if it's an array, add each element.
59+
if (paramStyle === QueryParamStyle.Form) {
60+
return httpParams.set(key, value, {explode: explode, delimiter: ','});
61+
} else if (paramStyle === QueryParamStyle.SpaceDelimited) {
62+
return httpParams.set(key, value, {explode: explode, delimiter: ' '});
6063
} else {
61-
throw Error("key may not be null if value is Date");
64+
// PipeDelimited
65+
return httpParams.set(key, value, {explode: explode, delimiter: '|'});
6266
}
6367
} else {
64-
Object.keys(value).forEach(k => {
65-
const paramKey = key ? `${key}.${k}` : k;
66-
httpParams = this.addToHttpParamsRecursive(httpParams, value[k], paramKey);
67-
});
68+
// Otherwise, if it's an object, add each field.
69+
if (paramStyle === QueryParamStyle.Form) {
70+
if (explode) {
71+
Object.keys(value).forEach(k => {
72+
httpParams = this.addToHttpParams(httpParams, k, value[k], paramStyle, explode);
73+
});
74+
return httpParams;
75+
} else {
76+
return concatHttpParamsObject(httpParams, key, value, ',');
77+
}
78+
} else if (paramStyle === QueryParamStyle.SpaceDelimited) {
79+
return concatHttpParamsObject(httpParams, key, value, ' ');
80+
} else {
81+
// PipeDelimited
82+
return concatHttpParamsObject(httpParams, key, value, '|');
83+
}
6884
}
69-
return httpParams;
70-
} else if (key != null) {
71-
return httpParams.append(key, value);
7285
}
73-
throw Error("key may not be null if value is not object or array");
7486
}
7587
}

modules/openapi-generator/src/main/resources/typescript-angular/api.service.mustache

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
import { Inject, Injectable, Optional } from '@angular/core';
55
import { HttpClient, HttpHeaders, HttpParams,
6-
HttpResponse, HttpEvent, HttpParameterCodec{{#httpContextInOptions}}, HttpContext {{/httpContextInOptions}}
6+
HttpResponse, HttpEvent{{#httpContextInOptions}}, HttpContext {{/httpContextInOptions}}
77
} from '@angular/common/http';
8-
import { CustomHttpParameterCodec } from '../encoder';
98
import { Observable } from 'rxjs';
9+
import { OpenApiHttpParams, QueryParamStyle } from '../query.params';
1010

1111
{{#imports}}
1212
// @ts-ignore
@@ -87,6 +87,7 @@ export class {{classname}} extends BaseService {
8787
{{/useSingleRequestParameter}}
8888
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
8989
* @param reportProgress flag to report request and response progress.
90+
* @param options additional options
9091
{{#isDeprecated}}
9192
* @deprecated
9293
{{/isDeprecated}}
@@ -107,32 +108,46 @@ export class {{classname}} extends BaseService {
107108
{{/allParams}}
108109

109110
{{#hasQueryParamsOrAuth}}
110-
let localVarQueryParameters = new HttpParams({encoder: this.encoder});
111+
let localVarQueryParameters = new OpenApiHttpParams(this.encoder);
111112
{{#queryParams}}
112-
{{#isArray}}
113-
if ({{paramName}}) {
114-
{{#isQueryParamObjectFormatJson}}
115-
localVarQueryParameters = this.addToHttpParams(localVarQueryParameters,
116-
<any>{{paramName}}, '{{baseName}}');
117-
{{/isQueryParamObjectFormatJson}}
118-
{{^isQueryParamObjectFormatJson}}
119-
{{#isCollectionFormatMulti}}
120-
{{paramName}}.forEach((element) => {
121-
localVarQueryParameters = this.addToHttpParams(localVarQueryParameters,
122-
<any>element, '{{baseName}}');
123-
})
124-
{{/isCollectionFormatMulti}}
125-
{{^isCollectionFormatMulti}}
126-
localVarQueryParameters = this.addToHttpParams(localVarQueryParameters,
127-
[...{{paramName}}].join(COLLECTION_FORMATS['{{collectionFormat}}']), '{{baseName}}');
128-
{{/isCollectionFormatMulti}}
129-
{{/isQueryParamObjectFormatJson}}
130-
}
131-
{{/isArray}}
132-
{{^isArray}}
133-
localVarQueryParameters = this.addToHttpParams(localVarQueryParameters,
134-
<any>{{paramName}}, '{{baseName}}'{{#isDeepObject}}, true{{/isDeepObject}});
135-
{{/isArray}}
113+
114+
localVarQueryParameters = this.addToHttpParams(
115+
localVarQueryParameters,
116+
'{{baseName}}',
117+
<any>{{paramName}},
118+
{{#isQueryParamObjectFormatJson}}
119+
QueryParamStyle.Json,
120+
{{/isQueryParamObjectFormatJson}}
121+
{{^isQueryParamObjectFormatJson}}
122+
{{^style}}
123+
{{#queryIsJsonMimeType}}
124+
QueryParamStyle.Json,
125+
{{/queryIsJsonMimeType}}
126+
{{^queryIsJsonMimeType}}
127+
QueryParamStyle.Form,
128+
{{/queryIsJsonMimeType}}
129+
{{/style}}
130+
{{#style}}
131+
{{#isDeepObject}}
132+
QueryParamStyle.DeepObject,
133+
{{/isDeepObject}}
134+
{{#isFormStyle}}
135+
QueryParamStyle.Form,
136+
{{/isFormStyle}}
137+
{{#isSpaceDelimited}}
138+
QueryParamStyle.SpaceDelimited,
139+
{{/isSpaceDelimited}}
140+
{{#isPipeDelimited}}
141+
QueryParamStyle.PipeDelimited,
142+
{{/isPipeDelimited}}
143+
{{#queryIsJsonMimeType}}
144+
QueryParamStyle.Json,
145+
{{/queryIsJsonMimeType}}
146+
{{/style}}
147+
{{/isQueryParamObjectFormatJson}}
148+
{{isExplode}},
149+
);
150+
136151
{{/queryParams}}
137152

138153
{{/hasQueryParamsOrAuth}}
@@ -291,7 +306,7 @@ export class {{classname}} extends BaseService {
291306
{{/hasFormParams}}
292307
{{/bodyParam}}
293308
{{#hasQueryParamsOrAuth}}
294-
params: localVarQueryParameters,
309+
params: localVarQueryParameters.toHttpParams(),
295310
{{/hasQueryParamsOrAuth}}
296311
{{#isResponseFile}}
297312
responseType: "blob",

modules/openapi-generator/src/main/resources/typescript-angular/encoder.mustache

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,18 @@ export class CustomHttpParameterCodec implements HttpParameterCodec {
1818
return decodeURIComponent(v);
1919
}
2020
}
21+
22+
export class IdentityHttpParameterCodec implements HttpParameterCodec {
23+
encodeKey(k: string): string {
24+
return k;
25+
}
26+
encodeValue(v: string): string {
27+
return v;
28+
}
29+
decodeKey(k: string): string {
30+
return k;
31+
}
32+
decodeValue(v: string): string {
33+
return v;
34+
}
35+
}

0 commit comments

Comments
 (0)