Skip to content

Commit

Permalink
feat: add utility for checking composite schemas with a looser constr…
Browse files Browse the repository at this point in the history
…aint

The existing `schemaHasConstraint` function will only determine a schema meets
the given constraint when every element of a `oneOf` or `anyOf` list. This is
desired behavior in the majority of cases. However, in some cases, it is
helpful to note when a schema includes a `oneOf` or `anyOf` list with only a
single element meeting the constraint, instead of all of them. The new utility
function, `schemaLooselyHasConstraint`, delivers this functionality.

Signed-off-by: Dustin Popp <[email protected]>
  • Loading branch information
dpopp07 committed Nov 15, 2024
1 parent e3fa095 commit 8ae0e07
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/utilities/src/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2017 - 2023 IBM Corporation.
* Copyright 2017 - 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

Expand All @@ -9,6 +9,7 @@ module.exports = {
isObject: require('./is-object'),
schemaHasConstraint: require('./schema-has-constraint'),
schemaHasProperty: require('./schema-has-property'),
schemaLooselyHasConstraint: require('./schema-loosely-has-constraint'),
schemaRequiresProperty: require('./schema-requires-property'),
validateComposedSchemas: require('./validate-composed-schemas'),
validateNestedSchemas: require('./validate-nested-schemas'),
Expand Down
41 changes: 41 additions & 0 deletions packages/utilities/src/utils/schema-loosely-has-constraint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

const isObject = require('./is-object');

/**
* This function is a looser adaptation of the "schemaHasConstraint" function in the utilities package.
* Here we process oneOf and anyOf lists the same as allOf, where we return true if one (or more)
* of the oneOf/anyOf elements has the constraint (rather than all of the elements).
*/
function schemaLooselyHasConstraint(schema, hasConstraint) {
if (!isObject(schema)) {
return false;
}

if (hasConstraint(schema)) {
return true;
}

const anySchemaHasConstraintReducer = (previousResult, currentSchema) => {
return (
previousResult || schemaLooselyHasConstraint(currentSchema, hasConstraint)
);
};

for (const applicator of ['allOf', 'oneOf', 'anyOf']) {
if (
Array.isArray(schema[applicator]) &&
schema[applicator].length > 0 &&
schema[applicator].reduce(anySchemaHasConstraintReducer, false)
) {
return true;
}
}

return false;
}

module.exports = schemaLooselyHasConstraint;
170 changes: 170 additions & 0 deletions packages/utilities/test/schema-loosely-has-constraint.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

const { schemaLooselyHasConstraint } = require('../src');

const fredIsNull = s => s.fred === null;

describe('Utility function: schemaLooselyHasConstraint()', () => {
it('should return `false` for `undefined`', async () => {
expect(schemaLooselyHasConstraint(undefined, fredIsNull)).toBe(false);
});

it('should return `false` for `null`', async () => {
expect(schemaLooselyHasConstraint(null, fredIsNull)).toBe(false);
});

it('should return `false` for empty schema', async () => {
expect(schemaLooselyHasConstraint({}, fredIsNull)).toBe(false);
});

it('should return `true` for a compliant simple schema', async () => {
const compliantSimpleSchema = { fred: null };
expect(schemaLooselyHasConstraint(compliantSimpleSchema, fredIsNull)).toBe(
true
);
});

it('should return `false` for a schema with empty `oneOf`', async () => {
const schemaWithEmptyOneOf = { oneOf: [] };
expect(schemaLooselyHasConstraint(schemaWithEmptyOneOf, fredIsNull)).toBe(
false
);
});

it('should return `false` for a schema with empty `anyOf`', async () => {
const schemaWithEmptyOneOf = { anyOf: [] };
expect(schemaLooselyHasConstraint(schemaWithEmptyOneOf, fredIsNull)).toBe(
false
);
});

it('should return `false` for a schema with empty `allOf`', async () => {
const schemaWithEmptyOneOf = { allOf: [] };
expect(schemaLooselyHasConstraint(schemaWithEmptyOneOf, fredIsNull)).toBe(
false
);
});

it('should return `true` for a schema with all-compliant `oneOf` schemas', async () => {
const schemaWithAllCompliantOneOfs = {
oneOf: [{ fred: null }, { fred: null }, { fred: null }],
};
expect(
schemaLooselyHasConstraint(schemaWithAllCompliantOneOfs, fredIsNull)
).toBe(true);
});

it('should return `true` for a schema with all-compliant `anyOf` schemas', async () => {
const schemaWithAllCompliantAnyOfs = {
anyOf: [{ fred: null }, { fred: null }, { fred: null }],
};
expect(
schemaLooselyHasConstraint(schemaWithAllCompliantAnyOfs, fredIsNull)
).toBe(true);
});

it('should return `true` for a schema with one of many compliant `allOf` schemas', async () => {
const schemaWithOneCompliantAllOf = {
allOf: [{}, { fred: null }, {}],
};
expect(
schemaLooselyHasConstraint(schemaWithOneCompliantAllOf, fredIsNull)
).toBe(true);
});

it('should return `true` for a schema with one of many compliant `oneOf` schemas', async () => {
const schemaWithOneCompliantOneOf = {
anyOf: [{}, { fred: null }, {}],
};
expect(
schemaLooselyHasConstraint(schemaWithOneCompliantOneOf, fredIsNull)
).toBe(true);
});

it('should return `true` for a schema with one of many compliant `anyOf` schemas', async () => {
const schemaWithOneCompliantAnyOf = {
anyOf: [{}, { fred: null }, {}],
};
expect(
schemaLooselyHasConstraint(schemaWithOneCompliantAnyOf, fredIsNull)
).toBe(true);
});

it('should return `true` for `oneOf` compliance even without `anyOf` or `allOf` compliance', async () => {
const schemaWithOnlyOneOfCompliance = {
oneOf: [{ fred: null }, { fred: null }],
anyOf: [{ fred: null }, {}],
allOf: [{}, {}],
};
expect(
schemaLooselyHasConstraint(schemaWithOnlyOneOfCompliance, fredIsNull)
).toBe(true);
});

it('should return `true` for `anyOf` compliance even without `oneOf` or `allOf` compliance', async () => {
const schemaWithOnlyAnyOfCompliance = {
anyOf: [{ fred: null }, { fred: null }],
oneOf: [{ fred: null }, {}],
allOf: [{}, {}],
};
expect(
schemaLooselyHasConstraint(schemaWithOnlyAnyOfCompliance, fredIsNull)
).toBe(true);
});

it('should return `true` for `allOf` compliance even without `oneOf` or `anyOf` compliance', async () => {
const schemaWithOnlyAllOfCompliance = {
allOf: [{}, { fred: null }, {}],
oneOf: [{}, { fred: null }],
anyOf: [{ fred: null }, {}],
};
expect(
schemaLooselyHasConstraint(schemaWithOnlyAllOfCompliance, fredIsNull)
).toBe(true);
});

it('should recurse through `oneOf` and `allOf`', async () => {
const schemaWithAllOfInOneOf = {
oneOf: [
{
allOf: [{ fred: null }, {}],
},
{ fred: null },
],
};
expect(schemaLooselyHasConstraint(schemaWithAllOfInOneOf, fredIsNull)).toBe(
true
);
});

it('should recurse through `allOf` and `anyOf`', async () => {
const schemaWithAnyOfInAllOf = {
allOf: [
{
anyOf: [{ fred: null }, { fred: null }],
},
{},
],
};
expect(schemaLooselyHasConstraint(schemaWithAnyOfInAllOf, fredIsNull)).toBe(
true
);
});

it('should recurse through `anyOf` and `oneOf`', async () => {
const schemaWithAnyOfInAllOf = {
anyOf: [
{
oneOf: [{ fred: null }, { fred: null }],
},
{ fred: null },
],
};
expect(schemaLooselyHasConstraint(schemaWithAnyOfInAllOf, fredIsNull)).toBe(
true
);
});
});

0 comments on commit 8ae0e07

Please sign in to comment.