Skip to content

Commit a240b75

Browse files
committed
Continue implementing OpenAPI 3.2.0
1 parent 5772fe4 commit a240b75

21 files changed

+378
-166
lines changed

package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
},
4848
"devDependencies": {
4949
"@angular/common": "^20.3.12",
50+
"@angular/compiler": "^20.3.12",
5051
"@types/js-yaml": "^4.0.9",
5152
"@types/node": "^24",
5253
"@types/swagger-schema-official": "^2.0.25",
@@ -61,4 +62,4 @@
6162
"engines": {
6263
"node": ">=18.0.0"
6364
}
64-
}
65+
}

src/core/types.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export interface Parameter {
2929
/** The name of the parameter. */
3030
name: string;
3131
/** The location of the parameter. */
32-
in: "query" | "path" | "header" | "cookie" | "formData";
32+
in: "query" | "path" | "header" | "cookie" | "formData" | "querystring";
3333
/** Determines whether this parameter is mandatory. */
3434
required?: boolean;
3535
/** The schema defining the type of the parameter. */
@@ -40,6 +40,14 @@ export interface Parameter {
4040
format?: string;
4141
/** A brief description of the parameter. */
4242
description?: string;
43+
/** Describes how the parameter value will be serialized. */
44+
style?: string;
45+
/** When true, parameter values of type `array` or `object` generate separate parameters. */
46+
explode?: boolean;
47+
/** Allows sending reserved characters through. */
48+
allowReserved?: boolean;
49+
/** A map containing the representations for the parameter. For complex serialization scenarios. */
50+
content?: Record<string, { schema?: SwaggerDefinition | { $ref: string } }>;
4351
}
4452

4553
/** A processed, unified representation of a single API operation (e.g., GET /users/{id}). */
@@ -120,7 +128,7 @@ export interface SwaggerDefinition {
120128
nullable?: boolean;
121129
required?: string[];
122130
/** An example of the schema representation. */
123-
example?: unknown; // FIX: Added the 'example' property.
131+
example?: unknown;
124132
}
125133

126134
/** Represents a security scheme recognized by the API. */

src/core/utils.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ type UnifiedParameter = SwaggerOfficialParameter & {
221221
type?: string,
222222
format?: string,
223223
items?: SwaggerDefinition | { $ref: string }
224+
// Additions for OAS3 and Swagger2 compatibility
225+
collectionFormat?: 'csv' | 'ssv' | 'tsv' | 'pipes' | 'multi' | string,
226+
style?: string,
227+
explode?: boolean
224228
};
225229

226230
// Helper type that adds OpenAPI 3.x properties to the Swagger 2.0 Operation type
@@ -267,10 +271,41 @@ export function extractPaths(swaggerPaths: { [p: string]: Path } | undefined): P
267271

268272
const param: Parameter = {
269273
name: p.name,
270-
in: p.in as "query" | "path" | "header" | "cookie",
274+
in: p.in as "query" | "path" | "header" | "cookie" | "querystring",
271275
schema: finalSchema as SwaggerDefinition,
272276
};
273277

278+
// Carry over OAS3 style properties if they exist, but only if they're not undefined to satisfy `exactOptionalPropertyTypes`.
279+
if (p.style !== undefined) {
280+
param.style = p.style;
281+
}
282+
if (p.explode !== undefined) {
283+
param.explode = p.explode;
284+
}
285+
286+
// Swagger 2.0 collectionFormat translation
287+
const collectionFormat = p.collectionFormat;
288+
if (collectionFormat) {
289+
switch (collectionFormat) {
290+
case 'csv':
291+
param.style = 'form';
292+
param.explode = false;
293+
break;
294+
case 'ssv':
295+
param.style = 'spaceDelimited';
296+
param.explode = false;
297+
break;
298+
case 'pipes':
299+
param.style = 'pipeDelimited';
300+
param.explode = false;
301+
break;
302+
case 'multi':
303+
param.style = 'form';
304+
param.explode = true;
305+
break;
306+
}
307+
}
308+
274309
if (p.required !== undefined) {
275310
param.required = p.required;
276311
}

src/service/emit/admin/admin.generator.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ export class AdminGenerator {
4343
listGen.generate(resource, adminDir);
4444
}
4545

46-
// THE DEFINITIVE FIX: Generate a form if the resource is marked as editable.
47-
// This is more robust and correctly handles the failing 'poly' resource case.
4846
if (resource.isEditable) {
4947
const formResult = formGen.generate(resource, adminDir);
5048
if (formResult.usesCustomValidators) {

src/service/emit/admin/form-component.generator.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,6 @@ export class FormComponentGenerator {
377377
docs: [`Getter for the ${singularCamel} FormArray.`]
378378
});
379379

380-
// FIX: Move the `docs` from the parameter to the method itself and format with @param.
381380
const createMethod = classDeclaration.addMethod({
382381
name: `create${singularPascal}`,
383382
scope: Scope.Private,
@@ -497,7 +496,6 @@ export class FormComponentGenerator {
497496
body += `if (petType) {\n`;
498497
body += ` this.form.get(this.discriminatorPropName)?.setValue(petType, { emitEvent: true });\n`;
499498
for (const subSchemaRef of oneOfProp.schema.oneOf!) {
500-
// THE DEFINITIVE FIX: Guard against inline schemas that are not refs.
501499
if (!subSchemaRef.$ref) {
502500
continue; // Skip primitives like { type: 'string' }
503501
}
@@ -532,7 +530,6 @@ export class FormComponentGenerator {
532530
parameters: [{ name: 'type', type: 'string' }]
533531
});
534532

535-
// THE DEFINITIVE FIX: Check if any of the `oneOf` options are objects.
536533
const oneOfHasObjects = prop.schema.oneOf!.some(s => this.parser.resolve(s)?.properties);
537534

538535
// If none of the `oneOf` options are objects (i.e., they are all primitives like string/number),

src/service/emit/admin/form-control.mapper.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export interface FormControlInfo {
77
}
88

99
export function mapSchemaToFormControl(schema: SwaggerDefinition): FormControlInfo | null {
10-
// FIX: Add a null check at the very beginning to prevent crashes.
1110
if (!schema) {
1211
return null;
1312
}

src/service/emit/admin/html/form-component-html.builder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ export function generateFormComponentHtml(resource: Resource, parser: SwaggerPar
6565
const subSchema = option.schema;
6666

6767
// ====================================================================
68-
// THE FIX: Create a helper function to recursively merge properties from `allOf`.
69-
// This ensures inherited properties (like `name` from `BasePet`) are included.
68+
// This helper function recursively merges properties from `allOf`.
69+
// ensuring inherited properties (like `name` from `BasePet`) are included.
7070
// ====================================================================
7171
const getAllProperties = (schema: SwaggerDefinition): Record<string, SwaggerDefinition> => {
7272
let allProperties: Record<string, SwaggerDefinition> = { ...schema.properties };

src/service/emit/orchestrator.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ export async function emitClientLibrary(outputRoot: string, parser: SwaggerParse
6363
// generate() returns the names of the tokens used (e.g., 'apiKey', 'bearerToken'),
6464
// which the ProviderGenerator needs to create the correct configuration interface.
6565
const interceptorResult = interceptorGenerator.generate(outputRoot);
66-
// FIX: Ensure tokenNames is always an array, even if the interceptor isn't generated.
6766
tokenNames = interceptorResult?.tokenNames || [];
6867

6968
if (Object.values(securitySchemes).some(s => s.type === 'oauth2')) {

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import {
55
ParameterDeclarationStructure,
66
} from 'ts-morph';
77
import { GeneratorConfig, PathInfo, SwaggerDefinition } from '../../../core/types.js';
8-
import { SwaggerParser } from '../../../core/parser.js';
98
import { camelCase, getTypeScriptType, isDataTypeInterface } from '../../../core/utils.js';
109
import { HttpContext, HttpHeaders, HttpParams } from '@angular/common/http';
10+
import { SwaggerParser } from "@src/core/parser.js";
1111

1212
/** A strongly-typed representation of Angular's HttpRequest options. */
1313
interface HttpRequestOptions {
@@ -119,15 +119,30 @@ export class ServiceMethodGenerator {
119119
urlTemplate = urlTemplate.replace(`{${p.name}}`, `\${${camelCase(p.name)}}`);
120120
});
121121

122-
const lines = [`const url = \`\${this.basePath}${urlTemplate}\`;`];
122+
const lines: string[] = [];
123+
124+
const cookieParams = operation.parameters?.filter(p => p.in === 'cookie') ?? [];
125+
if (cookieParams.length > 0) {
126+
lines.push(`// TODO: Cookie parameters are not handled by Angular's HttpClient. You may need to handle them manually.
127+
console.warn('The following cookie parameters are not automatically handled:', ${JSON.stringify(cookieParams.map(p => p.name))});`);
128+
}
129+
130+
const querystringParams = operation.parameters?.filter(p => p.in === 'querystring') ?? [];
131+
if (querystringParams.length > 0) {
132+
lines.push(`// TODO: querystring parameters are not handled by Angular's HttpClient. You may need to handle them manually by constructing the URL.
133+
console.warn('The following querystring parameters are not automatically handled:', ${JSON.stringify(querystringParams.map(p => p.name))});`);
134+
}
135+
lines.push(`const url = \`\${this.basePath}${urlTemplate}\`;`);
136+
123137
const requestOptions: HttpRequestOptions = {};
124138

125139
const queryParams = operation.parameters?.filter(p => p.in === 'query') ?? [];
126140
if (queryParams.length > 0) {
127141
lines.push(`let params = new HttpParams({ fromObject: options?.params ?? {} });`);
128142
queryParams.forEach(p => {
129143
const paramName = camelCase(p.name);
130-
lines.push(`if (${paramName} != null) { params = HttpParamsBuilder.addToHttpParams(params, ${paramName}, '${p.name}'); }`);
144+
const paramDefJson = JSON.stringify(p);
145+
lines.push(`if (${paramName} != null) { params = HttpParamsBuilder.serializeQueryParam(params, ${paramDefJson}, ${paramName}); }`);
131146
});
132147
requestOptions.params = 'params' as any; // Use string placeholder
133148
}
@@ -143,10 +158,10 @@ export class ServiceMethodGenerator {
143158
}
144159

145160
let optionProperties = `
146-
observe: options?.observe,
147-
reportProgress: options?.reportProgress,
148-
responseType: options?.responseType,
149-
withCredentials: options?.withCredentials,
161+
observe: options?.observe,
162+
reportProgress: options?.reportProgress,
163+
responseType: options?.responseType,
164+
withCredentials: options?.withCredentials,
150165
context: this.createContextWithClientId(options?.context)`;
151166

152167
if (requestOptions.params) {

0 commit comments

Comments
 (0)