Skip to content

Commit b41b800

Browse files
fix: use ajv for schema ref resolving (#454)
1 parent 260edc7 commit b41b800

File tree

3 files changed

+292
-188
lines changed

3 files changed

+292
-188
lines changed

index.js

Lines changed: 67 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ const { randomUUID } = require('crypto')
1313
const validate = require('./schema-validator')
1414

1515
let largeArraySize = 2e4
16-
let stringSimilarity = null
1716
let largeArrayMechanism = 'default'
1817
const validLargeArrayMechanisms = [
1918
'default',
@@ -45,6 +44,7 @@ function isValidSchema (schema, name) {
4544
function mergeLocation (source, dest) {
4645
return {
4746
schema: dest.schema || source.schema,
47+
schemaRef: dest.schemaRef || source.schemaRef,
4848
root: dest.root || source.root,
4949
externalSchema: dest.externalSchema || source.externalSchema
5050
}
@@ -55,6 +55,7 @@ const objectReferenceSerializersMap = new Map()
5555
const schemaReferenceMap = new Map()
5656

5757
let ajvInstance = null
58+
let schemaRefResolver = null
5859

5960
class Serializer {
6061
constructor (options = {}) {
@@ -223,6 +224,40 @@ class Serializer {
223224
}
224225
}
225226

227+
function getSchema (ref, location) {
228+
let ajvSchema
229+
let schemaRef
230+
231+
if (ref[0] === '#') {
232+
schemaRef = location.schemaRef + ref
233+
} else {
234+
schemaRef = ref
235+
location.schemaRef = ref.split('#')[0]
236+
}
237+
238+
try {
239+
ajvSchema = schemaRefResolver.getSchema(schemaRef)
240+
} catch (error) {
241+
throw new Error(`Cannot find reference "${ref}"`)
242+
}
243+
244+
if (ajvSchema === undefined) {
245+
throw new Error(`Cannot find reference "${ref}"`)
246+
}
247+
248+
let schema = ajvSchema.schema
249+
if (schema.$ref !== undefined) {
250+
schema = getSchema(schema.$ref, location).schema
251+
}
252+
253+
return {
254+
root: schema,
255+
schema,
256+
schemaRef: location.schemaRef,
257+
externalSchema: location.externalSchema
258+
}
259+
}
260+
226261
function build (schema, options) {
227262
arrayItemsReferenceSerializersMap.clear()
228263
objectReferenceSerializersMap.clear()
@@ -256,11 +291,28 @@ function build (schema, options) {
256291
}
257292
})
258293

294+
schemaRefResolver = new Ajv()
295+
const mainSchemaRef = schema.$id || randomUUID()
296+
259297
isValidSchema(schema)
298+
schemaRefResolver.addSchema(schema, mainSchemaRef)
260299
if (options.schema) {
261-
// eslint-disable-next-line
262-
for (var key of Object.keys(options.schema)) {
263-
isValidSchema(options.schema[key], key)
300+
for (const key of Object.keys(options.schema)) {
301+
const externalSchema = options.schema[key]
302+
isValidSchema(externalSchema, key)
303+
304+
if (externalSchema.$id !== undefined) {
305+
if (externalSchema.$id[0] === '#') {
306+
schemaRefResolver.addSchema(externalSchema, key + externalSchema.$id)
307+
} else {
308+
schemaRefResolver.addSchema(externalSchema)
309+
if (externalSchema.$id !== key) {
310+
schemaRefResolver.addSchema({ $ref: externalSchema.$id }, key)
311+
}
312+
}
313+
} else {
314+
schemaRefResolver.addSchema(externalSchema, key)
315+
}
264316
}
265317
}
266318

@@ -290,12 +342,13 @@ function build (schema, options) {
290342

291343
let location = {
292344
schema,
345+
schemaRef: mainSchemaRef,
293346
root: schema,
294347
externalSchema: options.schema
295348
}
296349

297350
if (schema.$ref) {
298-
location = refFinder(schema.$ref, location)
351+
location = getSchema(schema.$ref, location)
299352
schema = location.schema
300353
}
301354

@@ -326,6 +379,7 @@ function build (schema, options) {
326379
const stringifyFunc = contextFunc(ajvInstance, serializer)
327380

328381
ajvInstance = null
382+
schemaRefResolver = null
329383
arrayItemsReferenceSerializersMap.clear()
330384
objectReferenceSerializersMap.clear()
331385
schemaReferenceMap.clear()
@@ -413,7 +467,7 @@ function addPatternProperties (location) {
413467
Object.keys(pp).forEach((regex, index) => {
414468
let ppLocation = mergeLocation(location, { schema: pp[regex] })
415469
if (pp[regex].$ref) {
416-
ppLocation = refFinder(pp[regex].$ref, location)
470+
ppLocation = getSchema(pp[regex].$ref, location)
417471
pp[regex] = ppLocation.schema
418472
}
419473

@@ -461,7 +515,7 @@ function additionalProperty (location) {
461515
}
462516
let apLocation = mergeLocation(location, { schema: ap })
463517
if (ap.$ref) {
464-
apLocation = refFinder(ap.$ref, location)
518+
apLocation = getSchema(ap.$ref, location)
465519
ap = apLocation.schema
466520
}
467521

@@ -490,140 +544,9 @@ function addAdditionalProperties (location) {
490544
return { code, laterCode: additionalPropertyCode.laterCode }
491545
}
492546

493-
function idFinder (schema, searchedId) {
494-
let objSchema
495-
const explore = (schema, searchedId) => {
496-
Object.keys(schema || {}).forEach((key, i, a) => {
497-
if (key === '$id' && schema[key] === searchedId) {
498-
objSchema = schema
499-
} else if (objSchema === undefined && typeof schema[key] === 'object') {
500-
explore(schema[key], searchedId)
501-
}
502-
})
503-
}
504-
explore(schema, searchedId)
505-
return objSchema
506-
}
507-
508-
function refFinder (ref, location) {
509-
const externalSchema = location.externalSchema
510-
let root = location.root
511-
let schema = location.schema
512-
513-
if (externalSchema && externalSchema[ref]) {
514-
return {
515-
schema: externalSchema[ref],
516-
root: externalSchema[ref],
517-
externalSchema
518-
}
519-
}
520-
521-
// Split file from walk
522-
ref = ref.split('#')
523-
524-
// Check schemaReferenceMap for $id entry
525-
if (ref[0] && schemaReferenceMap.has(ref[0])) {
526-
schema = schemaReferenceMap.get(ref[0])
527-
root = schemaReferenceMap.get(ref[0])
528-
if (schema.$ref) {
529-
return refFinder(schema.$ref, {
530-
schema,
531-
root,
532-
externalSchema
533-
})
534-
}
535-
} else if (ref[0]) { // If external file
536-
schema = externalSchema[ref[0]]
537-
root = externalSchema[ref[0]]
538-
539-
if (schema === undefined) {
540-
findBadKey(externalSchema, [ref[0]])
541-
}
542-
543-
if (schema.$ref) {
544-
return refFinder(schema.$ref, {
545-
schema,
546-
root,
547-
externalSchema
548-
})
549-
}
550-
}
551-
552-
let code = 'return schema'
553-
// If it has a path
554-
if (ref[1]) {
555-
// ref[1] could contain a JSON pointer - ex: /definitions/num
556-
// or plain name fragment id without suffix # - ex: customId
557-
const walk = ref[1].split('/')
558-
if (walk.length === 1) {
559-
const targetId = `#${ref[1]}`
560-
let dereferenced = idFinder(schema, targetId)
561-
if (dereferenced === undefined && !ref[0]) {
562-
// eslint-disable-next-line
563-
for (var key of Object.keys(externalSchema)) {
564-
dereferenced = idFinder(externalSchema[key], targetId)
565-
if (dereferenced !== undefined) {
566-
root = externalSchema[key]
567-
break
568-
}
569-
}
570-
}
571-
572-
return {
573-
schema: dereferenced,
574-
root,
575-
externalSchema
576-
}
577-
} else {
578-
// eslint-disable-next-line
579-
for (var i = 1; i < walk.length; i++) {
580-
code += `[${JSON.stringify(walk[i])}]`
581-
}
582-
}
583-
}
584-
let result
585-
try {
586-
result = (new Function('schema', code))(root)
587-
} catch (err) {}
588-
589-
if (result === undefined && ref[1]) {
590-
const walk = ref[1].split('/')
591-
findBadKey(schema, walk.slice(1))
592-
}
593-
594-
if (result.$ref) {
595-
return refFinder(result.$ref, {
596-
schema,
597-
root,
598-
externalSchema
599-
})
600-
}
601-
602-
return {
603-
schema: result,
604-
root,
605-
externalSchema
606-
}
607-
608-
function findBadKey (obj, keys) {
609-
if (keys.length === 0) return null
610-
const key = keys.shift()
611-
if (obj[key] === undefined) {
612-
stringSimilarity = stringSimilarity || require('string-similarity')
613-
const { bestMatch } = stringSimilarity.findBestMatch(key, Object.keys(obj))
614-
if (bestMatch.rating >= 0.5) {
615-
throw new Error(`Cannot find reference ${JSON.stringify(key)}, did you mean ${JSON.stringify(bestMatch.target)}?`)
616-
} else {
617-
throw new Error(`Cannot find reference ${JSON.stringify(key)}`)
618-
}
619-
}
620-
return findBadKey(obj[key], keys)
621-
}
622-
}
623-
624547
function buildCode (location, code, laterCode, locationPath) {
625548
if (location.schema.$ref) {
626-
location = refFinder(location.schema.$ref, location)
549+
location = getSchema(location.schema.$ref, location)
627550
}
628551

629552
const schema = location.schema
@@ -632,7 +555,7 @@ function buildCode (location, code, laterCode, locationPath) {
632555
Object.keys(schema.properties || {}).forEach((key) => {
633556
let propertyLocation = mergeLocation(location, { schema: schema.properties[key] })
634557
if (schema.properties[key].$ref) {
635-
propertyLocation = refFinder(schema.properties[key].$ref, location)
558+
propertyLocation = getSchema(schema.properties[key].$ref, location)
636559
schema.properties[key] = propertyLocation.schema
637560
}
638561

@@ -682,7 +605,7 @@ function buildCode (location, code, laterCode, locationPath) {
682605
function mergeAllOfSchema (location, schema, mergedSchema) {
683606
for (let allOfSchema of schema.allOf) {
684607
if (allOfSchema.$ref) {
685-
allOfSchema = refFinder(allOfSchema.$ref, mergeLocation(location, { schema: allOfSchema })).schema
608+
allOfSchema = getSchema(allOfSchema.$ref, mergeLocation(location, { schema: allOfSchema })).schema
686609
}
687610

688611
let allOfSchemaType = allOfSchema.type
@@ -934,7 +857,7 @@ function buildArray (location, code, functionName, locationPath) {
934857
schema[fjsCloned] = true
935858
}
936859

937-
location = refFinder(schema.items.$ref, location)
860+
location = getSchema(schema.items.$ref, location)
938861
schema.items = location.schema
939862

940863
if (arrayItemsReferenceSerializersMap.has(schema.items)) {
@@ -1068,7 +991,7 @@ function dereferenceOfRefs (location, type) {
1068991
// follow the refs
1069992
let sLocation = mergeLocation(location, { schema: s })
1070993
while (s.$ref) {
1071-
sLocation = refFinder(s.$ref, sLocation)
994+
sLocation = getSchema(s.$ref, sLocation)
1072995
schema[type][index] = sLocation.schema
1073996
s = schema[type][index]
1074997
}
@@ -1087,7 +1010,7 @@ function buildValue (laterCode, locationPath, input, location) {
10871010
let schema = location.schema
10881011

10891012
if (schema.$ref) {
1090-
schema = refFinder(schema.$ref, location)
1013+
schema = getSchema(schema.$ref, location)
10911014
}
10921015

10931016
if (schema.type === undefined) {

test/allof.test.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,14 @@ test('object with external $refs in allOf', (t) => {
402402
}
403403
},
404404
second: {
405-
id2: {
406-
$id: '#id2',
407-
type: 'object',
408-
properties: {
409-
id2: {
410-
type: 'integer'
405+
definitions: {
406+
id2: {
407+
$id: '#id2',
408+
type: 'object',
409+
properties: {
410+
id2: {
411+
type: 'integer'
412+
}
411413
}
412414
}
413415
}
@@ -422,7 +424,7 @@ test('object with external $refs in allOf', (t) => {
422424
$ref: 'first#/definitions/id1'
423425
},
424426
{
425-
$ref: 'second#id2'
427+
$ref: 'second#/definitions/id2'
426428
}
427429
]
428430
}

0 commit comments

Comments
 (0)