Skip to content

Commit

Permalink
feat: track logical paths in recursive validation functions
Browse files Browse the repository at this point in the history
Signed-off-by: Dan Hudlow <[email protected]>
  • Loading branch information
hudlow committed Dec 23, 2024
1 parent 4265623 commit 4b724cd
Show file tree
Hide file tree
Showing 8 changed files with 658 additions and 151 deletions.
39 changes: 37 additions & 2 deletions docs/openapi-ruleset-utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,14 @@ composed by those schemas.
Composed schemas **do not** include nested schemas (`property`, `additionalProperties`,
`patternProperties`, and `items` schemas).

The provided validate function is called with two arguments:
- `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.

WARNING: It is only safe to use this function for a "resolved" schema — it cannot traverse `$ref`
references.

Expand Down Expand Up @@ -404,14 +412,26 @@ schemas.
Nested schemas included via `allOf`, `oneOf`, and `anyOf` are validated, but composed schemas
are not themselves validated. By default, nested schemas included via `not` are not validated.

The provided validate function is called with three arguments:
- `nestedSchema`: the nested schema
- `path`: the array of path segments to locate the nested schema within the resolved document
- `logicalPath`: the array of path segments to locate an instance of `nestedSchema` within an
instance of `schema` (the schema for which `validateNestedSchemas()` was originally called.)
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.

WARNING: It is only safe to use this function for a "resolved" schema — it cannot traverse `$ref`
references.

#### Parameters

- **`schema`** `<object>`: simple or composite OpenAPI 3.x schema object
- **`path`** `<Array>`: path array for the provided schema
- **`validate`** `<function>`: a `(schema, path) => errors` function to validate a simple schema
- **`validate`** `<function>`: a `(schema, path, logicalPath) => errors` function to validate a simple schema
- **`includeSelf`** `<boolean>`: validate the provided schema in addition to its nested schemas (defaults to `true`)
- **`includeNot`** `<boolean>`: validate schemas composed with `not` (defaults to `false`)

Expand All @@ -433,14 +453,29 @@ Subschemas include property schemas, 'additionalProperties', and 'patternPropert
(such as those in an 'allOf', 'anyOf' or 'oneOf' property), plus all subschemas
of those schemas.

The provided `validate()` function is called with three arguments:
- `subschema`: the composed or nested schema
- `path`: the array of path segments to locate the subschema within the resolved document
- `logicalPath`: the array of path segments to locate an instance of `subschema` within an
instance of `schema` (the schema for which `validateSubschemas()` was originally called.)
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 composed schemas
- 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 composed or nested schemas.

WARNING: It is only safe to use this function for a "resolved" schema — it cannot traverse `$ref`
references.

#### Parameters

- **`schema`** `<object>`: simple or composite OpenAPI 3.x schema object
- **`path`** `<Array>`: path array for the provided schema
- **`validate`** `<function>`: a `(schema, path) => errors` function to validate a simple schema
- **`validate`** `<function>`: a `(schema, path, logicalPath) => errors` function to validate a simple schema
- **`includeSelf`** `<boolean>`: validate the provided schema in addition to its subschemas (defaults to `true`)
- **`includeNot`** `<boolean>`: validate schemas composed with `not` (defaults to `true`)

Expand Down
52 changes: 52 additions & 0 deletions packages/utilities/src/utils/schema-path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

'use strict';

/**
* @private
*/
class SchemaPath extends Array {
constructor(physical, logical = []) {
super(...physical);
this.logical = Object.freeze([...logical]);
Object.freeze(this);
}

withProperty(name) {
return new SchemaPath(
[...this, 'properties', name],
[...this.logical, name]
);
}

withAdditionalProperty() {
return new SchemaPath(
[...this, 'additionalProperties'],
[...this.logical, '*']
);
}

withPatternProperty(pattern) {
return new SchemaPath(
[...this, 'patternProperties', pattern],
[...this.logical, '*']
);
}

withArrayItem() {
return new SchemaPath([...this, 'items'], [...this.logical, '[]']);
}

withApplicator(applicator, index) {
return new SchemaPath([...this, applicator, index], this.logical);
}

withNot() {
return new SchemaPath([...this, 'not'], this.logical);
}
}

module.exports = SchemaPath;
30 changes: 26 additions & 4 deletions packages/utilities/src/utils/validate-composed-schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
* SPDX-License-Identifier: Apache2.0
*/

/**
* @private
*/
const SchemaPath = require('./schema-path');

/**
* Performs validation on a schema and all of its composed schemas.
*
Expand All @@ -19,6 +24,14 @@
* Composed schemas **do not** include nested schemas (`property`, `additionalProperties`,
* `patternProperties`, and `items` schemas).
*
* The provided validate function is called with two arguments:
* - `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.
*
* WARNING: It is only safe to use this function for a "resolved" schema — it cannot traverse `$ref`
* references.
* @param {object} schema simple or composite OpenAPI 3.x schema object
Expand All @@ -30,7 +43,7 @@
*/
function validateComposedSchemas(
schema,
path,
p, // internally, we pass a SchemaPath to this, but the external contract is just an array
validate,
includeSelf = true,
includeNot = true
Expand All @@ -42,22 +55,31 @@ function validateComposedSchemas(
}

const errors = [];
const path = p instanceof SchemaPath ? p : new SchemaPath(p);

if (includeSelf) {
errors.push(...validate(schema, path));
// We intentionally do not use `path.logical` here because the documented external contract
// for `validateComposedSchemas()` is that the validate function is called with two arguments.
// Tracking the logical path is only useful when `validateComposedSchemas()` is embedded in
// another utility that can pass it an instance of `SchemaPath` (which is not exported).
errors.push(...validate(schema, [...path], p?.logical));
}

if (includeNot && schema.not) {
errors.push(
...validateComposedSchemas(schema.not, [...path, 'not'], validate)
...validateComposedSchemas(schema.not, path.withNot(), validate)
);
}

for (const applicatorType of ['allOf', 'oneOf', 'anyOf']) {
if (Array.isArray(schema[applicatorType])) {
schema[applicatorType].forEach((s, i) => {
errors.push(
...validateComposedSchemas(s, [...path, applicatorType, i], validate)
...validateComposedSchemas(
s,
path.withApplicator(applicatorType, i),
validate
)
);
});
}
Expand Down
37 changes: 27 additions & 10 deletions packages/utilities/src/utils/validate-nested-schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
* @private
*/
const isObject = require('./is-object');
/**
* @private
*/
const SchemaPath = require('./schema-path');

/**
* Performs validation on a schema and all of its nested schemas.
Expand All @@ -27,26 +31,38 @@ const isObject = require('./is-object');
* Nested schemas included via `allOf`, `oneOf`, and `anyOf` are validated, but composed schemas
* are not themselves validated. By default, nested schemas included via `not` are not validated.
*
* The provided validate function is called with three arguments:
* - `nestedSchema`: the nested schema
* - `path`: the array of path segments to locate the nested schema within the resolved document
* - `logicalPath`: the array of path segments to locate an instance of `nestedSchema` within an
* instance of `schema` (the schema for which `validateNestedSchemas()` was originally called.)
* 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.
*
* WARNING: It is only safe to use this function for a "resolved" schema — it cannot traverse `$ref`
* references.
* @param {object} schema simple or composite OpenAPI 3.x schema object
* @param {Array} path path array for the provided schema
* @param {Function} validate a `(schema, path) => errors` function to validate a simple schema
* @param {Function} validate a `(schema, path, logicalPath) => errors` function to validate a simple schema
* @param {boolean} includeSelf validate the provided schema in addition to its nested schemas (defaults to `true`)
* @param {boolean} includeNot validate schemas composed with `not` (defaults to `false`)
* @returns {Array} validation errors
*/
function validateNestedSchemas(
schema,
path,
p, // internally, we pass a SchemaPath to this, but the external contract is just an array
validate,
includeSelf = true,
includeNot = false
) {
// Make sure 'schema' is an object.
if (!isObject(schema)) {
throw new Error(
`the entity at location ${path.join('.')} must be a schema object`
`the entity at location ${p.join('.')} must be a schema object`
);
}

Expand All @@ -57,17 +73,18 @@ function validateNestedSchemas(
}

const errors = [];
const path = p instanceof SchemaPath ? p : new SchemaPath(p);

if (includeSelf) {
errors.push(...validate(schema, path));
errors.push(...validate(schema, [...path], [...path.logical]));
}

if (schema.properties) {
for (const property of Object.entries(schema.properties)) {
errors.push(
...validateNestedSchemas(
property[1],
[...path, 'properties', property[0]],
path.withProperty(property[0]),
validate,
true,
includeNot
Expand All @@ -80,7 +97,7 @@ function validateNestedSchemas(
errors.push(
...validateNestedSchemas(
schema.items,
[...path, 'items'],
path.withArrayItem(),
validate,
true,
includeNot
Expand All @@ -95,7 +112,7 @@ function validateNestedSchemas(
errors.push(
...validateNestedSchemas(
schema.additionalProperties,
[...path, 'additionalProperties'],
path.withAdditionalProperty(),
validate,
true,
includeNot
Expand All @@ -107,7 +124,7 @@ function validateNestedSchemas(
errors.push(
...validateNestedSchemas(
schema.not,
[...path, 'not'],
path.withNot(),
validate,
false,
includeNot
Expand All @@ -121,7 +138,7 @@ function validateNestedSchemas(
errors.push(
...validateNestedSchemas(
s,
[...path, applicatorType, i],
path.withApplicator(applicatorType, i),
validate,
false,
includeNot
Expand All @@ -139,7 +156,7 @@ function validateNestedSchemas(
errors.push(
...validateNestedSchemas(
entry[1],
[...path, 'patternProperties', entry[0]],
path.withPatternProperty(entry[0]),
validate,
true,
includeNot
Expand Down
30 changes: 28 additions & 2 deletions packages/utilities/src/utils/validate-subschemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
* SPDX-License-Identifier: Apache2.0
*/

/**
* @private
*/
const SchemaPath = require('./schema-path');
/**
* @private
*/
Expand All @@ -27,11 +31,26 @@ const validateNestedSchemas = require('./validate-nested-schemas');
* (such as those in an 'allOf', 'anyOf' or 'oneOf' property), plus all subschemas
* of those schemas.
*
* The provided `validate()` function is called with three arguments:
* - `subschema`: the composed or nested schema
* - `path`: the array of path segments to locate the subschema within the resolved document
* - `logicalPath`: the array of path segments to locate an instance of `subschema` within an
* instance of `schema` (the schema for which `validateSubschemas()` was originally called.)
* 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 composed schemas
* - 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 composed or nested schemas.
*
* WARNING: It is only safe to use this function for a "resolved" schema — it cannot traverse `$ref`
* references.
* @param {object} schema simple or composite OpenAPI 3.x schema object
* @param {Array} path path array for the provided schema
* @param {Function} validate a `(schema, path) => errors` function to validate a simple schema
* @param {Function} validate a `(schema, path, logicalPath) => errors` function to validate a simple schema
* @param {boolean} includeSelf validate the provided schema in addition to its subschemas (defaults to `true`)
* @param {boolean} includeNot validate schemas composed with `not` (defaults to `true`)
* @returns {Array} validation errors
Expand All @@ -46,7 +65,14 @@ function validateSubschemas(
return validateNestedSchemas(
schema,
path,
(s, p) => validateComposedSchemas(s, p, validate, true, includeNot),
(s, p, lp) =>
validateComposedSchemas(
s,
new SchemaPath(p, lp),
validate,
true,
includeNot
),
includeSelf,
includeNot
);
Expand Down
Loading

0 comments on commit 4b724cd

Please sign in to comment.