44 AnnotationElement ,
55 isArrayElement ,
66 isStringElement ,
7+ isParseResultElement ,
78 cloneDeep ,
89} from '@speclynx/apidom-datamodel' ;
910import {
@@ -18,14 +19,12 @@ import { toValue } from '@speclynx/apidom-core';
1819import * as url from '../../../util/url.ts' ;
1920import type { ReferenceOptions } from '../../../options/index.ts' ;
2021import { merge as mergeOptions } from '../../../options/util.ts' ;
21- import dereference from '../../index.ts' ;
22-
23- // shared key for recursion state (works across JSON/YAML documents)
24- const ARAZZO_DEREFERENCE_RECURSION_KEY = 'arazzo-1' ;
22+ import dereference , { dereferenceApiDOM } from '../../index.ts' ;
2523
2624interface DereferenceSourceDescriptionContext {
2725 baseURI : string ;
2826 options : ReferenceOptions ;
27+ strategyName : string ;
2928 currentDepth : number ;
3029 visitedUrls : Set < string > ;
3130}
@@ -66,7 +65,8 @@ async function dereferenceSourceDescription(
6665 return parseResult ;
6766 }
6867
69- const retrievalURI = url . resolve ( ctx . baseURI , sourceDescriptionURI ) ;
68+ // normalize URI for consistent cycle detection and refSet cache key matching
69+ const retrievalURI = url . sanitize ( url . stripHash ( url . resolve ( ctx . baseURI , sourceDescriptionURI ) ) ) ;
7070
7171 // skip if already visited (cycle detection)
7272 if ( ctx . visitedUrls . has ( retrievalURI ) ) {
@@ -79,23 +79,59 @@ async function dereferenceSourceDescription(
7979 }
8080 ctx . visitedUrls . add ( retrievalURI ) ;
8181
82+ // check if source description was already parsed (e.g., during parse phase with sourceDescriptions: true)
83+ const existingParseResult = sourceDescription . meta . get ( 'parseResult' ) ;
84+
8285 try {
83- const sourceDescriptionDereferenced = await dereference (
84- retrievalURI ,
85- mergeOptions ( ctx . options , {
86- parse : {
87- mediaType : 'text/plain' , // allow parser plugin detection
88- } ,
89- dereference : {
90- strategyOpts : {
91- [ ARAZZO_DEREFERENCE_RECURSION_KEY ] : {
92- sourceDescriptionsDepth : ctx . currentDepth + 1 ,
93- sourceDescriptionsVisitedUrls : ctx . visitedUrls ,
86+ let sourceDescriptionDereferenced : ParseResultElement ;
87+
88+ if ( isParseResultElement ( existingParseResult ) ) {
89+ // use existing parsed result - just dereference it (no re-fetch/re-parse)
90+ sourceDescriptionDereferenced = await dereferenceApiDOM (
91+ existingParseResult ,
92+ mergeOptions ( ctx . options , {
93+ parse : {
94+ mediaType : 'text/plain' , // allow dereference strategy detection via ApiDOM inspection
95+ } ,
96+ resolve : { baseURI : retrievalURI } ,
97+ dereference : {
98+ strategyOpts : {
99+ // nested documents should dereference all their source descriptions
100+ // (parent's name filter doesn't apply to nested documents)
101+ // set at strategy-specific level to override any inherited filters
102+ [ ctx . strategyName ] : {
103+ sourceDescriptions : true ,
104+ sourceDescriptionsDepth : ctx . currentDepth + 1 ,
105+ sourceDescriptionsVisitedUrls : ctx . visitedUrls ,
106+ } ,
94107 } ,
95108 } ,
96- } ,
97- } ) ,
98- ) ;
109+ } ) ,
110+ ) ;
111+ } else {
112+ // no existing parse result - fetch, parse, and dereference
113+ sourceDescriptionDereferenced = await dereference (
114+ retrievalURI ,
115+ mergeOptions ( ctx . options , {
116+ parse : {
117+ mediaType : 'text/plain' , // allow parser plugin detection
118+ } ,
119+ dereference : {
120+ strategyOpts : {
121+ // nested documents should dereference all their source descriptions
122+ // (parent's name filter doesn't apply to nested documents)
123+ // set at strategy-specific level to override any inherited filters
124+ [ ctx . strategyName ] : {
125+ sourceDescriptions : true ,
126+ sourceDescriptionsDepth : ctx . currentDepth + 1 ,
127+ sourceDescriptionsVisitedUrls : ctx . visitedUrls ,
128+ } ,
129+ } ,
130+ } ,
131+ } ) ,
132+ ) ;
133+ }
134+
99135 // merge dereferenced result into our parse result
100136 for ( const item of sourceDescriptionDereferenced ) {
101137 parseResult . push ( item ) ;
@@ -159,18 +195,27 @@ async function dereferenceSourceDescription(
159195 *
160196 * @param parseResult - ParseResult containing a parsed (optionally dereferenced) Arazzo specification
161197 * @param parseResultRetrievalURI - URI from which the parseResult was retrieved
162- * @param options - Full ReferenceOptions (caller responsibility to construct)
198+ * @param options - Full ReferenceOptions. Pass `sourceDescriptions` as an array of names
199+ * in `dereference.strategyOpts` to filter which source descriptions to process.
163200 * @param strategyName - Strategy name for options lookup (defaults to 'arazzo-1')
164- * @returns Array of ParseResultElements. On success, returns one ParseResultElement per
165- * source description (each with class 'source-description' and name/type metadata).
201+ * @returns Array of ParseResultElements. Returns one ParseResultElement per source description
202+ * (each with class 'source-description' and name/type metadata).
166203 * May return early with a single-element array containing a warning annotation when:
167204 * - The API is not an Arazzo specification
168205 * - The sourceDescriptions field is missing or not an array
169206 * - Maximum dereference depth is exceeded (error annotation)
170- * Returns an empty array when sourceDescriptions option is disabled or no names match.
207+ * Returns an empty array when no source description names match the filter .
171208 *
172209 * @example
173210 * ```typescript
211+ * // Dereference all source descriptions
212+ * await dereferenceSourceDescriptions(parseResult, uri, options);
213+ *
214+ * // Filter by name
215+ * await dereferenceSourceDescriptions(parseResult, uri, mergeOptions(options, {
216+ * dereference: { strategyOpts: { sourceDescriptions: ['petStore'] } },
217+ * }));
218+ *
174219 * // Access dereferenced document from source description element
175220 * const sourceDesc = parseResult.api.sourceDescriptions.get(0);
176221 * const dereferencedDoc = sourceDesc.meta.get('parseResult');
@@ -215,8 +260,8 @@ export async function dereferenceSourceDescriptions(
215260 options ?. dereference ?. strategyOpts ?. sourceDescriptionsMaxDepth ??
216261 + Infinity ;
217262
218- // recursion state comes from shared key (works across JSON/YAML)
219- const sharedOpts = options ?. dereference ?. strategyOpts ?. [ ARAZZO_DEREFERENCE_RECURSION_KEY ] ?? { } ;
263+ // recursion state comes from strategy-specific options
264+ const sharedOpts = options ?. dereference ?. strategyOpts ?. [ strategyName ] ?? { } ;
220265 const currentDepth = sharedOpts . sourceDescriptionsDepth ?? 0 ;
221266 const visitedUrls : Set < string > = sharedOpts . sourceDescriptionsVisitedUrls ?? new Set ( ) ;
222267
@@ -236,20 +281,16 @@ export async function dereferenceSourceDescriptions(
236281 const ctx : DereferenceSourceDescriptionContext = {
237282 baseURI,
238283 options,
284+ strategyName,
239285 currentDepth,
240286 visitedUrls,
241287 } ;
242288
243- // determine which source descriptions to dereference
289+ // determine which source descriptions to dereference (array filters by name)
244290 const sourceDescriptionsOption =
245291 options ?. dereference ?. strategyOpts ?. [ strategyName ] ?. sourceDescriptions ??
246292 options ?. dereference ?. strategyOpts ?. sourceDescriptions ;
247293
248- // handle false or other falsy values - no source descriptions should be dereferenced
249- if ( ! sourceDescriptionsOption ) {
250- return results ;
251- }
252-
253294 const sourceDescriptions = Array . isArray ( sourceDescriptionsOption )
254295 ? api . sourceDescriptions . filter ( ( sd ) => {
255296 if ( ! isSourceDescriptionElement ( sd ) ) return false ;
0 commit comments