Skip to content

Commit f3b4183

Browse files
fix: merged schemas reference resolving (#546)
* fix: merged schemas reference resolving * refactor: move Location class to separate module
1 parent e6b3c5f commit f3b4183

File tree

3 files changed

+144
-46
lines changed

3 files changed

+144
-46
lines changed

index.js

Lines changed: 44 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const validate = require('./lib/schema-validator')
1010
const Serializer = require('./lib/serializer')
1111
const Validator = require('./lib/validator')
1212
const RefResolver = require('./lib/ref-resolver')
13+
const Location = require('./lib/location')
1314

1415
let largeArraySize = 2e4
1516
let largeArrayMechanism = 'default'
@@ -40,22 +41,13 @@ function isValidSchema (schema, name) {
4041
}
4142
}
4243

43-
function mergeLocation (location, key) {
44-
return {
45-
schema: location.schema[key],
46-
schemaId: location.schemaId,
47-
jsonPointer: location.jsonPointer + '/' + key,
48-
isValidated: location.isValidated
49-
}
50-
}
51-
5244
function resolveRef (location, ref) {
5345
let hashIndex = ref.indexOf('#')
5446
if (hashIndex === -1) {
5547
hashIndex = ref.length
5648
}
5749

58-
const schemaId = ref.slice(0, hashIndex) || location.schemaId
50+
const schemaId = ref.slice(0, hashIndex) || location.getOriginSchemaId()
5951
const jsonPointer = ref.slice(hashIndex) || '#'
6052

6153
const schema = refResolver.getSchema(schemaId, jsonPointer)
@@ -68,11 +60,12 @@ function resolveRef (location, ref) {
6860
validatorSchemasIds.add(schemaId)
6961
}
7062

63+
const newLocation = new Location(schema, schemaId, jsonPointer, location.isValidated)
7164
if (schema.$ref !== undefined) {
72-
return resolveRef({ schema, schemaId, jsonPointer }, schema.$ref)
65+
return resolveRef(newLocation, schema.$ref)
7366
}
7467

75-
return { schema, schemaId, jsonPointer, isValidated: location.isValidated }
68+
return newLocation
7669
}
7770

7871
const contextFunctionsNamesBySchema = new Map()
@@ -125,7 +118,7 @@ function build (schema, options) {
125118
}
126119
}
127120

128-
const location = { schema, schemaId: rootSchemaId, jsonPointer: '#', isValidated: false }
121+
const location = new Location(schema, rootSchemaId)
129122
const code = buildValue(location, 'input')
130123

131124
const contextFunctionCode = `
@@ -253,17 +246,17 @@ function buildExtraObjectPropertiesSerializer (location) {
253246
) continue
254247
`
255248

256-
const patternPropertiesLocation = mergeLocation(location, 'patternProperties')
249+
const patternPropertiesLocation = location.getPropertyLocation('patternProperties')
257250
const patternPropertiesSchema = patternPropertiesLocation.schema
258251

