Skip to content

Commit

Permalink
fix: update merged schema local refs
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-tymoshenko committed Nov 25, 2023
1 parent 4f6f7b8 commit 0d36211
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 114 deletions.
198 changes: 114 additions & 84 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function resolveRef (context, location, ref) {
hashIndex = ref.length
}

const schemaId = ref.slice(0, hashIndex) || location.getOriginSchemaId()
const schemaId = ref.slice(0, hashIndex) || location.schemaId
const jsonPointer = ref.slice(hashIndex) || '#'

const schema = context.refResolver.getSchema(schemaId, jsonPointer)
Expand Down Expand Up @@ -407,107 +407,137 @@ function buildInnerObject (context, location) {
return code
}

function mergeAllOfSchema (context, location, schema, mergedSchema) {
const allOfLocation = location.getPropertyLocation('allOf')
function mergeAllOfSchema (context, location) {
const schema = location.schema
const schemaWithoutAllOf = clone(schema)
const clonedAllOfSchemas = schemaWithoutAllOf.allOf

for (let i = 0; i < schema.allOf.length; i++) {
let allOfSchema = schema.allOf[i]
delete schemaWithoutAllOf.allOf

if (allOfSchema.$ref) {
const allOfSchemaLocation = allOfLocation.getPropertyLocation(i)
allOfSchema = resolveRef(context, allOfSchemaLocation, allOfSchema.$ref).schema
}
const allOfSchemasLocation = location.getPropertyLocation('allOf')
allOfSchemasLocation.schema = clonedAllOfSchemas

let allOfSchemaType = allOfSchema.type
if (allOfSchemaType === undefined) {
allOfSchemaType = inferTypeByKeyword(allOfSchema)
}
const locations = [
new Location(
schemaWithoutAllOf,
location.schemaId,
location.jsonPointer
)
]

if (allOfSchemaType !== undefined) {
if (
mergedSchema.type !== undefined &&
mergedSchema.type !== allOfSchemaType
) {
throw new Error('allOf schemas have different type values')
}
mergedSchema.type = allOfSchemaType
}
for (let i = 0; i < clonedAllOfSchemas.length; i++) {
const allOfSchemaLocation = allOfSchemasLocation.getPropertyLocation(i)
locations.push(allOfSchemaLocation)
}

if (allOfSchema.format !== undefined) {
if (
mergedSchema.format !== undefined &&
mergedSchema.format !== allOfSchema.format
) {
throw new Error('allOf schemas have different format values')
}
mergedSchema.format = allOfSchema.format
}
const mergedLocation = mergeSchemas(context, locations)
return mergedLocation
}

if (allOfSchema.nullable !== undefined) {
if (
mergedSchema.nullable !== undefined &&
mergedSchema.nullable !== allOfSchema.nullable
) {
throw new Error('allOf schemas have different nullable values')
}
mergedSchema.nullable = allOfSchema.nullable
}
function mergeSchemas (context, locations) {
const mergedSchema = {}

if (allOfSchema.properties !== undefined) {
if (mergedSchema.properties === undefined) {
mergedSchema.properties = {}
}
Object.assign(mergedSchema.properties, allOfSchema.properties)
}
for (let location of locations) {
let schema = location.schema
resolveRelativeRefs(schema, location.schemaId)

if (allOfSchema.additionalProperties !== undefined) {
if (mergedSchema.additionalProperties === undefined) {
mergedSchema.additionalProperties = {}
}
Object.assign(mergedSchema.additionalProperties, allOfSchema.additionalProperties)
}
if (schema.$ref) {
const { $ref, ...schemaWithoutRef } = schema
const refSchemaLocation = resolveRef(context, location, $ref)
refSchemaLocation.schema = clone(refSchemaLocation.schema)

if (allOfSchema.patternProperties !== undefined) {
if (mergedSchema.patternProperties === undefined) {
mergedSchema.patternProperties = {}
}
Object.assign(mergedSchema.patternProperties, allOfSchema.patternProperties)
}
const newSchemaLocation = new Location(
schemaWithoutRef,
location.schemaId,
location.jsonPointer
)

if (allOfSchema.required !== undefined) {
if (mergedSchema.required === undefined) {
mergedSchema.required = []
}
mergedSchema.required.push(...allOfSchema.required)
location = mergeSchemas(
context,
[refSchemaLocation, newSchemaLocation]
)
schema = location.schema
}

if (allOfSchema.oneOf !== undefined) {
if (mergedSchema.oneOf === undefined) {
mergedSchema.oneOf = []
}
mergedSchema.oneOf.push(...allOfSchema.oneOf)
}
for (const key in schema) {
const value = schema[key]

if (allOfSchema.anyOf !== undefined) {
if (mergedSchema.anyOf === undefined) {
mergedSchema.anyOf = []
if (key === '$id') continue
if (key === 'allOf') {
if (mergedSchema.allOf === undefined) {
mergedSchema.allOf = []
}
mergedSchema.allOf.push(...value)
} else if (key === 'anyOf') {
if (mergedSchema.anyOf === undefined) {
mergedSchema.anyOf = []
}
mergedSchema.anyOf.push(...value)
} else if (key === 'oneOf') {
if (mergedSchema.oneOf === undefined) {
mergedSchema.oneOf = []
}
mergedSchema.oneOf.push(...value)
} else if (key === 'required') {
if (mergedSchema.required === undefined) {
mergedSchema.required = []
}
mergedSchema.required.push(...value)
} else if (key === 'properties') {
if (mergedSchema.properties === undefined) {
mergedSchema.properties = {}
}
Object.assign(mergedSchema.properties, value)
} else if (key === 'patternProperties') {
if (mergedSchema.patternProperties === undefined) {
mergedSchema.patternProperties = {}
}
Object.assign(mergedSchema.patternProperties, value)
} else if (key === 'additionalProperties') {
if (mergedSchema.additionalProperties === undefined) {
mergedSchema.additionalProperties = {}
}
Object.assign(mergedSchema.additionalProperties, value)
} else if (key === 'type') {
if (mergedSchema.type !== undefined && mergedSchema.type !== value) {
throw new Error('allOf schemas have different type values')
}
mergedSchema.type = value
} else if (key === 'format') {
if (mergedSchema.format !== undefined && mergedSchema.format !== value) {
throw new Error('allOf schemas have different format values')
}
mergedSchema.format = value
} else if (key === 'nullable') {
if (mergedSchema.nullable !== undefined && mergedSchema.nullable !== value) {
throw new Error('allOf schemas have different nullable values')
}
mergedSchema.nullable = value
}
mergedSchema.anyOf.push(...allOfSchema.anyOf)
}
}
const mergedSchemaId = `__fjs_merged_${schemaIdCounter++}`
const mergedSchemaLocation = new Location(mergedSchema, mergedSchemaId)
context.refResolver.addSchema(mergedSchema, mergedSchemaId)
return mergedSchemaLocation
}

if (allOfSchema.allOf !== undefined) {
mergeAllOfSchema(context, location, allOfSchema, mergedSchema)
function resolveRelativeRefs (schema, schemaId) {
if (schema.$ref?.charAt(0) === '#') {
schema.$ref = schemaId + schema.$ref
}
for (const subSchema of Object.values(schema)) {
if (
typeof subSchema === 'object' &&
subSchema.$id === undefined
) {
resolveRelativeRefs(subSchema, schemaId)
}
}
delete mergedSchema.allOf

mergedSchema.$id = `__fjs_merged_${schemaIdCounter++}`
context.refResolver.addSchema(mergedSchema)
location.addMergedSchema(mergedSchema, mergedSchema.$id)
}

function addIfThenElse (context, location, input) {
context.validatorSchemasIds.add(location.getSchemaId())
context.validatorSchemasIds.add(location.schemaId)

const schema = merge({}, location.schema)
const thenSchema = schema.then
Expand Down Expand Up @@ -872,16 +902,16 @@ function buildValue (context, location, input) {
}

if (schema.allOf) {
mergeAllOfSchema(context, location, schema, clone(schema))
schema = location.schema
const mergedSchemaLocation = mergeAllOfSchema(context, location)
return buildValue(context, mergedSchemaLocation, input)
}

const type = schema.type

let code = ''

if ((type === undefined || type === 'object') && (schema.anyOf || schema.oneOf)) {
context.validatorSchemasIds.add(location.getSchemaId())
context.validatorSchemasIds.add(location.schemaId)

if (schema.type === 'object') {
context.wrapObjects = false
Expand Down
31 changes: 1 addition & 30 deletions lib/location.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ class Location {
this.schema = schema
this.schemaId = schemaId
this.jsonPointer = jsonPointer
this.mergedSchemaId = null
}

getPropertyLocation (propertyName) {
Expand All @@ -14,39 +13,11 @@ class Location {
this.schemaId,
this.jsonPointer + '/' + propertyName
)

if (this.mergedSchemaId !== null) {
propertyLocation.addMergedSchema(
this.schema[propertyName],
this.mergedSchemaId,
this.jsonPointer + '/' + propertyName
)
}

return propertyLocation
}

// Use this method to get current schema location.
// Use it when you need to create reference to the current location.
getSchemaId () {
return this.mergedSchemaId || this.schemaId
}

// Use this method to get original schema id for resolving user schema $refs
// Don't join it with a JSON pointer to get the current location.
getOriginSchemaId () {
return this.schemaId
}

getSchemaRef () {
const schemaId = this.getSchemaId()
return schemaId + this.jsonPointer
}

addMergedSchema (mergedSchema, schemaId, jsonPointer = '#') {
this.schema = mergedSchema
this.mergedSchemaId = schemaId
this.jsonPointer = jsonPointer
return this.schemaId + this.jsonPointer
}
}

Expand Down
52 changes: 52 additions & 0 deletions test/allof.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,58 @@ test('allof with local anchor reference', (t) => {
t.equal(stringify(data), JSON.stringify(data))
})

test('allOf: multiple nested $ref properties', (t) => {
t.plan(2)

const externalSchema1 = {
$id: 'externalSchema1',
oneOf: [
{ $ref: '#/definitions/id1' }
],
definitions: {
id1: {
type: 'object',
properties: {
id1: {
type: 'integer'
}
},
additionalProperties: false
}
}
}

const externalSchema2 = {
$id: 'externalSchema2',
oneOf: [
{ $ref: '#/definitions/id2' }
],
definitions: {
id2: {
type: 'object',
properties: {
id2: {
type: 'integer'
}
},
additionalProperties: false
}
}
}

const schema = {
allOf: [
{ $ref: 'externalSchema1' },
{ $ref: 'externalSchema2' }
]
}

const stringify = build(schema, { schema: [externalSchema1, externalSchema2] })

t.equal(stringify({ id1: 1 }), JSON.stringify({ id1: 1 }))
t.equal(stringify({ id2: 2 }), JSON.stringify({ id2: 2 }))
})

test('allOf: throw Error if types mismatch ', (t) => {
t.plan(1)

Expand Down

0 comments on commit 0d36211

Please sign in to comment.