From 1cc2aa53a5d33c17d0e9c59b13eed77d86ad91c3 Mon Sep 17 00:00:00 2001 From: Martin Hansen Date: Mon, 22 Feb 2021 00:22:47 +0100 Subject: [PATCH] WIP Extract complex resolvers --- .travis.yml | 1 - package.json | 3 + src/common.js | 45 ++++ src/complex-resolvers/if.js | 29 +++ src/complex-resolvers/items.js | 98 +++++++++ src/complex-resolvers/properties.js | 80 ++++++++ src/index.js | 307 +++++----------------------- test/specs/custom-resolvers.spec.js | 108 ++++++++++ 8 files changed, 411 insertions(+), 260 deletions(-) create mode 100644 src/common.js create mode 100644 src/complex-resolvers/if.js create mode 100644 src/complex-resolvers/items.js create mode 100644 src/complex-resolvers/properties.js create mode 100644 test/specs/custom-resolvers.spec.js diff --git a/.travis.yml b/.travis.yml index 805176c..720d760 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: node_js after_success: npm run coverage node_js: - - 10 - 12 - 14 - node diff --git a/package.json b/package.json index e229e65..c52e2c1 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.6.0", "description": "Simplify your schema by combining allOf into the root schema, safely.", "main": "src/index.js", + "engines": { + "node": ">=12.0.0" + }, "scripts": { "eslint": "eslint src test", "test": "npm run eslint && nyc --reporter=html --reporter=text mocha test/specs", diff --git a/src/common.js b/src/common.js new file mode 100644 index 0000000..a667550 --- /dev/null +++ b/src/common.js @@ -0,0 +1,45 @@ +const flatten = require('lodash/flatten') +const flattenDeep = require('lodash/flattenDeep') +const isPlainObject = require('lodash/isPlainObject') +const uniq = require('lodash/uniq') +const uniqWith = require('lodash/uniqWith') +const without = require('lodash/without') + +function deleteUndefinedProps(returnObject) { + // cleanup empty + for (const prop in returnObject) { + if (has(returnObject, prop) && isEmptySchema(returnObject[prop])) { + delete returnObject[prop] + } + } + return returnObject +} + +const allUniqueKeys = (arr) => uniq(flattenDeep(arr.map(keys))) +const getValues = (schemas, key) => schemas.map(schema => schema && schema[key]) +const has = (obj, propName) => Object.prototype.hasOwnProperty.call(obj, propName) +const keys = obj => { + if (isPlainObject(obj) || Array.isArray(obj)) { + return Object.keys(obj) + } else { + return [] + } +} + +const notUndefined = (val) => val !== undefined +const isSchema = (val) => isPlainObject(val) || val === true || val === false +const isEmptySchema = (obj) => (!keys(obj).length) && obj !== false && obj !== true +const withoutArr = (arr, ...rest) => without.apply(null, [arr].concat(flatten(rest))) + +module.exports = { + allUniqueKeys, + deleteUndefinedProps, + getValues, + has, + isEmptySchema, + isSchema, + keys, + notUndefined, + uniqWith, + withoutArr +} diff --git a/src/complex-resolvers/if.js b/src/complex-resolvers/if.js new file mode 100644 index 0000000..f1a8014 --- /dev/null +++ b/src/complex-resolvers/if.js @@ -0,0 +1,29 @@ +const { has } = require('../common') + +const conditonalRelated = ['if', 'then', 'else'] + +module.exports = { + // test with same if-then-else resolver + keywords: ['if', 'then', 'else'], + resolver(schemas, paths, mergers, options) { + const allWithConditional = schemas.filter(schema => + conditonalRelated.some(keyword => has(schema, keyword))) + + // merge sub schemas completely + // if,then,else must not be merged to the base schema, but if they contain allOf themselves, that should be merged + function merge(schema) { + const obj = {} + if (has(schema, 'if')) obj.if = mergers.if([schema.if]) + if (has(schema, 'then')) obj.then = mergers.then([schema.then]) + if (has(schema, 'else')) obj.else = mergers.else([schema.else]) + return obj + } + + // first schema with any of the 3 keywords is used as base + const first = merge(allWithConditional.shift()) + return allWithConditional.reduce((all, schema) => { + all.allOf = (all.allOf || []).concat(merge(schema)) + return all + }, first) + } +} diff --git a/src/complex-resolvers/items.js b/src/complex-resolvers/items.js new file mode 100644 index 0000000..7456491 --- /dev/null +++ b/src/complex-resolvers/items.js @@ -0,0 +1,98 @@ + +const compare = require('json-schema-compare') +const forEach = require('lodash/forEach') +const { + allUniqueKeys, + deleteUndefinedProps, + has, + isSchema, + notUndefined, + uniqWith +} = require('../common') + +function removeFalseSchemasFromArray(target) { + forEach(target, function(schema, index) { + if (schema === false) { + target.splice(index, 1) + } + }) +} + +function getItemSchemas(subSchemas, key) { + return subSchemas.map(function(sub) { + if (!sub) { + return undefined + } + + if (Array.isArray(sub.items)) { + const schemaAtPos = sub.items[key] + if (isSchema(schemaAtPos)) { + return schemaAtPos + } else if (has(sub, 'additionalItems')) { + return sub.additionalItems + } + } else { + return sub.items + } + + return undefined + }) +} + +function getAdditionalSchemas(subSchemas) { + return subSchemas.map(function(sub) { + if (!sub) { + return undefined + } + if (Array.isArray(sub.items)) { + return sub.additionalItems + } + return sub.items + }) +} + +// Provide source when array +function mergeItems(group, mergeSchemas, items) { + const allKeys = allUniqueKeys(items) + return allKeys.reduce(function(all, key) { + const schemas = getItemSchemas(group, key) + const compacted = uniqWith(schemas.filter(notUndefined), compare) + all[key] = mergeSchemas(compacted, key) + return all + }, []) +} + +module.exports = { + keywords: ['items', 'additionalItems'], + resolver(values, parents, mergers) { + // const createSubMerger = groupKey => (schemas, key) => mergeSchemas(schemas, parents.concat(groupKey, key)) + const items = values.map(s => s.items) + const itemsCompacted = items.filter(notUndefined) + const returnObject = {} + + // if all items keyword values are schemas, we can merge them as simple schemas + // if not we need to merge them as mixed + if (itemsCompacted.every(isSchema)) { + returnObject.items = mergers.items(items) + } else { + returnObject.items = mergeItems(values, mergers.items, items) + } + + let schemasAtLastPos + if (itemsCompacted.every(Array.isArray)) { + schemasAtLastPos = values.map(s => s.additionalItems) + } else if (itemsCompacted.some(Array.isArray)) { + schemasAtLastPos = getAdditionalSchemas(values) + } + + if (schemasAtLastPos) { + returnObject.additionalItems = mergers.additionalItems(schemasAtLastPos) + } + + if (returnObject.additionalItems === false && Array.isArray(returnObject.items)) { + removeFalseSchemasFromArray(returnObject.items) + } + + return deleteUndefinedProps(returnObject) + } +} diff --git a/src/complex-resolvers/properties.js b/src/complex-resolvers/properties.js new file mode 100644 index 0000000..453f569 --- /dev/null +++ b/src/complex-resolvers/properties.js @@ -0,0 +1,80 @@ + +const compare = require('json-schema-compare') +const forEach = require('lodash/forEach') +const { + allUniqueKeys, + deleteUndefinedProps, + getValues, + keys, + notUndefined, + uniqWith, + withoutArr +} = require('../common') + +function removeFalseSchemas(target) { + forEach(target, function(schema, prop) { + if (schema === false) { + delete target[prop] + } + }) +} + +function mergeSchemaGroup(group, mergeSchemas) { + const allKeys = allUniqueKeys(group) + return allKeys.reduce(function(all, key) { + const schemas = getValues(group, key) + const compacted = uniqWith(schemas.filter(notUndefined), compare) + all[key] = mergeSchemas(compacted, key) + return all + }, {}) +} + +module.exports = { + keywords: ['properties', 'patternProperties', 'additionalProperties'], + resolver(values, parents, mergers, options) { + // first get rid of all non permitted properties + if (!options.ignoreAdditionalProperties) { + values.forEach(function(subSchema) { + const otherSubSchemas = values.filter(s => s !== subSchema) + const ownKeys = keys(subSchema.properties) + const ownPatternKeys = keys(subSchema.patternProperties) + const ownPatterns = ownPatternKeys.map(k => new RegExp(k)) + otherSubSchemas.forEach(function(other) { + const allOtherKeys = keys(other.properties) + const keysMatchingPattern = allOtherKeys.filter(k => ownPatterns.some(pk => pk.test(k))) + const additionalKeys = withoutArr(allOtherKeys, ownKeys, keysMatchingPattern) + additionalKeys.forEach(function(key) { + other.properties[key] = mergers.properties([ + other.properties[key], subSchema.additionalProperties + ], key) + }) + }) + }) + + // remove disallowed patternProperties + values.forEach(function(subSchema) { + const otherSubSchemas = values.filter(s => s !== subSchema) + const ownPatternKeys = keys(subSchema.patternProperties) + if (subSchema.additionalProperties === false) { + otherSubSchemas.forEach(function(other) { + const allOtherPatterns = keys(other.patternProperties) + const additionalPatternKeys = withoutArr(allOtherPatterns, ownPatternKeys) + additionalPatternKeys.forEach(key => delete other.patternProperties[key]) + }) + } + }) + } + + const returnObject = { + additionalProperties: mergers.additionalProperties(values.map(s => s.additionalProperties)), + patternProperties: mergeSchemaGroup(values.map(s => s.patternProperties), mergers.patternProperties), + properties: mergeSchemaGroup(values.map(s => s.properties), mergers.properties) + } + + if (returnObject.additionalProperties === false) { + removeFalseSchemas(returnObject.properties) + } + + return deleteUndefinedProps(returnObject) + } +} diff --git a/src/index.js b/src/index.js index 9ace777..8a86ccc 100644 --- a/src/index.js +++ b/src/index.js @@ -8,20 +8,17 @@ const intersection = require('lodash/intersection') const intersectionWith = require('lodash/intersectionWith') const isEqual = require('lodash/isEqual') const isPlainObject = require('lodash/isPlainObject') +const isFunction = require('lodash/isFunction') const pullAll = require('lodash/pullAll') const sortBy = require('lodash/sortBy') -const forEach = require('lodash/forEach') const uniq = require('lodash/uniq') const uniqWith = require('lodash/uniqWith') -const without = require('lodash/without') -const has = (obj, propName) => Object.prototype.hasOwnProperty.call(obj, propName) -const withoutArr = (arr, ...rest) => without.apply(null, [arr].concat(flatten(rest))) -const isPropertyRelated = (key) => contains(propertyRelated, key) -const isItemsRelated = (key) => contains(itemsRelated, key) -const isConditionalRelated = (key) => contains(conditonalRelated, key) +const ifResolver = require('./complex-resolvers/if') +const propertiesResolver = require('./complex-resolvers/properties') +const itemsResolver = require('./complex-resolvers/items') + const contains = (arr, val) => arr.indexOf(val) !== -1 -const isEmptySchema = (obj) => (!keys(obj).length) && obj !== false && obj !== true const isSchema = (val) => isPlainObject(val) || val === true || val === false const isFalse = (val) => val === false const isTrue = (val) => val === true @@ -56,27 +53,6 @@ function getValues(schemas, key) { return schemas.map(schema => schema && schema[key]) } -function getItemSchemas(subSchemas, key) { - return subSchemas.map(function(sub) { - if (!sub) { - return undefined - } - - if (Array.isArray(sub.items)) { - const schemaAtPos = sub.items[key] - if (isSchema(schemaAtPos)) { - return schemaAtPos - } else if (has(sub, 'additionalItems')) { - return sub.additionalItems - } - } else { - return sub.items - } - - return undefined - }) -} - function tryMergeSchemaGroups(schemaGroups, mergeSchemas) { return schemaGroups.map(function(schemas, index) { try { @@ -87,18 +63,6 @@ function tryMergeSchemaGroups(schemaGroups, mergeSchemas) { }).filter(notUndefined) } -function getAdditionalSchemas(subSchemas) { - return subSchemas.map(function(sub) { - if (!sub) { - return undefined - } - if (Array.isArray(sub.items)) { - return sub.additionalItems - } - return sub.items - }) -} - function keys(obj) { if (isPlainObject(obj) || Array.isArray(obj)) { return Object.keys(obj) @@ -121,15 +85,6 @@ function getAnyOfCombinations(arrOfArrays, combinations) { return getAnyOfCombinations(rest, values.map(item => (item))) } -function mergeWithArray(base, newItems) { - if (Array.isArray(base)) { - base.splice.apply(base, [0, 0].concat(newItems)) - return base - } else { - return newItems - } -} - function throwIncompatible(values, paths) { let asJSON try { @@ -142,16 +97,6 @@ function throwIncompatible(values, paths) { throw new Error('Could not resolve values for path:"' + paths.join('.') + '". They are probably incompatible. Values: \n' + asJSON) } -function cleanupReturnValue(returnObject) { - // cleanup empty - for (const prop in returnObject) { - if (has(returnObject, prop) && isEmptySchema(returnObject[prop])) { - delete returnObject[prop] - } - } - return returnObject -} - function createRequiredSubMerger(mergeSchemas, key, parents) { return function(schemas, subKey) { if (subKey === undefined) { @@ -162,102 +107,45 @@ function createRequiredSubMerger(mergeSchemas, key, parents) { } } -function callGroupResolver(keys, resolverName, schemas, mergeSchemas, options, parents) { - if (keys.length) { - const resolver = options.resolvers[resolverName] - if (!resolver) { +function callGroupResolver(complexKeywords, resolverName, schemas, mergeSchemas, options, parents) { + if (complexKeywords.length) { + const resolverConfig = options.complexResolvers[resolverName] + if (!resolverConfig || !resolverConfig.resolver) { throw new Error('No resolver found for ' + resolverName) } - const compacted = uniqWith(schemas.map(function(schema) { - return keys.reduce(function(all, key) { - if (schema[key] !== undefined) { - all[key] = schema[key] - } - return all - }, {}) - }).filter(notUndefined), compare) - - const map = { - properties: propertyRelated, - items: itemsRelated, - if: conditonalRelated - } - - const isIf = resolverName === 'if' - const related = map[resolverName] - - const mergers = related.reduce(function(all, key) { - if (contains(schemaGroupProps, key)) { - all[key] = createRequiredSubMerger(mergeSchemas, key, parents) - } else { - all[key] = function(schemas) { - return mergeSchemas(schemas, null, parents.concat(key)) - } - } + // extract all keywords from all the schemas that have one or more + // then remove all undefined ones and not unique + const extractedKeywordsOnly = schemas.map(schema => complexKeywords.reduce((all, key) => { + if (schema[key] !== undefined) all[key] = schema[key] return all - }, {}) + }, {})) + const unique = uniqWith(extractedKeywordsOnly, compare) - if (resolverName === 'items') { - mergers.itemsArray = createRequiredSubMerger(mergeSchemas, 'items', parents) - mergers.items = function(schemas) { - return mergeSchemas(schemas, null, parents.concat('items')) - } - } + // create mergers that automatically add the path of the keyword for use in the complex resolver + const mergers = resolverConfig.keywords.reduce((all, key) => ({ + ...all, + [key]: (schemas, extraKey = []) => mergeSchemas(schemas, null, parents.concat(key, extraKey)) + }), {}) - const result = resolver(compacted, parents.concat(resolverName), mergers, options) + // const merger = (schemas, key) => mergeSchemas(schemas, null, key ? parents.concat(key) : parents) + const result = resolverConfig.resolver(unique, parents.concat(resolverName), mergers, options) if (!isPlainObject(result)) { - throwIncompatible(compacted, parents.concat(resolverName)) + throwIncompatible(unique, parents.concat(resolverName)) } - if (isIf) { - return result - } else { - return cleanupReturnValue(result) - } + return result } } -// Provide source when array -function mergeSchemaGroup(group, mergeSchemas, source) { - const allKeys = allUniqueKeys(source || group) - const extractor = source - ? getItemSchemas - : getValues - return allKeys.reduce(function(all, key) { - const schemas = extractor(group, key) - const compacted = uniqWith(schemas.filter(notUndefined), compare) - all[key] = mergeSchemas(compacted, key) - return all - }, source - ? [] - : {}) -} - -function removeFalseSchemas(target) { - forEach(target, function(schema, prop) { - if (schema === false) { - delete target[prop] - } - }) -} - -function removeFalseSchemasFromArray(target) { - forEach(target, function(schema, index) { - if (schema === false) { - target.splice(index, 1) - } - }) -} - function createRequiredMetaArray(arr) { return { required: arr } } -const propertyRelated = ['properties', 'patternProperties', 'additionalProperties'] -const itemsRelated = ['items', 'additionalItems'] -const conditonalRelated = ['if', 'then', 'else'] +// const propertyRelated = ['properties', 'patternProperties', 'additionalProperties'] +// const itemsRelated = ['items', 'additionalItems'] +// const conditonalRelated = ['if', 'then', 'else'] const schemaGroupProps = ['properties', 'patternProperties', 'definitions', 'dependencies'] const schemaArrays = ['anyOf', 'oneOf'] const schemaProps = [ @@ -286,52 +174,6 @@ const defaultResolvers = { } } }, - properties(values, key, mergers, options) { - // first get rid of all non permitted properties - if (!options.ignoreAdditionalProperties) { - values.forEach(function(subSchema) { - const otherSubSchemas = values.filter(s => s !== subSchema) - const ownKeys = keys(subSchema.properties) - const ownPatternKeys = keys(subSchema.patternProperties) - const ownPatterns = ownPatternKeys.map(k => new RegExp(k)) - otherSubSchemas.forEach(function(other) { - const allOtherKeys = keys(other.properties) - const keysMatchingPattern = allOtherKeys.filter(k => ownPatterns.some(pk => pk.test(k))) - const additionalKeys = withoutArr(allOtherKeys, ownKeys, keysMatchingPattern) - additionalKeys.forEach(function(key) { - other.properties[key] = mergers.properties([ - other.properties[key], subSchema.additionalProperties - ], key) - }) - }) - }) - - // remove disallowed patternProperties - values.forEach(function(subSchema) { - const otherSubSchemas = values.filter(s => s !== subSchema) - const ownPatternKeys = keys(subSchema.patternProperties) - if (subSchema.additionalProperties === false) { - otherSubSchemas.forEach(function(other) { - const allOtherPatterns = keys(other.patternProperties) - const additionalPatternKeys = withoutArr(allOtherPatterns, ownPatternKeys) - additionalPatternKeys.forEach(key => delete other.patternProperties[key]) - }) - } - }) - } - - const returnObject = { - additionalProperties: mergers.additionalProperties(values.map(s => s.additionalProperties)), - patternProperties: mergeSchemaGroup(values.map(s => s.patternProperties), mergers.patternProperties), - properties: mergeSchemaGroup(values.map(s => s.properties), mergers.properties) - } - - if (returnObject.additionalProperties === false) { - removeFalseSchemas(returnObject.properties) - } - - return returnObject - }, dependencies(compacted, paths, mergeSchemas) { const allChildren = allUniqueKeys(compacted) @@ -359,34 +201,6 @@ const defaultResolvers = { return all }, {}) }, - items(values, paths, mergers) { - const items = values.map(s => s.items) - const itemsCompacted = items.filter(notUndefined) - const returnObject = {} - - if (itemsCompacted.every(isSchema)) { - returnObject.items = mergers.items(items) - } else { - returnObject.items = mergeSchemaGroup(values, mergers.itemsArray, items) - } - - let schemasAtLastPos - if (itemsCompacted.every(Array.isArray)) { - schemasAtLastPos = values.map(s => s.additionalItems) - } else if (itemsCompacted.some(Array.isArray)) { - schemasAtLastPos = getAdditionalSchemas(values) - } - - if (schemasAtLastPos) { - returnObject.additionalItems = mergers.additionalItems(schemasAtLastPos) - } - - if (returnObject.additionalItems === false && Array.isArray(returnObject.items)) { - removeFalseSchemasFromArray(returnObject.items) - } - - return returnObject - }, oneOf(compacted, paths, mergeSchemas) { const combinations = getAnyOfCombinations(cloneDeep(compacted)) const result = tryMergeSchemaGroups(combinations, mergeSchemas) @@ -416,27 +230,6 @@ const defaultResolvers = { if (enums.length) { return sortBy(enums) } - }, - if(values, props, mergers, options) { - const allWithConditional = values.filter(schema => - conditonalRelated.some(keyword => has(schema, keyword))) - - // merge sub schemas completely - // if,then,else must not be merged to the base schema, but if they contain allOf themselves, that should be merged - function merge(schema) { - const obj = {} - if (has(schema, 'if')) obj.if = mergers.if([schema.if]) - if (has(schema, 'then')) obj.then = mergers.then([schema.then]) - if (has(schema, 'else')) obj.else = mergers.else([schema.else]) - return obj - } - - // first schema with any of the 3 keywords is used as base - const first = merge(allWithConditional.shift()) - return allWithConditional.reduce((all, schema) => { - all.allOf = (all.allOf || []).concat(merge(schema)) - return all - }, first) } } @@ -453,6 +246,8 @@ defaultResolvers.description = first defaultResolvers.examples = examples defaultResolvers.exclusiveMaximum = minimumValue defaultResolvers.exclusiveMinimum = maximumValue +defaultResolvers.if = ifResolver +defaultResolvers.items = itemsResolver defaultResolvers.maximum = minimumValue defaultResolvers.maxItems = minimumValue defaultResolvers.maxLength = minimumValue @@ -461,6 +256,7 @@ defaultResolvers.minimum = maximumValue defaultResolvers.minItems = maximumValue defaultResolvers.minLength = maximumValue defaultResolvers.minProperties = maximumValue +defaultResolvers.properties = propertiesResolver defaultResolvers.propertyNames = schemaResolver defaultResolvers.required = required defaultResolvers.title = first @@ -470,10 +266,15 @@ function merger(rootSchema, options, totalSchemas) { totalSchemas = totalSchemas || [] options = defaultsDeep(options, { ignoreAdditionalProperties: false, - resolvers: defaultResolvers, + resolvers: cloneDeep(defaultResolvers), deep: true }) + const allResolverEntries = Object.entries(options.resolvers) + options.resolvers = Object.fromEntries(allResolverEntries.filter(([key, val]) => isFunction(val))) + const complexResolvers = allResolverEntries.filter(([key, val]) => isPlainObject(val)) + options.complexResolvers = Object.fromEntries(complexResolvers) + function mergeSchemas(schemas, base, parents) { schemas = cloneDeep(schemas.filter(notUndefined)) parents = parents || [] @@ -504,14 +305,11 @@ function merger(rootSchema, options, totalSchemas) { }, options, totalSchemas) } - const propertyKeys = allKeys.filter(isPropertyRelated) - pullAll(allKeys, propertyKeys) + const complexKeysArr = complexResolvers.map(([resolverKeyword, resolverConf]) => + allKeys.filter(k => [resolverKeyword, ...resolverConf.keywords].includes(k))) - const itemKeys = allKeys.filter(isItemsRelated) - pullAll(allKeys, itemKeys) - - const conditonalKeys = allKeys.filter(isConditionalRelated) - pullAll(allKeys, conditonalKeys) + // remove all complex keys before simple resolvers + complexKeysArr.forEach(keys => pullAll(allKeys, keys)) allKeys.forEach(function(key) { const values = getValues(schemas, key) @@ -543,15 +341,9 @@ function merger(rootSchema, options, totalSchemas) { } } - let calledWithArray = false - const reportUnresolved = unresolvedSchemas => { - calledWithArray = Array.isArray(unresolvedSchemas) - return addToAllOf(unresolvedSchemas) - } - - merged[key] = resolver(compacted, parents.concat(key), merger, options, reportUnresolved) + merged[key] = resolver(compacted, parents.concat(key), merger, options) - if (merged[key] === undefined && !calledWithArray) { + if (merged[key] === undefined) { throwIncompatible(compacted, parents.concat(key)) } else if (merged[key] === undefined) { delete merged[key] @@ -559,15 +351,12 @@ function merger(rootSchema, options, totalSchemas) { } }) - Object.assign(merged, callGroupResolver(propertyKeys, 'properties', schemas, mergeSchemas, options, parents)) - Object.assign(merged, callGroupResolver(itemKeys, 'items', schemas, mergeSchemas, options, parents)) - Object.assign(merged, callGroupResolver(conditonalKeys, 'if', schemas, mergeSchemas, options, parents)) - - function addToAllOf(unresolvedSchemas) { - merged.allOf = mergeWithArray(merged.allOf, unresolvedSchemas) - } - - return merged + return complexResolvers.reduce((all, [resolverKeyword, config], index) => { + return { + ...all, + ...callGroupResolver(complexKeysArr[index], resolverKeyword, schemas, mergeSchemas, options, parents) + } + }, merged) } const allSchemas = flattenDeep(getAllOf(rootSchema)) diff --git a/test/specs/custom-resolvers.spec.js b/test/specs/custom-resolvers.spec.js new file mode 100644 index 0000000..3964053 --- /dev/null +++ b/test/specs/custom-resolvers.spec.js @@ -0,0 +1,108 @@ +const chai = require('chai') +const merger = require('../../src') +const expect = chai.expect + +describe('simple resolver', () => { + it('merges as expected (with enum)', () => { + const result = merger({ + enum: [1, 2], + allOf: [{ + enum: [2, 3] + }] + }) + + expect(result).to.eql({ + enum: [2] + }) + + const opts = { + resolvers: { + enum(schemas, paths, mergeSchemas, options) { + expect(options).to.eql(opts) + expect(schemas).to.have.length(2) + expect(paths).to.have.length(1) + expect(paths).to.eql(['enum']) + + // inner merge test + const innerSchemas = [{ + minLength: 1 + }, { + minLength: 7 + }] + + const innerResult = mergeSchemas(innerSchemas) + expect(innerResult).to.eql({ + minLength: 7 + }) + + return [5] + } + } + } + + const resultCustom = merger({ + enum: [1, 2], + allOf: [{ + enum: [2, 3] + }] + }, opts) + + expect(resultCustom).to.eql({ + enum: [5] + }) + }) + + describe('group resolvers', () => { + it('works as intended with if then else copy resolver', () => { + const conditonalRelated = ['if', 'then', 'else'] + const has = (obj, propName) => Object.prototype.hasOwnProperty.call(obj, propName) + const opts = { + resolvers: { + if: { + // test with same if-then-else resolver + additionalKeywords: ['then', 'else'], + resolver(schemas, paths, mergers, options) { + const allWithConditional = schemas.filter(schema => + conditonalRelated.some(keyword => has(schema, keyword))) + + // merge sub schemas completely + // if,then,else must not be merged to the base schema, but if they contain allOf themselves, that should be merged + function merge(schema) { + const obj = {} + if (has(schema, 'if')) obj.if = mergers.if([schema.if]) + if (has(schema, 'then')) obj.then = mergers.then([schema.then]) + if (has(schema, 'else')) obj.else = mergers.else([schema.else]) + return obj + } + + // first schema with any of the 3 keywords is used as base + const first = merge(allWithConditional.shift()) + return allWithConditional.reduce((all, schema) => { + all.allOf = (all.allOf || []).concat(merge(schema)) + return all + }, first) + } + } + } + } + + const resultCustom = merger({ + allOf: [{ + if: { + required: ['def'] + }, + then: {}, + else: {} + }] + }, opts) + + expect(resultCustom).to.eql({ + if: { + required: ['def'] + }, + then: {}, + else: {} + }) + }) + }) +})