259252
if (patternPropertiesSchema !== undefined) {
260253
for (const propertyKey in patternPropertiesSchema) {
261-
const propertyLocation = mergeLocation(patternPropertiesLocation, propertyKey)
254+
const propertyLocation = patternPropertiesLocation.getPropertyLocation(propertyKey)
262255

263256
try {
264257
RegExp(propertyKey)
265258
} catch (err) {
266-
const jsonPointer = propertyLocation.schema + propertyLocation.jsonPointer
259+
const jsonPointer = propertyLocation.getSchemaRef()
267260
throw new Error(`${err.message}. Invalid pattern property regexp key ${propertyKey} at ${jsonPointer}`)
268261
}
269262

@@ -278,7 +271,7 @@ function buildExtraObjectPropertiesSerializer (location) {
278271
}
279272
}
280273

281-
const additionalPropertiesLocation = mergeLocation(location, 'additionalProperties')
274+
const additionalPropertiesLocation = location.getPropertyLocation('additionalProperties')
282275
const additionalPropertiesSchema = additionalPropertiesLocation.schema
283276

284277
if (additionalPropertiesSchema !== undefined) {
@@ -288,7 +281,7 @@ function buildExtraObjectPropertiesSerializer (location) {
288281
json += serializer.asString(key) + ':' + JSON.stringify(value)
289282
`
290283
} else {
291-
const propertyLocation = mergeLocation(location, 'additionalProperties')
284+
const propertyLocation = location.getPropertyLocation('additionalProperties')
292285
code += `
293286
${addComma}
294287
json += serializer.asString(key) + ':'
@@ -309,9 +302,9 @@ function buildInnerObject (location) {
309302

310303
let code = ''
311304

312-
const propertiesLocation = mergeLocation(location, 'properties')
305+
const propertiesLocation = location.getPropertyLocation('properties')
313306
Object.keys(schema.properties || {}).forEach((key) => {
314-
let propertyLocation = mergeLocation(propertiesLocation, key)
307+
let propertyLocation = propertiesLocation.getPropertyLocation(key)
315308
if (propertyLocation.schema.$ref) {
316309
propertyLocation = resolveRef(location, propertyLocation.schema.$ref)
317310
}
@@ -362,13 +355,13 @@ function buildInnerObject (location) {
362355
}
363356

364357
function mergeAllOfSchema (location, schema, mergedSchema) {
365-
const allOfLocation = mergeLocation(location, 'allOf')
358+
const allOfLocation = location.getPropertyLocation('allOf')
366359

367360
for (let i = 0; i < schema.allOf.length; i++) {
368361
let allOfSchema = schema.allOf[i]
369362

370363
if (allOfSchema.$ref) {
371-
const allOfSchemaLocation = mergeLocation(allOfLocation, i)
364+
const allOfSchemaLocation = allOfLocation.getPropertyLocation(i)
372365
allOfSchema = resolveRef(allOfSchemaLocation, allOfSchema.$ref).schema
373366
}
374367

@@ -457,13 +450,12 @@ function mergeAllOfSchema (location, schema, mergedSchema) {
457450

458451
mergedSchema.$id = `merged_${randomUUID()}`
459452
refResolver.addSchema(mergedSchema)
460-
location.schemaId = mergedSchema.$id
461-
location.jsonPointer = '#'
453+
location.addMergedSchema(mergedSchema, mergedSchema.$id)
462454
}
463455

464456
function addIfThenElse (location, input) {
465457
location.isValidated = true
466-
validatorSchemasIds.add(location.schemaId)
458+
validatorSchemasIds.add(location.getSchemaId())
467459

468460
const schema = merge({}, location.schema)
469461
const thenSchema = schema.then
@@ -473,13 +465,13 @@ function addIfThenElse (location, input) {
473465
delete schema.then
474466
delete schema.else
475467

476-
const ifLocation = mergeLocation(location, 'if')
477-
const ifSchemaRef = ifLocation.schemaId + ifLocation.jsonPointer
468+
const ifLocation = location.getPropertyLocation('if')
469+
const ifSchemaRef = ifLocation.getSchemaRef()
478470

479-
const thenLocation = mergeLocation(location, 'then')
471+
const thenLocation = location.getPropertyLocation('then')
480472
thenLocation.schema = merge(schema, thenSchema)
481473

482-
const elseLocation = mergeLocation(location, 'else')
474+
const elseLocation = location.getPropertyLocation('else')
483475
elseLocation.schema = merge(schema, elseSchema)
484476

485477
return `
@@ -508,10 +500,14 @@ function buildObject (location) {
508500
const functionName = generateFuncName()
509501
contextFunctionsNamesBySchema.set(schema, functionName)
510502

511-
const schemaId = location.schemaId === rootSchemaId ? '' : location.schemaId
503+
let schemaRef = location.getSchemaRef()
504+
if (schemaRef.startsWith(rootSchemaId)) {
505+
schemaRef = schemaRef.replace(rootSchemaId, '')
506+
}
507+
512508
let functionCode = `
513509
function ${functionName} (input) {
514-
// ${schemaId + location.jsonPointer}
510+
// ${schemaRef}
515511
`
516512

517513
functionCode += `
@@ -534,7 +530,7 @@ function buildObject (location) {
534530
function buildArray (location) {
535531
const schema = location.schema
536532

537-
let itemsLocation = mergeLocation(location, 'items')
533+
let itemsLocation = location.getPropertyLocation('items')
538534
itemsLocation.schema = itemsLocation.schema || {}
539535

540536
if (itemsLocation.schema.$ref) {
@@ -550,10 +546,14 @@ function buildArray (location) {
550546
const functionName = generateFuncName()
551547
contextFunctionsNamesBySchema.set(schema, functionName)
552548

553-
const schemaId = location.schemaId === rootSchemaId ? '' : location.schemaId
549+
let schemaRef = location.getSchemaRef()
550+
if (schemaRef.startsWith(rootSchemaId)) {
551+
schemaRef = schemaRef.replace(rootSchemaId, '')
552+
}
553+
554554
let functionCode = `
555555
function ${functionName} (obj) {
556-
// ${schemaId + location.jsonPointer}
556+
// ${schemaRef}
557557
`
558558

559559
functionCode += `
@@ -586,7 +586,7 @@ function buildArray (location) {
586586
if (Array.isArray(itemsSchema)) {
587587
for (let i = 0; i < itemsSchema.length; i++) {
588588
const item = itemsSchema[i]
589-
const tmpRes = buildValue(mergeLocation(itemsLocation, i), `obj[${i}]`)
589+
const tmpRes = buildValue(itemsLocation.getPropertyLocation(i), `obj[${i}]`)
590590
functionCode += `
591591
if (${i} < arrayLength) {
592592
if (${buildArrayTypeCondition(item.type, `[${i}]`)}) {
@@ -682,11 +682,11 @@ function buildMultiTypeSerializer (location, input) {
682682

683683
let code = ''
684684

685-
const locationClone = clone(location)
686685
types.forEach((type, index) => {
686+
location.schema = { ...location.schema, type }
687+
const nestedResult = buildSingleTypeSerializer(location, input)
688+
687689
const statement = index === 0 ? 'if' : 'else if'
688-
locationClone.schema.type = type
689-
const nestedResult = buildSingleTypeSerializer(locationClone, input)
690690
switch (type) {
691691
case 'null':
692692
code += `
@@ -831,10 +831,8 @@ function buildValue (location, input) {
831831
}
832832

833833
if (schema.allOf) {
834-
const mergedSchema = clone(schema)
835-
mergeAllOfSchema(location, schema, mergedSchema)
836-
schema = mergedSchema
837-
location.schema = mergedSchema
834+
mergeAllOfSchema(location, schema, clone(schema))
835+
schema = location.schema
838836
}
839837

840838
const type = schema.type
@@ -843,14 +841,14 @@ function buildValue (location, input) {
843841

844842
if (type === undefined && (schema.anyOf || schema.oneOf)) {
845843
location.isValidated = true
846-
validatorSchemasIds.add(location.schemaId)
844+
validatorSchemasIds.add(location.getSchemaId())
847845

848846
const type = schema.anyOf ? 'anyOf' : 'oneOf'
849-
const anyOfLocation = mergeLocation(location, type)
847+
const anyOfLocation = location.getPropertyLocation(type)
850848

851849
for (let index = 0; index < location.schema[type].length; index++) {
852-
const optionLocation = mergeLocation(anyOfLocation, index)
853-
const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer
850+
const optionLocation = anyOfLocation.getPropertyLocation(index)
851+
const schemaRef = optionLocation.getSchemaRef()
854852
const nestedResult = buildValue(optionLocation, input)
855853
code += `
856854
${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))

lib/location.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict'
2+
3+
class Location {
4+
constructor (schema, schemaId, jsonPointer = '#', isValidated = false) {
5+
this.schema = schema
6+
this.schemaId = schemaId
7+
this.jsonPointer = jsonPointer
8+
this.isValidated = isValidated
9+
this.mergedSchemaId = null
10+
}
11+
12+
getPropertyLocation (propertyName) {
13+
const propertyLocation = new Location(
14+
this.schema[propertyName],
15+
this.schemaId,
16+
this.jsonPointer + '/' + propertyName,
17+
this.isValidated
18+
)
19+
20+
if (this.mergedSchemaId !== null) {
21+
propertyLocation.addMergedSchema(
22+
this.schema[propertyName],
23+
this.mergedSchemaId,
24+
this.jsonPointer + '/' + propertyName
25+
)
26+
}
27+
28+
return propertyLocation
29+
}
30+
31+
// Use this method to get current schema location.
32+
// Use it when you need to create reference to the current location.
33+
getSchemaId () {
34+
return this.mergedSchemaId || this.schemaId
35+
}
36+
37+
// Use this method to get original schema id for resolving user schema $refs
38+
// Don't join it with a JSON pointer to get the current location.
39+
getOriginSchemaId () {
40+
return this.schemaId
41+
}
42+
43+
getSchemaRef () {
44+
const schemaId = this.getSchemaId()
45+
return schemaId + this.jsonPointer
46+
}
47+
48+
addMergedSchema (mergedSchema, schemaId, jsonPointer = '#') {
49+
this.schema = mergedSchema
50+
this.mergedSchemaId = schemaId
51+
this.jsonPointer = jsonPointer
52+
}
53+
}
54+
55+
module.exports = Location

test/allof.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,48 @@ test('object with external $refs in allOf', (t) => {
437437
})
438438
t.equal(value, '{"id1":1,"id2":2}')
439439
})
440+
441+
test('allof with local anchor reference', (t) => {
442+
t.plan(1)
443+
444+
const externalSchemas = {
445+
Test: {
446+
$id: 'Test',
447+
definitions: {
448+
Problem: {
449+
type: 'object',
450+
properties: {
451+
type: {
452+
type: 'string'
453+
}
454+
}
455+
},
456+
ValidationFragment: {
457+
type: 'string'
458+
},
459+
ValidationErrorProblem: {
460+
type: 'object',
461+
allOf: [
462+
{
463+
$ref: '#/definitions/Problem'
464+
},
465+
{
466+
type: 'object',
467+
properties: {
468+
validation: {
469+
$ref: '#/definitions/ValidationFragment'
470+
}
471+
}
472+
}
473+
]
474+
}
475+
}
476+
}
477+
}
478+
479+
const schema = { $ref: 'Test#/definitions/ValidationErrorProblem' }
480+
const stringify = build(schema, { schema: externalSchemas })
481+
const data = { type: 'foo', validation: 'bar' }
482+
483+
t.equal(stringify(data), JSON.stringify(data))
484+
})

0 commit comments

Comments
 (0)