@@ -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 */
2840export 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