json-schema-merge-allof
Advanced tools
Comparing version 0.5.1 to 0.5.2
{ | ||
"name": "json-schema-merge-allof", | ||
"version": "0.5.1", | ||
"version": "0.5.2", | ||
"description": "Simplify your schema by combining allOf into the root schema, safely.", | ||
@@ -32,2 +32,3 @@ "main": "src/index.js", | ||
"devDependencies": { | ||
"ajv": "^5.2.3", | ||
"chai": "^4.1.2", | ||
@@ -34,0 +35,0 @@ "coveralls": "^3.0.0", |
@@ -12,2 +12,3 @@ # json-schema-merge-allof [![Build Status](https://travis-ci.org/mokkabonna/json-schema-merge-allof.svg?branch=master)](https://travis-ci.org/mokkabonna/json-schema-merge-allof) [![Coverage Status](https://coveralls.io/repos/github/mokkabonna/json-schema-merge-allof/badge.svg?branch=master)](https://coveralls.io/github/mokkabonna/json-schema-merge-allof?branch=master) | ||
- **Real** and **safe** merging of schemas combined with **allOf** | ||
- Takes away all allOf found in the whole schema | ||
- Lossless in terms of validation rules, merged schema does not validate more or less than original schema | ||
@@ -18,5 +19,8 @@ - Results in a more readable root schema | ||
- Validates in a way not possible by regular simple meta validators | ||
- Correctly considers additionalProperties, patternProperties and properties as a part of an whole when merging schemas containing those | ||
- Correctly considers items and additionalItems as a whole when merging schemas containing those | ||
- Supports merging schemas with items as array and direct schema | ||
- Supports all JSON schema core/validation keywords | ||
- Option to override common impossibility like adding properties when using **additionalProperties: false** | ||
- Pluggable keyword resolvers | ||
- Option to override common impossibility like adding properties when using **additionalProperties: false** | ||
- Supports all JSON schema core/validation keywords | ||
@@ -110,3 +114,3 @@ ## How | ||
You can set a default resolver that catches any unknown keyword. Let's say you want to use the same strategy as the ones for the meta keywords, to use the first value found. You van accomplish that like this: | ||
You can set a default resolver that catches any unknown keyword. Let's say you want to use the same strategy as the ones for the meta keywords, to use the first value found. You can accomplish that like this: | ||
@@ -113,0 +117,0 @@ ```js |
377
src/index.js
@@ -18,2 +18,23 @@ var cloneDeep = require('lodash/cloneDeep') | ||
var withoutArr = (arr, ...rest) => without.apply(null, [arr].concat(flatten(rest))) | ||
var isPropertyRelated = (key) => contains(propertyRelated, key) | ||
var isItemsRelated = (key) => contains(itemsRelated, key) | ||
var contains = (arr, val) => arr.indexOf(val) !== -1 | ||
var isEmptySchema = (obj) => (!keys(obj).length) && obj !== false && obj !== true | ||
var isSchema = (val) => isPlainObject(val) || val === true || val === false | ||
var isFalse = (val) => val === false | ||
var isTrue = (val) => val === true | ||
var schemaResolver = (compacted, key, mergeSchemas, totalSchemas, parent) => mergeSchemas(compacted, compacted[0]) | ||
var stringArray = (values) => sortBy(uniq(flattenDeep(values))) | ||
var notUndefined = (val) => val !== undefined | ||
var allUniqueKeys = (arr) => uniq(flattenDeep(arr.map(keys))) | ||
// resolvers | ||
var first = compacted => compacted[0] | ||
var required = compacted => stringArray(compacted) | ||
var maximumValue = compacted => Math.max.apply(Math, compacted) | ||
var minimumValue = compacted => Math.min.apply(Math, compacted) | ||
var uniqueItems = compacted => compacted.some(isTrue) | ||
var examples = compacted => uniqWith(flatten(compacted), isEqual) | ||
function compareProp(key) { | ||
@@ -64,2 +85,12 @@ return function(a, b) { | ||
function tryMergeSchemaGroups(schemaGroups, mergeSchemas) { | ||
return schemaGroups.map(function(schemas) { | ||
try { | ||
return mergeSchemas(schemas) | ||
} catch (e) { | ||
return undefined | ||
} | ||
}).filter(notUndefined) | ||
} | ||
function getAdditionalSchemas(subSchemas) { | ||
@@ -83,15 +114,2 @@ return subSchemas.map(function(sub) { | ||
function withoutArr(arr) { | ||
var rest = flatten([].slice.call(arguments, 1)) | ||
return without.apply(null, [arr].concat(rest)) | ||
} | ||
function isPropertyRelated(key) { | ||
return contains(propertyRelated, key) | ||
} | ||
function isItemsRelated(key) { | ||
return contains(itemsRelated, key) | ||
} | ||
function getAnyOfCombinations(arrOfArrays, combinations) { | ||
@@ -111,6 +129,2 @@ combinations = combinations || [] | ||
function contains(arr, val) { | ||
return arr.indexOf(val) !== -1 | ||
} | ||
function mergeWithArray(base, newItems) { | ||
@@ -125,18 +139,2 @@ if (Array.isArray(base)) { | ||
function isEmptySchema(obj) { | ||
return (!keys(obj).length) && obj !== false && obj !== true | ||
} | ||
function isSchema(val) { | ||
return isPlainObject(val) || val === true || val === false | ||
} | ||
function isFalse(val) { | ||
return val === false | ||
} | ||
function isTrue(val) { | ||
return val === true | ||
} | ||
function throwIncompatible(values, key) { | ||
@@ -152,14 +150,72 @@ var asJSON | ||
function schemaResolver(compacted, key, mergeSchemas, totalSchemas, parent) { | ||
return mergeSchemas(compacted, compacted[0]) | ||
function cleanupReturnValue(returnObject) { | ||
// cleanup empty | ||
for (var prop in returnObject) { | ||
if (returnObject.hasOwnProperty(prop) && isEmptySchema(returnObject[prop])) { | ||
delete returnObject[prop] | ||
} | ||
} | ||
return returnObject | ||
} | ||
function stringArray(values) { | ||
return sortBy(uniq(flattenDeep(values))) | ||
function callGroupResolver(keys, resolverName, schemas, mergeSchemas, options) { | ||
if (keys.length) { | ||
var resolver = options.resolvers[resolverName] | ||
if (!resolver) { | ||
throw new Error('No resolver found for ' + resolverName) | ||
} | ||
var 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) | ||
var result = resolver(compacted, resolverName, mergeSchemas, options) | ||
if (!isPlainObject(result)) { | ||
throwIncompatible(compacted, resolverName) | ||
} | ||
return cleanupReturnValue(result) | ||
} | ||
} | ||
function notUndefined(val) { | ||
return val !== undefined | ||
// Provide source when array | ||
function mergeSchemaGroup(group, mergeSchemas, source) { | ||
var allKeys = allUniqueKeys(source || group) | ||
var extractor = source ? getItemSchemas : getValues | ||
return allKeys.reduce(function(all, key) { | ||
var schemas = extractor(group, key) | ||
var compacted = uniqWith(schemas.filter(notUndefined), compare) | ||
all[key] = mergeSchemas(compacted) | ||
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 | ||
} | ||
} | ||
var propertyRelated = ['properties', 'patternProperties', 'additionalProperties'] | ||
@@ -179,7 +235,6 @@ var itemsRelated = ['items', 'additionalItems'] | ||
var defaultResolvers = { | ||
type: function(compacted, key) { | ||
type(compacted, key) { | ||
if (compacted.some(Array.isArray)) { | ||
var normalized = compacted.map(function(val) { | ||
return Array.isArray(val) | ||
? val : [val] | ||
return Array.isArray(val) ? val : [val] | ||
}) | ||
@@ -195,3 +250,3 @@ var common = intersection.apply(null, normalized) | ||
}, | ||
properties: function(values, key, mergeSchemas, totalSchemas, options) { | ||
properties(values, key, mergeSchemas, options) { | ||
// first get rid of all non permitted properties | ||
@@ -228,57 +283,16 @@ if (!options.ignoreAdditionalProperties) { | ||
var properties = values.map(s => s.properties) | ||
var allProperties = uniq(flattenDeep(properties.map(keys))) | ||
var returnObject = {} | ||
var returnObject = { | ||
additionalProperties: mergeSchemas(values.map(s => s.additionalProperties)), | ||
patternProperties: mergeSchemaGroup(values.map(s => s.patternProperties), mergeSchemas), | ||
properties: mergeSchemaGroup(values.map(s => s.properties), mergeSchemas) | ||
} | ||
returnObject.additionalProperties = mergeSchemas(values.map(s => s.additionalProperties)) | ||
var allPatternProps = values.map(function(schema) { | ||
return schema.patternProperties | ||
}) | ||
var allPatternKeys = uniq(flattenDeep(allPatternProps.map(keys))) | ||
returnObject.patternProperties = allPatternKeys.reduce(function(all, patternKey) { | ||
var subSchemas = getValues(allPatternProps, patternKey) | ||
var compacted = uniqWith(subSchemas.filter(notUndefined), compare) | ||
all[patternKey] = mergeSchemas(compacted) | ||
return all | ||
}, {}) | ||
returnObject.properties = allProperties.reduce(function(all, propKey) { | ||
var propSchemas = getValues(properties, propKey) | ||
var innerCompacted = uniqWith(propSchemas.filter(notUndefined), compare) | ||
var foundBase = totalSchemas.find(function(schema) { | ||
return schema === all[propKey] && schema !== undefined && isPlainObject(schema) | ||
}) | ||
if (foundBase) { | ||
all[propKey] = foundBase | ||
return all | ||
} | ||
totalSchemas.splice.apply(totalSchemas, [0, 0].concat(innerCompacted)) | ||
all[propKey] = mergeSchemas(innerCompacted, all[propKey] || {}, true) | ||
return all | ||
}, properties[0] || {}) | ||
if (returnObject.additionalProperties === false) { | ||
forEach(returnObject.properties, function(schema, prop) { | ||
if (schema === false) { | ||
delete returnObject.properties[prop] | ||
} | ||
}) | ||
removeFalseSchemas(returnObject.properties) | ||
} | ||
// cleanup empty | ||
for (var prop in returnObject) { | ||
if (returnObject.hasOwnProperty(prop) && isEmptySchema(returnObject[prop])) { | ||
delete returnObject[prop] | ||
} | ||
} | ||
return returnObject | ||
}, | ||
dependencies: function(compacted, key, mergeSchemas) { | ||
var allChildren = uniq(flattenDeep(compacted.map(keys))) | ||
dependencies(compacted, key, mergeSchemas) { | ||
var allChildren = allUniqueKeys(compacted) | ||
@@ -290,5 +304,12 @@ return allChildren.reduce(function(all, childKey) { | ||
// to support dependencies | ||
var allArray = innerCompacted.every(Array.isArray) | ||
if (allArray) { | ||
all[childKey] = stringArray(innerCompacted) | ||
var innerArrays = innerCompacted.filter(Array.isArray) | ||
if (innerArrays.length) { | ||
if (innerArrays.length === innerCompacted.length) { | ||
all[childKey] = stringArray(innerCompacted) | ||
} else { | ||
var innerSchemas = innerCompacted.filter(isSchema) | ||
var arrayMetaScheams = innerArrays.map(createRequiredMetaArray) | ||
all[childKey] = mergeSchemas(innerSchemas.concat(arrayMetaScheams)) | ||
} | ||
return all | ||
@@ -303,6 +324,5 @@ } | ||
}, | ||
items: function(values, key, mergeSchemas) { | ||
items(values, key, mergeSchemas) { | ||
var items = values.map(s => s.items) | ||
var itemsCompacted = items.filter(notUndefined) | ||
var returnObject = {} | ||
@@ -313,10 +333,3 @@ | ||
} else { | ||
var allItems = uniq(flattenDeep(items.map(keys))) | ||
returnObject.items = allItems.reduce(function(all, index) { | ||
var itemSchemas = getItemSchemas(values, index) | ||
var innerCompacted = uniqWith(itemSchemas.filter(notUndefined), compare) | ||
all[index] = mergeSchemas(innerCompacted) | ||
return all | ||
}, []) | ||
returnObject.items = mergeSchemaGroup(values, mergeSchemas, items) | ||
} | ||
@@ -335,33 +348,13 @@ | ||
if (returnObject.additionalItems === false) { | ||
forEach(returnObject.items, function(schema, prop) { | ||
if (schema === false) { | ||
returnObject.items.splice(prop, 1) | ||
} | ||
}) | ||
if (returnObject.additionalItems === false && Array.isArray(returnObject.items)) { | ||
removeFalseSchemasFromArray(returnObject.items) | ||
} | ||
// cleanup empty | ||
for (var prop in returnObject) { | ||
if (returnObject.hasOwnProperty(prop) && isEmptySchema(returnObject[prop])) { | ||
delete returnObject[prop] | ||
} | ||
} | ||
return returnObject | ||
}, | ||
oneOf: function(compacted, key, mergeSchemas) { | ||
oneOf(compacted, key, mergeSchemas) { | ||
var combinations = getAnyOfCombinations(cloneDeep(compacted)) | ||
var result = combinations.map(function(combination) { | ||
try { | ||
return mergeSchemas(combination) | ||
} catch (e) { | ||
return undefined | ||
} | ||
}).filter(notUndefined) | ||
var result = tryMergeSchemaGroups(combinations, mergeSchemas) | ||
var unique = uniqWith(result, compare) | ||
// TODO implement merging to main schema if only one left | ||
if (unique.length) { | ||
@@ -371,3 +364,3 @@ return unique | ||
}, | ||
not: function(compacted) { | ||
not(compacted) { | ||
return { | ||
@@ -377,15 +370,3 @@ anyOf: compacted | ||
}, | ||
first: function(compacted) { | ||
return compacted[0] | ||
}, | ||
required: function(compacted) { | ||
return stringArray(compacted) | ||
}, | ||
minLength: function(compacted) { | ||
return Math.max.apply(Math, compacted) | ||
}, | ||
maxLength: function(compacted) { | ||
return Math.min.apply(Math, compacted) | ||
}, | ||
pattern: function(compacted, key, mergeSchemas, totalSchemas, reportUnresolved) { | ||
pattern(compacted, key, mergeSchemas, totalSchemas, reportUnresolved) { | ||
reportUnresolved(compacted.map(function(regexp) { | ||
@@ -397,3 +378,3 @@ return { | ||
}, | ||
multipleOf: function(compacted) { | ||
multipleOf(compacted) { | ||
var integers = compacted.slice(0) | ||
@@ -407,11 +388,3 @@ var factor = 1 | ||
}, | ||
uniqueItems: function(compacted) { | ||
return compacted.some(function(val) { | ||
return val === true | ||
}) | ||
}, | ||
examples: function(compacted) { | ||
return uniqWith(flatten(compacted), isEqual) | ||
}, | ||
enum: function(compacted, key) { | ||
enum(compacted, key) { | ||
var enums = intersectionWith.apply(null, compacted.concat(isEqual)) | ||
@@ -424,22 +397,28 @@ if (enums.length) { | ||
defaultResolvers.$id = defaultResolvers.first | ||
defaultResolvers.$schema = defaultResolvers.first | ||
defaultResolvers.$ref = defaultResolvers.first // TODO correct? probably throw | ||
defaultResolvers.title = defaultResolvers.first | ||
defaultResolvers.description = defaultResolvers.first | ||
defaultResolvers.default = defaultResolvers.first | ||
defaultResolvers.minimum = defaultResolvers.minLength | ||
defaultResolvers.exclusiveMinimum = defaultResolvers.minLength | ||
defaultResolvers.minItems = defaultResolvers.minLength | ||
defaultResolvers.minProperties = defaultResolvers.minLength | ||
defaultResolvers.maximum = defaultResolvers.maxLength | ||
defaultResolvers.exclusiveMaximum = defaultResolvers.maxLength | ||
defaultResolvers.maxItems = defaultResolvers.maxLength | ||
defaultResolvers.maxProperties = defaultResolvers.maxLength | ||
defaultResolvers.contains = schemaResolver | ||
defaultResolvers.$id = first | ||
defaultResolvers.$ref = first | ||
defaultResolvers.$schema = first | ||
defaultResolvers.additionalItems = schemaResolver | ||
defaultResolvers.additionalProperties = schemaResolver | ||
defaultResolvers.anyOf = defaultResolvers.oneOf | ||
defaultResolvers.additionalProperties = schemaResolver | ||
defaultResolvers.contains = schemaResolver | ||
defaultResolvers.default = first | ||
defaultResolvers.definitions = defaultResolvers.dependencies | ||
defaultResolvers.description = first | ||
defaultResolvers.examples = examples | ||
defaultResolvers.exclusiveMaximum = minimumValue | ||
defaultResolvers.exclusiveMinimum = maximumValue | ||
defaultResolvers.first = first | ||
defaultResolvers.maximum = minimumValue | ||
defaultResolvers.maxItems = minimumValue | ||
defaultResolvers.maxLength = minimumValue | ||
defaultResolvers.maxProperties = minimumValue | ||
defaultResolvers.minimum = maximumValue | ||
defaultResolvers.minItems = maximumValue | ||
defaultResolvers.minLength = maximumValue | ||
defaultResolvers.minProperties = maximumValue | ||
defaultResolvers.propertyNames = schemaResolver | ||
defaultResolvers.definitions = defaultResolvers.dependencies | ||
defaultResolvers.required = required | ||
defaultResolvers.title = first | ||
defaultResolvers.uniqueItems = uniqueItems | ||
@@ -474,3 +453,3 @@ function merger(rootSchema, options, totalSchemas) { | ||
var allKeys = uniq(flattenDeep(schemas.map(keys))) | ||
var allKeys = allUniqueKeys(schemas) | ||
@@ -484,57 +463,9 @@ if (contains(allKeys, 'allOf')) { | ||
var propertyKeys = allKeys.filter(isPropertyRelated) | ||
Object.assign(merged, callGroupResolver(propertyKeys, 'properties', schemas, mergeSchemas, options)) | ||
pullAll(allKeys, propertyKeys) | ||
if (propertyKeys.length) { | ||
var propResolver = options.resolvers.properties | ||
if (!propResolver) { | ||
throw new Error('No resolver found for properties.') | ||
} | ||
var compacted = uniqWith(schemas.map(function(schema) { | ||
return propertyKeys.reduce(function(all, key) { | ||
if (schema[key] !== undefined) { | ||
all[key] = schema[key] | ||
} | ||
return all | ||
}, {}) | ||
}).filter(notUndefined), compare) | ||
var result = propResolver(compacted, 'properties', mergeSchemas, totalSchemas, options) | ||
if (!isPlainObject(result)) { | ||
throwIncompatible(compacted, 'properties') | ||
} | ||
Object.assign(merged, result) | ||
pullAll(allKeys, propertyKeys) | ||
} | ||
var itemKeys = allKeys.filter(isItemsRelated) | ||
Object.assign(merged, callGroupResolver(itemKeys, 'items', schemas, mergeSchemas, options)) | ||
pullAll(allKeys, itemKeys) | ||
if (itemKeys.length) { | ||
var itemsResolver = options.resolvers.items | ||
if (!itemsResolver) { | ||
throw new Error('No resolver found for items.') | ||
} | ||
var itemsCompacted = uniqWith(schemas.map(function(schema) { | ||
return itemKeys.reduce(function(all, key) { | ||
if (schema[key] !== undefined) { | ||
all[key] = schema[key] | ||
} | ||
return all | ||
}, {}) | ||
}).filter(notUndefined), compare) | ||
var itemsResult = itemsResolver(itemsCompacted, 'items', mergeSchemas, totalSchemas, options) | ||
if (!isPlainObject(itemsResult)) { | ||
throwIncompatible(itemsCompacted, 'items') | ||
} | ||
Object.assign(merged, itemsResult) | ||
pullAll(allKeys, itemKeys) | ||
} | ||
allKeys.forEach(function(key) { | ||
@@ -541,0 +472,0 @@ var values = getValues(schemas, key) |
var chai = require('chai') | ||
var merger = require('../../src') | ||
var mergerModule = require('../../src') | ||
var Ajv = require('ajv') | ||
var $RefParser = require('json-schema-ref-parser') | ||
var expect = chai.expect | ||
var ajv = new Ajv() | ||
function merger(schema, options) { | ||
var result = mergerModule(schema, options) | ||
try { | ||
if (!ajv.validateSchema(result)) { | ||
throw new Error('Schema returned by resolver isn\'t valid.') | ||
} | ||
return result | ||
} catch (e) { | ||
if (!/stack/i.test(e.message)) { | ||
throw e | ||
} | ||
} | ||
} | ||
describe('module', function() { | ||
@@ -1381,2 +1397,31 @@ it('combines simple usecase', function() { | ||
}) | ||
it('merges mixed mode dependency', function() { | ||
var result = merger({ | ||
dependencies: { | ||
'bar': { | ||
type: [ | ||
'string', 'null', 'integer' | ||
], | ||
required: ['abc'] | ||
} | ||
}, | ||
allOf: [{ | ||
dependencies: { | ||
'bar': ['prop4'] | ||
} | ||
}] | ||
}) | ||
expect(result).to.eql({ | ||
dependencies: { | ||
'bar': { | ||
type: [ | ||
'string', 'null', 'integer' | ||
], | ||
required: ['abc', 'prop4'] | ||
} | ||
} | ||
}) | ||
}) | ||
}) | ||
@@ -1383,0 +1428,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
184
83375
15
3113