Skip to content

Commit 3cbf047

Browse files
authored
chore(rulesets): use createRulesetFunction for all oas functions (#2491)
1 parent a64ca92 commit 3cbf047

File tree

11 files changed

+340
-297
lines changed

11 files changed

+340
-297
lines changed

packages/rulesets/src/oas/functions/__tests__/typedEnum.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { typedEnum } from '../typedEnum';
1+
import typedEnum from '../typedEnum';
22
import { Document } from '@stoplight/spectral-core';
33
import * as Parsers from '@stoplight/spectral-parsers';
44

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,50 @@
1-
import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';
1+
import { createRulesetFunction, IFunctionResult } from '@stoplight/spectral-core';
22
import { isObject } from './utils/isObject';
33

4-
export const oasDiscriminator: IFunction = (schema, _opts, { path }) => {
5-
/**
6-
* This function verifies:
7-
*
8-
* 1. The discriminator property name is defined at this schema.
9-
* 2. The discriminator property is in the required property list.
10-
*/
11-
12-
if (!isObject(schema)) return;
13-
14-
if (typeof schema.discriminator !== 'string') return;
15-
16-
const discriminatorName = schema.discriminator;
17-
18-
const results: IFunctionResult[] = [];
19-
20-
if (!isObject(schema.properties) || !Object.keys(schema.properties).some(k => k === discriminatorName)) {
21-
results.push({
22-
message: `The discriminator property must be defined in this schema.`,
23-
path: [...path, 'properties'],
24-
});
25-
}
26-
27-
if (!Array.isArray(schema.required) || !schema.required.some(n => n === discriminatorName)) {
28-
results.push({
29-
message: `The discriminator property must be in the required property list.`,
30-
path: [...path, 'required'],
31-
});
32-
}
33-
34-
return results;
4+
type Input = {
5+
discriminator: string;
6+
[key: string]: unknown;
357
};
368

37-
export default oasDiscriminator;
9+
export default createRulesetFunction<Input, null>(
10+
{
11+
input: {
12+
type: 'object',
13+
properties: {
14+
discriminator: {
15+
type: 'string',
16+
},
17+
},
18+
required: ['discriminator'],
19+
},
20+
options: null,
21+
},
22+
function oasDiscriminator(schema, _opts, { path }) {
23+
/**
24+
* This function verifies:
25+
*
26+
* 1. The discriminator property name is defined at this schema.
27+
* 2. The discriminator property is in the required property list.
28+
*/
29+
30+
const discriminatorName = schema.discriminator;
31+
32+
const results: IFunctionResult[] = [];
33+
34+
if (!isObject(schema.properties) || !Object.keys(schema.properties).some(k => k === discriminatorName)) {
35+
results.push({
36+
message: `The discriminator property must be defined in this schema.`,
37+
path: [...path, 'properties'],
38+
});
39+
}
40+
41+
if (!Array.isArray(schema.required) || !schema.required.some(n => n === discriminatorName)) {
42+
results.push({
43+
message: `The discriminator property must be in the required property list.`,
44+
path: [...path, 'required'],
45+
});
46+
}
47+
48+
return results;
49+
},
50+
);
Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,39 @@
1-
import type { IFunction } from '@stoplight/spectral-core';
1+
import { createRulesetFunction } from '@stoplight/spectral-core';
22
import { isObject } from './utils/isObject';
33

44
const validConsumeValue = /(application\/x-www-form-urlencoded|multipart\/form-data)/;
55

6-
export const oasOpFormDataConsumeCheck: IFunction = targetVal => {
7-
if (!isObject(targetVal)) return;
8-
9-
const parameters: unknown = targetVal.parameters;
10-
const consumes: unknown = targetVal.consumes;
11-
12-
if (!Array.isArray(parameters) || !Array.isArray(consumes)) {
13-
return;
14-
}
6+
type Input = {
7+
consumes: unknown[];
8+
parameters: unknown[];
9+
};
1510

16-
if (parameters.some(p => isObject(p) && p.in === 'formData') && !validConsumeValue.test(consumes?.join(','))) {
17-
return [
18-
{
19-
message: 'Consumes must include urlencoded, multipart, or form-data media type when using formData parameter.',
11+
export default createRulesetFunction<Input, null>(
12+
{
13+
input: {
14+
type: 'object',
15+
properties: {
16+
consumes: {
17+
type: 'array',
18+
},
19+
parameters: {
20+
type: 'array',
21+
},
2022
},
21-
];
22-
}
23+
required: ['consumes', 'parameters'],
24+
},
25+
options: null,
26+
},
27+
function oasOpFormDataConsumeCheck({ parameters, consumes }) {
28+
if (parameters.some(p => isObject(p) && p.in === 'formData') && !validConsumeValue.test(consumes?.join(','))) {
29+
return [
30+
{
31+
message:
32+
'Consumes must include urlencoded, multipart, or form-data media type when using formData parameter.',
33+
},
34+
];
35+
}
2336

24-
return;
25-
};
26-
27-
export default oasOpFormDataConsumeCheck;
37+
return;
38+
},
39+
);
Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,43 @@
1-
import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';
1+
import type { IFunctionResult } from '@stoplight/spectral-core';
2+
import { createRulesetFunction } from '@stoplight/spectral-core';
23
import { getAllOperations } from './utils/getAllOperations';
34
import { isObject } from './utils/isObject';
45

5-
export const oasOpIdUnique: IFunction = targetVal => {
6-
if (!isObject(targetVal) || !isObject(targetVal.paths)) return;
6+
export default createRulesetFunction<Record<string, unknown>, null>(
7+
{
8+
input: {
9+
type: 'object',
10+
},
11+
options: null,
12+
},
13+
function oasOpIdUnique(paths) {
14+
const results: IFunctionResult[] = [];
715

8-
const results: IFunctionResult[] = [];
16+
const seenIds: unknown[] = [];
917

10-
const { paths } = targetVal;
18+
for (const { path, operation } of getAllOperations(paths)) {
19+
const pathValue = paths[path];
1120

12-
const seenIds: unknown[] = [];
21+
if (!isObject(pathValue)) continue;
1322

14-
for (const { path, operation } of getAllOperations(paths)) {
15-
const pathValue = paths[path];
23+
const operationValue = pathValue[operation];
1624

17-
if (!isObject(pathValue)) continue;
25+
if (!isObject(operationValue) || !('operationId' in operationValue)) {
26+
continue;
27+
}
1828

19-
const operationValue = pathValue[operation];
29+
const { operationId } = operationValue;
2030

21-
if (!isObject(operationValue) || !('operationId' in operationValue)) {
22-
continue;
31+
if (seenIds.includes(operationId)) {
32+
results.push({
33+
message: 'operationId must be unique.',
34+
path: ['paths', path, operation, 'operationId'],
35+
});
36+
} else {
37+
seenIds.push(operationId);
38+
}
2339
}
2440

25-
const { operationId } = operationValue;
26-
27-
if (seenIds.includes(operationId)) {
28-
results.push({
29-
message: 'operationId must be unique.',
30-
path: ['paths', path, operation, 'operationId'],
31-
});
32-
} else {
33-
seenIds.push(operationId);
34-
}
35-
}
36-
37-
return results;
38-
};
39-
40-
export default oasOpIdUnique;
41+
return results;
42+
},
43+
);
Lines changed: 71 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,87 @@
1-
import type { IFunction, IFunctionResult } from '@stoplight/spectral-core';
2-
import type { Dictionary } from '@stoplight/types';
1+
import type { IFunctionResult } from '@stoplight/spectral-core';
32
import { isObject } from './utils/isObject';
3+
import { createRulesetFunction } from '@stoplight/spectral-core';
44

55
function computeFingerprint(param: Record<string, unknown>): string {
66
return `${String(param.in)}-${String(param.name)}`;
77
}
88

9-
export const oasOpParams: IFunction = (params, _opts, { path }) => {
10-
/**
11-
* This function verifies:
12-
*
13-
* 1. Operations must have unique `name` + `in` parameters.
14-
* 2. Operation cannot have both `in:body` and `in:formData` parameters
15-
* 3. Operation must have only one `in:body` parameter.
16-
*/
17-
18-
if (!Array.isArray(params)) return;
19-
20-
if (params.length < 2) return;
21-
22-
const results: IFunctionResult[] = [];
23-
24-
const count: Dictionary<number[]> = {
25-
body: [],
26-
formData: [],
27-
};
28-
const list: string[] = [];
29-
const duplicates: number[] = [];
30-
31-
let index = -1;
32-
33-
for (const param of params) {
34-
index++;
35-
36-
if (!isObject(param)) continue;
37-
38-
// skip params that are refs
39-
if ('$ref' in param) continue;
40-
41-
// Operations must have unique `name` + `in` parameters.
42-
const fingerprint = computeFingerprint(param);
43-
if (list.includes(fingerprint)) {
44-
duplicates.push(index);
45-
} else {
46-
list.push(fingerprint);
9+
export default createRulesetFunction<unknown[], null>(
10+
{
11+
input: {
12+
type: 'array',
13+
},
14+
options: null,
15+
},
16+
function oasOpParams(params, _opts, { path }) {
17+
/**
18+
* This function verifies:
19+
*
20+
* 1. Operations must have unique `name` + `in` parameters.
21+
* 2. Operation cannot have both `in:body` and `in:formData` parameters
22+
* 3. Operation must have only one `in:body` parameter.
23+
*/
24+
25+
if (!Array.isArray(params)) return;
26+
27+
if (params.length < 2) return;
28+
29+
const results: IFunctionResult[] = [];
30+
31+
const count: Record<string, number[]> = {
32+
body: [],
33+
formData: [],
34+
};
35+
const list: string[] = [];
36+
const duplicates: number[] = [];
37+
38+
let index = -1;
39+
40+
for (const param of params) {
41+
index++;
42+
43+
if (!isObject(param)) continue;
44+
45+
// skip params that are refs
46+
if ('$ref' in param) continue;
47+
48+
// Operations must have unique `name` + `in` parameters.
49+
const fingerprint = computeFingerprint(param);
50+
if (list.includes(fingerprint)) {
51+
duplicates.push(index);
52+
} else {
53+
list.push(fingerprint);
54+
}
55+
56+
if (typeof param.in === 'string' && param.in in count) {
57+
count[param.in].push(index);
58+
}
4759
}
4860

49-
if (typeof param.in === 'string' && param.in in count) {
50-
count[param.in].push(index);
61+
if (duplicates.length > 0) {
62+
for (const i of duplicates) {
63+
results.push({
64+
message: 'A parameter in this operation already exposes the same combination of "name" and "in" values.',
65+
path: [...path, i],
66+
});
67+
}
5168
}
52-
}
5369

54-
if (duplicates.length > 0) {
55-
for (const i of duplicates) {
70+
if (count.body.length > 0 && count.formData.length > 0) {
5671
results.push({
57-
message: 'A parameter in this operation already exposes the same combination of "name" and "in" values.',
58-
path: [...path, i],
72+
message: 'Operation must not have both "in:body" and "in:formData" parameters.',
5973
});
6074
}
61-
}
62-
63-
if (count.body.length > 0 && count.formData.length > 0) {
64-
results.push({
65-
message: 'Operation must not have both "in:body" and "in:formData" parameters.',
66-
});
67-
}
6875

69-
if (count.body.length > 1) {
70-
for (let i = 1; i < count.body.length; i++) {
71-
results.push({
72-
message: 'Operation must not have more than a single instance of the "in:body" parameter.',
73-
path: [...path, count.body[i]],
74-
});
76+
if (count.body.length > 1) {
77+
for (let i = 1; i < count.body.length; i++) {
78+
results.push({
79+
message: 'Operation must not have more than a single instance of the "in:body" parameter.',
80+
path: [...path, count.body[i]],
81+
});
82+
}
7583
}
76-
}
77-
78-
return results;
79-
};
8084

81-
export default oasOpParams;
85+
return results;
86+
},
87+
);

packages/rulesets/src/oas/functions/oasOpSuccessResponse.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { createRulesetFunction } from '@stoplight/spectral-core';
22
import { oas3 } from '@stoplight/spectral-formats';
33

4-
export const oasOpSuccessResponse = createRulesetFunction<Record<string, unknown>, null>(
4+
export default createRulesetFunction<Record<string, unknown>, null>(
55
{
66
input: {
77
type: 'object',
88
},
99
options: null,
1010
},
11-
(input, opts, context) => {
11+
function oasOpSuccessResponse(input, opts, context) {
1212
const isOAS3X = context.document.formats?.has(oas3) === true;
1313

1414
for (const response of Object.keys(input)) {
@@ -28,5 +28,3 @@ export const oasOpSuccessResponse = createRulesetFunction<Record<string, unknown
2828
];
2929
},
3030
);
31-
32-
export default oasOpSuccessResponse;

0 commit comments

Comments
 (0)