@@ -12,12 +12,12 @@ import {
1212 ServerObject ,
1313 SwaggerDefinition ,
1414 SwaggerSpec
15- } from './types.js' ;
15+ } from './types/index .js' ;
1616import * as fs from 'node:fs' ;
1717import * as path from 'node:path' ;
1818import { pathToFileURL } from 'node:url' ;
1919import yaml from 'js-yaml' ;
20- import { extractPaths , isUrl , pascalCase } from './utils.js' ;
20+ import { extractPaths , isUrl , pascalCase } from './utils/index .js' ;
2121import { validateSpec } from './validator.js' ;
2222import { JSON_SCHEMA_2020_12_DIALECT , OAS_3_1_DIALECT } from './constants.js' ;
2323
@@ -132,9 +132,15 @@ export class SwaggerParser {
132132 // If a cache isn't provided, create one with just the entry spec.
133133 this . specCache = specCache || new Map < string , SwaggerSpec > ( [ [ this . documentUri , spec ] ] ) ;
134134
135- // Ensure the entry spec's internal $ids are indexed if constructed manually without the factory
135+ // Ensure the entry spec's internal $ids and $anchors are indexed if constructed manually without the factory
136136 if ( ! specCache ) {
137- SwaggerParser . indexSchemaIds ( spec , documentUri , this . specCache ) ;
137+ const baseUri = spec . $self ? new URL ( spec . $self , documentUri ) . href : documentUri ;
138+ // Aliasing: if $self differs from the retrieval URI, we map the baseUri to the spec too
139+ // so it can be resolved by its logical ID.
140+ if ( baseUri !== documentUri ) {
141+ this . specCache . set ( baseUri , spec ) ;
142+ }
143+ SwaggerParser . indexSchemaIds ( spec , baseUri , this . specCache ) ;
138144 }
139145
140146 this . schemas = Object . entries ( this . getDefinitions ( ) ) . map ( ( [ name , definition ] ) => ( {
@@ -187,7 +193,13 @@ export class SwaggerParser {
187193
188194 const baseUri = spec . $self ? new URL ( spec . $self , uri ) . href : uri ;
189195
190- // Important: Index any `$id` properties within the document immediately.
196+ // Aliasing: map the logical base URI to the spec as well, if different.
197+ // This ensures lookups via $ref using the ID/Self URI find the cached object.
198+ if ( baseUri !== uri ) {
199+ cache . set ( baseUri , spec ) ;
200+ }
201+
202+ // Important: Index any `$id` and `$anchor` properties within the document immediately.
191203 // This allows internal sub-schemas to be referenced by their global URI (OAS 3.1 / JSON Schema).
192204 this . indexSchemaIds ( spec , baseUri , cache ) ;
193205
@@ -206,9 +218,9 @@ export class SwaggerParser {
206218 }
207219
208220 /**
209- * Traverses a document structure to find JSON Schema `$id` keywords.
210- * Maps the resolved absolute URI of the ID to the schema configuration object in the cache.
211- * This creates "virtual" documents in the cache, supporting `$ref` resolution by ID.
221+ * Traverses a document structure to find JSON Schema `$id`, `$anchor` and `$dynamicAnchor` keywords.
222+ * Maps the resolved absolute URI of the ID or anchor to the schema object in the cache.
223+ * This creates "virtual" endpoints in the cache, supporting `$ref` resolution by ID or anchor fragment .
212224 *
213225 * @param spec The document root or fragment to traverse.
214226 * @param baseUri The current base URI of the scope.
@@ -226,21 +238,47 @@ export class SwaggerParser {
226238 let nextBase = currentBase ;
227239
228240 // Check for $id (OAS 3.1 / JSON Schema definition)
241+ // A schema with an `$id` changes the resolution scope for itself and its children.
229242 if ( '$id' in obj && typeof obj . $id === 'string' ) {
230243 try {
231244 // Resolve $id against the current base.
232- // $id can be relative or absolute.
233- // If absolute, it resets the base.
245+ // $id can be relative (rare) or absolute.
234246 nextBase = new URL ( obj . $id , currentBase ) . href ;
235247
236- // Cache this object as a distinct "document" at this URI
248+ // Cache this object as a distinct "document" at this URI.
237249 // We use cast because cache expects SwaggerSpec, but these are Schema definitions.
238- // Our resolution logic handles both.
250+ // Our resolution logic handles both types .
239251 if ( ! cache . has ( nextBase ) ) {
240252 cache . set ( nextBase , obj as SwaggerSpec ) ;
241253 }
242254 } catch ( e ) {
243- // Ignore invalid $id values
255+ // Ignore invalid $id values, proceed with current scope
256+ }
257+ }
258+
259+ // Check for $anchor (OAS 3.1 / JSON Schema 2020-12)
260+ // An anchor creates a unique identifier for the schema within the *current* base scope.
261+ // Syntax: The URI ref is `currentBase#anchorName`.
262+ if ( '$anchor' in obj && typeof obj . $anchor === 'string' ) {
263+ // Anchors strictly must not contain '#'.
264+ const anchor = obj . $anchor ;
265+ const anchorUri = `${ nextBase } #${ anchor } ` ;
266+
267+ // Cache this specific object at the fully resolved anchor URI
268+ if ( ! cache . has ( anchorUri ) ) {
269+ cache . set ( anchorUri , obj as SwaggerSpec ) ;
270+ }
271+ }
272+
273+ // Check for $dynamicAnchor (OAS 3.1 / JSON Schema 2020-12)
274+ // Similar to $anchor, but primarily for dynamic referencing.
275+ // For static resolution purposes, we treat it like a standard anchor target.
276+ if ( '$dynamicAnchor' in obj && typeof obj . $dynamicAnchor === 'string' ) {
277+ const anchor = obj . $dynamicAnchor ;
278+ const anchorUri = `${ nextBase } #${ anchor } ` ;
279+
280+ if ( ! cache . has ( anchorUri ) ) {
281+ cache . set ( anchorUri , obj as SwaggerSpec ) ;
244282 }
245283 }
246284
@@ -440,20 +478,34 @@ export class SwaggerParser {
440478 const currentDocSpec = this . specCache . get ( currentDocUri ) ;
441479
442480 // logicalBaseUri calculation: checks $self first, then falls back to current physical URI.
443- // NOTE: For $id resolution, the cache key IS the $id (resolved), so looking up by URI works
444- // naturally if `indexSchemaIds` has populated the cache.
445481 const logicalBaseUri = currentDocSpec ?. $self ? new URL ( currentDocSpec . $self , currentDocUri ) . href : currentDocUri ;
446482
447483 // The target file's physical URI is resolved using the logical base.
448- // If ref is absolute (e.g. based on $id), URL construction handles it correctly.
449- const targetFileUri = filePath ? new URL ( filePath , logicalBaseUri ) . href : currentDocUri ;
484+ // If ref is absolute (e.g. based on $id or http URI), URL construction handles it correctly.
485+ const targetUri = filePath ? new URL ( filePath , logicalBaseUri ) . href : logicalBaseUri ;
486+
487+ // 1. Direct Cache Lookup (Supports $id and $anchor lookups)
488+ // If the ref points to a known $id or $anchor (e.g. http://full.uri#anchor),
489+ // `targetUri` will contain the base and `jsonPointer` will contain the anchor name.
490+ // We construct the potential key and check the cache.
491+ // Note: jsonPointer here might be an actual pointer path OR an anchor name.
492+ // Parser's indexSchemaIds puts anchors in the cache as "baseUri#anchor".
493+ const fullUriKey = jsonPointer ? `${ targetUri } #${ jsonPointer } ` : targetUri ;
494+
495+ if ( this . specCache . has ( fullUriKey ) ) {
496+ return this . specCache . get ( fullUriKey ) as unknown as T ;
497+ }
450498
451- const targetSpec = this . specCache . get ( targetFileUri ) ;
499+ // 2. Spec File Cache Lookup - Fallback to traversing the document
500+ const targetSpec = this . specCache . get ( targetUri ) ;
452501 if ( ! targetSpec ) {
453- console . warn ( `[Parser] Unresolved external file reference: ${ targetFileUri } . File was not pre-loaded.` ) ;
502+ console . warn ( `[Parser] Unresolved external file reference: ${ targetUri } . File was not pre-loaded.` ) ;
454503 return undefined ;
455504 }
456505
506+ // 3. JSON Pointer Traversal
507+ // If we are here, it means `ref` was not a direct ID or Anchor match.
508+ // We treat `jsonPointer` as a standard JSON pointer traversing the `targetSpec`.
457509 let result : any = targetSpec ;
458510 if ( jsonPointer ) {
459511 // Gracefully handle pointers that are just "/" or empty
@@ -463,20 +515,22 @@ export class SwaggerParser {
463515 if ( typeof result === 'object' && result !== null && Object . prototype . hasOwnProperty . call ( result , decodedPart ) ) {
464516 result = result [ decodedPart ] ;
465517 } else {
466- console . warn ( `[Parser] Failed to resolve reference part "${ decodedPart } " in path "${ ref } " within file ${ targetFileUri } ` ) ;
518+ // It's common to fail here if `jsonPointer` was actually an anchor name that wasn't indexed.
519+ // But if indexSchemaIds ran correctly, we should have hit Step 1.
520+ console . warn ( `[Parser] Failed to resolve reference part "${ decodedPart } " in path "${ ref } " within file ${ targetUri } ` ) ;
467521 return undefined ;
468522 }
469523 }
470524 }
471525
472526 // Handle nested $refs recursively, passing the physical URI of the new document context.
473527 if ( isRefObject ( result ) ) {
474- return this . resolveReference ( result . $ref , targetFileUri ) ;
528+ return this . resolveReference ( result . $ref , targetUri ) ;
475529 }
476530
477531 // Handle nested $dynamicRefs recursively
478532 if ( isDynamicRefObject ( result ) ) {
479- return this . resolveReference ( result . $dynamicRef , targetFileUri ) ;
533+ return this . resolveReference ( result . $dynamicRef , targetUri ) ;
480534 }
481535
482536 return result as T ;
0 commit comments