@@ -102,32 +102,89 @@ export const findAllRefs = (
102102const invalidSegment = ( pathSegment : string ) =>
103103 pathSegment === '#' || pathSegment === undefined || pathSegment === '' ;
104104
105+ /**
106+ * Map for tracking schema resolution to prevent infinite recursion.
107+ * Key: schema object reference, Value: Set of paths being resolved from that schema
108+ */
109+ type ResolutionTrackingMap = Map < JsonSchema , Set < string > > ;
110+ interface ResolveContext {
111+ rootSchema : JsonSchema ;
112+ resolutionMap : ResolutionTrackingMap ;
113+ }
114+
105115/**
106116 * Resolve the given schema path in order to obtain a subschema.
107117 * @param {JsonSchema } schema the root schema from which to start
108118 * @param {string } schemaPath the schema path to be resolved
109119 * @param {JsonSchema } rootSchema the actual root schema
110- * @returns {JsonSchema } the resolved sub-schema
120+ * @returns {JsonSchema } the resolved sub-schema or undefined
111121 */
112122export const resolveSchema = (
113123 schema : JsonSchema ,
114124 schemaPath : string ,
115125 rootSchema : JsonSchema
116- ) : JsonSchema => {
117- const segments = schemaPath ?. split ( '/' ) . map ( decode ) ;
118- return resolveSchemaWithSegments ( schema , segments , rootSchema ) ;
126+ ) : JsonSchema | undefined => {
127+ const result = doResolveSchema ( schema , schemaPath , {
128+ rootSchema,
129+ resolutionMap : new Map ( ) ,
130+ } ) ;
131+ return result ;
119132} ;
120133
121- const resolveSchemaWithSegments = (
134+ const doResolveSchema = (
122135 schema : JsonSchema ,
123- pathSegments : string [ ] ,
124- rootSchema : JsonSchema
136+ schemaPath : string ,
137+ ctx : ResolveContext
138+ ) : JsonSchema | undefined => {
139+ let resolvedSchema : JsonSchema | undefined = undefined ;
140+ // If the schema has a $ref, we resolve it first before continuing.
141+ if ( schema && typeof schema . $ref === 'string' ) {
142+ const baseSchema = resolvePath ( ctx . rootSchema , schema . $ref , ctx ) ;
143+ if ( baseSchema !== undefined ) {
144+ resolvedSchema = resolvePath ( baseSchema , schemaPath , ctx ) ;
145+ }
146+ }
147+ // With later versions of JSON Schema, the $ref can also be used next to other properties,
148+ // therefore we also try to resolve the path from the schema itself, even if it has a $ref.
149+ if ( resolvedSchema === undefined ) {
150+ resolvedSchema = resolvePath ( schema , schemaPath , ctx ) ;
151+ }
152+ return resolvedSchema ;
153+ } ;
154+
155+ const resolvePath = (
156+ schema : JsonSchema ,
157+ schemaPath : string | undefined ,
158+ ctx : ResolveContext
125159) : JsonSchema => {
126- // use typeof because schema can by of any type - check singleSegmentResolveSchema below
127- if ( typeof schema ?. $ref === 'string' ) {
128- schema = resolveSchema ( rootSchema , schema . $ref , rootSchema ) ;
160+ let visitedPaths : Set < string > | undefined = ctx . resolutionMap . get ( schema ) ;
161+ if ( ! visitedPaths ) {
162+ visitedPaths = new Set ( ) ;
163+ ctx . resolutionMap . set ( schema , visitedPaths ) ;
164+ }
165+ if ( visitedPaths . has ( schemaPath ) ) {
166+ // We were already asked to resolve this path from this schema, we must be stuck in a circular reference.
167+ return undefined ;
129168 }
130169
170+ visitedPaths . add ( schemaPath ) ;
171+
172+ const resolvedSchema = resolvePathSegmentsWithCombinatorFallback (
173+ schema ,
174+ schemaPath ?. split ( '/' ) . map ( decode ) ,
175+ ctx
176+ ) ;
177+
178+ visitedPaths . delete ( schemaPath ) ;
179+
180+ return resolvedSchema ;
181+ } ;
182+
183+ const resolvePathSegmentsWithCombinatorFallback = (
184+ schema : JsonSchema ,
185+ pathSegments : string [ ] ,
186+ ctx : ResolveContext
187+ ) : JsonSchema | undefined => {
131188 if ( ! pathSegments || pathSegments . length === 0 ) {
132189 return schema ;
133190 }
@@ -136,49 +193,109 @@ const resolveSchemaWithSegments = (
136193 return undefined ;
137194 }
138195
139- const [ segment , ...remainingSegments ] = pathSegments ;
196+ const resolvedSchema = resolvePathSegments ( schema , pathSegments , ctx ) ;
197+ if ( resolvedSchema !== undefined ) {
198+ return resolvedSchema ;
199+ }
140200
141- if ( invalidSegment ( segment ) ) {
142- return resolveSchemaWithSegments ( schema , remainingSegments , rootSchema ) ;
201+ // If the schema is not found, try combinators
202+ const subSchemas = [ ] . concat (
203+ schema . oneOf ?? [ ] ,
204+ schema . allOf ?? [ ] ,
205+ schema . anyOf ?? [ ] ,
206+ ( schema as JsonSchema7 ) . then ?? [ ] ,
207+ ( schema as JsonSchema7 ) . else ?? [ ]
208+ ) ;
209+
210+ for ( const subSchema of subSchemas ) {
211+ let resolvedSubSchema = subSchema ;
212+ // check whether the subSchema is a $ref. If it is, resolve it first.
213+ if ( subSchema && typeof subSchema . $ref === 'string' ) {
214+ resolvedSubSchema = doResolveSchema ( ctx . rootSchema , subSchema . $ref , ctx ) ;
215+ }
216+ const alternativeResolveResult = resolvePathSegmentsWithCombinatorFallback (
217+ resolvedSubSchema ,
218+ pathSegments ,
219+ ctx
220+ ) ;
221+ if ( alternativeResolveResult ) {
222+ return alternativeResolveResult ;
223+ }
143224 }
144225
145- const singleSegmentResolveSchema = get ( schema , segment ) ;
226+ return undefined ;
227+ } ;
146228
147- const resolvedSchema = resolveSchemaWithSegments (
148- singleSegmentResolveSchema ,
149- remainingSegments ,
150- rootSchema
151- ) ;
152- if ( resolvedSchema ) {
153- return resolvedSchema ;
229+ const resolvePathSegments = (
230+ schema : JsonSchema ,
231+ pathSegments : string [ ] ,
232+ ctx : ResolveContext
233+ ) : JsonSchema | undefined => {
234+ if ( ! pathSegments || pathSegments . length === 0 ) {
235+ return schema ;
154236 }
155237
156- if ( segment === 'properties' || segment === 'items' ) {
157- // Let's try to resolve the path, assuming oneOf/allOf/anyOf/then/else was omitted.
158- // We only do this when traversing an object or array as we want to avoid
159- // following a property which is named oneOf, allOf, anyOf, then or else.
160- let alternativeResolveResult = undefined ;
238+ if ( isEmpty ( schema ) ) {
239+ return undefined ;
240+ }
241+
242+ // perform a single step
243+ const singleStepResult = resolveSingleStep ( schema , pathSegments ) ;
161244
162- const subSchemas = [ ] . concat (
163- schema . oneOf ?? [ ] ,
164- schema . allOf ?? [ ] ,
165- schema . anyOf ?? [ ] ,
166- ( schema as JsonSchema7 ) . then ?? [ ] ,
167- ( schema as JsonSchema7 ) . else ?? [ ]
245+ // Check whether resolving the next step was successful and returned a schema which has a $ref itself.
246+ // In this case, we need to resolve the $ref first before continuing.
247+ if (
248+ singleStepResult . schema &&
249+ typeof singleStepResult . schema . $ref === 'string'
250+ ) {
251+ singleStepResult . schema = doResolveSchema (
252+ ctx . rootSchema ,
253+ singleStepResult . schema . $ref ,
254+ ctx
168255 ) ;
256+ }
169257
170- for ( const subSchema of subSchemas ) {
171- alternativeResolveResult = resolveSchemaWithSegments (
172- subSchema ,
173- [ segment , ...remainingSegments ] ,
174- rootSchema
175- ) ;
176- if ( alternativeResolveResult ) {
177- break ;
178- }
179- }
180- return alternativeResolveResult ;
258+ return resolvePathSegmentsWithCombinatorFallback (
259+ singleStepResult . schema ,
260+ singleStepResult . remainingPathSegments ,
261+ ctx
262+ ) ;
263+ } ;
264+
265+ interface ResolveSingleStepResult {
266+ schema : JsonSchema | undefined ;
267+ remainingPathSegments : string [ ] ;
268+ resolvedSegment ?: string ;
269+ }
270+ /**
271+ * Tries to resolve the next "step" of the pathSegments.
272+ * Often this will be a single segment, but it might be multiples ones in case there are invalid segments.
273+ */
274+ const resolveSingleStep = (
275+ schema : JsonSchema ,
276+ pathSegments : string [ ]
277+ ) : ResolveSingleStepResult => {
278+ if ( ! pathSegments || pathSegments . length === 0 ) {
279+ return { schema, remainingPathSegments : [ ] } ;
181280 }
182281
183- return undefined ;
282+ if ( isEmpty ( schema ) ) {
283+ return {
284+ schema : undefined ,
285+ remainingPathSegments : pathSegments ,
286+ } ;
287+ }
288+
289+ const [ segment , ...remainingPathSegments ] = pathSegments ;
290+
291+ if ( invalidSegment ( segment ) ) {
292+ return resolveSingleStep ( schema , remainingPathSegments ) ;
293+ }
294+
295+ const singleSegmentResolveSchema = get ( schema , segment ) ;
296+ return {
297+ schema : singleSegmentResolveSchema ,
298+ remainingPathSegments,
299+ resolvedSegment : segment ,
300+ } ;
184301} ;
0 commit comments