Skip to content

Commit 6f3abf3

Browse files
committed
Improve code that is generated
1 parent 5fa8ce0 commit 6f3abf3

File tree

7 files changed

+568
-429
lines changed

7 files changed

+568
-429
lines changed

src/core/parser.ts

Lines changed: 106 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,38 @@ import {
2020
pascalCase
2121
} from './utils.js';
2222

23+
/**
24+
* Represents a resolved option for a polymorphic (`oneOf`) schema,
25+
* linking a discriminator value to its corresponding schema definition.
26+
*/
27+
export interface PolymorphicOption {
28+
/** The value of the discriminator property for this specific schema type (e.g., 'cat'). */
29+
name: string;
30+
/** The fully resolved SwaggerDefinition for this schema type. */
31+
schema: SwaggerDefinition;
32+
}
33+
2334
/**
2435
* A wrapper class for a raw OpenAPI/Swagger specification object.
2536
* It provides a structured and reliable API to access different parts of the spec,
26-
* normalizing differences between Swagger 2.0 and OpenAPI 3.x.
37+
* normalizing differences between Swagger 2.0 and OpenAPI 3.x and providing
38+
* helpful utilities like `$ref` resolution.
2739
*/
2840
export class SwaggerParser {
2941
/** The raw, parsed OpenAPI/Swagger specification object. */
3042
public readonly spec: SwaggerSpec;
3143
/** The configuration object for the generator. */
3244
public readonly config: GeneratorConfig;
33-
/** A normalized array of all schemas (definitions) found in the specification. */
45+
/** A normalized array of all top-level schemas (definitions) found in the specification. */
3446
public readonly schemas: { name: string; definition: SwaggerDefinition; }[];
3547
/** A flattened and processed list of all API operations (paths). */
3648
public readonly operations: PathInfo[];
3749
/** A normalized record of all security schemes defined in the specification. */
3850
public readonly security: Record<string, SecurityScheme>;
3951

4052
/**
41-
* Initializes a new instance of the SwaggerParser.
53+
* Initializes a new instance of the SwaggerParser. It is generally recommended
54+
* to use the static `create` factory method instead of this constructor directly.
4255
* @param spec The raw OpenAPI/Swagger specification object.
4356
* @param config The generator configuration.
4457
*/
@@ -53,8 +66,7 @@ export class SwaggerParser {
5366
/**
5467
* Asynchronously creates a SwaggerParser instance from a file path or URL.
5568
* This is the recommended factory method for creating a parser instance.
56-
*
57-
* @param inputPath The local path or remote URL of the OpenAPI/Swagger specification.
69+
* @param inputPath The local file path or remote URL of the OpenAPI/Swagger specification.
5870
* @param config The generator configuration.
5971
* @returns A promise that resolves to a new SwaggerParser instance.
6072
*/
@@ -65,35 +77,38 @@ export class SwaggerParser {
6577
}
6678

6779
/**
68-
* Loads the raw content of the specification from a file or URL.
80+
* Loads the raw content of the specification from a local file or a remote URL.
6981
* @param pathOrUrl The path or URL to load from.
7082
* @returns A promise that resolves to the string content.
7183
* @private
7284
*/
7385
private static async loadContent(pathOrUrl: string): Promise<string> {
74-
if (isUrl(pathOrUrl)) {
75-
const response = await fetch(pathOrUrl);
76-
if (!response.ok) throw new Error(`Failed to fetch spec from ${pathOrUrl}: ${response.statusText}`);
77-
return response.text();
78-
} else {
79-
if (!fs.existsSync(pathOrUrl)) throw new Error(`Input file not found at ${pathOrUrl}`);
80-
return fs.readFileSync(pathOrUrl, 'utf8');
86+
try {
87+
if (isUrl(pathOrUrl)) {
88+
const response = await fetch(pathOrUrl);
89+
if (!response.ok) throw new Error(`Failed to fetch spec from ${pathOrUrl}: ${response.statusText}`);
90+
return response.text();
91+
} else {
92+
if (!fs.existsSync(pathOrUrl)) throw new Error(`Input file not found at ${pathOrUrl}`);
93+
return fs.readFileSync(pathOrUrl, 'utf8');
94+
}
95+
} catch (e) {
96+
const message = e instanceof Error ? e.message : String(e);
97+
throw new Error(`Failed to read content from "${pathOrUrl}": ${message}`);
8198
}
8299
}
83100

84101
/**
85-
* Parses the string content of a specification into a JavaScript object.
86-
* It automatically detects whether the content is JSON or YAML based on file extension or content sniffing.
87-
*
88-
* @param content The raw string content.
102+
* Parses the string content of a specification into a JavaScript object,
103+
* auto-detecting whether it is JSON or YAML.
104+
* @param content The raw string content of the specification.
89105
* @param pathOrUrl The original path, used for error messaging and format detection.
90106
* @returns The parsed SwaggerSpec object.
91107
* @private
92108
*/
93109
private static parseSpecContent(content: string, pathOrUrl: string): SwaggerSpec {
94-
const extension = path.extname(pathOrUrl).toLowerCase();
95110
try {
96-
// Prefer YAML parsing for .yaml/.yml or if it looks like YAML
111+
const extension = path.extname(pathOrUrl).toLowerCase();
97112
if (['.yaml', '.yml'].includes(extension) || (!extension && content.trim().startsWith('openapi:'))) {
98113
return yaml.load(content) as SwaggerSpec;
99114
} else {
@@ -105,107 +120,103 @@ export class SwaggerParser {
105120
}
106121
}
107122

108-
/**
109-
* Retrieves the entire parsed specification object.
110-
* @returns The SwaggerSpec object.
111-
*/
112-
public getSpec(): SwaggerSpec {
113-
return this.spec;
114-
}
123+
/** Retrieves the entire parsed specification object. */
124+
public getSpec(): SwaggerSpec { return this.spec; }
115125

116-
/**
117-
* Retrieves all schema definitions from the specification, normalizing for
118-
* both OpenAPI 3.x (`components/schemas`) and Swagger 2.0 (`definitions`).
119-
*
120-
* @returns A record mapping schema names to their definitions.
121-
*/
122-
public getDefinitions(): Record<string, SwaggerDefinition> {
123-
return this.spec.definitions || this.spec.components?.schemas || {};
124-
}
126+
/** Retrieves all schema definitions from the specification, normalizing for OpenAPI 3 and Swagger 2. */
127+
public getDefinitions(): Record<string, SwaggerDefinition> { return this.spec.definitions || this.spec.components?.schemas || {}; }
125128

126-
/**
127-
* Retrieves a single schema definition by its name.
128-
* @param name The name of the schema to retrieve.
129-
* @returns The SwaggerDefinition, or `undefined` if not found.
130-
*/
131-
public getDefinition(name: string): SwaggerDefinition | undefined {
132-
return this.getDefinitions()[name];
133-
}
129+
/** Retrieves a single schema definition by its original name from the specification. */
130+
public getDefinition(name: string): SwaggerDefinition | undefined { return this.getDefinitions()[name]; }
134131

135-
/**
136-
* Retrieves all security scheme definitions from the specification, normalizing
137-
* for OpenAPI 3.x (`components/securitySchemes`) and Swagger 2.0 (`securityDefinitions`).
138-
*
139-
* @returns A record mapping security scheme names to their definitions.
140-
*/
141-
public getSecuritySchemes(): Record<string, SecurityScheme> {
142-
return (this.spec.components?.securitySchemes || this.spec.securityDefinitions || {}) as Record<string, SecurityScheme>;
143-
}
132+
/** Retrieves all security scheme definitions from the specification. */
133+
public getSecuritySchemes(): Record<string, SecurityScheme> { return (this.spec.components?.securitySchemes || this.spec.securityDefinitions || {}) as Record<string, SecurityScheme>; }
144134

145135
/**
146136
* Resolves a JSON reference (`$ref`) object to its corresponding definition within the specification.
147-
* This method only supports local references (e.g., '#/components/schemas/User').
148-
*
137+
* If the provided object is not a `$ref`, it is returned as is.
149138
* @template T The expected type of the resolved object.
150-
* @param obj The object to resolve. If it's not a `$ref` object, it's returned as is.
151-
* @returns The resolved definition, or the original object if not a `$ref`. Returns `undefined` if the reference cannot be resolved.
139+
* @param obj The object to resolve.
140+
* @returns The resolved definition, the original object if not a ref, or `undefined` if the reference is invalid.
152141
*/
153-
public resolve<T>(obj: T | { $ref: string }): T | undefined {
154-
if (obj && typeof obj === 'object' && '$ref' in obj && typeof obj.$ref === 'string') {
155-
const ref = obj.$ref;
156-
if (!ref.startsWith('#/')) {
157-
console.warn(`[Parser] Unsupported external or non-root reference: ${ref}`);
158-
return undefined;
159-
}
160-
const parts = ref.substring(2).split('/');
161-
let current: unknown = this.spec;
162-
for (const part of parts) {
163-
if (typeof current === 'object' && current !== null && Object.prototype.hasOwnProperty.call(current, part)) {
164-
current = (current as Record<string, unknown>)[part];
165-
} else {
166-
console.warn(`[Parser] Failed to resolve reference part "${part}" in path "${ref}"`);
167-
return undefined;
168-
}
169-
}
170-
return current as T;
142+
public resolve<T>(obj: T | { $ref: string } | null | undefined): T | undefined {
143+
if (obj === null) return null as unknown as undefined;
144+
if (obj === undefined) return undefined;
145+
if (typeof obj === 'object' && '$ref' in obj && typeof (obj as any).$ref === 'string') {
146+
return this.resolveReference((obj as any).$ref);
171147
}
172148
return obj as T;
173149
}
174150

175151
/**
176152
* Resolves a JSON reference string (e.g., '#/components/schemas/User') directly to its definition.
177-
* This is a simplified lookup that assumes the reference points to a top-level schema/definition.
178-
* It does not traverse complex paths. If the reference is invalid or unsupported
179-
* (not a local string starting with '#/'), it logs a warning and returns `undefined`.
180-
*
153+
* This robust implementation can traverse any valid local path within the specification.
154+
* It gracefully handles invalid paths and non-local references by returning `undefined`.
181155
* @param ref The JSON reference string.
182-
* @returns The resolved SwaggerDefinition, or undefined if not found or the reference is invalid.
156+
* @returns The resolved definition, or `undefined` if the reference is not found or is invalid.
183157
*/
184-
public resolveReference(ref: string): SwaggerDefinition | undefined {
185-
if (typeof ref !== 'string' || !ref.startsWith('#/')) {
158+
public resolveReference<T = SwaggerDefinition>(ref: string): T | undefined {
159+
if (typeof ref !== 'string') {
186160
console.warn(`[Parser] Encountered an unsupported or invalid reference: ${ref}`);
187161
return undefined;
188162
}
189-
const parts = ref.split('/');
190-
const definitionName = parts.pop()!;
191-
// This is a simplified lookup assuming refs point to top-level schemas or definitions.
192-
return this.getDefinition(definitionName);
163+
if (!ref.startsWith('#/')) {
164+
console.warn(`[Parser] Unsupported external or non-root reference: ${ref}`);
165+
return undefined;
166+
}
167+
const pathParts = ref.substring(2).split('/');
168+
let current: any = this.spec;
169+
for (const part of pathParts) {
170+
if (typeof current === 'object' && current !== null && Object.prototype.hasOwnProperty.call(current, part)) {
171+
current = current[part];
172+
} else {
173+
console.warn(`[Parser] Failed to resolve reference part "${part}" in path "${ref}"`);
174+
return undefined;
175+
}
176+
}
177+
return current as T;
193178
}
194179

195180
/**
196-
* Checks if the loaded specification is a valid OpenAPI 3.x or Swagger 2.0 file
197-
* by inspecting the `openapi` or `swagger` version fields.
198-
* This method is lenient and only checks for the presence of a version string starting with '2.' or '3.'.
199-
* @returns `true` if the spec version is recognized, `false` otherwise.
181+
* For a polymorphic schema (one with `oneOf` and a `discriminator`), this method
182+
* resolves all possible sub-types and returns them with their discriminator values.
183+
* It supports both explicit `mapping` in the discriminator object and implicit resolution
184+
* by inspecting the `enum` value of the discriminator property in each `oneOf` schema.
185+
* @param schema The polymorphic schema definition to analyze.
186+
* @returns An array of `PolymorphicOption` objects, each linking a discriminator value to its resolved schema.
200187
*/
201-
public isValidSpec(): boolean {
202-
return !!((this.spec.swagger && this.spec.swagger.startsWith('2.')) || (this.spec.openapi && this.spec.openapi.startsWith('3.')));
188+
public getPolymorphicSchemaOptions(schema: SwaggerDefinition): PolymorphicOption[] {
189+
if (!schema.oneOf || !schema.discriminator) {
190+
return [];
191+
}
192+
const dPropName = schema.discriminator.propertyName;
193+
194+
// Strategy 1: Use the explicit mapping if it exists.
195+
const mapping = schema.discriminator.mapping || {};
196+
if (Object.keys(mapping).length > 0) {
197+
return Object.entries(mapping).map(([name, ref]) => {
198+
const resolvedSchema = this.resolveReference(ref);
199+
return resolvedSchema ? { name, schema: resolvedSchema } : null;
200+
}).filter((opt): opt is PolymorphicOption => !!opt);
201+
}
202+
203+
// Strategy 2: Infer from the `oneOf` array directly by resolving each ref and reading its discriminator property.
204+
return schema.oneOf.map(refSchema => {
205+
if (!refSchema.$ref) return null;
206+
const resolvedSchema = this.resolveReference(refSchema.$ref);
207+
if (!resolvedSchema || !resolvedSchema.properties || !resolvedSchema.properties[dPropName]?.enum) {
208+
return null;
209+
}
210+
// The actual discriminator value (e.g., 'cat') must be read from the resolved schema's enum.
211+
const name = resolvedSchema.properties[dPropName].enum![0] as string;
212+
return { name, schema: resolvedSchema };
213+
}).filter((opt): opt is PolymorphicOption => !!opt);
203214
}
204215

205-
/**
206-
* Gets the version of the loaded specification.
207-
* @returns An object containing the type ('swagger' or 'openapi') and version string, or `null` if unrecognized.
208-
*/
216+
/** Checks if the loaded specification is a valid OpenAPI 3.x or Swagger 2.0 file. */
217+
public isValidSpec(): boolean { return !!((this.spec.swagger && this.spec.swagger.startsWith('2.')) || (this.spec.openapi && this.spec.openapi.startsWith('3.'))); }
218+
219+
/** Gets the version of the loaded specification. */
209220
public getSpecVersion(): { type: 'swagger' | 'openapi'; version: string } | null {
210221
if (this.spec.swagger) return { type: 'swagger', version: this.spec.swagger };
211222
if (this.spec.openapi) return { type: 'openapi', version: this.spec.openapi };

0 commit comments

Comments
 (0)