Skip to content

Commit 61c9fde

Browse files
committed
Abstract the http client and majorly expand EXTENDING.md with diagrams and such so it's clear how to extend
1 parent c700d8f commit 61c9fde

File tree

11 files changed

+853
-298
lines changed

11 files changed

+853
-298
lines changed

EXTENDING.md

Lines changed: 321 additions & 131 deletions
Large diffs are not rendered by default.

src/generators/angular/angular-client.generator.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { ServiceTestGenerator } from "./test/service-test-generator.js";
1818

1919
// Angular Utilities
2020
import { TokenGenerator } from './utils/token.generator.js';
21+
// NOTE: HttpParamsBuilderGenerator is replaced/gutted, ensuring only Codec generation if present,
22+
// but typically for full abstraction we assume logic moved to shared.
23+
// We keep it if it generates the ApiParameterCodec only.
2124
import { HttpParamsBuilderGenerator } from './utils/http-params-builder.generator.js';
2225
import { FileDownloadGenerator } from './utils/file-download.generator.js';
2326
import { DateTransformerGenerator } from './utils/date-transformer.generator.js';
@@ -34,6 +37,7 @@ import { ExtensionTokensGenerator } from './utils/extension-tokens.generator.js'
3437
import { WebhookHelperGenerator } from './utils/webhook-helper.generator.js';
3538

