Skip to content

Commit 9216855

Browse files
committed
Continue implementing OpenAPI 3.2.0
1 parent ea2aa56 commit 9216855

File tree

9 files changed

+297
-71
lines changed

9 files changed

+297
-71
lines changed

src/core/utils.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ export function getTypeScriptType(schema: SwaggerDefinition | undefined | null,
124124
if (val === null) return 'null';
125125
if (typeof val === 'string') return `'${val.replace(/'/g, "\\'")}'`;
126126
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
127-
// For complex objects in const, fallback to 'any' or simple types is safer than partial object literals
128127
return 'any';
129128
}
130129

@@ -156,11 +155,6 @@ export function getTypeScriptType(schema: SwaggerDefinition | undefined | null,
156155
// Handling for 'not' keyword: Exclude<any, Type> or Exclude<KnownType, Type>
157156
if (schema.not) {
158157
const notType = getTypeScriptType(schema.not, config, knownTypes);
159-
/**
160-
* Note: Generating `Exclude<any, T>` in TS effectively just stays `any` or `unknown` depending on usage,
161-
* making strict `not` validation irrelevant at build time unless used within composite types.
162-
* We return a utility type string for clarity.
163-
*/
164158
return `Exclude<any, ${notType}>`;
165159
}
166160

@@ -173,7 +167,7 @@ export function getTypeScriptType(schema: SwaggerDefinition | undefined | null,
173167
switch (schema.type) {
174168
case 'string':
175169
type = (schema.format === 'date' || schema.format === 'date-time') && config.options.dateType === 'Date' ? 'Date' : 'string';
176-
// OAS 3.1 support: contentMediaType='image/png' implies binary data -> Blob
170+
// OAS 3.1 support: contentMediaType implies binary data -> Blob
177171
if (schema.format === 'binary' || schema.contentMediaType) {
178172
type = 'Blob';
179173
}
@@ -258,7 +252,9 @@ type UnifiedParameter = SwaggerOfficialParameter & {
258252
// Additions for OAS3 and Swagger2 compatibility
259253
collectionFormat?: 'csv' | 'ssv' | 'tsv' | 'pipes' | 'multi' | string,
260254
style?: string,
261-
explode?: boolean
255+
explode?: boolean,
256+
allowReserved?: boolean,
257+
content?: Record<string, { schema?: SwaggerDefinition }>
262258
};
263259

264260
// Helper type that adds OpenAPI 3.x properties to the Swagger 2.0 Operation type
@@ -309,13 +305,20 @@ export function extractPaths(swaggerPaths: { [p: string]: Path } | undefined): P
309305
schema: finalSchema as SwaggerDefinition,
310306
};
311307

308+
if (p.content) {
309+
param.content = p.content as any;
310+
}
311+
312312
// Carry over OAS3 style properties if they exist, but only if they're not undefined to satisfy `exactOptionalPropertyTypes`.
313313
if (p.style !== undefined) {
314314
param.style = p.style;
315315
}
316316
if (p.explode !== undefined) {
317317
param.explode = p.explode;
318318
}
319+
if (p.allowReserved !== undefined) {
320+
param.allowReserved = p.allowReserved;
321+
}
319322

320323
// Swagger 2.0 collectionFormat translation
321324
const collectionFormat = p.collectionFormat;

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
OptionalKind,
55
ParameterDeclarationStructure,
66
} from 'ts-morph';
7-
import { GeneratorConfig, PathInfo, SwaggerDefinition } from '../../../core/types.js';
7+
import { GeneratorConfig, Parameter, PathInfo, SwaggerDefinition } from '../../../core/types.js';
88
import { camelCase, getTypeScriptType, isDataTypeInterface } from '../../../core/utils.js';
99
import { HttpContext, HttpHeaders, HttpParams } from '@angular/common/http';
1010
import { SwaggerParser } from "@src/core/parser.js";
@@ -88,7 +88,16 @@ export class ServiceMethodGenerator {
8888

8989
(operation.parameters ?? []).forEach(param => {
9090
const paramName = camelCase(param.name);
91-
const paramType = getTypeScriptType(param.schema, this.config, knownTypes);
91+
// Robust schema retrieval: check `content` for schema if basic schema is missing/default
92+
let effectiveSchema = param.schema;
93+
if (param.content) {
94+
const firstType = Object.keys(param.content)[0];
95+
if (firstType && param.content[firstType].schema) {
96+
effectiveSchema = param.content[firstType].schema as SwaggerDefinition;
97+
}
98+
}
99+
100+
const paramType = getTypeScriptType(effectiveSchema, this.config, knownTypes);
92101

93102
parameters.push({
94103
name: paramName,
@@ -112,13 +121,21 @@ export class ServiceMethodGenerator {
112121
return parameters.sort((a, b) => (a.hasQuestionToken ? 1 : 0) - (b.hasQuestionToken ? 1 : 0));
113122
}
114123

124+
private isJsonContent(p: Parameter): boolean {
125+
if (!p.content) return false;
126+
const keys = Object.keys(p.content);
127+
return keys.some(k => k.includes('application/json') || k.includes('*/*'));
128+
}
129+
115130
private buildMethodBody(operation: PathInfo, parameters: OptionalKind<ParameterDeclarationStructure>[]): string {
116131
let urlTemplate = operation.path;
117132
operation.parameters?.filter(p => p.in === 'path').forEach(p => {
118133
const jsParam = camelCase(p.name);
119134
const style = p.style || 'simple';
120135
const explode = p.explode ?? false;
121-
urlTemplate = urlTemplate.replace(`{${p.name}}`, `\${HttpParamsBuilder.serializePathParam('${p.name}', ${jsParam}, '${style}', ${explode})}`);
136+
const allowReserved = p.allowReserved ?? false;
137+
const serializationArg = this.isJsonContent(p) ? ", 'json'" : "";
138+
urlTemplate = urlTemplate.replace(`{${p.name}}`, `\${HttpParamsBuilder.serializePathParam('${p.name}', ${jsParam}, '${style}', ${explode}, ${allowReserved}${serializationArg})}`);
122139
});
123140

124141
const lines: string[] = [];
@@ -153,7 +170,8 @@ console.warn('The following querystring parameters are not automatically handled
153170
headerParams.forEach(p => {
154171
const paramName = camelCase(p.name);
155172
const explode = p.explode ?? false;
156-
lines.push(`if (${paramName} != null) { headers = headers.set('${p.name}', HttpParamsBuilder.serializeHeaderParam('${p.name}', ${paramName}, ${explode})); }`);
173+
const serializationArg = this.isJsonContent(p) ? ", 'json'" : "";
174+
lines.push(`if (${paramName} != null) { headers = headers.set('${p.name}', HttpParamsBuilder.serializeHeaderParam('${p.name}', ${paramName}, ${explode}${serializationArg})); }`);
157175
});
158176

159177
if (cookieParams.length > 0) {
@@ -162,7 +180,8 @@ console.warn('The following querystring parameters are not automatically handled
162180
const paramName = camelCase(p.name);
163181
const style = p.style || 'form';
164182
const explode = p.explode ?? true;
165-
lines.push(`if (${paramName} != null) { __cookies.push(HttpParamsBuilder.serializeCookieParam('${p.name}', ${paramName}, '${style}', ${explode})); }`);
183+
const serializationArg = this.isJsonContent(p) ? ", 'json'" : "";
184+
lines.push(`if (${paramName} != null) { __cookies.push(HttpParamsBuilder.serializeCookieParam('${p.name}', ${paramName}, '${style}', ${explode}${serializationArg})); }`);
166185
});
167186
lines.push(`if (__cookies.length > 0) { headers = headers.set('Cookie', __cookies.join('; ')); }`);
168187
}

src/service/emit/utility/auth-interceptor.generator.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { SecurityScheme } from '../../../core/types.js';
99
* Generates the `auth.interceptor.ts` file. This interceptor is responsible for
1010
* attaching API keys and/or Bearer tokens to outgoing HTTP requests based on the
1111
* security schemes defined in the OpenAPI specification.
12-
* It currently supports `apiKey` (in header or query), `http` (bearer), `oauth2`, and `openIdConnect`.
12+
* It supports `apiKey`, `http` (Bearer), `oauth2`, `openIdConnect`, and recognizes `mutualTLS`.
1313
*/
1414
export class AuthInterceptorGenerator {
1515
/**
@@ -21,7 +21,6 @@ export class AuthInterceptorGenerator {
2121

2222
/**
2323
* Generates the auth interceptor file if any **supported** security schemes are defined in the spec.
24-
* A scheme is supported if it's an `apiKey` in the header/query or an `http`/`oauth2`/`openIdConnect` (Bearer).
2524
*
2625
* @param outputDir The root output directory.
2726
* @returns An object containing the names of the tokens for supported schemes (e.g., `['apiKey', 'bearerToken']`),
@@ -32,9 +31,10 @@ export class AuthInterceptorGenerator {
3231

3332
const hasSupportedApiKey = securitySchemes.some(s => s.type === 'apiKey' && (s.in === 'header' || s.in === 'query'));
3433
const hasBearer = securitySchemes.some(s => this.isBearerScheme(s));
34+
const hasMutualTLS = securitySchemes.some(s => s.type === 'mutualTLS');
3535

3636
// If no supported schemes are found, do not generate the file at all.
37-
if (!hasSupportedApiKey && !hasBearer) {
37+
if (!hasSupportedApiKey && !hasBearer && !hasMutualTLS) {
3838
return;
3939
}
4040

@@ -45,7 +45,7 @@ export class AuthInterceptorGenerator {
4545
sourceFile.insertText(0, UTILITY_GENERATOR_HEADER_COMMENT);
4646

4747
const tokenImports: string[] = [];
48-
const tokenNames: string[] = []; // This will be the return value
48+
const tokenNames: string[] = [];
4949

5050
if (hasSupportedApiKey) {
5151
tokenImports.push('API_KEY_TOKEN');
@@ -96,7 +96,6 @@ export class AuthInterceptorGenerator {
9696
let statementsBody = 'let authReq = req;';
9797
let bearerLogicAdded = false;
9898

99-
// De-duplicate schemes for generation loop
10099
const uniqueSchemes = Array.from(new Set(securitySchemes.map(s => JSON.stringify(s)))).map(s => JSON.parse(s) as SecurityScheme);
101100

102101
for (const scheme of uniqueSchemes) {
@@ -111,6 +110,8 @@ export class AuthInterceptorGenerator {
111110
statementsBody += `\nif (this.bearerToken) { const token = typeof this.bearerToken === 'function' ? this.bearerToken() : this.bearerToken; if (token) { authReq = authReq.clone({ setHeaders: { ...authReq.headers.keys().reduce((acc, key) => ({ ...acc, [key]: authReq.headers.getAll(key) }), {}), 'Authorization': \`Bearer \${token}\` } }); } }`;
112111
bearerLogicAdded = true;
113112
}
113+
} else if (scheme.type === 'mutualTLS') {
114+
statementsBody += `\n// Security Scheme '${scheme.name || 'MutualTLS'}' (mutualTLS) is assumed to be handled by the browser/client configuration.`;
114115
}
115116
}
116117

0 commit comments

Comments
 (0)