Skip to content

Commit d9ef27f

Browse files
feat(functions): add or function (#2798)
* feat(functions): add or function * fix(functions): update xor tests --------- Co-authored-by: cuttingclyde <GitHub-cuttingus@sneakemail.com>
1 parent a56dd24 commit d9ef27f

File tree

6 files changed

+382
-26
lines changed

6 files changed

+382
-26
lines changed

docs/reference/functions.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,34 @@ unused-definition:
245245
reusableObjectsLocation: "#/definitions"
246246
```
247247

248+
## or
249+
250+
Communicate that one or more of these properties is required to be defined. `functionOptions` must contain at least two properties. **or** will require that _at least_ one of them is defined.
251+
252+
<!-- title: functionOptions -->
253+
254+
| name | description | type | required? |
255+
| ---------- | ----------------------- | ---------- | --------- |
256+
| properties | the properties to check | `string[]` | yes |
257+
258+
<!-- title: example -->
259+
260+
```yaml
261+
schemas-descriptive-text-exists:
262+
description: Defined schemas must have one or more of `title`, `summary` and/or `description` fields.
263+
given: "$.components.schemas.*"
264+
then:
265+
function: or
266+
functionOptions:
267+
properties:
268+
- title
269+
- summary
270+
- description
271+
```
272+
248273
## xor
249274
250-
Communicate that one of these properties is required, and no more than one is allowed to be defined.
275+
Communicate that one of these properties is required, and no more than one is allowed to be defined. `functionOptions` must contain at least two properties. **xor** will require that _exactly_ one of them is defined.
251276

252277
<!-- title: functionOptions -->
253278

@@ -259,7 +284,7 @@ Communicate that one of these properties is required, and no more than one is al
259284

260285
```yaml
261286
components-examples-value-or-externalValue:
262-
description: Examples should have either a `value` or `externalValue` field.
287+
description: Examples should have either a `value` or `externalValue` field, but not both.
263288
given: "$.components.examples.*"
264289
then:
265290
function: xor
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import '@stoplight/spectral-test-utils/matchers';
2+
3+
import { RulesetValidationError } from '@stoplight/spectral-core';
4+
import testFunction from './__helpers__/tester';
5+
import or from '../or';
6+
import AggregateError = require('es-aggregate-error');
7+
8+
const runOr = testFunction.bind(null, or);
9+
10+
describe('Core Functions / Or', () => {
11+
it('given no properties, should return an error message', async () => {
12+
expect(
13+
await runOr(
14+
{
15+
version: '1.0.0',
16+
title: 'Swagger Petstore',
17+
termsOfService: 'http://swagger.io/terms/',
18+
},
19+
{ properties: ['yada-yada', 'whatever'] },
20+
),
21+
).toEqual([
22+
{
23+
message: 'At least one of "yada-yada" or "whatever" must be defined',
24+
path: [],
25+
},
26+
]);
27+
});
28+
29+
it('given both properties, should return no error message', async () => {
30+
expect(
31+
await runOr(
32+
{
33+
version: '1.0.0',
34+
title: 'Swagger Petstore',
35+
termsOfService: 'http://swagger.io/terms/',
36+
},
37+
{ properties: ['version', 'title'] },
38+
),
39+
).toEqual([]);
40+
});
41+
42+
it('given invalid input, should show no error message', async () => {
43+
return expect(await runOr(null, { properties: ['version', 'title'] })).toEqual([]);
44+
});
45+
46+
it('given only one of the properties, should return no error message', async () => {
47+
expect(
48+
await runOr(
49+
{
50+
version: '1.0.0',
51+
title: 'Swagger Petstore',
52+
termsOfService: 'http://swagger.io/terms/',
53+
},
54+
{ properties: ['something', 'title'] },
55+
),
56+
).toEqual([]);
57+
});
58+
59+
it('given one of 3 properties, should return no error message', async () => {
60+
expect(
61+
await runOr(
62+
{
63+
type: 'string',
64+
format: 'date',
65+
},
66+
{ properties: ['default', 'pattern', 'format'] },
67+
),
68+
).toEqual([]);
69+
});
70+
71+
it('given two of 3 properties, should return no error message', async () => {
72+
expect(
73+
await runOr(
74+
{
75+
type: 'string',
76+
default: '2024-05-01',
77+
format: 'date',
78+
},
79+
{ properties: ['default', 'pattern', 'format'] },
80+
),
81+
).toEqual([]);
82+
});
83+
84+
it('given three of 3 properties, should return no error message', async () => {
85+
expect(
86+
await runOr(
87+
{
88+
type: 'string',
89+
default: '2024-05-01',
90+
pattern: '\\d{4}-\\d{2}-\\d{2}',
91+
format: 'date',
92+
},
93+
{ properties: ['default', 'pattern', 'format'] },
94+
),
95+
).toEqual([]);
96+
});
97+
98+
it('given multiple of 5 properties, should return no error message', async () => {
99+
expect(
100+
await runOr(
101+
{
102+
version: '1.0.0',
103+
title: 'Swagger Petstore',
104+
termsOfService: 'http://swagger.io/terms/',
105+
},
106+
{ properties: ['version', 'title', 'termsOfService', 'bar', 'five'] },
107+
),
108+
).toEqual([]);
109+
});
110+
111+
it('given none of 5 properties, should return an error message', async () => {
112+
expect(
113+
await runOr(
114+
{
115+
version: '1.0.0',
116+
title: 'Swagger Petstore',
117+
termsOfService: 'http://swagger.io/terms/',
118+
},
119+
{ properties: ['yada-yada', 'foo', 'bar', 'four', 'five'] },
120+
),
121+
).toEqual([
122+
{
123+
message: 'At least one of "yada-yada" or "foo" or "bar" or 2 other properties must be defined',
124+
path: [],
125+
},
126+
]);
127+
});
128+
129+
it('given only one of 4 properties, should return no error message', async () => {
130+
expect(
131+
await runOr(
132+
{
133+
version: '1.0.0',
134+
title: 'Swagger Petstore',
135+
termsOfService: 'http://swagger.io/terms/',
136+
},
137+
{ properties: ['title', 'foo', 'bar', 'four'] },
138+
),
139+
).toEqual([]);
140+
});
141+
142+
describe('validation', () => {
143+
it.each([{ properties: ['foo', 'bar'] }])('given valid %p options, should not throw', async opts => {
144+
expect(await runOr([], opts)).toEqual([]);
145+
});
146+
147+
it.each([{ properties: ['foo', 'bar', 'three'] }])('given valid %p options, should not throw', async opts => {
148+
expect(await runOr([], opts)).toEqual([]);
149+
});
150+
151+
it.each<[unknown, RulesetValidationError[]]>([
152+
[
153+
null,
154+
[
155+
new RulesetValidationError(
156+
'invalid-function-options',
157+
'"or" function has invalid options specified. Example valid options: { "properties": ["default", "example"] }, { "properties": ["title", "summary", "description"] }, etc.',
158+
['rules', 'my-rule', 'then', 'functionOptions'],
159+
),
160+
],
161+
],
162+
[
163+
2,
164+
[
165+
new RulesetValidationError(
166+
'invalid-function-options',
167+
'"or" function has invalid options specified. Example valid options: { "properties": ["default", "example"] }, { "properties": ["title", "summary", "description"] }, etc.',
168+
['rules', 'my-rule', 'then', 'functionOptions'],
169+
),
170+
],
171+
],
172+
[
173+
{ properties: ['foo', 'bar'], foo: true },
174+
[
175+
new RulesetValidationError('invalid-function-options', '"or" function does not support "foo" option', [
176+
'rules',
177+
'my-rule',
178+
'then',
179+
'functionOptions',
180+
'foo',
181+
]),
182+
],
183+
],
184+
[
185+
{ properties: ['foo', {}] },
186+
[
187+
new RulesetValidationError(
188+
'invalid-function-options',
189+
'"or" requires at least two enumerated "properties", i.e. ["default", "example"], ["title", "summary", "description"], etc.',
190+
['rules', 'my-rule', 'then', 'functionOptions', 'properties'],
191+
),
192+
],
193+
],
194+
[
195+
{ properties: [] },
196+
[
197+
new RulesetValidationError(
198+
'invalid-function-options',
199+
'"or" requires at least two enumerated "properties", i.e. ["default", "example"], ["title", "summary", "description"], etc.',
200+
['rules', 'my-rule', 'then', 'functionOptions', 'properties'],
201+
),
202+
],
203+
],
204+
])('given invalid %p options, should throw', async (opts, errors) => {
205+
await expect(runOr({}, opts)).rejects.toThrowAggregateError(new AggregateError(errors));
206+
});
207+
});
208+
});

packages/functions/src/__tests__/xor.test.ts

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('Core Functions / Xor', () => {
2020
),
2121
).toEqual([
2222
{
23-
message: '"yada-yada" and "whatever" must not be both defined or both undefined',
23+
message: 'At least one of "yada-yada" or "whatever" must be defined',
2424
path: [],
2525
},
2626
]);
@@ -38,7 +38,7 @@ describe('Core Functions / Xor', () => {
3838
),
3939
).toEqual([
4040
{
41-
message: '"yada-yada", "whatever" and "foo" must not be both defined or both undefined',
41+
message: 'At least one of "yada-yada" or "whatever" or "foo" must be defined',
4242
path: [],
4343
},
4444
]);
@@ -56,13 +56,13 @@ describe('Core Functions / Xor', () => {
5656
),
5757
).toEqual([
5858
{
59-
message: '"version" and "title" must not be both defined or both undefined',
59+
message: 'Just one of "version" and "title" must be defined',
6060
path: [],
6161
},
6262
]);
6363
});
6464

