Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(functions): expand xor function and add or function #2765

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions docs/reference/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,34 @@ 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.)

<!-- title: functionOptions -->

| name | description | type | required? |
| ---------- | ----------------------- | ---------- | --------- |
| properties | the properties to check | `string[]` | yes |

<!-- title: example -->

```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.
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.)

<!-- title: functionOptions -->

Expand All @@ -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
Expand Down
243 changes: 243 additions & 0 deletions packages/functions/src/__tests__/or.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
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));
});
});
});
Loading