3639
// Shared Utilities
40+
import { ParameterSerializerGenerator } from "../shared/parameter-serializer.generator.js";
3741
import { ServerGenerator } from '../shared/server.generator.js';
3842
import { ServerUrlGenerator } from '../shared/server-url.generator.js';
3943
import { XmlBuilderGenerator } from '../shared/xml-builder.generator.js';
@@ -79,6 +83,7 @@ export class AngularClientGenerator extends AbstractClientGenerator {
7983
new InfoGenerator(parser, project).generate(outputRoot);
8084
new ServerGenerator(parser, project).generate(outputRoot);
8185
new ServerUrlGenerator(parser, project).generate(outputRoot);
86+
new ParameterSerializerGenerator(project).generate(outputRoot); // NEW
8287

8388
new CallbackGenerator(parser, project).generate(outputRoot);
8489
new WebhookGenerator(parser, project).generate(outputRoot);
@@ -92,27 +97,18 @@ export class AngularClientGenerator extends AbstractClientGenerator {
9297
const servicesDir = path.join(outputRoot, 'services');
9398
const controllerGroups = groupPathsByCanonicalController(parser);
9499

95-
for (const [controllerName, operations] of Object.entries(controllerGroups)) {
96-
if (!operations || operations.length === 0) continue;
97-
for (const op of operations) {
98-
if (!op.methodName) {
99-
if (op.operationId) {
100-
op.methodName = camelCase(op.operationId);
101-
} else {
102-
op.methodName = camelCase(`${op.method}${op.path.replace(/\//g, '_')}`);
103-
}
104-
}
105-
}
106-
new ServiceGenerator(parser, project, config)
107-
.generateServiceFile(controllerName, operations, servicesDir);
108-
}
100+
// Generate Services using the Refactored Service Generator
101+
new ServiceGenerator(parser, project, config).generate(servicesDir, controllerGroups);
102+
109103
new ServiceIndexGenerator(project).generateIndex(outputRoot);
110104
console.log('✅ Services generated.');
111105

112106
// Generate Utilities (tokens, helpers, etc)
113107
new TokenGenerator(project, config.clientName).generate(outputRoot);
114108
new ExtensionTokensGenerator(project).generate(outputRoot);
109+
// Note: This now likely only generates the ApiParameterCodec
115110
new HttpParamsBuilderGenerator(project).generate(outputRoot);
111+
116112
new FileDownloadGenerator(project).generate(outputRoot);
117113
new XmlBuilderGenerator(project).generate(outputRoot);
118114
new XmlParserGenerator(project).generate(outputRoot);

src/generators/angular/service/service-method.generator.ts

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -98,20 +98,20 @@ export class ServiceMethodGenerator {
9898
lines.push(`}`);
9999
});
100100

101-
// 2. Path Construction
101+
// 2. Path Construction (Using generic serializer)
102102
let urlTemplate = model.urlTemplate;
103103
model.pathParams.forEach(p => {
104-
const serializeCall = `HttpParamsBuilder.serializePathParam('${p.originalName}', ${p.paramName}, '${p.style || 'simple'}', ${p.explode}, ${p.allowReserved}${p.serializationLink === 'json' ? ", 'json'" : ""})`;
104+
const serializeCall = `ParameterSerializer.serializePathParam('${p.originalName}', ${p.paramName}, '${p.style || 'simple'}', ${p.explode}, ${p.allowReserved}${p.serializationLink === 'json' ? ", 'json'" : ""})`;
105105
urlTemplate = urlTemplate.replace(`{${p.originalName}}`, `\${${serializeCall}}`);
106106
});
107107

108-
// 3. Query String Logic (Legacy)
108+
// 3. Query String Logic (Legacy) - adapted to generic serializer
109109
const qsParam = rawOp.parameters?.find(p => (p.in as any) === 'querystring');
110110
let queryStringVariable = '';
111111
if (qsParam) {
112112
const pName = camelCase(qsParam.name);
113113
const hint = (qsParam as any).content?.['application/json'] ? ", 'json'" : "";
114-
lines.push(`const queryString = HttpParamsBuilder.serializeRawQuerystring(${pName}${hint});`);
114+
lines.push(`const queryString = ParameterSerializer.serializeRawQuerystring(${pName}${hint});`);
115115
queryStringVariable = "${queryString ? '?' + queryString : ''}";
116116
}
117117

@@ -123,10 +123,11 @@ export class ServiceMethodGenerator {
123123
}
124124
lines.push(`const url = \`\${basePath}${urlTemplate}${queryStringVariable}\`;`);
125125

126-
// 5. Query Params
126+
// 5. Query Params (Using generic serializer and adapting to Angular HttpParams)
127127
const standardQueryParams = model.queryParams.filter(p => p.originalName !== qsParam?.name);
128128

129129
if (standardQueryParams.length > 0) {
130+
// Angular HttpParams requires 'encoder'
130131
lines.push(`let params = new HttpParams({ encoder: new ApiParameterCodec(), fromObject: options?.params ?? {} });`);
131132
standardQueryParams.forEach(p => {
132133
const configObj = JSON.stringify({
@@ -135,17 +136,20 @@ export class ServiceMethodGenerator {
135136
style: p.style,
136137
explode: p.explode,
137138
allowReserved: p.allowReserved,
138-
serialization: p.serializationLink
139+
serialization: p.serializationLink,
140+
// Support for OAS 3.2 allowEmptyValue - passed via config object
141+
allowEmptyValue: (p as any).allowEmptyValue
139142
});
140-
lines.push(`if (${p.paramName} != null) { params = HttpParamsBuilder.serializeQueryParam(params, ${configObj}, ${p.paramName}); }`);
143+
lines.push(`const serialized_${p.paramName} = ParameterSerializer.serializeQueryParam(${configObj}, ${p.paramName});`);
144+
lines.push(`serialized_${p.paramName}.forEach(entry => params = params.append(entry.key, entry.value));`);
141145
});
142146
}
143147

144148
// 6. Headers
145149
lines.push(`let headers = options?.headers instanceof HttpHeaders ? options.headers : new HttpHeaders(options?.headers ?? {});`);
146150
model.headerParams.forEach(p => {
147151
const hint = p.serializationLink === 'json' ? ", 'json'" : "";
148-
lines.push(`if (${p.paramName} != null) { headers = headers.set('${p.originalName}', HttpParamsBuilder.serializeHeaderParam('${p.originalName}', ${p.paramName}, ${p.explode}${hint})); }`);
152+
lines.push(`if (${p.paramName} != null) { headers = headers.set('${p.originalName}', ParameterSerializer.serializeHeaderParam(${p.paramName}, ${p.explode}${hint})); }`);
149153
});
150154

151155
// 7. Cookies
@@ -157,15 +161,12 @@ export class ServiceMethodGenerator {
157161
lines.push(`const __cookies: string[] = [];`);
158162
model.cookieParams.forEach(p => {
159163
const hint = p.serializationLink === 'json' ? ", 'json'" : "";
160-
lines.push(`if (${p.paramName} != null) { __cookies.push(HttpParamsBuilder.serializeCookieParam('${p.originalName}', ${p.paramName}, '${p.style || 'form'}', ${p.explode}, ${p.allowReserved}${hint})); }`);
164+
lines.push(`if (${p.paramName} != null) { __cookies.push(ParameterSerializer.serializeCookieParam('${p.originalName}', ${p.paramName}, '${p.style || 'form'}', ${p.explode}, ${p.allowReserved}${hint})); }`);
161165
});
162166
lines.push(`if (__cookies.length > 0) { headers = headers.set('Cookie', __cookies.join('; ')); }`);
163167
}
164168

165169
// 8. Content Negotiation Setup
166-
// Detect requested media type from headers
167-
// If negotiation is active, we don't default responseType blindly.
168-
// We check if the user ASKED for XML.
169170
if (hasContentNegotiation) {
170171
lines.push(`const acceptHeader = headers.get('Accept');`);
171172
}
@@ -179,15 +180,9 @@ export class ServiceMethodGenerator {
179180
contextConstruction += `.set(EXTENSIONS_CONTEXT_TOKEN, ${JSON.stringify(model.extensions)})`;
180181
}
181182

182-
// Determine the responseType value to pass to Angular.
183-
// If we are negotiating, we switch based on acceptHeader.
184-
// Otherwise, use model default.
185183
let responseTypeVal = `options?.responseType`;
186184

187185
if (hasContentNegotiation) {
188-
// Build a condition stack
189-
// if (acceptHeader?.includes('application/xml')) 'text' else default
190-
// Note: JSON-Seq/XML variants ALWAYS require 'text' to allow manual parsing
191186
const xmlOrSeqCondition = model.responseVariants
192187
.filter(v => v.serialization === 'xml' || v.serialization.startsWith('json-'))
193188
.map(v => `acceptHeader?.includes('${v.mediaType}')`)
@@ -217,7 +212,6 @@ export class ServiceMethodGenerator {
217212
// 10. Body Handling
218213
let bodyArgument = 'null';
219214
const body = model.body;
220-
// ... (body logic same as before)
221215
const legacyFormData = rawOp.parameters?.filter(p => (p as any).in === 'formData');
222216
const isUrlEnc = rawOp.consumes?.includes('application/x-www-form-urlencoded');
223217

@@ -246,7 +240,10 @@ export class ServiceMethodGenerator {
246240
lines.push(`}`);
247241
}
248242
} else if (body.type === 'urlencoded') {
249-
lines.push(`const formBody = HttpParamsBuilder.serializeUrlEncodedBody(${body.paramName}, ${JSON.stringify(body.config)});`);
243+
// Use generic serializer then adapt to Angular HttpParams
244+
lines.push(`const urlParamEntries = ParameterSerializer.serializeUrlEncodedBody(${body.paramName}, ${JSON.stringify(body.config)});`);
245+
lines.push(`let formBody = new HttpParams({ encoder: new ApiParameterCodec() });`);
246+
lines.push(`urlParamEntries.forEach(entry => formBody = formBody.append(entry.key, entry.value));`);
250247
bodyArgument = 'formBody';
251248
} else if (body.type === 'multipart') {
252249
lines.push(`const multipartConfig = ${JSON.stringify(body.config)};`);
@@ -264,7 +261,6 @@ export class ServiceMethodGenerator {
264261
}
265262

266263
if (isSSE) {
267-
// SSE logic remains same
268264
lines.push(`
269265
return new Observable<${model.responseType}>(observer => {
270266
const eventSource = new EventSource(url);
@@ -282,7 +278,6 @@ export class ServiceMethodGenerator {
282278
const isStandardBody = ['post', 'put', 'patch', 'query'].includes(httpMethod);
283279
const isStandardNonBody = ['get', 'delete', 'head', 'options', 'jsonp'].includes(httpMethod);
284280

285-
// Using generic <any> here because the specific mapping logic below casts it to the correct union member
286281
const returnGeneric = `any`;
287282

288283
let httpCall = '';
@@ -301,13 +296,10 @@ export class ServiceMethodGenerator {
301296
}
302297

303298
// 12. Response Transformation Logic
304-
// If we have content negotiation, we need runtime checks inside the map
305-
306299
if (hasContentNegotiation) {
307300
lines.push(`return ${httpCall}.pipe(`);
308301
lines.push(` map(response => {`);
309302

310-
// Generate if/else blocks for each variant
311303
model.responseVariants.forEach(v => {
312304
const check = `acceptHeader?.includes('${v.mediaType}')`;
313305
lines.push(` // Handle ${v.mediaType}`);
@@ -328,14 +320,12 @@ export class ServiceMethodGenerator {
328320
lines.push(` return response.split('${delimiter}').filter((p: string) => p.trim().length > 0).map((i: string) => JSON.parse(i));`);
329321
lines.push(` }`);
330322
} else if (v.decodingConfig) {
331-
// JSON with decoding config
332323
lines.push(` if (${check}) {`);
333324
lines.push(` return ContentDecoder.decode(response, ${JSON.stringify(v.decodingConfig)});`);
334325
lines.push(` }`);
335326
}
336327
});
337328

338-
// Fallback if nothing matched but we still need default handling (e.g. default json decoding setup)
339329
const def = model.responseVariants.find(v => v.isDefault);
340330
if (def && def.decodingConfig) {
341331
lines.push(` // Default decoding`);
@@ -348,7 +338,6 @@ export class ServiceMethodGenerator {
348338
lines.push(`);`);
349339

350340
} else {
351-
// Legacy / Single Variant Logic
352341
const isSeq = model.responseSerialization === 'json-seq' || model.responseSerialization === 'json-lines';
353342
const isXmlResp = model.responseSerialization === 'xml';
354343

@@ -396,18 +385,14 @@ export class ServiceMethodGenerator {
396385
}];
397386
}
398387

399-
// 1. Strict Accept Overloads
400388
const distinctVariants = variants.filter((v, i, a) => a.findIndex(t => t.mediaType === v.mediaType) === i && v.mediaType !== '');
401389

402390
if (distinctVariants.length > 1) {
403391
for (const variant of distinctVariants) {
404-
// We generate a signature that requires specific headers if using this variant
405-
// Note: TS Structural typing for headers inside options is complex.
406-
// We approximate by specifying headers type as having the Accept key.
407392
overloads.push({
408393
parameters: [...parameters, {
409394
name: 'options',
410-
hasQuestionToken: false, // Mandatory to select this overload
395+
hasQuestionToken: false,
411396
type: `RequestOptions & { headers: { 'Accept': '${variant.mediaType}' } }`
412397
}],
413398
returnType: `Observable<${variant.type}>`,
@@ -416,9 +401,6 @@ export class ServiceMethodGenerator {
416401
}
417402
}
418403

419-
// 2. Standard Overloads (Default Priority / No explicit Accept)
420-
421-
// 2a. Body
422404
overloads.push({
423405
parameters: [...parameters, {
424406
name: 'options',
@@ -429,7 +411,6 @@ export class ServiceMethodGenerator {
429411
docs: [`${methodName}. \n${paramsDocs}\n@param options The options for this request.${deprecationDoc}`]
430412
});
431413

432-
// 2b. Response (Full)
433414
overloads.push({
434415
parameters: [...parameters, {
435416
name: 'options',
@@ -440,7 +421,6 @@ export class ServiceMethodGenerator {
440421
docs: [`${methodName}. \n${paramsDocs}\n@param options The options for this request, with response observation enabled.${deprecationDoc}`]
441422
});
442423

443-
// 2c. Events
444424
overloads.push({
445425
parameters: [...parameters, {
446426
name: 'options',

0 commit comments

Comments
 (0)