Skip to content

Commit

Permalink
WIP Extract complex resolvers
Browse files Browse the repository at this point in the history
  • Loading branch information
mokkabonna committed Feb 21, 2021
1 parent 2243aa8 commit 1cc2aa5
Show file tree
Hide file tree
Showing 8 changed files with 411 additions and 260 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
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
}
29 changes: 29 additions & 0 deletions src/complex-resolvers/if.js
Original file line number Diff line number Diff line change
@@ -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)
}
}
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)
}
}
Loading

0 comments on commit 1cc2aa5

Please sign in to comment.