Skip to content

Commit 4bea470

Browse files
committed
Continue implementing OpenAPI 3.2.0
1 parent 093eadb commit 4bea470

File tree

5 files changed

+333
-318
lines changed

5 files changed

+333
-318
lines changed

src/service/emit/service/service-method.generator.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,6 @@ console.warn('The following querystring parameters are not automatically handled
235235
const nonBodyOpParams = new Set((operation.parameters ?? []).map(p => camelCase(p.name)));
236236
const bodyParam = parameters.find(p => !nonBodyOpParams.has(p.name!));
237237

238-
// --- DETECT CONTENT TYPE FOR MULTIPART / URLENCODED ---
239238
const hasMultipartContent = !!operation.requestBody?.content?.['multipart/form-data'];
240239
const hasUrlEncodedContent = !!operation.requestBody?.content?.['application/x-www-form-urlencoded'];
241240

@@ -244,7 +243,9 @@ console.warn('The following querystring parameters are not automatically handled
244243
const formDataParams = operation.parameters?.filter(p => (p as { in?: string }).in === 'formData');
245244

246245
const multipartContent = operation.requestBody?.content?.['multipart/form-data'];
246+
const urlEncodedContent = operation.requestBody?.content?.['application/x-www-form-urlencoded'];
247247
const hasOas3MultipartBody = isMultipartForm && !!bodyParam && !!multipartContent;
248+
const hasOas3UrlEncodedBody = isUrlEncodedForm && !!bodyParam && !!urlEncodedContent;
248249

249250
if (isUrlEncodedForm && formDataParams?.length) {
250251
lines.push(`let formBody = new HttpParams();`);
@@ -253,6 +254,15 @@ console.warn('The following querystring parameters are not automatically handled
253254
lines.push(`if (${paramName} != null) { formBody = formBody.append('${p.name}', ${paramName}); }`);
254255
});
255256
bodyArgument = 'formBody';
257+
} else if (hasOas3UrlEncodedBody) {
258+
// OAS 3.0 UrlEncoded Body Handling
259+
const bodyName = bodyParam!.name;
260+
const encodings = urlEncodedContent!.encoding || {};
261+
const encodingMapString = JSON.stringify(encodings);
262+
// We use a builder method to handle style/explode per property rules on the body object
263+
lines.push(`const formBody = HttpParamsBuilder.serializeUrlEncodedBody(${bodyName}, ${encodingMapString});`);
264+
bodyArgument = 'formBody';
265+
256266
} else if (isMultipartForm && formDataParams?.length) {
257267
lines.push(`const formData = new FormData();`);
258268
formDataParams.forEach(p => {
@@ -273,10 +283,11 @@ console.warn('The following querystring parameters are not automatically handled
273283

274284
lines.push(` Object.entries(${bodyName}).forEach(([key, value]) => {`);
275285
lines.push(` if (value === undefined || value === null) return;`);
276-
// Check for specific encoding first
277286
lines.push(` const encoding = encodings[key];`);
278287
lines.push(` if (encoding?.contentType) {`);
279-
lines.push(` const blob = new Blob([JSON.stringify(value)], { type: encoding.contentType });`);
288+
// Improved Content-Type Handling: Not everything is JSON.
289+
lines.push(` const content = encoding.contentType.includes('application/json') ? JSON.stringify(value) : String(value);`);
290+
lines.push(` const blob = new Blob([content], { type: encoding.contentType });`);
280291
lines.push(` formData.append(key, blob);`);
281292
lines.push(` } else {`);
282293
// Standard append

src/service/emit/utility/http-params-builder.ts

Lines changed: 114 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,34 @@ export class HttpParamsBuilderGenerator {
2222
{
2323
namedImports: ["HttpParams"],
2424
moduleSpecifier: "@angular/common/http",
25-
},
26-
{
27-
namedImports: ["Parameter"],
28-
moduleSpecifier: "../models",
29-
},
25+
}
3026
]);
3127

28+
sourceFile.addInterface({
29+
name: "ParameterStub",
30+
isExported: true,
31+
properties: [
32+
{ name: "name", type: "string" },
33+
{ name: "in", type: "string" },
34+
{ name: "style", type: "string", hasQuestionToken: true },
35+
{ name: "explode", type: "boolean", hasQuestionToken: true },
36+
{ name: "allowEmptyValue", type: "boolean", hasQuestionToken: true },
37+
{ name: "content", type: "Record<string, any>", hasQuestionToken: true },
38+
{ name: "schema", type: "any", hasQuestionToken: true }
39+
]
40+
});
41+
42+
sourceFile.addInterface({
43+
name: "EncodingConfig",
44+
isExported: true,
45+
properties: [
46+
{ name: "contentType", type: "string", hasQuestionToken: true },
47+
{ name: "style", type: "string", hasQuestionToken: true },
48+
{ name: "explode", type: "boolean", hasQuestionToken: true },
49+
{ name: "allowReserved", type: "boolean", hasQuestionToken: true }
50+
]
51+
});
52+
3253
const classDeclaration = sourceFile.addClass({
3354
name: "HttpParamsBuilder",
3455
isExported: true,
@@ -41,7 +62,7 @@ export class HttpParamsBuilderGenerator {
4162
scope: Scope.Public,
4263
parameters: [
4364
{ name: "params", type: "HttpParams" },
44-
{ name: "parameter", type: "Parameter" },
65+
{ name: "parameter", type: "ParameterStub" },
4566
{ name: "value", type: "any" },
4667
],
4768
returnType: "HttpParams",
@@ -55,6 +76,25 @@ export class HttpParamsBuilderGenerator {
5576
statements: this.getSerializeQueryParamBody(),
5677
});
5778

79+
classDeclaration.addMethod({
80+
name: "serializeUrlEncodedBody",
81+
isStatic: true,
82+
scope: Scope.Public,
83+
parameters: [
84+
{ name: "body", type: "any" },
85+
{ name: "encodings", type: "Record<string, EncodingConfig>", initializer: "{}" }
86+
],
87+
returnType: "HttpParams",
88+
docs: [
89+
"Serializes a body object into HttpParams for application/x-www-form-urlencoded requests.",
90+
"Applies OpenAPI 'encoding' rules (style, explode) per property.",
91+
"@param body The object to serialize.",
92+
"@param encodings A map of property names to encoding configurations.",
93+
"@returns An HttpParams object representing the form body."
94+
],
95+
statements: this.getSerializeUrlEncodedBodyBody()
96+
});
97+
5898
classDeclaration.addMethod({
5999
name: "serializePathParam",
60100
isStatic: true,
@@ -165,6 +205,36 @@ return encodeURIComponent(value);
165205
`
166206
});
167207

208+
// Add private helper for recursive deepObject serialization
209+
classDeclaration.addMethod({
210+
name: "appendDeepObject",
211+
isStatic: true,
212+
scope: Scope.Private,
213+
parameters: [
214+
{ name: "params", type: "HttpParams" },
215+
{ name: "key", type: "string" },
216+
{ name: "value", type: "any" }
217+
],
218+
returnType: "HttpParams",
219+
statements: `
220+
if (value === null || value === undefined) {
221+
return params;
222+
}
223+
224+
if (Array.isArray(value)) {
225+
value.forEach((item, index) => {
226+
params = this.appendDeepObject(params, \`\${key}[\${index}]\`, item);
227+
});
228+
} else if (typeof value === 'object' && !(value instanceof Date)) {
229+
Object.entries(value).forEach(([prop, val]) => {
230+
params = this.appendDeepObject(params, \`\${key}[\${prop}]\`, val);
231+
});
232+
} else {
233+
params = params.append(key, this.formatValue(value));
234+
}
235+
return params;`
236+
});
237+
168238
sourceFile.formatText();
169239
}
170240

@@ -176,10 +246,10 @@ return encodeURIComponent(value);
176246
177247
const name = parameter.name;
178248
179-
// Handle OAS 3.2 deprecated allowEmptyValue logic.
180-
if (value === '' && parameter.allowEmptyValue === false) {
181-
return params;
182-
}
249+
// Handle OAS 3.2 deprecated allowEmptyValue logic.
250+
if (value === '' && parameter.allowEmptyValue === false) {
251+
return params;
252+
}
183253
184254
// Handle content-based serialization (mutually exclusive with style/explode in OAS3)
185255
if (parameter.content) {
@@ -193,10 +263,10 @@ return encodeURIComponent(value);
193263
// Defaulting logic from OAS spec
194264
const style = parameter.style ?? 'form';
195265
const explode = parameter.explode ?? (style === 'form');
196-
const schema = parameter.schema ?? { type: parameter.type };
266+
const schema = parameter.schema ?? { type: typeof value }; // Fallback if schema is missing
197267
198-
const isArray = schema.type === 'array';
199-
const isObject = schema.type === 'object';
268+
const isArray = Array.isArray(value);
269+
const isObject = typeof value === 'object' && value !== null && !isArray && !(value instanceof Date);
200270
201271
switch (style) {
202272
case 'form':
@@ -245,23 +315,13 @@ return encodeURIComponent(value);
245315
246316
case 'deepObject':
247317
if (isObject && explode) {
248-
Object.entries(value as Record<string, any>).forEach(([key, propValue]) => {
249-
if (propValue != null) {
250-
if (Array.isArray(propValue)) {
251-
propValue.forEach(item => {
252-
if (item != null) params = params.append(\`\${name}[\${key}]\`, this.formatValue(item));
253-
});
254-
} else {
255-
params = params.append(\`\${name}[\${key}]\`, this.formatValue(propValue));
256-
}
257-
}
258-
});
259-
return params;
318+
return this.appendDeepObject(params, name, value);
260319
}
261320
break;
262321
}
263322
264323
if (Array.isArray(value)) {
324+
// Fallback/default for array if style didnt match
265325
value.forEach(item => {
266326
if (item != null) params = params.append(name, this.formatValue(item));
267327
});
@@ -271,6 +331,35 @@ return encodeURIComponent(value);
271331
return params;`;
272332
}
273333

334+
private getSerializeUrlEncodedBodyBody(): string {
335+
return `
336+
let params = new HttpParams();
337+
if (body === null || body === undefined) return params;
338+
339+
Object.entries(body).forEach(([key, value]) => {
340+
if (value === undefined || value === null) return;
341+
342+
const encoding = encodings[key] || {};
343+
// Default content-type for form-urlencoded is form style, explode true
344+
const style = encoding.style ?? 'form';
345+
const explode = encoding.explode ?? true;
346+
347+
// Use existing serializeQueryParam logic by constructing a minimal parameter definition
348+
const paramDef: ParameterStub = {
349+
name: key,
350+
in: 'query', // Reuse query logic (same as form-urlencoded)
351+
style,
352+
explode,
353+
schema: { type: typeof value }
354+
};
355+
356+
params = this.serializeQueryParam(params, paramDef, value);
357+
});
358+
359+
return params;
360+
`;
361+
}
362+
274363
private getSerializePathParamBody(): string {
275364
return `
276365
if (value === null || value === undefined) return '';

0 commit comments

Comments
 (0)