From b51741e60259d5d838f67ca19d18ff1fd96a1196 Mon Sep 17 00:00:00 2001 From: Clyde Cutting Date: Wed, 1 May 2024 18:05:20 -0500 Subject: [PATCH 1/6] Expand XOR function to any number of properties, add similar OR function --- docs/reference/functions.md | 29 ++- packages/functions/src/__tests__/or.test.ts | 240 +++++++++++++++++++ packages/functions/src/__tests__/xor.test.ts | 122 +++++++--- packages/functions/src/optionSchemas.ts | 28 ++- packages/functions/src/or.ts | 41 ++++ packages/functions/src/xor.ts | 28 ++- 6 files changed, 448 insertions(+), 40 deletions(-) create mode 100644 packages/functions/src/__tests__/or.test.ts create mode 100644 packages/functions/src/or.ts diff --git a/docs/reference/functions.md b/docs/reference/functions.md index 1620f4470..c36c2b5c0 100644 --- a/docs/reference/functions.md +++ b/docs/reference/functions.md @@ -245,9 +245,34 @@ unused-definition: reusableObjectsLocation: "#/definitions" ``` +## or + +Communicates that one or more of these properties is required to be defined. FunctionOptions must contain any non-zero number of properties, **or** will require that _at least_ one of them is defined. (For only one property specified, this is the same as the `defined` rule for that property.) + + + +| name | description | type | required? | +| ---------- | ----------------------- | ---------- | --------- | +| properties | the properties to check | `string[]` | yes | + + + +```yaml +schemas-descriptive-text-exists: + description: Defined schemas must have one or more of `title`, `summary` and/or `description` fields. + given: "$.components.schemas.*" + then: + function: or + functionOptions: + properties: + - title + - summary + - description +``` + ## xor -Communicate that one of these properties is required, and no more than one is allowed to be defined. +Communicates that one of these properties is required, and no more than one is allowed to be defined. FunctionOptions must contain any non-zero number of properties, **xor** will require that _exactly_ one of them is defined. (For only one property specified, this is the same as the `defined` rule for that property.) @@ -259,7 +284,7 @@ Communicate that one of these properties is required, and no more than one is al ```yaml components-examples-value-or-externalValue: - description: Examples should have either a `value` or `externalValue` field. + description: Examples should have either a `value` or `externalValue` field, but not both. given: "$.components.examples.*" then: function: xor diff --git a/packages/functions/src/__tests__/or.test.ts b/packages/functions/src/__tests__/or.test.ts new file mode 100644 index 000000000..4fdcb81fa --- /dev/null +++ b/packages/functions/src/__tests__/or.test.ts @@ -0,0 +1,240 @@ +import '@stoplight/spectral-test-utils/matchers'; + +import { RulesetValidationError } from '@stoplight/spectral-core'; +import testFunction from './__helpers__/tester'; +import or from '../or'; +import AggregateError = require('es-aggregate-error'); + +const runOr = testFunction.bind(null, or); + +describe('Core Functions / Or', () => { + it('given no properties, should return an error message', async () => { + expect( + await runOr( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['yada-yada', 'whatever'] }, + ), + ).toEqual([ + { + message: 'At least one of "yada-yada" or "whatever" must be defined', + path: [], + }, + ]); + }); + + it('given both properties, should return no error message', async () => { + expect( + await runOr( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['version', 'title'] }, + ), + ).toEqual([]); + }); + + it('given invalid input, should show no error message', async () => { + return expect(await runOr(null, { properties: ['version', 'title'] })).toEqual([]); + }); + + it('given only one of the properties, should return no error message', async () => { + expect( + await runOr( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['something', 'title'] }, + ), + ).toEqual([]); + }); + + it('given none of 1 property, should return an error message', async () => { + expect( + await runOr( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['yada-yada'] }, + ), + ).toEqual([ + { + message: 'At least one of "yada-yada" must be defined', + path: [], + }, + ]); + }); + + it('given only one of 1 property, should return no error message', async () => { + expect( + await runOr( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['title'] }, + ), + ).toEqual([]); + }); + + it('given one of 3 properties, should return no error message', async () => { + expect( + await runOr( + { + format: 'date', + }, + { properties: ['default', 'pattern', 'format'] }, + ), + ).toEqual([]); + }); + + it('given two of 3 properties, should return no error message', async () => { + expect( + await runOr( + { + default: '2024-05-01', + format: 'date', + }, + { properties: ['default', 'pattern', 'format'] }, + ), + ).toEqual([]); + }); + + it('given three of 3 properties, should return no error message', async () => { + expect( + await runOr( + { + default: '2024-05-01', + pattern: '\\d{4}-\\d{2}-\\d{2}', + format: 'date', + }, + { properties: ['default', 'pattern', 'format'] }, + ), + ).toEqual([]); + }); + + it('given multiple of 5 properties, should return no error message', async () => { + expect( + await runOr( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['version', 'title', 'termsOfService', 'bar', 'five'] }, + ), + ).toEqual([]); + }); + + it('given none of 5 properties, should return an error message', async () => { + expect( + await runOr( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['yada-yada', 'foo', 'bar', 'four', 'five'] }, + ), + ).toEqual([ + { + message: 'At least one of "yada-yada" or "foo" or "bar" or 2 other properties must be defined', + path: [], + }, + ]); + }); + + it('given only one of 4 properties, should return no error message', async () => { + expect( + await runOr( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['title', 'foo', 'bar', 'four'] }, + ), + ).toEqual([]); + }); + + describe('validation', () => { + it.each([{ properties: ['foo', 'bar'] }])('given valid %p options, should not throw', async opts => { + expect(await runOr([], opts)).toEqual([]); + }); + + it.each([{ properties: ['foo'] }])('given valid %p options, should not throw', async opts => { + expect(await runOr([], opts)).toEqual([]); + }); + + it.each([{ properties: ['foo', 'bar', 'three'] }])('given valid %p options, should not throw', async opts => { + expect(await runOr([], opts)).toEqual([]); + }); + + it.each<[unknown, RulesetValidationError[]]>([ + [ + null, + [ + new RulesetValidationError( + 'invalid-function-options', + '"or" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["default", "example"] }, { "properties": ["title", "summary", "description"] }, etc.', + ['rules', 'my-rule', 'then', 'functionOptions'], + ), + ], + ], + [ + 2, + [ + new RulesetValidationError( + 'invalid-function-options', + '"or" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["default", "example"] }, { "properties": ["title", "summary", "description"] }, etc.', + ['rules', 'my-rule', 'then', 'functionOptions'], + ), + ], + ], + [ + { properties: ['foo', 'bar'], foo: true }, + [ + new RulesetValidationError('invalid-function-options', '"or" function does not support "foo" option', [ + 'rules', + 'my-rule', + 'then', + 'functionOptions', + 'foo', + ]), + ], + ], + [ + { properties: ['foo', {}] }, + [ + new RulesetValidationError( + 'invalid-function-options', + '"or" requires one or more enumerated "properties", i.e. ["id"], ["default", "example"], ["title", "summary", "description"], etc.', + ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], + ), + ], + ], + [ + { properties: [] }, + [ + new RulesetValidationError( + 'invalid-function-options', + '"or" requires one or more enumerated "properties", i.e. ["id"], ["default", "example"], ["title", "summary", "description"], etc.', + ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], + ), + ], + ], + ])('given invalid %p options, should throw', async (opts, errors) => { + await expect(runOr({}, opts)).rejects.toThrowAggregateError(new AggregateError(errors)); + }); + }); +}); diff --git a/packages/functions/src/__tests__/xor.test.ts b/packages/functions/src/__tests__/xor.test.ts index 6ea29e233..be9cae584 100644 --- a/packages/functions/src/__tests__/xor.test.ts +++ b/packages/functions/src/__tests__/xor.test.ts @@ -20,7 +20,7 @@ describe('Core Functions / Xor', () => { ), ).toEqual([ { - message: '"yada-yada" and "whatever" must not be both defined or both undefined', + message: 'At least one of "yada-yada" or "whatever" must be defined', path: [], }, ]); @@ -38,13 +38,13 @@ describe('Core Functions / Xor', () => { ), ).toEqual([ { - message: '"version" and "title" must not be both defined or both undefined', + message: 'Just one of "version" and "title" must be defined', path: [], }, ]); }); - it('given invalid input, should should no error message', async () => { + it('given invalid input, should show no error message', async () => { return expect(await runXor(null, { properties: ['version', 'title'] })).toEqual([]); }); @@ -61,18 +61,106 @@ describe('Core Functions / Xor', () => { ).toEqual([]); }); + it('given none of 1 property, should return an error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['yada-yada'] }, + ), + ).toEqual([ + { + message: 'At least one of "yada-yada" must be defined', + path: [], + }, + ]); + }); + + it('given only one of 1 property, should return no error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['title'] }, + ), + ).toEqual([]); + }); + + it('given multiple of 5 properties, should return an error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['version', 'title', 'termsOfService', 'bar', 'five'] }, + ), + ).toEqual([ + { + message: 'Just one of "version" and "title" and "termsOfService" must be defined', + path: [], + }, + ]); + }); + + it('given none of 5 properties, should return an error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['yada-yada', 'foo', 'bar', 'four', 'five'] }, + ), + ).toEqual([ + { + message: 'At least one of "yada-yada" or "foo" or "bar" or 2 other properties must be defined', + path: [], + }, + ]); + }); + + it('given only one of 4 properties, should return no error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['title', 'foo', 'bar', 'four'] }, + ), + ).toEqual([]); + }); + describe('validation', () => { it.each([{ properties: ['foo', 'bar'] }])('given valid %p options, should not throw', async opts => { expect(await runXor([], opts)).toEqual([]); }); + it.each([{ properties: ['foo'] }])('given valid %p options, should not throw', async opts => { + expect(await runXor([], opts)).toEqual([]); + }); + + it.each([{ properties: ['foo', 'bar', 'three'] }])('given valid %p options, should not throw', async opts => { + expect(await runXor([], opts)).toEqual([]); + }); + it.each<[unknown, RulesetValidationError[]]>([ [ null, [ new RulesetValidationError( 'invalid-function-options', - '"xor" function has invalid options specified. Example valid options: { "properties": ["id", "name"] }, { "properties": ["country", "street"] }', + '"xor" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["country", "street"] }, { "properties": ["one", "two", "three"] }, etc.', ['rules', 'my-rule', 'then', 'functionOptions'], ), ], @@ -82,7 +170,7 @@ describe('Core Functions / Xor', () => { [ new RulesetValidationError( 'invalid-function-options', - '"xor" function has invalid options specified. Example valid options: { "properties": ["id", "name"] }, { "properties": ["country", "street"] }', + '"xor" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["country", "street"] }, { "properties": ["one", "two", "three"] }, etc.', ['rules', 'my-rule', 'then', 'functionOptions'], ), ], @@ -99,32 +187,12 @@ describe('Core Functions / Xor', () => { ]), ], ], - [ - { properties: ['foo', 'bar', 'baz'] }, - [ - new RulesetValidationError( - 'invalid-function-options', - '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]', - ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], - ), - ], - ], [ { properties: ['foo', {}] }, [ new RulesetValidationError( 'invalid-function-options', - '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]', - ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], - ), - ], - ], - [ - { properties: ['foo'] }, - [ - new RulesetValidationError( - 'invalid-function-options', - '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]', + '"xor" requires one or more enumerated "properties", i.e. ["id"], ["country", "street"], ["one", "two", "three"], etc.', ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], ), ], @@ -134,7 +202,7 @@ describe('Core Functions / Xor', () => { [ new RulesetValidationError( 'invalid-function-options', - '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]', + '"xor" requires one or more enumerated "properties", i.e. ["id"], ["country", "street"], ["one", "two", "three"], etc.', ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], ), ], diff --git a/packages/functions/src/optionSchemas.ts b/packages/functions/src/optionSchemas.ts index f9a93c15e..fe619fd01 100644 --- a/packages/functions/src/optionSchemas.ts +++ b/packages/functions/src/optionSchemas.ts @@ -96,6 +96,26 @@ export const optionSchemas: Record = { type: `"length" function has invalid options specified. Example valid options: { "min": 2 }, { "max": 5 }, { "min": 0, "max": 10 }`, }, }, + or: { + type: 'object', + properties: { + properties: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, // OR is valid with one item (then it is redundant with 'defined' function) + // maxItems: 2, // No maximum limit is necessary, OR is valid for any amount, just one must be defined + errorMessage: `"or" requires one or more enumerated "properties", i.e. ["id"], ["default", "example"], ["title", "summary", "description"], etc.`, + description: 'The properties to check.', + }, + }, + additionalProperties: false, + required: ['properties'], + errorMessage: { + type: `"or" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["default", "example"] }, { "properties": ["title", "summary", "description"] }, etc.`, + }, + }, pattern: { type: 'object', additionalProperties: false, @@ -206,16 +226,16 @@ export const optionSchemas: Record = { items: { type: 'string', }, - minItems: 2, - maxItems: 2, - errorMessage: `"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]`, + minItems: 1, // XOR is valid with one item (then it is redundant with 'defined' function) + // maxItems: 2, // No maximum limit is necessary, XOR is valid for any amount, just one must be defined + errorMessage: `"xor" requires one or more enumerated "properties", i.e. ["id"], ["country", "street"], ["one", "two", "three"], etc.`, description: 'The properties to check.', }, }, additionalProperties: false, required: ['properties'], errorMessage: { - type: `"xor" function has invalid options specified. Example valid options: { "properties": ["id", "name"] }, { "properties": ["country", "street"] }`, + type: `"xor" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["country", "street"] }, { "properties": ["one", "two", "three"] }, etc.`, }, }, }; diff --git a/packages/functions/src/or.ts b/packages/functions/src/or.ts new file mode 100644 index 000000000..afed84f66 --- /dev/null +++ b/packages/functions/src/or.ts @@ -0,0 +1,41 @@ +import { createRulesetFunction, IFunctionResult } from '@stoplight/spectral-core'; +import { optionSchemas } from './optionSchemas'; + +export type Options = { + /** test to verify if one (but not all) of the provided keys are present in object */ + properties: string[]; +}; + +export default createRulesetFunction, Options>( + { + input: { + type: 'object', + }, + options: optionSchemas.or, + }, + function or(targetVal, { properties }) { + if (properties.length == 0) return; + // There need be no maximum limit on number of properties + + const results: IFunctionResult[] = []; + + const intersection = Object.keys(targetVal).filter(value => -1 !== properties.indexOf(value)); + if (intersection.length == 0) { + if (properties.length > 4) { + // List first three properties and remaining count in error message + const shortprops = properties.slice(0, 3); + const count = String(properties.length - 3) + ' other properties must be defined'; + results.push({ + message: 'At least one of "' + shortprops.join('" or "') + '" or ' + count, + }); + } else { + // List all of one to four properties directly in error message + results.push({ + message: 'At least one of "' + properties.join('" or "') + '" must be defined', + }); + } + } + + return results; + }, +); diff --git a/packages/functions/src/xor.ts b/packages/functions/src/xor.ts index e9a122251..10683819a 100644 --- a/packages/functions/src/xor.ts +++ b/packages/functions/src/xor.ts @@ -1,6 +1,4 @@ import { createRulesetFunction, IFunctionResult } from '@stoplight/spectral-core'; -import { printValue } from '@stoplight/spectral-runtime'; - import { optionSchemas } from './optionSchemas'; export type Options = { @@ -16,16 +14,32 @@ export default createRulesetFunction, Options>( options: optionSchemas.xor, }, function xor(targetVal, { properties }) { - if (properties.length !== 2) return; + if (properties.length == 0) return; + // There need be no maximum limit on number of properties const results: IFunctionResult[] = []; const intersection = Object.keys(targetVal).filter(value => -1 !== properties.indexOf(value)); - if (intersection.length !== 1) { + if (intersection.length == 0) { + if (properties.length > 4) { + // List first three properties and remaining count in error message + const shortprops = properties.slice(0, 3); + const count = String(properties.length - 3) + ' other properties must be defined'; + results.push({ + message: 'At least one of "' + shortprops.join('" or "') + '" or ' + count, + }); + } else { + // List all of one to four properties directly in error message + results.push({ + message: 'At least one of "' + properties.join('" or "') + '" must be defined', + }); + } + } + + if (intersection.length > 1) { + // List all defined properties in error message results.push({ - message: `${printValue(properties[0])} and ${printValue( - properties[1], - )} must not be both defined or both undefined`, + message: 'Just one of "' + intersection.join('" and "') + '" must be defined', }); } From 443ffbdfbb85bdb3935ba66833574120c0bb26c4 Mon Sep 17 00:00:00 2001 From: Clyde Cutting Date: Wed, 1 May 2024 19:13:14 -0500 Subject: [PATCH 2/6] Use original language: Communicate --- docs/reference/functions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/functions.md b/docs/reference/functions.md index c36c2b5c0..d9d655aba 100644 --- a/docs/reference/functions.md +++ b/docs/reference/functions.md @@ -247,7 +247,7 @@ unused-definition: ## or -Communicates that one or more of these properties is required to be defined. FunctionOptions must contain any non-zero number of properties, **or** will require that _at least_ one of them is defined. (For only one property specified, this is the same as the `defined` rule for that property.) +Communicate that one or more of these properties is required to be defined. FunctionOptions must contain any non-zero number of properties, **or** will require that _at least_ one of them is defined. (For only one property specified, this is the same as the `defined` rule for that property.) @@ -272,7 +272,7 @@ schemas-descriptive-text-exists: ## xor -Communicates that one of these properties is required, and no more than one is allowed to be defined. FunctionOptions must contain any non-zero number of properties, **xor** will require that _exactly_ one of them is defined. (For only one property specified, this is the same as the `defined` rule for that property.) +Communicate that one of these properties is required, and no more than one is allowed to be defined. FunctionOptions must contain any non-zero number of properties, **xor** will require that _exactly_ one of them is defined. (For only one property specified, this is the same as the `defined` rule for that property.) From 56e06bf43295ed8a8b18803f7774ce55a3bcef46 Mon Sep 17 00:00:00 2001 From: Clyde Cutting Date: Thu, 2 May 2024 13:58:01 -0500 Subject: [PATCH 3/6] Add unmentioned property to tests --- packages/functions/src/__tests__/or.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/functions/src/__tests__/or.test.ts b/packages/functions/src/__tests__/or.test.ts index 4fdcb81fa..bf576f7ba 100644 --- a/packages/functions/src/__tests__/or.test.ts +++ b/packages/functions/src/__tests__/or.test.ts @@ -91,6 +91,7 @@ describe('Core Functions / Or', () => { expect( await runOr( { + type: 'string', format: 'date', }, { properties: ['default', 'pattern', 'format'] }, @@ -102,6 +103,7 @@ describe('Core Functions / Or', () => { expect( await runOr( { + type: 'string', default: '2024-05-01', format: 'date', }, @@ -114,6 +116,7 @@ describe('Core Functions / Or', () => { expect( await runOr( { + type: 'string', default: '2024-05-01', pattern: '\\d{4}-\\d{2}-\\d{2}', format: 'date', From 61945dc7fe5169b92e957a6cd5937b378d902f5d Mon Sep 17 00:00:00 2001 From: Clyde Cutting Date: Sat, 4 May 2024 17:28:52 -0500 Subject: [PATCH 4/6] Replace new function with (default true) option on function --- docs/reference/functions.md | 37 +-- packages/functions/src/__tests__/or.test.ts | 243 ---------------- packages/functions/src/__tests__/xor.test.ts | 286 ++++++++++++++++++- packages/functions/src/optionSchemas.ts | 29 +- packages/functions/src/or.ts | 41 --- packages/functions/src/xor.ts | 15 +- 6 files changed, 302 insertions(+), 349 deletions(-) delete mode 100644 packages/functions/src/__tests__/or.test.ts delete mode 100644 packages/functions/src/or.ts diff --git a/docs/reference/functions.md b/docs/reference/functions.md index d9d655aba..380267fb3 100644 --- a/docs/reference/functions.md +++ b/docs/reference/functions.md @@ -245,50 +245,27 @@ unused-definition: reusableObjectsLocation: "#/definitions" ``` -## or - -Communicate that one or more of these properties is required to be defined. FunctionOptions must contain any non-zero number of properties, **or** will require that _at least_ one of them is defined. (For only one property specified, this is the same as the `defined` rule for that property.) - - - -| name | description | type | required? | -| ---------- | ----------------------- | ---------- | --------- | -| properties | the properties to check | `string[]` | yes | - - - -```yaml -schemas-descriptive-text-exists: - description: Defined schemas must have one or more of `title`, `summary` and/or `description` fields. - given: "$.components.schemas.*" - then: - function: or - functionOptions: - properties: - - title - - summary - - description -``` - ## xor -Communicate that one of these properties is required, and no more than one is allowed to be defined. FunctionOptions must contain any non-zero number of properties, **xor** will require that _exactly_ one of them is defined. (For only one property specified, this is the same as the `defined` rule for that property.) +Communicate that one of these properties is required, and exactly one or more than one is allowed to be defined, depending on `exclusive` option. FunctionOptions must contain any non-zero number of properties, `exclusive: true` (the default) requires that _exactly_ one of them is defined, `exclusive: false` requires that _at least_ one but possibly more than one is defined. (For only one property specified, `exclusive` is not relevant and this is the same as the `defined` function for the single property.) -| name | description | type | required? | -| ---------- | ----------------------- | ---------- | --------- | -| properties | the properties to check | `string[]` | yes | +| name | description | type | required? | +| ---------- | ------------------------------------------------------------------------------------------- | ---------- | --------- | +| properties | the properties to check | `string[]` | yes | +| exclusive | `true` by default for `xor`; when not truthy multiple matches are allowed for `or` behavior | `boolean` | no | ```yaml components-examples-value-or-externalValue: - description: Examples should have either a `value` or `externalValue` field, but not both. + description: Examples should have either a `value` or `externalValue` field, but not both; unless `exclusive` is false given: "$.components.examples.*" then: function: xor functionOptions: + exclusive: false properties: - externalValue - value diff --git a/packages/functions/src/__tests__/or.test.ts b/packages/functions/src/__tests__/or.test.ts deleted file mode 100644 index bf576f7ba..000000000 --- a/packages/functions/src/__tests__/or.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -import '@stoplight/spectral-test-utils/matchers'; - -import { RulesetValidationError } from '@stoplight/spectral-core'; -import testFunction from './__helpers__/tester'; -import or from '../or'; -import AggregateError = require('es-aggregate-error'); - -const runOr = testFunction.bind(null, or); - -describe('Core Functions / Or', () => { - it('given no properties, should return an error message', async () => { - expect( - await runOr( - { - version: '1.0.0', - title: 'Swagger Petstore', - termsOfService: 'http://swagger.io/terms/', - }, - { properties: ['yada-yada', 'whatever'] }, - ), - ).toEqual([ - { - message: 'At least one of "yada-yada" or "whatever" must be defined', - path: [], - }, - ]); - }); - - it('given both properties, should return no error message', async () => { - expect( - await runOr( - { - version: '1.0.0', - title: 'Swagger Petstore', - termsOfService: 'http://swagger.io/terms/', - }, - { properties: ['version', 'title'] }, - ), - ).toEqual([]); - }); - - it('given invalid input, should show no error message', async () => { - return expect(await runOr(null, { properties: ['version', 'title'] })).toEqual([]); - }); - - it('given only one of the properties, should return no error message', async () => { - expect( - await runOr( - { - version: '1.0.0', - title: 'Swagger Petstore', - termsOfService: 'http://swagger.io/terms/', - }, - { properties: ['something', 'title'] }, - ), - ).toEqual([]); - }); - - it('given none of 1 property, should return an error message', async () => { - expect( - await runOr( - { - version: '1.0.0', - title: 'Swagger Petstore', - termsOfService: 'http://swagger.io/terms/', - }, - { properties: ['yada-yada'] }, - ), - ).toEqual([ - { - message: 'At least one of "yada-yada" must be defined', - path: [], - }, - ]); - }); - - it('given only one of 1 property, should return no error message', async () => { - expect( - await runOr( - { - version: '1.0.0', - title: 'Swagger Petstore', - termsOfService: 'http://swagger.io/terms/', - }, - { properties: ['title'] }, - ), - ).toEqual([]); - }); - - it('given one of 3 properties, should return no error message', async () => { - expect( - await runOr( - { - type: 'string', - format: 'date', - }, - { properties: ['default', 'pattern', 'format'] }, - ), - ).toEqual([]); - }); - - it('given two of 3 properties, should return no error message', async () => { - expect( - await runOr( - { - type: 'string', - default: '2024-05-01', - format: 'date', - }, - { properties: ['default', 'pattern', 'format'] }, - ), - ).toEqual([]); - }); - - it('given three of 3 properties, should return no error message', async () => { - expect( - await runOr( - { - type: 'string', - default: '2024-05-01', - pattern: '\\d{4}-\\d{2}-\\d{2}', - format: 'date', - }, - { properties: ['default', 'pattern', 'format'] }, - ), - ).toEqual([]); - }); - - it('given multiple of 5 properties, should return no error message', async () => { - expect( - await runOr( - { - version: '1.0.0', - title: 'Swagger Petstore', - termsOfService: 'http://swagger.io/terms/', - }, - { properties: ['version', 'title', 'termsOfService', 'bar', 'five'] }, - ), - ).toEqual([]); - }); - - it('given none of 5 properties, should return an error message', async () => { - expect( - await runOr( - { - version: '1.0.0', - title: 'Swagger Petstore', - termsOfService: 'http://swagger.io/terms/', - }, - { properties: ['yada-yada', 'foo', 'bar', 'four', 'five'] }, - ), - ).toEqual([ - { - message: 'At least one of "yada-yada" or "foo" or "bar" or 2 other properties must be defined', - path: [], - }, - ]); - }); - - it('given only one of 4 properties, should return no error message', async () => { - expect( - await runOr( - { - version: '1.0.0', - title: 'Swagger Petstore', - termsOfService: 'http://swagger.io/terms/', - }, - { properties: ['title', 'foo', 'bar', 'four'] }, - ), - ).toEqual([]); - }); - - describe('validation', () => { - it.each([{ properties: ['foo', 'bar'] }])('given valid %p options, should not throw', async opts => { - expect(await runOr([], opts)).toEqual([]); - }); - - it.each([{ properties: ['foo'] }])('given valid %p options, should not throw', async opts => { - expect(await runOr([], opts)).toEqual([]); - }); - - it.each([{ properties: ['foo', 'bar', 'three'] }])('given valid %p options, should not throw', async opts => { - expect(await runOr([], opts)).toEqual([]); - }); - - it.each<[unknown, RulesetValidationError[]]>([ - [ - null, - [ - new RulesetValidationError( - 'invalid-function-options', - '"or" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["default", "example"] }, { "properties": ["title", "summary", "description"] }, etc.', - ['rules', 'my-rule', 'then', 'functionOptions'], - ), - ], - ], - [ - 2, - [ - new RulesetValidationError( - 'invalid-function-options', - '"or" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["default", "example"] }, { "properties": ["title", "summary", "description"] }, etc.', - ['rules', 'my-rule', 'then', 'functionOptions'], - ), - ], - ], - [ - { properties: ['foo', 'bar'], foo: true }, - [ - new RulesetValidationError('invalid-function-options', '"or" function does not support "foo" option', [ - 'rules', - 'my-rule', - 'then', - 'functionOptions', - 'foo', - ]), - ], - ], - [ - { properties: ['foo', {}] }, - [ - new RulesetValidationError( - 'invalid-function-options', - '"or" requires one or more enumerated "properties", i.e. ["id"], ["default", "example"], ["title", "summary", "description"], etc.', - ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], - ), - ], - ], - [ - { properties: [] }, - [ - new RulesetValidationError( - 'invalid-function-options', - '"or" requires one or more enumerated "properties", i.e. ["id"], ["default", "example"], ["title", "summary", "description"], etc.', - ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], - ), - ], - ], - ])('given invalid %p options, should throw', async (opts, errors) => { - await expect(runOr({}, opts)).rejects.toThrowAggregateError(new AggregateError(errors)); - }); - }); -}); diff --git a/packages/functions/src/__tests__/xor.test.ts b/packages/functions/src/__tests__/xor.test.ts index c9bc5b6b6..167ad1c2a 100644 --- a/packages/functions/src/__tests__/xor.test.ts +++ b/packages/functions/src/__tests__/xor.test.ts @@ -20,7 +20,7 @@ describe('Core Functions / Xor', () => { ), ).toEqual([ { - message: 'At least one of "yada-yada" or "whatever" must be defined', + message: 'Exactly one of "yada-yada" or "whatever" must be defined', path: [], }, ]); @@ -38,7 +38,7 @@ describe('Core Functions / Xor', () => { ), ).toEqual([ { - message: 'At least one of "yada-yada" or "whatever" or "foo" must be defined', + message: 'Exactly one of "yada-yada" or "whatever" or "foo" must be defined', path: [], }, ]); @@ -91,7 +91,7 @@ describe('Core Functions / Xor', () => { ), ).toEqual([ { - message: 'At least one of "yada-yada" must be defined', + message: 'Exactly one of "yada-yada" must be defined', path: [], }, ]); @@ -140,7 +140,7 @@ describe('Core Functions / Xor', () => { ), ).toEqual([ { - message: 'At least one of "yada-yada" or "foo" or "bar" or 2 other properties must be defined', + message: 'Exactly one of "yada-yada" or "foo" or "bar" or 2 other properties must be defined', path: [], }, ]); @@ -159,7 +159,169 @@ describe('Core Functions / Xor', () => { ).toEqual([]); }); - describe('validation', () => { + it('given no properties, for non-exclusive or, should return an error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['yada-yada', 'whatever'], exclusive: false }, + ), + ).toEqual([ + { + message: 'At least one of "yada-yada" or "whatever" must be defined', + path: [], + }, + ]); + }); + + it('given both properties, for non-exclusive or, should return no error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['version', 'title'], exclusive: false }, + ), + ).toEqual([]); + }); + + it('given invalid input, for non-exclusive or, should show no error message', async () => { + return expect(await runXor(null, { properties: ['version', 'title'], exclusive: false })).toEqual([]); + }); + + it('given only one of the properties, for non-exclusive or, should return no error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['something', 'title'], exclusive: false }, + ), + ).toEqual([]); + }); + + it('given none of 1 property, for non-exclusive or, should return an error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['yada-yada'], exclusive: false }, + ), + ).toEqual([ + { + message: 'At least one of "yada-yada" must be defined', + path: [], + }, + ]); + }); + + it('given only one of 1 property, for non-exclusive or, should return no error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['title'], exclusive: false }, + ), + ).toEqual([]); + }); + + it('given one of 3 properties, for non-exclusive or, should return no error message', async () => { + expect( + await runXor( + { + type: 'string', + format: 'date', + }, + { properties: ['default', 'pattern', 'format'], exclusive: false }, + ), + ).toEqual([]); + }); + + it('given two of 3 properties, for non-exclusive or, should return no error message', async () => { + expect( + await runXor( + { + type: 'string', + default: '2024-05-01', + format: 'date', + }, + { properties: ['default', 'pattern', 'format'], exclusive: false }, + ), + ).toEqual([]); + }); + + it('given three of 3 properties, for non-exclusive or, should return no error message', async () => { + expect( + await runXor( + { + type: 'string', + default: '2024-05-01', + pattern: '\\d{4}-\\d{2}-\\d{2}', + format: 'date', + }, + { properties: ['default', 'pattern', 'format'], exclusive: false }, + ), + ).toEqual([]); + }); + + it('given multiple of 5 properties, for non-exclusive or, should return no error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['version', 'title', 'termsOfService', 'bar', 'five'], exclusive: false }, + ), + ).toEqual([]); + }); + + it('given none of 5 properties, for non-exclusive or, should return an error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['yada-yada', 'foo', 'bar', 'four', 'five'], exclusive: false }, + ), + ).toEqual([ + { + message: 'At least one of "yada-yada" or "foo" or "bar" or 2 other properties must be defined', + path: [], + }, + ]); + }); + + it('given only one of 4 properties, for non-exclusive or, should return no error message', async () => { + expect( + await runXor( + { + version: '1.0.0', + title: 'Swagger Petstore', + termsOfService: 'http://swagger.io/terms/', + }, + { properties: ['title', 'foo', 'bar', 'four'], exclusive: false }, + ), + ).toEqual([]); + }); + + describe('validation for exclusive xor', () => { it.each([{ properties: ['foo', 'bar'] }])('given valid %p options, should not throw', async opts => { expect(await runXor([], opts)).toEqual([]); }); @@ -178,7 +340,7 @@ describe('Core Functions / Xor', () => { [ new RulesetValidationError( 'invalid-function-options', - '"xor" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["country", "street"] }, { "properties": ["one", "two", "three"] }, etc.', + '"xor" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["value", "externalValue"], "exclusive": true }, { "properties": ["title", "summary", "description"], "exclusive": false }, etc.', ['rules', 'my-rule', 'then', 'functionOptions'], ), ], @@ -188,7 +350,7 @@ describe('Core Functions / Xor', () => { [ new RulesetValidationError( 'invalid-function-options', - '"xor" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["country", "street"] }, { "properties": ["one", "two", "three"] }, etc.', + '"xor" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["value", "externalValue"], "exclusive": true }, { "properties": ["title", "summary", "description"], "exclusive": false }, etc.', ['rules', 'my-rule', 'then', 'functionOptions'], ), ], @@ -210,7 +372,7 @@ describe('Core Functions / Xor', () => { [ new RulesetValidationError( 'invalid-function-options', - '"xor" requires one or more enumerated "properties", i.e. ["id"], ["country", "street"], ["one", "two", "three"], etc.', + '"xor" requires one or more enumerated "properties", i.e. ["id"], ["value", "externalValue"], ["title", "summary", "description"], etc.', ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], ), ], @@ -220,7 +382,113 @@ describe('Core Functions / Xor', () => { [ new RulesetValidationError( 'invalid-function-options', - '"xor" requires one or more enumerated "properties", i.e. ["id"], ["country", "street"], ["one", "two", "three"], etc.', + '"xor" requires one or more enumerated "properties", i.e. ["id"], ["value", "externalValue"], ["title", "summary", "description"], etc.', + ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], + ), + ], + ], + ])('given invalid %p options, should throw', async (opts, errors) => { + await expect(runXor({}, opts)).rejects.toThrowAggregateError(new AggregateError(errors)); + }); + }); + + describe('validation for non-exclusive or', () => { + it.each([{ properties: ['foo', 'bar'], exclusive: true }])('given valid %p options, should not throw', async opts => { + expect(await runXor([], opts)).toEqual([]); + }); + + it.each([{ properties: ['foo'], exclusive: false }])('given valid %p options, should not throw', async opts => { + expect(await runXor([], opts)).toEqual([]); + }); + + it.each([{ properties: ['foo', 'bar', 'three'], exclusive: false }])('given valid %p options, should not throw', async opts => { + expect(await runXor([], opts)).toEqual([]); + }); + + it.each<[unknown, RulesetValidationError[]]>([ + [ + null, + [ + new RulesetValidationError( + 'invalid-function-options', + '"xor" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["value", "externalValue"], "exclusive": true }, { "properties": ["title", "summary", "description"], "exclusive": false }, etc.', + ['rules', 'my-rule', 'then', 'functionOptions'], + ), + ], + ], + [ + 2, + [ + new RulesetValidationError( + 'invalid-function-options', + '"xor" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["value", "externalValue"], "exclusive": true }, { "properties": ["title", "summary", "description"], "exclusive": false }, etc.', + ['rules', 'my-rule', 'then', 'functionOptions'], + ), + ], + ], + [ + { exclusive: false }, + [ + new RulesetValidationError( + 'invalid-function-options', + '"xor" function is missing "properties" option', + ['rules', 'my-rule', 'then', 'functionOptions'], + ), + ], + ], + [ + { exclusive: "false" }, + [ + new RulesetValidationError( + 'invalid-function-options', + '"xor" function is missing "properties" option', + ['rules', 'my-rule', 'then', 'functionOptions'], + ), + new RulesetValidationError( + 'invalid-function-options', + '"xor" function and its "exclusive" option accepts only the following types: boolean', + ['rules', 'my-rule', 'then', 'functionOptions', 'exclusive'], + ), + ], + ], + [ + { properties: ['foo', 'bar'], exclusive: "false" }, + [ + new RulesetValidationError( + 'invalid-function-options', + '"xor" function and its "exclusive" option accepts only the following types: boolean', + ['rules', 'my-rule', 'then', 'functionOptions', 'exclusive'], + ), + ], + ], + [ + { properties: ['foo', 'bar'], foo: true, exclusive: false }, + [ + new RulesetValidationError('invalid-function-options', '"xor" function does not support "foo" option', [ + 'rules', + 'my-rule', + 'then', + 'functionOptions', + 'foo', + ]), + ], + ], + [ + { properties: ['foo', {}], exclusive: false }, + [ + new RulesetValidationError( + 'invalid-function-options', + '"xor" requires one or more enumerated "properties", i.e. ["id"], ["value", "externalValue"], ["title", "summary", "description"], etc.', + ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], + ), + ], + ], + [ + { properties: [], exclusive: false }, + [ + new RulesetValidationError( + 'invalid-function-options', + '"xor" requires one or more enumerated "properties", i.e. ["id"], ["value", "externalValue"], ["title", "summary", "description"], etc.', ['rules', 'my-rule', 'then', 'functionOptions', 'properties'], ), ], diff --git a/packages/functions/src/optionSchemas.ts b/packages/functions/src/optionSchemas.ts index fe619fd01..820a3215c 100644 --- a/packages/functions/src/optionSchemas.ts +++ b/packages/functions/src/optionSchemas.ts @@ -96,26 +96,6 @@ export const optionSchemas: Record = { type: `"length" function has invalid options specified. Example valid options: { "min": 2 }, { "max": 5 }, { "min": 0, "max": 10 }`, }, }, - or: { - type: 'object', - properties: { - properties: { - type: 'array', - items: { - type: 'string', - }, - minItems: 1, // OR is valid with one item (then it is redundant with 'defined' function) - // maxItems: 2, // No maximum limit is necessary, OR is valid for any amount, just one must be defined - errorMessage: `"or" requires one or more enumerated "properties", i.e. ["id"], ["default", "example"], ["title", "summary", "description"], etc.`, - description: 'The properties to check.', - }, - }, - additionalProperties: false, - required: ['properties'], - errorMessage: { - type: `"or" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["default", "example"] }, { "properties": ["title", "summary", "description"] }, etc.`, - }, - }, pattern: { type: 'object', additionalProperties: false, @@ -228,14 +208,19 @@ export const optionSchemas: Record = { }, minItems: 1, // XOR is valid with one item (then it is redundant with 'defined' function) // maxItems: 2, // No maximum limit is necessary, XOR is valid for any amount, just one must be defined - errorMessage: `"xor" requires one or more enumerated "properties", i.e. ["id"], ["country", "street"], ["one", "two", "three"], etc.`, + errorMessage: `"xor" requires one or more enumerated "properties", i.e. ["id"], ["value", "externalValue"], ["title", "summary", "description"], etc.`, description: 'The properties to check.', }, + exclusive: { + type: 'boolean', + default: true, + description: 'Defaults to true. If false, multiple matches are allowed.', + }, }, additionalProperties: false, required: ['properties'], errorMessage: { - type: `"xor" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["country", "street"] }, { "properties": ["one", "two", "three"] }, etc.`, + type: `"xor" function has invalid options specified. Example valid options: { "properties": ["id"] }, { "properties": ["value", "externalValue"], "exclusive": true }, { "properties": ["title", "summary", "description"], "exclusive": false }, etc.`, }, }, }; diff --git a/packages/functions/src/or.ts b/packages/functions/src/or.ts deleted file mode 100644 index afed84f66..000000000 --- a/packages/functions/src/or.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { createRulesetFunction, IFunctionResult } from '@stoplight/spectral-core'; -import { optionSchemas } from './optionSchemas'; - -export type Options = { - /** test to verify if one (but not all) of the provided keys are present in object */ - properties: string[]; -}; - -export default createRulesetFunction, Options>( - { - input: { - type: 'object', - }, - options: optionSchemas.or, - }, - function or(targetVal, { properties }) { - if (properties.length == 0) return; - // There need be no maximum limit on number of properties - - const results: IFunctionResult[] = []; - - const intersection = Object.keys(targetVal).filter(value => -1 !== properties.indexOf(value)); - if (intersection.length == 0) { - if (properties.length > 4) { - // List first three properties and remaining count in error message - const shortprops = properties.slice(0, 3); - const count = String(properties.length - 3) + ' other properties must be defined'; - results.push({ - message: 'At least one of "' + shortprops.join('" or "') + '" or ' + count, - }); - } else { - // List all of one to four properties directly in error message - results.push({ - message: 'At least one of "' + properties.join('" or "') + '" must be defined', - }); - } - } - - return results; - }, -); diff --git a/packages/functions/src/xor.ts b/packages/functions/src/xor.ts index 10683819a..6297f7bf9 100644 --- a/packages/functions/src/xor.ts +++ b/packages/functions/src/xor.ts @@ -4,6 +4,7 @@ import { optionSchemas } from './optionSchemas'; export type Options = { /** test to verify if one (but not all) of the provided keys are present in object */ properties: string[]; + exclusive: boolean; }; export default createRulesetFunction, Options>( @@ -13,30 +14,36 @@ export default createRulesetFunction, Options>( }, options: optionSchemas.xor, }, - function xor(targetVal, { properties }) { + function xor(targetVal, opts: Options) { + const properties = opts.properties; if (properties.length == 0) return; // There need be no maximum limit on number of properties const results: IFunctionResult[] = []; const intersection = Object.keys(targetVal).filter(value => -1 !== properties.indexOf(value)); + const exclusive = (opts.exclusive == null) || opts.exclusive; + const exactlyOrAtLeast = exclusive ? "Exactly" : "At least"; + + // One-must-be-defined validation of both xor and or (non-exclusive) functions if (intersection.length == 0) { if (properties.length > 4) { // List first three properties and remaining count in error message const shortprops = properties.slice(0, 3); const count = String(properties.length - 3) + ' other properties must be defined'; results.push({ - message: 'At least one of "' + shortprops.join('" or "') + '" or ' + count, + message: exactlyOrAtLeast + ' one of "' + shortprops.join('" or "') + '" or ' + count, }); } else { // List all of one to four properties directly in error message results.push({ - message: 'At least one of "' + properties.join('" or "') + '" must be defined', + message: exactlyOrAtLeast + ' one of "' + properties.join('" or "') + '" must be defined', }); } } - if (intersection.length > 1) { + // Maximum-one-defined validation of xor function only + if (exclusive && intersection.length > 1) { // List all defined properties in error message results.push({ message: 'Just one of "' + intersection.join('" and "') + '" must be defined', From d8eec32c6ebb6d9c2640332e7f8e3480cb254ed8 Mon Sep 17 00:00:00 2001 From: Clyde Cutting Date: Mon, 20 May 2024 11:43:11 -0500 Subject: [PATCH 5/6] Update packages/functions/src/xor.ts Define exclusive as optional. Co-authored-by: Brenda Rearden --- packages/functions/src/xor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/functions/src/xor.ts b/packages/functions/src/xor.ts index 6297f7bf9..109defa62 100644 --- a/packages/functions/src/xor.ts +++ b/packages/functions/src/xor.ts @@ -4,7 +4,7 @@ import { optionSchemas } from './optionSchemas'; export type Options = { /** test to verify if one (but not all) of the provided keys are present in object */ properties: string[]; - exclusive: boolean; + exclusive?: boolean; }; export default createRulesetFunction, Options>( From d55f31d0b4dc57dc89420698860091c14ae5766a Mon Sep 17 00:00:00 2001 From: Clyde Cutting Date: Mon, 20 May 2024 11:44:49 -0500 Subject: [PATCH 6/6] Update packages/functions/src/xor.ts Safely handle any type of exclusive argument, not just boolean or null values. Co-authored-by: Brenda Rearden --- packages/functions/src/xor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/functions/src/xor.ts b/packages/functions/src/xor.ts index 109defa62..ac9b2cb87 100644 --- a/packages/functions/src/xor.ts +++ b/packages/functions/src/xor.ts @@ -22,7 +22,7 @@ export default createRulesetFunction, Options>( const results: IFunctionResult[] = []; const intersection = Object.keys(targetVal).filter(value => -1 !== properties.indexOf(value)); - const exclusive = (opts.exclusive == null) || opts.exclusive; + const exclusive = (typeof opts.exclusive === 'boolean') ? opts.exclusive : true; const exactlyOrAtLeast = exclusive ? "Exactly" : "At least"; // One-must-be-defined validation of both xor and or (non-exclusive) functions