From 35346a7d7d01fc778d063ba85d16137598903207 Mon Sep 17 00:00:00 2001 From: Ivan Tymoshenko Date: Sun, 4 Feb 2024 16:33:35 +0100 Subject: [PATCH] Revert "Revert "fix: update internal schema merging (#665)"" This reverts commit 9dd8cb6c5da1b80856207ea08cea76e0e1964b91. --- index.js | 397 +++++++++++++++++++++----------------- lib/location.js | 31 +-- lib/merge-schemas.js | 9 + package.json | 4 +- test/allof.test.js | 149 ++++++++++++-- test/anyof.test.js | 121 ++++++++++++ test/if-then-else.test.js | 67 ++++++- 7 files changed, 544 insertions(+), 234 deletions(-) create mode 100644 lib/merge-schemas.js diff --git a/index.js b/index.js index 20dd386b..54542411 100644 --- a/index.js +++ b/index.js @@ -2,14 +2,13 @@ /* eslint no-prototype-builtins: 0 */ -const merge = require('@fastify/deepmerge')() -const clone = require('rfdc')({ proto: true }) const { RefResolver } = require('json-schema-ref-resolver') -const validate = require('./lib/schema-validator') const Serializer = require('./lib/serializer') const Validator = require('./lib/validator') const Location = require('./lib/location') +const validate = require('./lib/schema-validator') +const mergeSchemas = require('./lib/merge-schemas') const SINGLE_TICK = /'/g @@ -32,6 +31,8 @@ const addComma = '!addComma && (addComma = true) || (json += \',\')' let schemaIdCounter = 0 +const mergedSchemaRef = Symbol('fjs-merged-schema-ref') + function isValidSchema (schema, name) { if (!validate(schema)) { if (name) { @@ -46,13 +47,15 @@ function isValidSchema (schema, name) { } } -function resolveRef (context, location, ref) { +function resolveRef (context, location) { + const ref = location.schema.$ref + let hashIndex = ref.indexOf('#') if (hashIndex === -1) { 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) @@ -62,12 +65,17 @@ function resolveRef (context, location, ref) { const newLocation = new Location(schema, schemaId, jsonPointer) if (schema.$ref !== undefined) { - return resolveRef(context, newLocation, schema.$ref) + return resolveRef(context, newLocation) } return newLocation } +function getMergedLocation (context, mergedSchemaId) { + const mergedSchema = context.refResolver.getSchema(mergedSchemaId, '#') + return new Location(mergedSchema, mergedSchemaId, '#') +} + function getSchemaId (schema, rootSchemaId) { if (schema.$id && schema.$id.charAt(0) !== '#') { return schema.$id @@ -85,7 +93,6 @@ function build (schema, options) { functionsCounter: 0, functionsNamesBySchema: new Map(), options, - wrapObjects: true, refResolver: new RefResolver(), rootSchemaId: schema.$id || `__fjs_root_${schemaIdCounter++}`, validatorSchemasIds: new Set() @@ -329,7 +336,7 @@ function buildInnerObject (context, location) { } let propertyLocation = propertiesLocation.getPropertyLocation(key) if (propertyLocation.schema.$ref) { - propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref) + propertyLocation = resolveRef(context, propertyLocation) } const sanitizedKey = JSON.stringify(key) @@ -353,16 +360,14 @@ function buildInnerObject (context, location) { code += ` let addComma = false - let json = '${context.wrapObjects ? '{' : ''}' + let json = '{' ` - const wrapObjects = context.wrapObjects - context.wrapObjects = true if (schema.properties) { for (const key of Object.keys(schema.properties)) { let propertyLocation = propertiesLocation.getPropertyLocation(key) if (propertyLocation.schema.$ref) { - propertyLocation = resolveRef(context, location, propertyLocation.schema.$ref) + propertyLocation = resolveRef(context, propertyLocation) } const sanitizedKey = JSON.stringify(key) @@ -400,139 +405,65 @@ function buildInnerObject (context, location) { code += buildExtraObjectPropertiesSerializer(context, location) } - context.wrapObjects = wrapObjects code += ` - return json${context.wrapObjects ? ' + \'}\'' : ''} + return json + '}' ` return code } -function mergeAllOfSchema (context, location, schema, mergedSchema) { - const allOfLocation = location.getPropertyLocation('allOf') - - for (let i = 0; i < schema.allOf.length; i++) { - let allOfSchema = schema.allOf[i] - - if (allOfSchema.$ref) { - const allOfSchemaLocation = allOfLocation.getPropertyLocation(i) - allOfSchema = resolveRef(context, allOfSchemaLocation, allOfSchema.$ref).schema +function mergeLocations (context, mergedSchemaId, mergedLocations) { + for (let i = 0; i < mergedLocations.length; i++) { + const location = mergedLocations[i] + const schema = location.schema + if (schema.$ref) { + mergedLocations[i] = resolveRef(context, location) } + } - let allOfSchemaType = allOfSchema.type - if (allOfSchemaType === undefined) { - allOfSchemaType = inferTypeByKeyword(allOfSchema) - } + const mergedSchemas = [] + for (const location of mergedLocations) { + const schema = cloneOriginSchema(location.schema, location.schemaId) + delete schema.$id - if (allOfSchemaType !== undefined) { - if ( - mergedSchema.type !== undefined && - mergedSchema.type !== allOfSchemaType - ) { - throw new Error('allOf schemas have different type values') - } - mergedSchema.type = allOfSchemaType - } + mergedSchemas.push(schema) + } - 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 mergedSchema = mergeSchemas(mergedSchemas) + const mergedLocation = new Location(mergedSchema, mergedSchemaId) - 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 - } + context.refResolver.addSchema(mergedSchema, mergedSchemaId) + return mergedLocation +} - if (allOfSchema.properties !== undefined) { - if (mergedSchema.properties === undefined) { - mergedSchema.properties = {} - } - Object.assign(mergedSchema.properties, allOfSchema.properties) - } +function cloneOriginSchema (schema, schemaId) { + const clonedSchema = Array.isArray(schema) ? [] : {} - if (allOfSchema.additionalProperties !== undefined) { - if (mergedSchema.additionalProperties === undefined) { - mergedSchema.additionalProperties = {} - } - Object.assign(mergedSchema.additionalProperties, allOfSchema.additionalProperties) - } + if ( + schema.$id !== undefined && + schema.$id.charAt(0) !== '#' + ) { + schemaId = schema.$id + } - if (allOfSchema.patternProperties !== undefined) { - if (mergedSchema.patternProperties === undefined) { - mergedSchema.patternProperties = {} - } - Object.assign(mergedSchema.patternProperties, allOfSchema.patternProperties) - } + if (schema[mergedSchemaRef]) { + clonedSchema[mergedSchemaRef] = schema[mergedSchemaRef] + } - if (allOfSchema.required !== undefined) { - if (mergedSchema.required === undefined) { - mergedSchema.required = [] - } - mergedSchema.required.push(...allOfSchema.required) - } + for (const key in schema) { + let value = schema[key] - if (allOfSchema.oneOf !== undefined) { - if (mergedSchema.oneOf === undefined) { - mergedSchema.oneOf = [] - } - mergedSchema.oneOf.push(...allOfSchema.oneOf) + if (key === '$ref' && value.charAt(0) === '#') { + value = schemaId + value } - if (allOfSchema.anyOf !== undefined) { - if (mergedSchema.anyOf === undefined) { - mergedSchema.anyOf = [] - } - mergedSchema.anyOf.push(...allOfSchema.anyOf) + if (typeof value === 'object' && value !== null) { + value = cloneOriginSchema(value, schemaId) } - if (allOfSchema.allOf !== undefined) { - mergeAllOfSchema(context, location, allOfSchema, mergedSchema) - } + clonedSchema[key] = value } - 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()) - - const schema = merge({}, location.schema) - const thenSchema = schema.then - const elseSchema = schema.else || { additionalProperties: true } - - delete schema.if - delete schema.then - delete schema.else - - const ifLocation = location.getPropertyLocation('if') - const ifSchemaRef = ifLocation.getSchemaRef() - - const thenLocation = location.getPropertyLocation('then') - thenLocation.schema = merge(schema, thenSchema) - - const elseLocation = location.getPropertyLocation('else') - elseLocation.schema = merge(schema, elseSchema) - - return ` - if (validator.validate("${ifSchemaRef}", ${input})) { - ${buildValue(context, thenLocation, input)} - } else { - ${buildValue(context, elseLocation, input)} - } - ` + return clonedSchema } function toJSON (variableName) { @@ -582,7 +513,7 @@ function buildArray (context, location) { itemsLocation.schema = itemsLocation.schema || {} if (itemsLocation.schema.$ref) { - itemsLocation = resolveRef(context, itemsLocation, itemsLocation.schema.$ref) + itemsLocation = resolveRef(context, itemsLocation) } const itemsSchema = itemsLocation.schema @@ -860,81 +791,193 @@ function buildConstSerializer (location, input) { return code } -function buildValue (context, location, input) { - let schema = location.schema +function buildAllOf (context, location, input) { + const schema = location.schema - if (typeof schema === 'boolean') { - return `json += JSON.stringify(${input})` + let mergedSchemaId = schema[mergedSchemaRef] + if (mergedSchemaId) { + const mergedLocation = getMergedLocation(context, mergedSchemaId) + return buildValue(context, mergedLocation, input) } - if (schema.$ref) { - location = resolveRef(context, location, schema.$ref) - schema = location.schema + mergedSchemaId = `__fjs_merged_${schemaIdCounter++}` + schema[mergedSchemaRef] = mergedSchemaId + + const { allOf, ...schemaWithoutAllOf } = location.schema + const locations = [ + new Location( + schemaWithoutAllOf, + location.schemaId, + location.jsonPointer + ) + ] + + const allOfsLocation = location.getPropertyLocation('allOf') + for (let i = 0; i < allOf.length; i++) { + locations.push(allOfsLocation.getPropertyLocation(i)) } - if (schema.type === undefined) { - const inferredType = inferTypeByKeyword(schema) - if (inferredType) { - schema.type = inferredType + const mergedLocation = mergeLocations(context, mergedSchemaId, locations) + return buildValue(context, mergedLocation, input) +} + +function buildOneOf (context, location, input) { + context.validatorSchemasIds.add(location.schemaId) + + const schema = location.schema + + const type = schema.anyOf ? 'anyOf' : 'oneOf' + const { [type]: oneOfs, ...schemaWithoutAnyOf } = location.schema + + const locationWithoutOneOf = new Location( + schemaWithoutAnyOf, + location.schemaId, + location.jsonPointer + ) + const oneOfsLocation = location.getPropertyLocation(type) + + let code = '' + + for (let index = 0; index < oneOfs.length; index++) { + const optionLocation = oneOfsLocation.getPropertyLocation(index) + const optionSchema = optionLocation.schema + + let mergedSchemaId = optionSchema[mergedSchemaRef] + let mergedLocation = null + if (mergedSchemaId) { + mergedLocation = getMergedLocation(context, mergedSchemaId) + } else { + mergedSchemaId = `__fjs_merged_${schemaIdCounter++}` + optionSchema[mergedSchemaRef] = mergedSchemaId + + mergedLocation = mergeLocations(context, mergedSchemaId, [ + locationWithoutOneOf, + optionLocation + ]) } + + const nestedResult = buildValue(context, mergedLocation, input) + const schemaRef = optionLocation.getSchemaRef() + code += ` + ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) + ${nestedResult} + ` } - if (schema.if && schema.then) { - return addIfThenElse(context, location, input) + let schemaRef = location.getSchemaRef() + if (schemaRef.startsWith(context.rootSchemaId)) { + schemaRef = schemaRef.replace(context.rootSchemaId, '') } - if (schema.allOf) { - mergeAllOfSchema(context, location, schema, clone(schema)) - schema = location.schema + code += ` + else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`) + ` + + return code +} + +function buildIfThenElse (context, location, input) { + context.validatorSchemasIds.add(location.schemaId) + + const { + if: ifSchema, + then: thenSchema, + else: elseSchema, + ...schemaWithoutIfThenElse + } = location.schema + + const rootLocation = new Location( + schemaWithoutIfThenElse, + location.schemaId, + location.jsonPointer + ) + + const ifLocation = location.getPropertyLocation('if') + const ifSchemaRef = ifLocation.getSchemaRef() + + const thenLocation = location.getPropertyLocation('then') + let thenMergedSchemaId = thenSchema[mergedSchemaRef] + let thenMergedLocation = null + if (thenMergedSchemaId) { + thenMergedLocation = getMergedLocation(context, thenMergedSchemaId) + } else { + thenMergedSchemaId = `__fjs_merged_${schemaIdCounter++}` + thenSchema[mergedSchemaRef] = thenMergedSchemaId + + thenMergedLocation = mergeLocations(context, thenMergedSchemaId, [ + rootLocation, + thenLocation + ]) } - const type = schema.type + if (!elseSchema) { + return ` + if (validator.validate("${ifSchemaRef}", ${input})) { + ${buildValue(context, thenMergedLocation, input)} + } else { + ${buildValue(context, rootLocation, input)} + } + ` + } - let code = '' + const elseLocation = location.getPropertyLocation('else') + let elseMergedSchemaId = elseSchema[mergedSchemaRef] + let elseMergedLocation = null + if (elseMergedSchemaId) { + elseMergedLocation = getMergedLocation(context, elseMergedSchemaId) + } else { + elseMergedSchemaId = `__fjs_merged_${schemaIdCounter++}` + elseSchema[mergedSchemaRef] = elseMergedSchemaId - if ((type === undefined || type === 'object') && (schema.anyOf || schema.oneOf)) { - context.validatorSchemasIds.add(location.getSchemaId()) + elseMergedLocation = mergeLocations(context, elseMergedSchemaId, [ + rootLocation, + elseLocation + ]) + } - if (schema.type === 'object') { - context.wrapObjects = false - const funcName = buildObject(context, location) - code += ` - json += '{' - json += ${funcName}(${input}) - json += ',' - ` + return ` + if (validator.validate("${ifSchemaRef}", ${input})) { + ${buildValue(context, thenMergedLocation, input)} + } else { + ${buildValue(context, elseMergedLocation, input)} } + ` +} - const type = schema.anyOf ? 'anyOf' : 'oneOf' - const anyOfLocation = location.getPropertyLocation(type) +function buildValue (context, location, input) { + let schema = location.schema - for (let index = 0; index < location.schema[type].length; index++) { - const optionLocation = anyOfLocation.getPropertyLocation(index) - const schemaRef = optionLocation.getSchemaRef() - const nestedResult = buildValue(context, optionLocation, input) - code += ` - ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input})) - ${nestedResult} - ` - } + if (typeof schema === 'boolean') { + return `json += JSON.stringify(${input})` + } - let schemaRef = location.getSchemaRef() - if (schemaRef.startsWith(context.rootSchemaId)) { - schemaRef = schemaRef.replace(context.rootSchemaId, '') - } + if (schema.$ref) { + location = resolveRef(context, location) + schema = location.schema + } - code += ` - else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`) - ` - if (schema.type === 'object') { - code += ` - json += '}' - ` - context.wrapObjects = true + if (schema.allOf) { + return buildAllOf(context, location, input) + } + + if (schema.anyOf || schema.oneOf) { + return buildOneOf(context, location, input) + } + + if (schema.if && schema.then) { + return buildIfThenElse(context, location, input) + } + + if (schema.type === undefined) { + const inferredType = inferTypeByKeyword(schema) + if (inferredType) { + schema.type = inferredType } - return code } + let code = '' + + const type = schema.type const nullable = schema.nullable === true if (nullable) { code += ` diff --git a/lib/location.js b/lib/location.js index 1311de81..0d9acb2d 100644 --- a/lib/location.js +++ b/lib/location.js @@ -5,7 +5,6 @@ class Location { this.schema = schema this.schemaId = schemaId this.jsonPointer = jsonPointer - this.mergedSchemaId = null } getPropertyLocation (propertyName) { @@ -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 } } diff --git a/lib/merge-schemas.js b/lib/merge-schemas.js new file mode 100644 index 00000000..bb27a8bf --- /dev/null +++ b/lib/merge-schemas.js @@ -0,0 +1,9 @@ +'use strict' + +const { mergeSchemas: _mergeSchemas } = require('@fastify/merge-json-schemas') + +function mergeSchemas (schemas) { + return _mergeSchemas(schemas, { onConflict: 'skip' }) +} + +module.exports = mergeSchemas diff --git a/package.json b/package.json index 5f9c472c..6c6fe9f8 100644 --- a/package.json +++ b/package.json @@ -51,13 +51,13 @@ "fast-json-stringify": "." }, "dependencies": { - "@fastify/deepmerge": "^1.0.0", "ajv": "^8.10.0", "ajv-formats": "^2.1.1", "fast-deep-equal": "^3.1.3", "fast-uri": "^2.1.0", "rfdc": "^1.2.0", - "json-schema-ref-resolver": "^1.0.1" + "json-schema-ref-resolver": "^1.0.1", + "@fastify/merge-json-schemas": "^0.1.0" }, "standard": { "ignore": [ diff --git a/test/allof.test.js b/test/allof.test.js index d0b8b74f..7ec2d0c8 100644 --- a/test/allof.test.js +++ b/test/allof.test.js @@ -553,8 +553,60 @@ 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 = { + anyOf: [ + { $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) + t.plan(3) const schema = { allOf: [ @@ -562,11 +614,18 @@ test('allOf: throw Error if types mismatch ', (t) => { { type: 'number' } ] } - t.throws(() => build(schema), new Error('allOf schemas have different type values')) + try { + build(schema) + t.fail('should throw the MergeError') + } catch (error) { + t.ok(error instanceof Error) + t.equal(error.message, 'Failed to merge "type" keyword schemas.') + t.same(error.schemas, [['string'], ['number']]) + } }) test('allOf: throw Error if format mismatch ', (t) => { - t.plan(1) + t.plan(3) const schema = { allOf: [ @@ -574,29 +633,87 @@ test('allOf: throw Error if format mismatch ', (t) => { { format: 'time' } ] } - t.throws(() => build(schema), new Error('allOf schemas have different format values')) + try { + build(schema) + t.fail('should throw the MergeError') + } catch (error) { + t.ok(error instanceof Error) + t.equal(error.message, 'Failed to merge "format" keyword schemas.') + t.same(error.schemas, ['date', 'time']) + } }) -test('allOf: throw Error if nullable mismatch /1', (t) => { +test('recursive nested allOfs', (t) => { t.plan(1) const schema = { - allOf: [ - { nullable: true }, - { nullable: false } - ] + type: 'object', + properties: { + foo: { + additionalProperties: false, + allOf: [{ $ref: '#' }] + } + } } - t.throws(() => build(schema), new Error('allOf schemas have different nullable values')) + + const data = { foo: {} } + const stringify = build(schema) + t.equal(stringify(data), JSON.stringify(data)) }) -test('allOf: throw Error if nullable mismatch /2', (t) => { +test('recursive nested allOfs', (t) => { t.plan(1) const schema = { - allOf: [ - { nullable: false }, - { nullable: true } - ] + type: 'object', + properties: { + foo: { + additionalProperties: false, + allOf: [{ allOf: [{ $ref: '#' }] }] + } + } + } + + const data = { foo: {} } + const stringify = build(schema) + t.equal(stringify(data), JSON.stringify(data)) +}) + +test('external recursive allOfs', (t) => { + t.plan(1) + + const externalSchema = { + type: 'object', + properties: { + foo: { + properties: { + bar: { type: 'string' } + }, + allOf: [{ $ref: '#' }] + } + } + } + + const schema = { + type: 'object', + properties: { + a: { $ref: 'externalSchema#/properties/foo' }, + b: { $ref: 'externalSchema#/properties/foo' } + } + } + + const data = { + a: { + foo: {}, + bar: '42', + baz: 42 + }, + b: { + foo: {}, + bar: '42', + baz: 42 + } } - t.throws(() => build(schema), new Error('allOf schemas have different nullable values')) + const stringify = build(schema, { schema: { externalSchema } }) + t.equal(stringify(data), '{"a":{"bar":"42","foo":{}},"b":{"bar":"42","foo":{}}}') }) diff --git a/test/anyof.test.js b/test/anyof.test.js index 03483329..b0d21af6 100644 --- a/test/anyof.test.js +++ b/test/anyof.test.js @@ -644,3 +644,124 @@ test('object with ref and validated properties', (t) => { const stringify = build(schema, { schema: externalSchemas }) t.equal(stringify({ id: 1, reference: 'hi' }), '{"id":1,"reference":"hi"}') }) + +test('anyOf required props', (t) => { + t.plan(3) + + const schema = { + type: 'object', + properties: { + prop1: { type: 'string' }, + prop2: { type: 'string' }, + prop3: { type: 'string' } + }, + required: ['prop1'], + anyOf: [{ required: ['prop2'] }, { required: ['prop3'] }] + } + const stringify = build(schema) + t.equal(stringify({ prop1: 'test', prop2: 'test2' }), '{"prop1":"test","prop2":"test2"}') + t.equal(stringify({ prop1: 'test', prop3: 'test3' }), '{"prop1":"test","prop3":"test3"}') + t.equal(stringify({ prop1: 'test', prop2: 'test2', prop3: 'test3' }), '{"prop1":"test","prop2":"test2","prop3":"test3"}') +}) + +test('anyOf required props', (t) => { + t.plan(3) + + const schema = { + type: 'object', + properties: { + prop1: { type: 'string' } + }, + anyOf: [ + { + properties: { + prop2: { type: 'string' } + } + }, + { + properties: { + prop3: { type: 'string' } + } + } + ] + } + const stringify = build(schema) + t.equal(stringify({ prop1: 'test1' }), '{"prop1":"test1"}') + t.equal(stringify({ prop2: 'test2' }), '{"prop2":"test2"}') + t.equal(stringify({ prop1: 'test1', prop2: 'test2' }), '{"prop1":"test1","prop2":"test2"}') +}) + +test('recursive nested anyOfs', (t) => { + t.plan(1) + + const schema = { + type: 'object', + properties: { + foo: { + additionalProperties: false, + anyOf: [{ $ref: '#' }] + } + } + } + + const data = { foo: {} } + const stringify = build(schema) + t.equal(stringify(data), JSON.stringify(data)) +}) + +test('recursive nested anyOfs', (t) => { + t.plan(1) + + const schema = { + type: 'object', + properties: { + foo: { + additionalProperties: false, + anyOf: [{ anyOf: [{ $ref: '#' }] }] + } + } + } + + const data = { foo: {} } + const stringify = build(schema) + t.equal(stringify(data), JSON.stringify(data)) +}) + +test('external recursive anyOfs', (t) => { + t.plan(1) + + const externalSchema = { + type: 'object', + properties: { + foo: { + properties: { + bar: { type: 'string' } + }, + anyOf: [{ $ref: '#' }] + } + } + } + + const schema = { + type: 'object', + properties: { + a: { $ref: 'externalSchema#/properties/foo' }, + b: { $ref: 'externalSchema#/properties/foo' } + } + } + + const data = { + a: { + foo: {}, + bar: '42', + baz: 42 + }, + b: { + foo: {}, + bar: '42', + baz: 42 + } + } + const stringify = build(schema, { schema: { externalSchema } }) + t.equal(stringify(data), '{"a":{"bar":"42","foo":{}},"b":{"bar":"42","foo":{}}}') +}) diff --git a/test/if-then-else.test.js b/test/if-then-else.test.js index bab3be7c..1e93be00 100644 --- a/test/if-then-else.test.js +++ b/test/if-then-else.test.js @@ -249,15 +249,7 @@ const alphabetOutput = JSON.stringify({ const deepFoobarOutput = JSON.stringify({ foobar: JSON.parse(foobarOutput) }) -const noElseGreetingOutput = JSON.stringify({ - kind: 'greeting', - foo: 'FOO', - bar: 42, - hi: 'HI', - hello: 45, - a: 'A', - b: 35 -}) +const noElseGreetingOutput = JSON.stringify({}) t.test('if-then-else', t => { const tests = [ @@ -419,3 +411,60 @@ t.test('if/else with array', (t) => { t.equal(stringify(['1']), JSON.stringify(['1'])) t.equal(stringify(['1', '2']), JSON.stringify([1, 2])) }) + +t.test('external recursive if/then/else', (t) => { + t.plan(1) + + const externalSchema = { + type: 'object', + properties: { + base: { type: 'string' }, + self: { $ref: 'externalSchema#' } + }, + if: { + type: 'object', + properties: { + foo: { type: 'string', const: '41' } + } + }, + then: { + type: 'object', + properties: { + bar: { type: 'string', const: '42' } + } + }, + else: { + type: 'object', + properties: { + baz: { type: 'string', const: '43' } + } + } + } + + const schema = { + type: 'object', + properties: { + a: { $ref: 'externalSchema#/properties/self' }, + b: { $ref: 'externalSchema#/properties/self' } + } + } + + const data = { + a: { + base: 'a', + foo: '41', + bar: '42', + baz: '43', + ignore: 'ignored' + }, + b: { + base: 'b', + foo: 'not-41', + bar: '42', + baz: '43', + ignore: 'ignored' + } + } + const stringify = build(schema, { schema: { externalSchema } }) + t.equal(stringify(data), '{"a":{"base":"a","bar":"42"},"b":{"base":"b","baz":"43"}}') +})