From 8ae0e07c6a5377bd0c5f94d01a5d8a8c6d6202e7 Mon Sep 17 00:00:00 2001 From: Dustin Popp Date: Thu, 14 Nov 2024 13:26:18 -0600 Subject: [PATCH] feat: add utility for checking composite schemas with a looser constraint 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 --- packages/utilities/src/utils/index.js | 3 +- .../utils/schema-loosely-has-constraint.js | 41 +++++ .../schema-loosely-has-constraint.test.js | 170 ++++++++++++++++++ 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 packages/utilities/src/utils/schema-loosely-has-constraint.js create mode 100644 packages/utilities/test/schema-loosely-has-constraint.test.js diff --git a/packages/utilities/src/utils/index.js b/packages/utilities/src/utils/index.js index 80a4e954a..1ef76c7f9 100644 --- a/packages/utilities/src/utils/index.js +++ b/packages/utilities/src/utils/index.js @@ -1,5 +1,5 @@ /** - * Copyright 2017 - 2023 IBM Corporation. + * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ @@ -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'), diff --git a/packages/utilities/src/utils/schema-loosely-has-constraint.js b/packages/utilities/src/utils/schema-loosely-has-constraint.js new file mode 100644 index 000000000..1d1711be4 --- /dev/null +++ b/packages/utilities/src/utils/schema-loosely-has-constraint.js @@ -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; diff --git a/packages/utilities/test/schema-loosely-has-constraint.test.js b/packages/utilities/test/schema-loosely-has-constraint.test.js new file mode 100644 index 000000000..103242c90 --- /dev/null +++ b/packages/utilities/test/schema-loosely-has-constraint.test.js @@ -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 + ); + }); +});