77import { SwaggerDefinition , SwaggerSpec , GeneratorConfig , SecurityScheme , PathInfo } from './types.js' ;
88import * as fs from 'node:fs' ;
99import * as path from 'node:path' ;
10+ import { pathToFileURL } from 'node:url' ;
1011import yaml from 'js-yaml' ;
1112import { extractPaths , isUrl , pascalCase } from './utils.js' ;
1213
@@ -39,26 +40,45 @@ export interface PolymorphicOption {
3940 * helpful utilities like `$ref` resolution.
4041 */
4142export class SwaggerParser {
42- /** The raw, parsed OpenAPI/Swagger specification object. */
43+ /** The raw, parsed OpenAPI/Swagger specification object for the entry document . */
4344 public readonly spec : SwaggerSpec ;
4445 /** The configuration object for the generator. */
4546 public readonly config : GeneratorConfig ;
46- /** A normalized array of all top-level schemas (definitions) found in the specification. */
47+ /** The full URI of the entry document. */
48+ public readonly documentUri : string ;
49+
50+ /** A normalized array of all top-level schemas (definitions) found in the entry specification. */
4751 public readonly schemas : { name : string ; definition : SwaggerDefinition ; } [ ] ;
48- /** A flattened and processed list of all API operations (paths). */
52+ /** A flattened and processed list of all API operations (paths) from the entry specification . */
4953 public readonly operations : PathInfo [ ] ;
50- /** A normalized record of all security schemes defined in the specification. */
54+ /** A normalized record of all security schemes defined in the entry specification. */
5155 public readonly security : Record < string , SecurityScheme > ;
5256
57+ /** A cache of all loaded specifications, keyed by their absolute URI. */
58+ private readonly specCache : Map < string , SwaggerSpec > ;
59+
5360 /**
5461 * Initializes a new instance of the SwaggerParser. It is generally recommended
5562 * to use the static `create` factory method instead of this constructor directly.
56- * @param spec The raw OpenAPI/Swagger specification object.
63+ * @param spec The raw OpenAPI/Swagger specification object for the entry document .
5764 * @param config The generator configuration.
65+ * @param specCache A map containing all pre-loaded and parsed specifications, including the entry spec.
66+ * @param documentUri The absolute URI of the entry document.
5867 */
59- public constructor ( spec : SwaggerSpec , config : GeneratorConfig ) {
68+ public constructor (
69+ spec : SwaggerSpec ,
70+ config : GeneratorConfig ,
71+ specCache ?: Map < string , SwaggerSpec > ,
72+ documentUri : string = 'file://entry-spec.json'
73+ ) {
6074 this . spec = spec ;
6175 this . config = config ;
76+ this . documentUri = documentUri ;
77+
78+ // If a cache isn't provided, create one with just the entry spec.
79+ this . specCache = specCache || new Map < string , SwaggerSpec > ( [ [ this . documentUri , spec ] ] ) ;
80+
81+
6282 this . schemas = Object . entries ( this . getDefinitions ( ) ) . map ( ( [ name , definition ] ) => ( {
6383 name : pascalCase ( name ) ,
6484 definition
@@ -69,15 +89,78 @@ export class SwaggerParser {
6989
7090 /**
7191 * Asynchronously creates a SwaggerParser instance from a file path or URL.
72- * This is the recommended factory method for creating a parser instance.
73- * @param inputPath The local file path or remote URL of the OpenAPI/Swagger specification.
92+ * This is the recommended factory method. It pre-loads and caches the entry document
93+ * and any other documents it references.
94+ * @param inputPath The local file path or remote URL of the entry OpenAPI/Swagger specification.
7495 * @param config The generator configuration.
75- * @returns A promise that resolves to a new SwaggerParser instance.
96+ * @returns A promise that resolves to a new, fully initialized SwaggerParser instance.
7697 */
7798 static async create ( inputPath : string , config : GeneratorConfig ) : Promise < SwaggerParser > {
78- const content = await this . loadContent ( inputPath ) ;
79- const spec = this . parseSpecContent ( content , inputPath ) ;
80- return new SwaggerParser ( spec , config ) ;
99+ const documentUri = isUrl ( inputPath )
100+ ? inputPath
101+ : pathToFileURL ( path . resolve ( process . cwd ( ) , inputPath ) ) . href ;
102+
103+ const cache = new Map < string , SwaggerSpec > ( ) ;
104+ await this . loadAndCacheSpecRecursive ( documentUri , cache , new Set < string > ( ) ) ;
105+
106+ const entrySpec = cache . get ( documentUri ) ! ;
107+ return new SwaggerParser ( entrySpec , config , cache , documentUri ) ;
108+ }
109+
110+ /**
111+ * Recursively traverses a specification, loading and caching all external references.
112+ * @param uri The absolute URI of the specification to load.
113+ * @param cache The map where loaded specs are stored.
114+ * @param visited A set to track already processed URIs to prevent infinite loops.
115+ * @private
116+ */
117+ private static async loadAndCacheSpecRecursive ( uri : string , cache : Map < string , SwaggerSpec > , visited : Set < string > ) : Promise < void > {
118+ if ( visited . has ( uri ) ) return ;
119+ visited . add ( uri ) ;
120+
121+ const content = await this . loadContent ( uri ) ;
122+ const spec = this . parseSpecContent ( content , uri ) ;
123+ cache . set ( uri , spec ) ;
124+
125+ const baseUri = spec . $self ? new URL ( spec . $self , uri ) . href : uri ;
126+
127+ const refs = this . findRefs ( spec ) ;
128+ for ( const ref of refs ) {
129+ const [ filePath ] = ref . split ( '#' , 2 ) ;
130+ if ( filePath ) { // It's a reference to another document
131+ const nextUri = new URL ( filePath , baseUri ) . href ;
132+ await this . loadAndCacheSpecRecursive ( nextUri , cache , visited ) ;
133+ }
134+ }
135+ }
136+
137+ /**
138+ * Recursively finds all unique `$ref` values within a given object.
139+ * @param obj The object to search.
140+ * @returns An array of unique `$ref` strings.
141+ * @private
142+ */
143+ private static findRefs ( obj : unknown ) : string [ ] {
144+ const refs = new Set < string > ( ) ;
145+
146+ function traverse ( current : unknown ) {
147+ if ( ! current || typeof current !== 'object' ) {
148+ return ;
149+ }
150+
151+ if ( isRefObject ( current ) ) {
152+ refs . add ( current . $ref ) ;
153+ }
154+
155+ for ( const key in current as object ) {
156+ if ( Object . prototype . hasOwnProperty . call ( current , key ) ) {
157+ traverse ( ( current as any ) [ key ] ) ;
158+ }
159+ }
160+ }
161+
162+ traverse ( obj ) ;
163+ return Array . from ( refs ) ;
81164 }
82165
83166 /**
@@ -88,13 +171,14 @@ export class SwaggerParser {
88171 */
89172 private static async loadContent ( pathOrUrl : string ) : Promise < string > {
90173 try {
91- if ( isUrl ( pathOrUrl ) ) {
174+ if ( isUrl ( pathOrUrl ) && ! pathOrUrl . startsWith ( 'file:' ) ) {
92175 const response = await fetch ( pathOrUrl ) ;
93176 if ( ! response . ok ) throw new Error ( `Failed to fetch spec from ${ pathOrUrl } : ${ response . statusText } ` ) ;
94177 return response . text ( ) ;
95178 } else {
96- if ( ! fs . existsSync ( pathOrUrl ) ) throw new Error ( `Input file not found at ${ pathOrUrl } ` ) ;
97- return fs . readFileSync ( pathOrUrl , 'utf8' ) ;
179+ const filePath = pathOrUrl . startsWith ( 'file:' ) ? new URL ( pathOrUrl ) . pathname : pathOrUrl ;
180+ if ( ! fs . existsSync ( filePath ) ) throw new Error ( `Input file not found at ${ filePath } ` ) ;
181+ return fs . readFileSync ( filePath , 'utf8' ) ;
98182 }
99183 } catch ( e ) {
100184 const message = e instanceof Error ? e . message : String ( e ) ;
@@ -124,69 +208,100 @@ export class SwaggerParser {
124208 }
125209 }
126210
127- /** Retrieves the entire parsed specification object. */
211+ /** Retrieves the entire parsed entry specification object. */
128212 public getSpec ( ) : SwaggerSpec {
129213 return this . spec ;
130214 }
131215
132- /** Retrieves all schema definitions from the specification, normalizing for OpenAPI 3 and Swagger 2. */
216+ /** Retrieves all schema definitions from the entry specification, normalizing for OpenAPI 3 and Swagger 2. */
133217 public getDefinitions ( ) : Record < string , SwaggerDefinition > {
134218 return this . spec . definitions || this . spec . components ?. schemas || { } ;
135219 }
136220
137- /** Retrieves a single schema definition by its original name from the specification. */
221+ /** Retrieves a single schema definition by its original name from the entry specification. */
138222 public getDefinition ( name : string ) : SwaggerDefinition | undefined {
139223 return this . getDefinitions ( ) [ name ] ;
140224 }
141225
142- /** Retrieves all security scheme definitions from the specification. */
226+ /** Retrieves all security scheme definitions from the entry specification. */
143227 public getSecuritySchemes ( ) : Record < string , SecurityScheme > {
144228 return ( this . spec . components ?. securitySchemes || this . spec . securityDefinitions || { } ) as Record < string , SecurityScheme > ;
145229 }
146230
147231 /**
148- * Resolves a JSON reference (`$ref`) object to its corresponding definition within the specification .
232+ * Synchronously resolves a JSON reference (`$ref`) object to its corresponding definition.
149233 * If the provided object is not a `$ref`, it is returned as is.
234+ * This method assumes all necessary files have been pre-loaded into the cache.
150235 * @template T The expected type of the resolved object.
151236 * @param obj The object to resolve.
152237 * @returns The resolved definition, the original object if not a ref, or `undefined` if the reference is invalid.
153238 */
154239 public resolve < T > ( obj : T | { $ref : string } | null | undefined ) : T | undefined {
155- if ( obj === null ) return null as unknown as undefined ;
156- if ( obj === undefined ) return undefined ;
240+ if ( obj === null || obj === undefined ) return undefined ;
157241 if ( isRefObject ( obj ) ) {
158242 return this . resolveReference ( obj . $ref ) ;
159243 }
160244 return obj as T ;
161245 }
162246
163247 /**
164- * Resolves a JSON reference string (e.g., '#/components/schemas/User') directly to its definition.
165- * This robust implementation can traverse any valid local path within the specification.
166- * It gracefully handles invalid paths and non-local references by returning `undefined`.
248+ * Synchronously resolves a JSON reference string (e.g., './schemas.yaml#/User') to its definition.
249+ * This method reads from the pre-populated cache and can handle nested references.
167250 * @param ref The JSON reference string.
168- * @returns The resolved definition, or `undefined` if the reference is not found or is invalid.
251+ * @param currentDocUri The absolute URI of the document containing the reference. Defaults to the entry document's base URI.
252+ * @returns The resolved definition, or `undefined` if the reference cannot be resolved.
169253 */
170- public resolveReference < T = SwaggerDefinition > ( ref : string ) : T | undefined {
254+ public resolveReference < T = SwaggerDefinition > ( ref : string , currentDocUri : string = this . documentUri ) : T | undefined {
171255 if ( typeof ref !== 'string' ) {
172256 console . warn ( `[Parser] Encountered an unsupported or invalid reference: ${ ref } ` ) ;
173257 return undefined ;
174258 }
175- if ( ! ref . startsWith ( '#/' ) ) {
176- console . warn ( `[Parser] Unsupported external or non-root reference: ${ ref } ` ) ;
259+
260+ const [ filePath , jsonPointer ] = ref . split ( '#' , 2 ) ;
261+
262+ // Get the specification for the current document context to determine its logical base URI.
263+ const currentDocSpec = this . specCache . get ( currentDocUri ) ;
264+
265+ // This can happen if an invalid URI is somehow passed as the context.
266+ if ( ! currentDocSpec ) {
267+ console . warn ( `[Parser] Unresolved document URI in cache: ${ currentDocUri } . Cannot resolve reference "${ ref } ".` ) ;
177268 return undefined ;
178269 }
179- const pathParts = ref . substring ( 2 ) . split ( '/' ) ;
180- let current : unknown = this . spec ;
181- for ( const part of pathParts ) {
182- if ( typeof current === 'object' && current !== null && Object . prototype . hasOwnProperty . call ( current , part ) ) {
183- current = ( current as Record < string , unknown > ) [ part ] ;
184- } else {
185- console . warn ( `[Parser] Failed to resolve reference part "${ part } " in path "${ ref } "` ) ;
186- return undefined ;
270+
271+ // The base for resolving relative file paths is the document's logical URI, derived from its $self,
272+ // falling back to its physical URI.
273+ const logicalBaseUri = currentDocSpec . $self ? new URL ( currentDocSpec . $self , currentDocUri ) . href : currentDocUri ;
274+
275+ // The target file's physical URI is resolved using the logical base. If the ref is local, it's just the current doc's physical URI.
276+ const targetFileUri = filePath ? new URL ( filePath , logicalBaseUri ) . href : currentDocUri ;
277+
278+ const targetSpec = this . specCache . get ( targetFileUri ) ;
279+ if ( ! targetSpec ) {
280+ console . warn ( `[Parser] Unresolved external file reference: ${ targetFileUri } . File was not pre-loaded.` ) ;
281+ return undefined ;
282+ }
283+
284+ let result : any = targetSpec ;
285+ if ( jsonPointer ) {
286+ // Gracefully handle pointers that are just "/" or empty
287+ const pointerParts = jsonPointer . split ( '/' ) . filter ( p => p !== '' ) ;
288+ for ( const part of pointerParts ) {
289+ const decodedPart = part . replace ( / ~ 1 / g, '/' ) . replace ( / ~ 0 / g, '~' ) ;
290+ if ( typeof result === 'object' && result !== null && Object . prototype . hasOwnProperty . call ( result , decodedPart ) ) {
291+ result = result [ decodedPart ] ;
292+ } else {
293+ console . warn ( `[Parser] Failed to resolve reference part "${ decodedPart } " in path "${ ref } " within file ${ targetFileUri } ` ) ;
294+ return undefined ;
295+ }
187296 }
188297 }
189- return current as T ;
298+
299+ // Handle nested $refs recursively, passing the physical URI of the new document context.
300+ if ( isRefObject ( result ) ) {
301+ return this . resolveReference ( result . $ref , targetFileUri ) ;
302+ }
303+
304+ return result as T ;
190305 }
191306
192307 /**
0 commit comments