Skip to content

Commit

Permalink
Support complex resolvers
Browse files Browse the repository at this point in the history
  • Loading branch information
mokkabonna authored and Martin Hansen committed Feb 22, 2021
1 parent 2243aa8 commit d17b970
Show file tree
Hide file tree
Showing 9 changed files with 393 additions and 576 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
language: node_js
after_success: npm run coverage
node_js:
- 10
- 12
- 14
- node
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,22 @@ The function is passed:


### Combined resolvers
No separate resolver is called for patternProperties and additionalProperties, only the properties resolver is called. Same for additionalItems, only items resolver is called. This is because those keywords need to be resolved together as they affect each other.
Some keyword are dependant on other keywords, like properties, patternProperties, additionalProperties. To create a resolver for these the resolver requires this structure:

Those two resolvers are expected to return an object containing the resolved values of all the associated keywords. The keys must be the name of the keywords. So the properties resolver need to return an object like this containing the resolved values for each keyword:
```js
mergeAllOf(schema, {
resolvers: {
properties:
keywords: ['properties', 'patternProperties', 'additionalProperties'],
resolver(values, parents, mergers, options) {

}
}
}
})
```

This type of resolvers are expected to return an object containing the resolved values of all the associated keywords. The keys must be the name of the keywords. So the properties resolver need to return an object like this containing the resolved values for each keyword:

```js
{
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 45 additions & 0 deletions src/common.js
Original file line number Diff line number Diff line change
@@ -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
}
98 changes: 98 additions & 0 deletions src/complex-resolvers/items.js
Original file line number Diff line number Diff line change
@@ -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)
}
}
80 changes: 80 additions & 0 deletions src/complex-resolvers/properties.js
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit d17b970

Please sign in to comment.