Skip to content

Commit

Permalink
feat: add collectFromComposedSchemas() and getExamplesForSchema()
Browse files Browse the repository at this point in the history
… to utilities (#708)

Signed-off-by: Dan Hudlow <[email protected]>
  • Loading branch information
hudlow authored Dec 23, 2024
1 parent 147dc0c commit bfbee14
Show file tree
Hide file tree
Showing 7 changed files with 474 additions and 26 deletions.
30 changes: 28 additions & 2 deletions docs/openapi-ruleset-utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,41 @@ to a specific language type or data structure in an SDK).

## Functions

### `collectFromComposedSchemas(schema, collector, includeSelf, includeNot)`

Returns an array of items collected by the provided `collector(schema) => item[]` function for a
simple or composite schema, and deduplicates primitives in the result. The collector function is
not run for `null` or `undefined` schemas.

#### Parameters

- **`schema`** `<object>`: simple or composite OpenAPI 3.x schema object
- **`collector`** `<function>`: a `(schema) => item[]` function to collect items from each simple schema
- **`includeSelf`** `<boolean>`: collect from the provided schema in addition to its composed schemas (defaults to `true`)
- **`includeNot`** `<boolean>`: collect from schemas composed with `not` (defaults to `false`)

#### Returns `Array`: collected items

### `getExamplesForSchema(schema)`

Returns an array of examples for a simple or composite schema. For each composed schema, if
`schema.examples` is present (and an array), `schema.example` is ignored.

#### Parameters

- **`schema`** `<object>`: simple or composite OpenAPI 3.x schema object

#### Returns `Array`: examples

### `getPropertyNamesForSchema(schema, propertyFilter)`

Returns an array of property names for a simple or composite schema,
optionally filtered by a lambda function.

#### Parameters

- **`schema`** `<object>`: simple or composite OpenAPI 3.0 schema object
- **`propertyFilter`** `<function>`: a `(schema) => boolean` function to perform filtering
- **`schema`** `<object>`: simple or composite OpenAPI 3.x schema object
- **`propertyFilter`** `<function>`: a `(propertyName, propertySchema) => boolean` function to perform filtering

#### Returns `Array`: property names

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

/**
* Returns an array of items collected by the provided `collector(schema) => item[]` function for a
* simple or composite schema, and deduplicates primitives in the result. The collector function is
* not run for `null` or `undefined` schemas.
* @param {object} schema simple or composite OpenAPI 3.x schema object
* @param {Function} collector a `(schema) => item[]` function to collect items from each simple schema
* @param {boolean} includeSelf collect from the provided schema in addition to its composed schemas (defaults to `true`)
* @param {boolean} includeNot collect from schemas composed with `not` (defaults to `false`)
* @returns {Array} collected items
*/
function collectFromComposedSchemas(
schema,
collector,
includeSelf = true,
includeNot = false
) {
const items = [];

if (schema === undefined || schema === null) {
return items;
}

if (includeSelf) {
items.push(...collector(schema));
}

if (includeNot) {
items.push(
...collectFromComposedSchemas(schema.not, collector, true, true)
);
}

for (const applicatorType of ['allOf', 'oneOf', 'anyOf']) {
if (Array.isArray(schema[applicatorType])) {
for (const applicatorSchema of schema[applicatorType]) {
items.push(
...collectFromComposedSchemas(
applicatorSchema,
collector,
true,
includeNot
)
);
}
}
}

return [...new Set(items)]; // de-duplicate
}

module.exports = collectFromComposedSchemas;
29 changes: 29 additions & 0 deletions packages/utilities/src/utils/get-examples-for-schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

/**
* @private
*/
const collectFromComposedSchemas = require('./collect-from-composed-schemas');

/**
* Returns an array of examples for a simple or composite schema. For each composed schema, if
* `schema.examples` is present (and an array), `schema.example` is ignored.
* @param {object} schema simple or composite OpenAPI 3.x schema object
* @returns {Array} examples
*/
function getExamplesForSchema(schema) {
return collectFromComposedSchemas(schema, s => {
if (Array.isArray(s.examples)) {
return s.examples;
} else if (s.example !== undefined) {
return [s.example];
}

return [];
});
}

module.exports = getExamplesForSchema;
40 changes: 16 additions & 24 deletions packages/utilities/src/utils/get-property-names-for-schema.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,38 @@
/**
* Copyright 2017 - 2023 IBM Corporation.
* Copyright 2017 - 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

/**
* @private
*/
const isObject = require('./is-object');
/**
* @private
*/
const collectFromComposedSchemas = require('./collect-from-composed-schemas');