65-
it('given invalid input, should should no error message', async () => {
65+
it('given invalid input, should show no error message', async () => {
6666
return expect(await runXor(null, { properties: ['version', 'title'] })).toEqual([]);
6767
});
6868

@@ -79,18 +79,71 @@ describe('Core Functions / Xor', () => {
7979
).toEqual([]);
8080
});
8181

82+
it('given multiple of 5 properties, should return an error message', async () => {
83+
expect(
84+
await runXor(
85+
{
86+
version: '1.0.0',
87+
title: 'Swagger Petstore',
88+
termsOfService: 'http://swagger.io/terms/',
89+
},
90+
{ properties: ['version', 'title', 'termsOfService', 'bar', 'five'] },
91+
),
92+
).toEqual([
93+
{
94+
message: 'Just one of "version" and "title" and "termsOfService" must be defined',
95+
path: [],
96+
},
97+
]);
98+
});
99+
100+
it('given none of 5 properties, should return an error message', async () => {
101+
expect(
102+
await runXor(
103+
{
104+
version: '1.0.0',
105+
title: 'Swagger Petstore',
106+
termsOfService: 'http://swagger.io/terms/',
107+
},
108+
{ properties: ['yada-yada', 'foo', 'bar', 'four', 'five'] },
109+
),
110+
).toEqual([
111+
{
112+
message: 'At least one of "yada-yada" or "foo" or "bar" or 2 other properties must be defined',
113+
path: [],
114+
},
115+
]);
116+
});
117+
118+
it('given only one of 4 properties, should return no error message', async () => {
119+
expect(
120+
await runXor(
121+
{
122+
version: '1.0.0',
123+
title: 'Swagger Petstore',
124+
termsOfService: 'http://swagger.io/terms/',
125+
},
126+
{ properties: ['title', 'foo', 'bar', 'four'] },
127+
),
128+
).toEqual([]);
129+
});
130+
82131
describe('validation', () => {
83132
it.each([{ properties: ['foo', 'bar'] }])('given valid %p options, should not throw', async opts => {
84133
expect(await runXor([], opts)).toEqual([]);
85134
});
86135

136+
it.each([{ properties: ['foo', 'bar', 'three'] }])('given valid %p options, should not throw', async opts => {
137+
expect(await runXor([], opts)).toEqual([]);
138+
});
139+
87140
it.each<[unknown, RulesetValidationError[]]>([
88141
[
89142
null,
90143
[
91144
new RulesetValidationError(
92145
'invalid-function-options',
93-
'"xor" function has invalid options specified. Example valid options: { "properties": ["id", "name"] }, { "properties": ["country", "street"] }',
146+
'"xor" function has invalid options specified. Example valid options: { "properties": ["country", "street"] }, { "properties": ["one", "two", "three"] }, etc.',
94147
['rules', 'my-rule', 'then', 'functionOptions'],
95148
),
96149
],
@@ -100,7 +153,7 @@ describe('Core Functions / Xor', () => {
100153
[
101154
new RulesetValidationError(
102155
'invalid-function-options',
103-
'"xor" function has invalid options specified. Example valid options: { "properties": ["id", "name"] }, { "properties": ["country", "street"] }',
156+
'"xor" function has invalid options specified. Example valid options: { "properties": ["country", "street"] }, { "properties": ["one", "two", "three"] }, etc.',
104157
['rules', 'my-rule', 'then', 'functionOptions'],
105158
),
106159
],
@@ -122,7 +175,7 @@ describe('Core Functions / Xor', () => {
122175
[
123176
new RulesetValidationError(
124177
'invalid-function-options',
125-
'"xor" and its "properties" option require at least 2-item tuples, i.e. ["id", "name"]',
178+
'"xor" requires at least two enumerated "properties", i.e. ["country", "street"], ["one", "two", "three"], etc.',
126179
['rules', 'my-rule', 'then', 'functionOptions', 'properties'],
127180
),
128181
],
@@ -132,7 +185,7 @@ describe('Core Functions / Xor', () => {
132185
[
133186
new RulesetValidationError(
134187
'invalid-function-options',
135-
'"xor" and its "properties" option require at least 2-item tuples, i.e. ["id", "name"]',
188+
'"xor" requires at least two enumerated "properties", i.e. ["country", "street"], ["one", "two", "three"], etc.',
136189
['rules', 'my-rule', 'then', 'functionOptions', 'properties'],
137190
),
138191
],
@@ -142,7 +195,7 @@ describe('Core Functions / Xor', () => {
142195
[
143196
new RulesetValidationError(
144197
'invalid-function-options',
145-
'"xor" and its "properties" option require at least 2-item tuples, i.e. ["id", "name"]',
198+
'"xor" requires at least two enumerated "properties", i.e. ["country", "street"], ["one", "two", "three"], etc.',
146199
['rules', 'my-rule', 'then', 'functionOptions', 'properties'],
147200
),
148201
],

0 commit comments

Comments
 (0)