@@ -36,7 +36,7 @@ export class OpenapiLoader {
3636 return JSON . parse ( cached ) ;
3737 }
3838
39- const spec = await this . loadFromSource ( profile . openapiSpecSource ) ;
39+ const spec = await this . loadAndResolveSpec ( profile . openapiSpecSource ) ;
4040 this . ensureCacheDir ( cachePath ) ;
4141
4242 const serialized = JSON . stringify ( spec , null , 2 ) ;
@@ -45,6 +45,17 @@ export class OpenapiLoader {
4545 return spec ;
4646 }
4747
48+ private async loadAndResolveSpec ( source : string ) : Promise < unknown > {
49+ const rawDocCache = new Map < string , unknown > ( ) ;
50+ const root = await this . loadDocument ( source , rawDocCache ) ;
51+ return this . resolveRefs ( root , {
52+ currentSource : source ,
53+ currentDocument : root ,
54+ rawDocCache,
55+ resolvingRefs : new Set < string > ( ) ,
56+ } ) ;
57+ }
58+
4859 private async loadFromSource ( source : string ) : Promise < unknown > {
4960 if ( source . startsWith ( "http://" ) || source . startsWith ( "https://" ) ) {
5061 const response = await axios . get ( source , { responseType : "text" } ) ;
@@ -55,6 +66,16 @@ export class OpenapiLoader {
5566 return this . parseSpec ( raw , source ) ;
5667 }
5768
69+ private async loadDocument ( source : string , rawDocCache : Map < string , unknown > ) : Promise < unknown > {
70+ if ( rawDocCache . has ( source ) ) {
71+ return rawDocCache . get ( source ) ;
72+ }
73+
74+ const loaded = await this . loadFromSource ( source ) ;
75+ rawDocCache . set ( source , loaded ) ;
76+ return loaded ;
77+ }
78+
5879 private parseSpec ( content : string | object , source : string ) : unknown {
5980 if ( typeof content !== "string" ) {
6081 return content ;
@@ -65,6 +86,128 @@ export class OpenapiLoader {
6586 return JSON . parse ( content ) ;
6687 }
6788
89+ private async resolveRefs (
90+ value : unknown ,
91+ context : {
92+ currentSource : string ;
93+ currentDocument : unknown ;
94+ rawDocCache : Map < string , unknown > ;
95+ resolvingRefs : Set < string > ;
96+ }
97+ ) : Promise < unknown > {
98+ if ( Array . isArray ( value ) ) {
99+ const items = await Promise . all ( value . map ( ( item ) => this . resolveRefs ( item , context ) ) ) ;
100+ return items ;
101+ }
102+
103+ if ( ! value || typeof value !== "object" ) {
104+ return value ;
105+ }
106+
107+ const record = value as Record < string , unknown > ;
108+ const ref = record . $ref ;
109+
110+ if ( typeof ref === "string" ) {
111+ const siblingEntries = Object . entries ( record ) . filter ( ( [ key ] ) => key !== "$ref" ) ;
112+ const resolvedRef = await this . resolveRef ( ref , context ) ;
113+ const resolvedSiblings = Object . fromEntries (
114+ await Promise . all (
115+ siblingEntries . map ( async ( [ key , siblingValue ] ) => [ key , await this . resolveRefs ( siblingValue , context ) ] as const )
116+ )
117+ ) ;
118+
119+ if ( resolvedRef && typeof resolvedRef === "object" && ! Array . isArray ( resolvedRef ) ) {
120+ return {
121+ ...( resolvedRef as Record < string , unknown > ) ,
122+ ...resolvedSiblings ,
123+ } ;
124+ }
125+
126+ return Object . keys ( resolvedSiblings ) . length > 0 ? resolvedSiblings : resolvedRef ;
127+ }
128+
129+ const resolvedEntries = await Promise . all (
130+ Object . entries ( record ) . map ( async ( [ key , nested ] ) => [ key , await this . resolveRefs ( nested , context ) ] as const )
131+ ) ;
132+ return Object . fromEntries ( resolvedEntries ) ;
133+ }
134+
135+ private async resolveRef (
136+ ref : string ,
137+ context : {
138+ currentSource : string ;
139+ currentDocument : unknown ;
140+ rawDocCache : Map < string , unknown > ;
141+ resolvingRefs : Set < string > ;
142+ }
143+ ) : Promise < unknown > {
144+ const { source, pointer } = this . splitRef ( ref , context . currentSource ) ;
145+ const cacheKey = `${ source } #${ pointer } ` ;
146+
147+ if ( context . resolvingRefs . has ( cacheKey ) ) {
148+ return { $ref : ref } ;
149+ }
150+
151+ context . resolvingRefs . add ( cacheKey ) ;
152+
153+ const targetDocument = source === context . currentSource
154+ ? context . currentDocument
155+ : await this . loadDocument ( source , context . rawDocCache ) ;
156+
157+ const targetValue = this . resolvePointer ( targetDocument , pointer ) ;
158+ const resolvedValue = await this . resolveRefs ( targetValue , {
159+ currentSource : source ,
160+ currentDocument : targetDocument ,
161+ rawDocCache : context . rawDocCache ,
162+ resolvingRefs : context . resolvingRefs ,
163+ } ) ;
164+
165+ context . resolvingRefs . delete ( cacheKey ) ;
166+ return resolvedValue ;
167+ }
168+
169+ private splitRef ( ref : string , currentSource : string ) : { source : string ; pointer : string } {
170+ const [ refSource , pointer = "" ] = ref . split ( "#" , 2 ) ;
171+ if ( ! refSource ) {
172+ return { source : currentSource , pointer } ;
173+ }
174+
175+ if ( refSource . startsWith ( "http://" ) || refSource . startsWith ( "https://" ) ) {
176+ return { source : refSource , pointer } ;
177+ }
178+
179+ if ( currentSource . startsWith ( "http://" ) || currentSource . startsWith ( "https://" ) ) {
180+ return { source : new URL ( refSource , currentSource ) . toString ( ) , pointer } ;
181+ }
182+
183+ return { source : path . resolve ( path . dirname ( currentSource ) , refSource ) , pointer } ;
184+ }
185+
186+ private resolvePointer ( document : unknown , pointer : string ) : unknown {
187+ if ( ! pointer ) {
188+ return document ;
189+ }
190+
191+ if ( ! pointer . startsWith ( "/" ) ) {
192+ return document ;
193+ }
194+
195+ const parts = pointer
196+ . slice ( 1 )
197+ . split ( "/" )
198+ . map ( ( part ) => part . replace ( / ~ 1 / g, "/" ) . replace ( / ~ 0 / g, "~" ) ) ;
199+
200+ let current : unknown = document ;
201+ for ( const part of parts ) {
202+ if ( ! current || typeof current !== "object" || ! ( part in ( current as Record < string , unknown > ) ) ) {
203+ return undefined ;
204+ }
205+ current = ( current as Record < string , unknown > ) [ part ] ;
206+ }
207+
208+ return current ;
209+ }
210+
68211 private isYamlSource ( source : string ) : boolean {
69212 const lower = source . toLowerCase ( ) . split ( "?" ) [ 0 ] ;
70213 return lower . endsWith ( ".yaml" ) || lower . endsWith ( ".yml" ) ;
0 commit comments