diff --git a/packages/utilities/src/utils/validate-composed-schemas.js b/packages/utilities/src/utils/validate-composed-schemas.js index 39e7d2ee..10aaa3cb 100644 --- a/packages/utilities/src/utils/validate-composed-schemas.js +++ b/packages/utilities/src/utils/validate-composed-schemas.js @@ -28,9 +28,13 @@ const SchemaPath = require('./schema-path'); * - `schema` — the composed schema * - `path` — the array of path segments to locate the composed schema within the resolved document * - * The provided `validate()` function is guaranteed to be called for a schema before any of its - * composed schemas. However, it is not guaranteed that the `validate()` function is called in any - * particular order for a schema's composed schemas. + * The provided `validate()` function is guaranteed to be called: + * - for a schema before any of its composed schemas + * - more recently for the schema that composes it (its "composition parent") than for that schema's + * siblings (or their descendants) in the composition tree + * + * However, it is not guaranteed that the `validate()` function is called in any particular order + * for a schema's directly composed schemas. * * WARNING: It is only safe to use this function for a "resolved" schema — it cannot traverse `$ref` * references. diff --git a/packages/utilities/src/utils/validate-nested-schemas.js b/packages/utilities/src/utils/validate-nested-schemas.js index b4c0de2c..1d738a73 100644 --- a/packages/utilities/src/utils/validate-nested-schemas.js +++ b/packages/utilities/src/utils/validate-nested-schemas.js @@ -39,9 +39,13 @@ const SchemaPath = require('./schema-path'); * Within the logical path, an arbitrary array item is represented by `[]` and an arbitrary * dictionary property is represented by `*`. * - * The provided `validate()` function is guaranteed to be called for a schema before any of its - * nested schemas. However, it is not guaranteed that the `validate()` function is called in any - * particular order for a schema's nested schemas. + * The provided `validate()` function is guaranteed to be called: + * - for a schema before any of its nested schemas + * - more recently for the schema that nests it (its "nesting parent") than for that schema's + * siblings (or their descendants) in the nesting tree + * + * However, it is not guaranteed that the `validate()` function is called in any particular order + * for a schema's directly nested schemas. * * WARNING: It is only safe to use this function for a "resolved" schema — it cannot traverse `$ref` * references. @@ -120,6 +124,23 @@ function validateNestedSchemas( ); } + if ( + schema.patternProperties && + typeof schema.patternProperties === 'object' + ) { + for (const entry of Object.entries(schema.patternProperties)) { + errors.push( + ...validateNestedSchemas( + entry[1], + path.withPatternProperty(entry[0]), + validate, + true, + includeNot + ) + ); + } + } + if (includeNot && schema.not) { errors.push( ...validateNestedSchemas( @@ -148,23 +169,6 @@ function validateNestedSchemas( } } - if ( - schema.patternProperties && - typeof schema.patternProperties === 'object' - ) { - for (const entry of Object.entries(schema.patternProperties)) { - errors.push( - ...validateNestedSchemas( - entry[1], - path.withPatternProperty(entry[0]), - validate, - true, - includeNot - ) - ); - } - } - return errors; } diff --git a/packages/utilities/src/utils/validate-subschemas.js b/packages/utilities/src/utils/validate-subschemas.js index baea5def..4cfd172e 100644 --- a/packages/utilities/src/utils/validate-subschemas.js +++ b/packages/utilities/src/utils/validate-subschemas.js @@ -42,9 +42,15 @@ const validateNestedSchemas = require('./validate-nested-schemas'); * The provided `validate()` function is guaranteed to be called: * - for a schema before any of its composed schemas * - for a schema before any of its nested schemas + * - more recently for the schema that composes it (its "composition parent") than for that schema's + * siblings (or their descendants) in the portion of composition tree local to the composition + * parent's branch of the nesting tree + * - more recently for the schema that nests it (its "nesting parent") than for that schema's + * siblings (or their descendants) in the portion of nesting tree local to the nesting parent's + * branch of the composition tree * * However, it is not guaranteed that the `validate()` function is called in any particular order - * for a schema's composed or nested schemas. + * for a schema's directly composed or directly nested schemas. * * WARNING: It is only safe to use this function for a "resolved" schema — it cannot traverse `$ref` * references. diff --git a/packages/utilities/test/schema-path.test.js b/packages/utilities/test/schema-path.test.js index 71dd3e92..5a853def 100644 --- a/packages/utilities/test/schema-path.test.js +++ b/packages/utilities/test/schema-path.test.js @@ -38,63 +38,111 @@ describe('Utility class: SchemaPath', () => { expect(new SchemaPath(['one', 'two'], []).logical).toEqual([]); }); it('logical path is preserved', async () => { - expect(new SchemaPath(['one', 'two'], ['foo', 'bar']).logical).toEqual(['foo', 'bar']); + expect(new SchemaPath(['one', 'two'], ['foo', 'bar']).logical).toEqual([ + 'foo', + 'bar', + ]); }); it('logical path is immutable', async () => { const path = new SchemaPath(['one', 'two'], ['foo', 'bar']); - expect(() => path.logical = []).toThrow(); + expect(() => (path.logical = [])).toThrow(); expect(() => path.logical.push('three')).toThrow(); }); }); describe('withProperty()', () => { it('physical path is correctly appended', async () => { - expect([...new SchemaPath(['one', 'two'], ['foo', 'bar']).withProperty('three')]).toEqual(['one', 'two', 'properties', 'three']); + expect([ + ...new SchemaPath(['one', 'two'], ['foo', 'bar']).withProperty('three'), + ]).toEqual(['one', 'two', 'properties', 'three']); }); it('logical path is correctly appended', async () => { - expect(new SchemaPath(['one', 'two'], ['foo', 'bar']).withProperty('three').logical).toEqual(['foo', 'bar', 'three']); + expect( + new SchemaPath(['one', 'two'], ['foo', 'bar']).withProperty('three') + .logical + ).toEqual(['foo', 'bar', 'three']); }); }); describe('withAdditionalProperty()', () => { it('physical path is correctly appended', async () => { - expect([...new SchemaPath(['one', 'two'], ['foo', 'bar']).withAdditionalProperty()]).toEqual(['one', 'two', 'additionalProperties']); + expect([ + ...new SchemaPath( + ['one', 'two'], + ['foo', 'bar'] + ).withAdditionalProperty(), + ]).toEqual(['one', 'two', 'additionalProperties']); }); it('logical path is correctly appended', async () => { - expect(new SchemaPath(['one', 'two'], ['foo', 'bar']).withAdditionalProperty('three').logical).toEqual(['foo', 'bar', '*']); + expect( + new SchemaPath(['one', 'two'], ['foo', 'bar']).withAdditionalProperty( + 'three' + ).logical + ).toEqual(['foo', 'bar', '*']); }); }); describe('withPatternProperty()', () => { it('physical path is correctly appended', async () => { - expect([...new SchemaPath(['one', 'two'], ['foo', 'bar']).withPatternProperty('baz')]).toEqual(['one', 'two', 'patternProperties', 'baz']); + expect([ + ...new SchemaPath(['one', 'two'], ['foo', 'bar']).withPatternProperty( + 'baz' + ), + ]).toEqual(['one', 'two', 'patternProperties', 'baz']); }); it('logical path is correctly appended', async () => { - expect(new SchemaPath(['one', 'two'], ['foo', 'bar']).withPatternProperty('baz').logical).toEqual(['foo', 'bar', '*']); + expect( + new SchemaPath(['one', 'two'], ['foo', 'bar']).withPatternProperty( + 'baz' + ).logical + ).toEqual(['foo', 'bar', '*']); }); }); describe('withArrayItem()', () => { it('physical path is correctly appended', async () => { - expect([...new SchemaPath(['one', 'two'], ['foo', 'bar']).withArrayItem()]).toEqual(['one', 'two', 'items']); + expect([ + ...new SchemaPath(['one', 'two'], ['foo', 'bar']).withArrayItem(), + ]).toEqual(['one', 'two', 'items']); }); it('logical path is correctly appended', async () => { - expect(new SchemaPath(['one', 'two'], ['foo', 'bar']).withArrayItem().logical).toEqual(['foo', 'bar', '[]']); + expect( + new SchemaPath(['one', 'two'], ['foo', 'bar']).withArrayItem().logical + ).toEqual(['foo', 'bar', '[]']); }); }); describe('withApplicator()', () => { it('physical path is correctly appended', async () => { - expect([...new SchemaPath(['one', 'two'], ['foo', 'bar']).withApplicator('oneOf', '0')]).toEqual(['one', 'two', 'oneOf', '0']); + expect([ + ...new SchemaPath(['one', 'two'], ['foo', 'bar']).withApplicator( + 'oneOf', + '0' + ), + ]).toEqual(['one', 'two', 'oneOf', '0']); }); it('logical path is correctly (not) appended', async () => { - expect(new SchemaPath(['one', 'two'], ['foo', 'bar']).withApplicator('oneOf', '0').logical).toEqual(['foo', 'bar']); + expect( + new SchemaPath(['one', 'two'], ['foo', 'bar']).withApplicator( + 'oneOf', + '0' + ).logical + ).toEqual(['foo', 'bar']); }); it('numeric index is coerced to string', async () => { - expect([...new SchemaPath(['one', 'two'], ['foo', 'bar']).withApplicator('oneOf', 0)]).toEqual(['one', 'two', 'oneOf', '0']); + expect([ + ...new SchemaPath(['one', 'two'], ['foo', 'bar']).withApplicator( + 'oneOf', + 0 + ), + ]).toEqual(['one', 'two', 'oneOf', '0']); }); }); describe('withNot()', () => { it('physical path is correctly appended', async () => { - expect([...new SchemaPath(['one', 'two'], ['foo', 'bar']).withNot()]).toEqual(['one', 'two', 'not']); + expect([ + ...new SchemaPath(['one', 'two'], ['foo', 'bar']).withNot(), + ]).toEqual(['one', 'two', 'not']); }); it('logical path is correctly (not) appended', async () => { - expect(new SchemaPath(['one', 'two'], ['foo', 'bar']).withNot().logical).toEqual(['foo', 'bar']); + expect( + new SchemaPath(['one', 'two'], ['foo', 'bar']).withNot().logical + ).toEqual(['foo', 'bar']); }); }); }); diff --git a/packages/utilities/test/validate-composed-schemas.test.js b/packages/utilities/test/validate-composed-schemas.test.js index d88abc23..6c71917b 100644 --- a/packages/utilities/test/validate-composed-schemas.test.js +++ b/packages/utilities/test/validate-composed-schemas.test.js @@ -201,6 +201,44 @@ describe('Utility function: validateComposedSchemas()', () => { expect(results).toEqual([1, 2, 3, 4, 5]); }); + it("should validate a schema's composition parent more recently than its parent's siblings", async () => { + const schema = { + allOf: [{ oneOf: [{}] }, { oneOf: [{}] }], + oneOf: [{ anyOf: [{}] }, { anyOf: [{}] }], + anyOf: [{ not: {} }, { not: {} }], + not: { allOf: [{}] }, + }; + + const visitedPaths = []; + + validateComposedSchemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return []; + }); + + expect(visitedPaths.indexOf('allOf.0.oneOf.0') - 1).toEqual( + visitedPaths.indexOf('allOf.0') + ); + expect(visitedPaths.indexOf('allOf.1.oneOf.0') - 1).toEqual( + visitedPaths.indexOf('allOf.1') + ); + expect(visitedPaths.indexOf('oneOf.0.anyOf.0') - 1).toEqual( + visitedPaths.indexOf('oneOf.0') + ); + expect(visitedPaths.indexOf('oneOf.1.anyOf.0') - 1).toEqual( + visitedPaths.indexOf('oneOf.1') + ); + expect(visitedPaths.indexOf('anyOf.0.not') - 1).toEqual( + visitedPaths.indexOf('anyOf.0') + ); + expect(visitedPaths.indexOf('anyOf.1.not') - 1).toEqual( + visitedPaths.indexOf('anyOf.1') + ); + expect(visitedPaths.indexOf('not.allOf.0') - 1).toEqual( + visitedPaths.indexOf('not') + ); + }); + it('recorded paths are accurate for each schema', async () => { const schema = { allOf: [{ oneOf: [{ anyOf: [{ not: {} }] }] }], diff --git a/packages/utilities/test/validate-nested-schemas.test.js b/packages/utilities/test/validate-nested-schemas.test.js index d91be544..61c42304 100644 --- a/packages/utilities/test/validate-nested-schemas.test.js +++ b/packages/utilities/test/validate-nested-schemas.test.js @@ -228,6 +228,42 @@ describe('Utility function: validateNestedSchemas()', () => { expect(results.sort()).toEqual([1, 2, 3, 4, 5, 6]); }); + it("should validate a schema's nesting parent more recently than its parent's siblings", async () => { + const schema = { + items: { additionalProperties: {} }, + additionalProperties: { patternProperties: { '^baz$': {} } }, + patternProperties: { + '^foo$': { properties: { baz: {} } }, + '^bar$': { properties: { baz: {} } }, + }, + properties: { + a: {}, + one: { items: {} }, + z: {}, + }, + }; + + const visitedPaths = []; + + validateNestedSchemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return []; + }); + + expect(visitedPaths.indexOf('items.additionalProperties') - 1).toEqual( + visitedPaths.indexOf('items') + ); + expect( + visitedPaths.indexOf('additionalProperties.patternProperties.^baz$') - 1 + ).toEqual(visitedPaths.indexOf('additionalProperties')); + expect( + visitedPaths.indexOf('patternProperties.^foo$.properties.baz') - 1 + ).toEqual(visitedPaths.indexOf('patternProperties.^foo$')); + expect(visitedPaths.indexOf('properties.one.items') - 1).toEqual( + visitedPaths.indexOf('properties.one') + ); + }); + it('should validate through `allOf` schema', async () => { const schema = { allOf: [{ properties: { inside_all_of: {} } }], diff --git a/packages/utilities/test/validate-subschemas.test.js b/packages/utilities/test/validate-subschemas.test.js index 4c243364..70c4754a 100644 --- a/packages/utilities/test/validate-subschemas.test.js +++ b/packages/utilities/test/validate-subschemas.test.js @@ -135,4 +135,214 @@ describe('Utility: validateSubschemas', () => { ); expect(visitedLogicalPaths.filter(p => p === '').length).toEqual(6); }); + + it('should validate a schema before validating its composed or nested schemas', async () => { + const schema = { + items: { + allOf: [ + { + properties: { + one: { + oneOf: [ + { + additionalProperties: { + anyOf: [ + { + patternProperties: { + '^foo$': { + not: {}, + }, + }, + }, + ], + }, + }, + ], + }, + }, + }, + ], + }, + }; + + const visitedPaths = []; + + const results = validateSubschemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return []; + }); + + expect(visitedPaths).toEqual([ + "", + "items", + "items.allOf.0", + "items.allOf.0.properties.one", + "items.allOf.0.properties.one.oneOf.0", + "items.allOf.0.properties.one.oneOf.0.additionalProperties", + "items.allOf.0.properties.one.oneOf.0.additionalProperties.anyOf.0", + "items.allOf.0.properties.one.oneOf.0.additionalProperties.anyOf.0.patternProperties.^foo$", + "items.allOf.0.properties.one.oneOf.0.additionalProperties.anyOf.0.patternProperties.^foo$.not", + ]); + }); + + it("should validate a schema's composition parent more recently than its parent's siblings", async () => { + const schema = { + allOf: [{ oneOf: [{}] }, { oneOf: [{}] }], + oneOf: [{ anyOf: [{}] }, { anyOf: [{}] }], + anyOf: [{ not: {} }, { not: {} }], + not: { allOf: [{}] }, + }; + + const visitedPaths = []; + + validateSubschemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return []; + }); + + expect(visitedPaths.indexOf('allOf.0.oneOf.0') - 1).toEqual( + visitedPaths.indexOf('allOf.0') + ); + expect(visitedPaths.indexOf('allOf.1.oneOf.0') - 1).toEqual( + visitedPaths.indexOf('allOf.1') + ); + expect(visitedPaths.indexOf('oneOf.0.anyOf.0') - 1).toEqual( + visitedPaths.indexOf('oneOf.0') + ); + expect(visitedPaths.indexOf('oneOf.1.anyOf.0') - 1).toEqual( + visitedPaths.indexOf('oneOf.1') + ); + expect(visitedPaths.indexOf('anyOf.0.not') - 1).toEqual( + visitedPaths.indexOf('anyOf.0') + ); + expect(visitedPaths.indexOf('anyOf.1.not') - 1).toEqual( + visitedPaths.indexOf('anyOf.1') + ); + expect(visitedPaths.indexOf('not.allOf.0') - 1).toEqual( + visitedPaths.indexOf('not') + ); + }); + + it("should validate a schema's nesting parent more recently than its parent's siblings", async () => { + const schema = { + allOf: [ + { + items: { additionalProperties: {} }, + additionalProperties: { patternProperties: { '^baz$': {} } }, + }, + { + additionalProperties: { patternProperties: { '^baz$': {} } }, + patternProperties: { + '^foo$': { properties: { baz: {} } }, + '^bar$': { properties: { baz: {} } }, + }, + }, + ], + oneOf: [ + { + additionalProperties: { patternProperties: { '^baz$': {} } }, + patternProperties: { + '^foo$': { properties: { baz: {} } }, + '^bar$': { properties: { baz: {} } }, + }, + }, + { + patternProperties: { + '^foo$': { properties: { baz: {} } }, + '^bar$': { properties: { baz: {} } }, + }, + properties: { + a: {}, + one: { items: {} }, + z: {}, + }, + }, + ], + anyOf: [ + { + patternProperties: { + '^foo$': { properties: { baz: {} } }, + '^bar$': { properties: { baz: {} } }, + }, + properties: { + a: {}, + one: { items: {} }, + z: {}, + }, + }, + { + properties: { + a: {}, + one: { items: {} }, + z: {}, + }, + items: { additionalProperties: {} }, + }, + ], + not: { + properties: { + a: {}, + one: { items: {} }, + z: {}, + }, + items: { additionalProperties: {} }, + }, + }; + + const visitedPaths = []; + + validateSubschemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return []; + }); + + expect( + visitedPaths.indexOf('allOf.0.items.additionalProperties') - 1 + ).toEqual(visitedPaths.indexOf('allOf.0.items')); + expect( + visitedPaths.indexOf( + 'allOf.0.additionalProperties.patternProperties.^baz$' + ) - 1 + ).toEqual(visitedPaths.indexOf('allOf.0.additionalProperties')); + expect( + visitedPaths.indexOf( + 'allOf.1.additionalProperties.patternProperties.^baz$' + ) - 1 + ).toEqual(visitedPaths.indexOf('allOf.1.additionalProperties')); + expect( + visitedPaths.indexOf('allOf.1.patternProperties.^foo$.properties.baz') - 1 + ).toEqual(visitedPaths.indexOf('allOf.1.patternProperties.^foo$')); + expect( + visitedPaths.indexOf( + 'oneOf.0.additionalProperties.patternProperties.^baz$' + ) - 1 + ).toEqual(visitedPaths.indexOf('oneOf.0.additionalProperties')); + expect( + visitedPaths.indexOf('oneOf.0.patternProperties.^foo$.properties.baz') - 1 + ).toEqual(visitedPaths.indexOf('oneOf.0.patternProperties.^foo$')); + expect( + visitedPaths.indexOf('oneOf.1.patternProperties.^foo$.properties.baz') - 1 + ).toEqual(visitedPaths.indexOf('oneOf.1.patternProperties.^foo$')); + expect(visitedPaths.indexOf('oneOf.1.properties.one.items') - 1).toEqual( + visitedPaths.indexOf('oneOf.1.properties.one') + ); + expect( + visitedPaths.indexOf('anyOf.0.patternProperties.^foo$.properties.baz') - 1 + ).toEqual(visitedPaths.indexOf('anyOf.0.patternProperties.^foo$')); + expect(visitedPaths.indexOf('anyOf.0.properties.one.items') - 1).toEqual( + visitedPaths.indexOf('anyOf.0.properties.one') + ); + expect(visitedPaths.indexOf('anyOf.1.properties.one.items') - 1).toEqual( + visitedPaths.indexOf('anyOf.1.properties.one') + ); + expect( + visitedPaths.indexOf('anyOf.1.items.additionalProperties') - 1 + ).toEqual(visitedPaths.indexOf('anyOf.1.items')); + expect(visitedPaths.indexOf('not.properties.one.items') - 1).toEqual( + visitedPaths.indexOf('not.properties.one') + ); + expect(visitedPaths.indexOf('not.items.additionalProperties') - 1).toEqual( + visitedPaths.indexOf('not.items') + ); + }); }); diff --git a/validate-composed-schemas.test.js b/validate-composed-schemas.test.js new file mode 100644 index 00000000..0877a3d7 --- /dev/null +++ b/validate-composed-schemas.test.js @@ -0,0 +1,384 @@ +/** + * Copyright 2017 - 2023 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const { validateComposedSchemas } = require('../src'); +const SchemaPath = require('../src/utils/schema-path'); + +describe('Utility function: validateComposedSchemas()', () => { + it('should validate a simple schema by default', async () => { + const schema = {}; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }); + + expect(visitedPaths).toEqual(['']); + expect(results).toEqual([1]); + }); + + it('should not validate a simple schema if `includeSelf` is `false`', async () => { + const schema = {}; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas( + schema, + [], + (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }, + false + ); + + expect(visitedPaths).toEqual([]); + expect(results).toEqual([]); + }); + + it('should validate a composed schema even if `includeSelf` is `false`', async () => { + const schema = { + allOf: [{}], + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas( + schema, + [], + (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }, + false + ); + + expect(visitedPaths).toEqual(['allOf.0']); + expect(results).toEqual([1]); + }); + + it('should validate `allOf` schemas', async () => { + const schema = { + allOf: [{}, {}], + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }); + + expect(visitedPaths.sort()).toEqual(['', 'allOf.0', 'allOf.1']); + expect(results.sort()).toEqual([1, 2, 3]); + }); + + it('should validate `oneOf` schemas', async () => { + const schema = { + oneOf: [{}, {}], + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }); + + expect(visitedPaths.sort()).toEqual(['', 'oneOf.0', 'oneOf.1']); + expect(results.sort()).toEqual([1, 2, 3]); + }); + + it('should validate `anyOf` schemas', async () => { + const schema = { + anyOf: [{}, {}], + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }); + + expect(visitedPaths.sort()).toEqual(['', 'anyOf.0', 'anyOf.1']); + expect(results.sort()).toEqual([1, 2, 3]); + }); + + it('should validate `not` schema', async () => { + const schema = { + not: {}, + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }); + + expect(visitedPaths.sort()).toEqual(['', 'not']); + expect(results.sort()).toEqual([1, 2]); + }); + + it('should not validate `not` schema if `includeNot` is false', async () => { + const schema = { + not: {}, + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas( + schema, + [], + (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }, + true, + false + ); + + expect(visitedPaths).toEqual(['']); + expect(results).toEqual([1]); + }); + + it('should recurse through `allOf`, `oneOf`, `anyOf`, and `not`', async () => { + const schema = { + allOf: [{ oneOf: [{ anyOf: [{ not: {} }] }] }], + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }); + + expect(visitedPaths.sort()).toEqual([ + '', + 'allOf.0', + 'allOf.0.oneOf.0', + 'allOf.0.oneOf.0.anyOf.0', + 'allOf.0.oneOf.0.anyOf.0.not', + ]); + expect(results.sort()).toEqual([1, 2, 3, 4, 5]); + }); + + it('should validate a schema before validating its composed schemas', async () => { + const schema = { + allOf: [{ oneOf: [{ anyOf: [{ not: {} }] }] }], + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }); + + expect(visitedPaths).toEqual([ + '', + 'allOf.0', + 'allOf.0.oneOf.0', + 'allOf.0.oneOf.0.anyOf.0', + 'allOf.0.oneOf.0.anyOf.0.not', + ]); + expect(results).toEqual([1, 2, 3, 4, 5]); + }); + + it("should validate a schema's composition parent more recently than its parent's siblings", async () => { + const schema = { + allOf: [{ oneOf: [{}]}], + oneOf: [{ anyOf: [{}]}], + anyOf: [{ not: {}}], + not: { allOf: [{}]}, + }; + + const visitedPaths = []; + + validateComposedSchemas(schema, [], (s, p) => { + visitedPaths.push(p.join('.')); + return []; + }); + + expect( + visitedPaths.indexOf('allOf.0.oneOf.0') - 1 + ).toEqual( + visitedPaths.indexOf('allOf.0') + ); + expect( + visitedPaths.indexOf('oneOf.0.anyOf.0') - 1 + ).toEqual( + visitedPaths.indexOf('oneOf.0') + ); + expect( + visitedPaths.indexOf('anyOf.0.not') - 1 + ).toEqual( + visitedPaths.indexOf('anyOf.0') + ); + expect( + visitedPaths.indexOf('not.allOf.0') - 1 + ).toEqual( + visitedPaths.indexOf('not') + ); + }); + + it('recorded paths are accurate for each schema', async () => { + const schema = { + allOf: [{ oneOf: [{ anyOf: [{ not: {} }] }] }], + }; + + function getObjectByPath(object, path) { + while (path.length) { + object = object[path.shift()]; + } + + return object; + } + + validateComposedSchemas(schema, [], (s, p) => { + expect(s).toBe(getObjectByPath(schema, [...p])); + + return []; + }); + }); + + it('should not validate `properties` schemas', async () => { + const schema = { + properties: { + one: {}, + two: {}, + three: {}, + }, + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas( + schema, + [], + (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }, + true, + false + ); + + expect(visitedPaths).toEqual(['']); + expect(results).toEqual([1]); + }); + + it('should not validate `additionalProperties` schema', async () => { + const schema = { + additionalProperties: {}, + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas( + schema, + [], + (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }, + true, + false + ); + + expect(visitedPaths).toEqual(['']); + expect(results).toEqual([1]); + }); + + it('should not validate `items` schema', async () => { + const schema = { + items: {}, + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas( + schema, + [], + (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }, + true, + false + ); + + expect(visitedPaths).toEqual(['']); + expect(results).toEqual([1]); + }); + + it('should skip schemas defined by a $ref', async () => { + const schema = { + allOf: [ + { + $ref: '#/components/schemas/SomeSchema', + }, + {}, + ], + }; + + const visitedPaths = []; + let index = 1; + + const results = validateComposedSchemas( + schema, + [], + (s, p) => { + visitedPaths.push(p.join('.')); + return [index++]; + }, + true, + false + ); + + expect(visitedPaths.sort()).toEqual(['', 'allOf.1']); + expect(results.sort()).toEqual([1, 2]); + }); + + // internal-only functionality + it('should preserve the logical path with which it is called', async () => { + const schema = {}; + + const visitedPaths = []; + const visitedLogicalPaths = []; + let index = 1; + + const results = validateComposedSchemas( + schema, + new SchemaPath([], ['foo']), + (s, p, lp) => { + visitedPaths.push(p.join('.')); + visitedLogicalPaths.push(lp.join('.')); + return [index++]; + } + ); + + expect(visitedPaths).toEqual(['']); + expect(visitedLogicalPaths).toEqual(['foo']); + expect(results).toEqual([1]); + }); +});