Skip to content

Commit 1cc2aa5

Browse files
committed
WIP Extract complex resolvers
1 parent 2243aa8 commit 1cc2aa5

File tree

8 files changed

+411
-260
lines changed

8 files changed

+411
-260
lines changed

.travis.yml

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
language: node_js
22
after_success: npm run coverage
33
node_js:
4-
- 10
54
- 12
65
- 14
76
- node

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"version": "0.6.0",
44
"description": "Simplify your schema by combining allOf into the root schema, safely.",
55
"main": "src/index.js",
6+
"engines": {
7+
"node": ">=12.0.0"
8+
},
69
"scripts": {
710
"eslint": "eslint src test",
811
"test": "npm run eslint && nyc --reporter=html --reporter=text mocha test/specs",

src/common.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const flatten = require('lodash/flatten')
2+
const flattenDeep = require('lodash/flattenDeep')
3+
const isPlainObject = require('lodash/isPlainObject')
4+
const uniq = require('lodash/uniq')
5+
const uniqWith = require('lodash/uniqWith')
6+
const without = require('lodash/without')
7+
8+
function deleteUndefinedProps(returnObject) {
9+
// cleanup empty
10+
for (const prop in returnObject) {
11+
if (has(returnObject, prop) && isEmptySchema(returnObject[prop])) {
12+
delete returnObject[prop]
13+
}
14+
}
15+
return returnObject
16+
}
17+
18+
const allUniqueKeys = (arr) => uniq(flattenDeep(arr.map(keys)))
19+
const getValues = (schemas, key) => schemas.map(schema => schema && schema[key])
20+
const has = (obj, propName) => Object.prototype.hasOwnProperty.call(obj, propName)
21+
const keys = obj => {
22+
if (isPlainObject(obj) || Array.isArray(obj)) {
23+
return Object.keys(obj)
24+
} else {
25+
return []
26+
}
27+
}
28+
29+
const notUndefined = (val) => val !== undefined
30+
const isSchema = (val) => isPlainObject(val) || val === true || val === false
31+
const isEmptySchema = (obj) => (!keys(obj).length) && obj !== false && obj !== true
32+
const withoutArr = (arr, ...rest) => without.apply(null, [arr].concat(flatten(rest)))
33+
34+
module.exports = {
35+
allUniqueKeys,
36+
deleteUndefinedProps,
37+
getValues,
38+
has,
39+
isEmptySchema,
40+
isSchema,
41+
keys,
42+
notUndefined,
43+
uniqWith,
44+
withoutArr
45+
}

src/complex-resolvers/if.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const { has } = require('../common')
2+
3+
const conditonalRelated = ['if', 'then', 'else']
4+
5+
module.exports = {
6+
// test with same if-then-else resolver
7+
keywords: ['if', 'then', 'else'],
8+
resolver(schemas, paths, mergers, options) {
9+
const allWithConditional = schemas.filter(schema =>
10+
conditonalRelated.some(keyword => has(schema, keyword)))
11+
12+
// merge sub schemas completely
13+
// if,then,else must not be merged to the base schema, but if they contain allOf themselves, that should be merged
14+
function merge(schema) {
15+
const obj = {}
16+
if (has(schema, 'if')) obj.if = mergers.if([schema.if])
17+
if (has(schema, 'then')) obj.then = mergers.then([schema.then])
18+
if (has(schema, 'else')) obj.else = mergers.else([schema.else])
19+
return obj
20+
}
21+
22+
// first schema with any of the 3 keywords is used as base
23+
const first = merge(allWithConditional.shift())
24+
return allWithConditional.reduce((all, schema) => {
25+
all.allOf = (all.allOf || []).concat(merge(schema))
26+
return all
27+
}, first)
28+
}
29+
}

src/complex-resolvers/items.js

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
2+
const compare = require('json-schema-compare')
3+
const forEach = require('lodash/forEach')
4+
const {
5+
allUniqueKeys,
6+
deleteUndefinedProps,
7+
has,
8+
isSchema,
9+
notUndefined,
10+
uniqWith
11+
} = require('../common')
12+
13+
function removeFalseSchemasFromArray(target) {
14+
forEach(target, function(schema, index) {
15+
if (schema === false) {
16+
target.splice(index, 1)
17+
}
18+
})
19+
}
20+
21+
function getItemSchemas(subSchemas, key) {
22+
return subSchemas.map(function(sub) {
23+
if (!sub) {
24+
return undefined
25+
}
26+
27+
if (Array.isArray(sub.items)) {
28+
const schemaAtPos = sub.items[key]
29+
if (isSchema(schemaAtPos)) {
30+
return schemaAtPos
31+
} else if (has(sub, 'additionalItems')) {
32+
return sub.additionalItems
33+
}
34+
} else {
35+
return sub.items
36+
}
37+
38+
return undefined
39+
})
40+
}
41+
42+
function getAdditionalSchemas(subSchemas) {
43+
return subSchemas.map(function(sub) {
44+
if (!sub) {
45+
return undefined
46+
}
47+
if (Array.isArray(sub.items)) {
48+
return sub.additionalItems
49+
}
50+
return sub.items
51+
})
52+
}
53+
54+
// Provide source when array
55+
function mergeItems(group, mergeSchemas, items) {
56+
const allKeys = allUniqueKeys(items)
57+
return allKeys.reduce(function(all, key) {
58+
const schemas = getItemSchemas(group, key)
59+
const compacted = uniqWith(schemas.filter(notUndefined), compare)
60+
all[key] = mergeSchemas(compacted, key)
61+
return all
62+
}, [])
63+
}
64+
65+
module.exports = {
66+
keywords: ['items', 'additionalItems'],
67+
resolver(values, parents, mergers) {
68+
// const createSubMerger = groupKey => (schemas, key) => mergeSchemas(schemas, parents.concat(groupKey, key))
69+
const items = values.map(s => s.items)
70+
const itemsCompacted = items.filter(notUndefined)
71+
const returnObject = {}
72+
73+
// if all items keyword values are schemas, we can merge them as simple schemas
74+
// if not we need to merge them as mixed
75+
if (itemsCompacted.every(isSchema)) {
76+
returnObject.items = mergers.items(items)
77+
} else {
78+
returnObject.items = mergeItems(values, mergers.items, items)
79+
}
80+
81+
let schemasAtLastPos
82+
if (itemsCompacted.every(Array.isArray)) {
83+
schemasAtLastPos = values.map(s => s.additionalItems)
84+
} else if (itemsCompacted.some(Array.isArray)) {
85+
schemasAtLastPos = getAdditionalSchemas(values)
86+
}
87+
88+
if (schemasAtLastPos) {
89+
returnObject.additionalItems = mergers.additionalItems(schemasAtLastPos)
90+
}
91+
92+
if (returnObject.additionalItems === false && Array.isArray(returnObject.items)) {
93+
removeFalseSchemasFromArray(returnObject.items)
94+
}
95+
96+
return deleteUndefinedProps(returnObject)
97+
}
98+
}

src/complex-resolvers/properties.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
2+
const compare = require('json-schema-compare')
3+
const forEach = require('lodash/forEach')
4+
const {
5+
allUniqueKeys,
6+
deleteUndefinedProps,
7+
getValues,
8+
keys,
9+
notUndefined,
10+
uniqWith,
11+
withoutArr
12+
} = require('../common')
13+
14+
function removeFalseSchemas(target) {
15+
forEach(target, function(schema, prop) {
16+
if (schema === false) {
17+
delete target[prop]
18+
}
19+
})
20+
}
21+
22+
function mergeSchemaGroup(group, mergeSchemas) {
23+
const allKeys = allUniqueKeys(group)
24+
return allKeys.reduce(function(all, key) {
25+
const schemas = getValues(group, key)
26+
const compacted = uniqWith(schemas.filter(notUndefined), compare)
27+
all[key] = mergeSchemas(compacted, key)
28+
return all
29+
}, {})
30+
}
31+
32+
module.exports = {
33+
keywords: ['properties', 'patternProperties', 'additionalProperties'],
34+
resolver(values, parents, mergers, options) {
35+
// first get rid of all non permitted properties
36+
if (!options.ignoreAdditionalProperties) {
37+
values.forEach(function(subSchema) {
38+
const otherSubSchemas = values.filter(s => s !== subSchema)
39+
const ownKeys = keys(subSchema.properties)
40+
const ownPatternKeys = keys(subSchema.patternProperties)
41+
const ownPatterns = ownPatternKeys.map(k => new RegExp(k))
42+
otherSubSchemas.forEach(function(other) {
43+
const allOtherKeys = keys(other.properties)
44+
const keysMatchingPattern = allOtherKeys.filter(k => ownPatterns.some(pk => pk.test(k)))
45+
const additionalKeys = withoutArr(allOtherKeys, ownKeys, keysMatchingPattern)
46+
additionalKeys.forEach(function(key) {
47+
other.properties[key] = mergers.properties([
48+
other.properties[key], subSchema.additionalProperties
49+
], key)
50+
})
51+
})
52+
})
53+
54+
// remove disallowed patternProperties
55+
values.forEach(function(subSchema) {
56+
const otherSubSchemas = values.filter(s => s !== subSchema)
57+
const ownPatternKeys = keys(subSchema.patternProperties)
58+
if (subSchema.additionalProperties === false) {
59+
otherSubSchemas.forEach(function(other) {
60+
const allOtherPatterns = keys(other.patternProperties)
61+
const additionalPatternKeys = withoutArr(allOtherPatterns, ownPatternKeys)
62+
additionalPatternKeys.forEach(key => delete other.patternProperties[key])
63+
})
64+
}
65+
})
66+
}
67+
68+
const returnObject = {
69+
additionalProperties: mergers.additionalProperties(values.map(s => s.additionalProperties)),
70+
patternProperties: mergeSchemaGroup(values.map(s => s.patternProperties), mergers.patternProperties),
71+
properties: mergeSchemaGroup(values.map(s => s.properties), mergers.properties)
72+
}
73+
74+
if (returnObject.additionalProperties === false) {
75+
removeFalseSchemas(returnObject.properties)
76+
}
77+
78+
return deleteUndefinedProps(returnObject)
79+
}
80+
}

0 commit comments

Comments
 (0)