/**
* Returns an array of property names for a simple or composite schema,
* optionally filtered by a lambda function.
* @param {object} schema simple or composite OpenAPI 3.0 schema object
* @param {Function} propertyFilter a `(schema) => boolean` function to perform filtering
* @param {object} schema simple or composite OpenAPI 3.x schema object
* @param {Function} propertyFilter a `(propertyName, propertySchema) => boolean` function to perform filtering
* @returns {Array} property names
*/
function getPropertyNamesForSchema(schema, propertyFilter = () => true) {
const propertyNames = [];

if (!isObject(schema)) {
return propertyNames;
}
return collectFromComposedSchemas(schema, s => {
const propertyNames = [];

if (isObject(schema.properties)) {
for (const propertyName of Object.keys(schema.properties)) {
if (propertyFilter(propertyName, schema.properties[propertyName])) {
propertyNames.push(propertyName);
if (isObject(s.properties)) {
for (const propertyName of Object.keys(s.properties)) {
if (propertyFilter(propertyName, s.properties[propertyName])) {
propertyNames.push(propertyName);
}
}
}
}

for (const applicatorType of ['allOf', 'oneOf', 'anyOf']) {
if (Array.isArray(schema[applicatorType])) {
for (const applicatorSchema of schema[applicatorType]) {
propertyNames.push(
...getPropertyNamesForSchema(applicatorSchema, propertyFilter)
);
}
}
}

return [...new Set(propertyNames)]; // de-duplicate
return propertyNames;
});
}

module.exports = getPropertyNamesForSchema;
2 changes: 2 additions & 0 deletions packages/utilities/src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/

module.exports = {
collectFromComposedSchemas: require('./collect-from-composed-schemas'),
getExamplesForSchema: require('./get-examples-for-schema'),
getPropertyNamesForSchema: require('./get-property-names-for-schema'),
...require('./get-schema-type'),
isObject: require('./is-object'),
Expand Down
189 changes: 189 additions & 0 deletions packages/utilities/test/collect-from-composed-schemas.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

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

describe('Utility function: collectFromComposedSchemas()', () => {
it('should return `[]` for `undefined` or `null`', async () => {
expect(collectFromComposedSchemas(undefined, () => ['foo'])).toEqual([]);
expect(collectFromComposedSchemas(null, () => ['foo'])).toEqual([]);
});

it('should not run collector for `undefined` or `null`', async () => {
expect(() =>
collectFromComposedSchemas(undefined, () => {
throw new Error();
})
).not.toThrow();
expect(() =>
collectFromComposedSchemas(null, () => {
throw new Error();
})
).not.toThrow();
});

it('should collect once from a simple schema', async () => {
const schemaFoo = { foo: Math.random() };
const collectedFrom = [];

collectFromComposedSchemas(schemaFoo, s => {
collectedFrom.push(s);

return [];
});

expect(collectedFrom.length).toEqual(1);
expect(collectedFrom[0]).toEqual(schemaFoo);
});

it('should collect from a composed schema', async () => {
const composedSchema = {
foo: Math.random(),
allOf: [
{
foo: Math.random(),
},
],
oneOf: [
{
foo: Math.random(),
},
],
anyOf: [
{
foo: Math.random(),
},
],
};

expect(
collectFromComposedSchemas(composedSchema, s => [s.foo]).sort()
).toEqual(
[
composedSchema.foo,
composedSchema.allOf[0].foo,
composedSchema.oneOf[0].foo,
composedSchema.anyOf[0].foo,
].sort()
);
});

it('should collect from a deeply composed schema', async () => {
const deeplyComposedSchema = {
foo: Math.random(),
allOf: [
{
foo: Math.random(),
oneOf: [
{
foo: Math.random(),
anyOf: [
{
foo: Math.random(),
},
],
},
],
},
],
};

expect(
collectFromComposedSchemas(deeplyComposedSchema, s => [s.foo]).sort()
).toEqual(
[
deeplyComposedSchema.foo,
deeplyComposedSchema.allOf[0].foo,
deeplyComposedSchema.allOf[0].oneOf[0].foo,
deeplyComposedSchema.allOf[0].oneOf[0].anyOf[0].foo,
].sort()
);
});

it('should de-duplicate primitive items collected in a composed schema', async () => {
const value = Math.random();

expect(
collectFromComposedSchemas(
{
foo: value,
allOf: [
{
foo: value + 0,
},
],
},
s => [s.foo]
)
).toEqual([value]);
});

it('should not deduplicate non-primitive examples for composed schema', async () => {
expect(
collectFromComposedSchemas(
{
foo: {},
allOf: [
{
foo: {},
},
],
},
s => [s.foo]
)
).toEqual([{}, {}]);
});

it('should not collect from self if includeSelf = false', async () => {
const composedSchema = {
foo: Math.random(),
allOf: [
{
foo: Math.random(),
},
],
};

expect(
collectFromComposedSchemas(composedSchema, s => [s.foo], false).sort()
).toEqual([composedSchema.allOf[0].foo].sort());
});

it('should not collect from `not` if includeNot = false', async () => {
const composedSchema = {
foo: Math.random(),
not: {
foo: Math.random(),
},
};

expect(
collectFromComposedSchemas(
composedSchema,
s => [s.foo],
true,
false
).sort()
).toEqual([composedSchema.foo].sort());
});

it('should collect from `not` if includeNot = true', async () => {
const composedSchema = {
foo: Math.random(),
not: {
foo: Math.random(),
},
};

expect(
collectFromComposedSchemas(
composedSchema,
s => [s.foo],
true,
true
).sort()
).toEqual([composedSchema.foo, composedSchema.not.foo].sort());
});
});
Loading

0 comments on commit bfbee14

Please sign in to comment.