@@ -20,6 +20,9 @@ const isDynamicRefObject = (obj: unknown): obj is DynamicRefObject =>
2020 $dynamicRef : unknown
2121 } ) . $dynamicRef === 'string' ;
2222
23+ /**
24+ * Resolves OpenAPI references ($ref and $dynamicRef) including context-aware resolution for OAS 3.1.
25+ */
2326export class ReferenceResolver {
2427 constructor (
2528 private specCache : Map < string , SwaggerSpec > ,
@@ -61,6 +64,7 @@ export class ReferenceResolver {
6164
6265 // $dynamicAnchor
6366 if ( '$dynamicAnchor' in obj && typeof obj . $dynamicAnchor === 'string' ) {
67+ // Store mapping for dynamic anchor in the cache map
6468 const anchorUri = `${ nextBase } #${ obj . $dynamicAnchor } ` ;
6569 if ( ! cache . has ( anchorUri ) ) {
6670 cache . set ( anchorUri , obj as SwaggerSpec ) ;
@@ -98,17 +102,24 @@ export class ReferenceResolver {
98102 return Array . from ( refs ) ;
99103 }
100104
101- public resolve < T > ( obj : T | { $ref : string } | { $dynamicRef : string } | null | undefined ) : T | undefined {
105+ /**
106+ * Resolves an object that might be a reference.
107+ * @param obj The object to resolve (or null).
108+ * @param resolutionStack The stack of URIs traversed so far (for context-aware $dynamicRef resolution).
109+ */
110+ public resolve < T > ( obj : T | { $ref : string } | {
111+ $dynamicRef : string
112+ } | null | undefined , resolutionStack : string [ ] = [ ] ) : T | undefined {
102113 if ( obj === null || obj === undefined ) return undefined ;
103114
104115 let resolved : T | undefined ;
105116 let refObj : RefObject | DynamicRefObject | null = null ;
106117
107118 if ( isRefObject ( obj ) ) {
108- resolved = this . resolveReference < T > ( obj . $ref ) ;
119+ resolved = this . resolveReference < T > ( obj . $ref , this . entryDocumentUri , resolutionStack ) ;
109120 refObj = obj ;
110121 } else if ( isDynamicRefObject ( obj ) ) {
111- resolved = this . resolveReference < T > ( obj . $dynamicRef ) ;
122+ resolved = this . resolveReference < T > ( obj . $dynamicRef , this . entryDocumentUri , resolutionStack ) ;
112123 refObj = obj ;
113124 } else {
114125 return obj as T ;
@@ -127,9 +138,18 @@ export class ReferenceResolver {
127138 return resolved ;
128139 }
129140
130- public resolveReference < T = SwaggerDefinition > ( ref : string , currentDocUri : string = this . entryDocumentUri ) : T | undefined {
141+ /**
142+ * Resolves a specific reference string.
143+ * @param ref The reference string (URI or fragment).
144+ * @param currentDocUri The URI of the document containing the reference.
145+ * @param resolutionStack The stack of unique schema URIs encountered during resolution. Used for $dynamicRef lookup.
146+ */
147+ public resolveReference < T = SwaggerDefinition > (
148+ ref : string ,
149+ currentDocUri : string = this . entryDocumentUri ,
150+ resolutionStack : string [ ] = [ ]
151+ ) : T | undefined {
131152 if ( typeof ref !== 'string' ) {
132- console . warn ( `[Parser] Encountered an unsupported or invalid reference: ${ ref } ` ) ;
133153 return undefined ;
134154 }
135155
@@ -138,20 +158,34 @@ export class ReferenceResolver {
138158 const logicalBaseUri = currentDocSpec ?. $self ? new URL ( currentDocSpec . $self , currentDocUri ) . href : currentDocUri ;
139159 const targetUri = filePath ? new URL ( filePath , logicalBaseUri ) . href : logicalBaseUri ;
140160
141- // 1. Direct Cache Lookup ($id/$anchor)
161+ // 1. Dynamic Anchor Resolution (OAS 3.1)
162+ // Dynamic resolution traverses the stack from the outermost (start of resolution)
163+ // to find the first context that defines this anchor.
164+ if ( jsonPointer && ! jsonPointer . includes ( '/' ) ) {
165+ for ( const scopeUri of resolutionStack ) {
166+ const dynamicKey = `${ scopeUri } #${ jsonPointer } ` ;
167+ if ( this . specCache . has ( dynamicKey ) ) {
168+ return this . specCache . get ( dynamicKey ) as unknown as T ;
169+ }
170+ }
171+ }
172+
173+ // 2. Direct Cache Lookup ($id/$anchor - static)
142174 const fullUriKey = jsonPointer ? `${ targetUri } #${ jsonPointer } ` : targetUri ;
143175 if ( this . specCache . has ( fullUriKey ) ) {
144176 return this . specCache . get ( fullUriKey ) as unknown as T ;
145177 }
146178
147- // 2 . Spec File Cache Lookup
179+ // 3 . Spec File Cache Lookup
148180 const targetSpec = this . specCache . get ( targetUri ) ;
149181 if ( ! targetSpec ) {
150- console . warn ( `[Parser] Unresolved external file reference: ${ targetUri } . File was not pre-loaded.` ) ;
182+ if ( filePath ) {
183+ console . warn ( `[Parser] Unresolved external file reference: ${ targetUri } . File was not pre-loaded.` ) ;
184+ }
151185 return undefined ;
152186 }
153187
154- // 3 . JSON Pointer Traversal
188+ // 4 . JSON Pointer Traversal
155189 let result : any = targetSpec ;
156190 if ( jsonPointer ) {
157191 const pointerParts = jsonPointer . split ( '/' ) . filter ( p => p !== '' ) ;
@@ -167,11 +201,16 @@ export class ReferenceResolver {
167201 }
168202
169203 // Handle nested Refs (Recursive resolution)
170- if ( isRefObject ( result ) ) {
171- return this . resolveReference ( result . $ref , targetUri ) ;
172- }
173- if ( isDynamicRefObject ( result ) ) {
174- return this . resolveReference ( result . $dynamicRef , targetUri ) ;
204+ if ( typeof result === 'object' && result !== null ) {
205+ // Push current scope to stack for dynamic resolution downstream
206+ const newStack = [ ...resolutionStack , fullUriKey ] ;
207+
208+ if ( isRefObject ( result ) ) {
209+ return this . resolveReference ( result . $ref , targetUri , newStack ) ;
210+ }
211+ if ( isDynamicRefObject ( result ) ) {
212+ return this . resolveReference ( result . $dynamicRef , targetUri , newStack ) ;
213+ }
175214 }
176215
177216 return result as T ;
0 commit comments