Skip to content

Commit

Permalink
Recurse through nested anyOf statements when filtering
Browse files Browse the repository at this point in the history
Fixes #30

Change-type: patch
Signed-off-by: Lucian <lucian.buzzo@gmail.com>
  • Loading branch information
LucianBuzzo committed Dec 3, 2018
1 parent 7c47956 commit 091193c
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 51 deletions.
110 changes: 60 additions & 50 deletions lib/index.js
Expand Up @@ -806,72 +806,82 @@ exports.filter = (() => {
filterAjv.removeSchema(/^.*$/)
matchAjv.removeSchema(/^.*$/)

const preparedSchema = exports.normaliseRequires(_.cloneDeep(schema))

// Parse the schema to create a schema that will be used for filtering
// properties
const filterValidator = filterAjv.compile(preparedSchema)

// A flag to help inform the return type
const calledWithArray = _.isArray(object)

// Run all the logic as if this function was called with an array, to help
// homogenise filter logic
const items = _.castArray(object)

// If the schema uses 'anyOf', each subschema is checked independently. to
// improve performance when dealing with large collections of elements,
// validators are created and cached before iterating over the collection
let fragmentValidators = []
const baseSchema = _.omit(preparedSchema, 'anyOf')

if (preparedSchema.anyOf) {
fragmentValidators = _.map(preparedSchema.anyOf, (fragment) => {
const mergedFragment = combineWithBaseSchema(
baseSchema,
fragment
)

return {
match: matchAjv.compile(
allowAdditionalProperties(mergedFragment)
),
fragment
}
})
}
const parse = (thisSchema) => {
const preparedSchema = exports.normaliseRequires(
_.cloneDeep(thisSchema)
)

// Parse the schema to create a schema that will be used for filtering
// properties
const filterValidator = filterAjv.compile(preparedSchema)

// If the schema uses 'anyOf', each subschema is checked independently. to
// improve performance when dealing with large collections of elements,
// validators are created and cached before iterating over the collection
let fragmentValidators = []
const baseSchema = _.omit(preparedSchema, 'anyOf')

if (preparedSchema.anyOf) {
fragmentValidators = _.map(preparedSchema.anyOf, (fragment) => {
const mergedFragment = combineWithBaseSchema(
baseSchema,
fragment
)

return {
match: matchAjv.compile(
allowAdditionalProperties(mergedFragment)
),
fragment
}
})
}

// Reduce is used here due to the behaviour of the AJV `removeAdditional`
// option, which mutates the object
const result = items.reduce((carry, item) => {
const matchers = collectMatchers(item, fragmentValidators)
// Reduce is used here due to the behaviour of the AJV `removeAdditional`
// option, which mutates the object
const result = []
for (const item of items) {
const matchers = collectMatchers(item, fragmentValidators)

// If no branches match we return the main schema validator
if (_.isEmpty(matchers)) {
if (filterValidator(item)) {
carry.push(item)
}
return carry
}
// If no branches match we return the main schema validator
if (_.isEmpty(matchers)) {
if (filterValidator(item)) {
result.push(item)
}
} else {
// Otherwise we merge the branch fragments together to create a new filterValidator
const filterSchema = makeFilter(baseSchema, matchers)

// Check if there are still `anyOf` branches to resolve
if (filterSchema.anyOf) {
return parse(filterSchema)
}

// Otherwise we merge the branch fragments together to create a new filterVlidator
const filterSchema = makeFilter(baseSchema, matchers)
const mergedFilterValidator = filterAjv.compile(filterSchema)

const mergedFilterValidator = filterAjv.compile(filterSchema)
if (mergedFilterValidator(item)) {
result.push(item)
}
}
}

if (mergedFilterValidator(item)) {
carry.push(item)
// If the function was called with an array, return an array, otherwise
// return the first array element, or null if its undefined
if (calledWithArray) {
return result
}
return carry
}, [])

// If the function was called with an array, return an array, otherwise
// return the first array element, or null if its undefined
if (calledWithArray) {
return result
return _.first(result) || null
}

return _.first(result) || null
return parse(schema)
}
})()

Expand Down
51 changes: 50 additions & 1 deletion test/index.spec.js
Expand Up @@ -1576,7 +1576,7 @@ _.each(SCORE_TEST_CASES, (testCase, index) => {
})
})

ava.test('Should pick second anyOf branch despite the first one not matching', (test) => {
ava.test('.filter() Should pick second anyOf branch despite the first one not matching', (test) => {
const element = {
id: 'd5da0783-2531-45af-be38-806e1ea4490c',
slug: 'view-all-views',
Expand Down Expand Up @@ -1624,3 +1624,52 @@ ava.test('Should pick second anyOf branch despite the first one not matching', (

test.not(filtered, null)
})

ava.test('.filter() should correctly filter using nested anyOf statements', (test) => {
const element = {
id: '1',
slug: 'user-janedoe',
type: 'user'
}

const schema = {
type: 'object',
properties: {
type: {
type: 'string',
const: 'user'
}
},
anyOf: [
{
type: 'object',
anyOf: [
{
properties: {
slug: {
const: 'user-janedoe',
type: 'string'
}
},
required: [
'slug'
],
type: 'object',
additionalProperties: true
}
]
}
],
required: [
'type'
],
additionalProperties: false
}

const filtered = skhema.filter(schema, element)

test.deepEqual(filtered, {
slug: 'user-janedoe',
type: 'user'
})
})

0 comments on commit 091193c

Please sign in to comment.