@redocly/openapi-core 1.0.0-beta.111 → 1.0.0-beta.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/config/all.js +0 -1
- package/lib/config/config-resolvers.js +22 -18
- package/lib/config/config.d.ts +4 -10
- package/lib/config/config.js +1 -1
- package/lib/config/load.d.ts +1 -1
- package/lib/config/load.js +10 -10
- package/lib/config/minimal.js +0 -1
- package/lib/config/recommended.js +0 -1
- package/lib/config/rules.d.ts +6 -3
- package/lib/config/rules.js +3 -2
- package/lib/config/types.d.ts +3 -0
- package/lib/ref-utils.d.ts +1 -0
- package/lib/ref-utils.js +5 -1
- package/lib/resolve.js +19 -0
- package/lib/rules/common/assertions/asserts.d.ts +22 -5
- package/lib/rules/common/assertions/asserts.js +25 -0
- package/lib/rules/common/assertions/index.d.ts +27 -2
- package/lib/rules/common/assertions/index.js +6 -29
- package/lib/rules/common/assertions/utils.d.ts +7 -14
- package/lib/rules/common/assertions/utils.js +129 -97
- package/lib/rules/common/spec.js +6 -0
- package/lib/rules/oas2/index.d.ts +0 -1
- package/lib/rules/oas2/index.js +0 -2
- package/lib/rules/oas3/index.js +0 -2
- package/lib/rules/utils.js +3 -0
- package/lib/types/oas2.js +11 -7
- package/lib/types/oas3.js +15 -10
- package/lib/types/oas3_1.js +1 -0
- package/lib/types/redocly-yaml.js +49 -27
- package/lib/utils.d.ts +2 -0
- package/lib/utils.js +13 -1
- package/lib/visitors.d.ts +2 -1
- package/lib/visitors.js +1 -0
- package/lib/walk.js +7 -1
- package/package.json +1 -1
- package/src/__tests__/bundle.test.ts +46 -0
- package/src/__tests__/lint.test.ts +24 -5
- package/src/benchmark/benches/rebilly.yaml +36 -28
- package/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +1 -3
- package/src/config/__tests__/config-resolvers.test.ts +6 -7
- package/src/config/__tests__/fixtures/load-redocly.yaml +2 -0
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml +6 -5
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml +0 -1
- package/src/config/__tests__/load.test.ts +4 -1
- package/src/config/all.ts +0 -1
- package/src/config/config-resolvers.ts +44 -31
- package/src/config/config.ts +6 -5
- package/src/config/load.ts +19 -9
- package/src/config/minimal.ts +0 -1
- package/src/config/recommended.ts +0 -1
- package/src/config/rules.ts +11 -3
- package/src/config/types.ts +2 -0
- package/src/ref-utils.ts +4 -0
- package/src/resolve.ts +25 -3
- package/src/rules/common/__tests__/spec.test.ts +170 -0
- package/src/rules/common/assertions/__tests__/asserts.test.ts +7 -3
- package/src/rules/common/assertions/__tests__/index.test.ts +41 -20
- package/src/rules/common/assertions/__tests__/utils.test.ts +43 -17
- package/src/rules/common/assertions/asserts.ts +60 -8
- package/src/rules/common/assertions/index.ts +36 -46
- package/src/rules/common/assertions/utils.ts +204 -127
- package/src/rules/common/spec.ts +7 -0
- package/src/rules/oas2/index.ts +0 -2
- package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +32 -0
- package/src/rules/oas3/index.ts +0 -2
- package/src/rules/utils.ts +4 -0
- package/src/types/oas2.ts +11 -7
- package/src/types/oas3.ts +15 -10
- package/src/types/oas3_1.ts +1 -0
- package/src/types/redocly-yaml.ts +49 -29
- package/src/utils.ts +11 -0
- package/src/visitors.ts +7 -1
- package/src/walk.ts +8 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/lib/rules/common/info-description.d.ts +0 -2
- package/lib/rules/common/info-description.js +0 -12
- package/src/rules/common/__tests__/info-description.test.ts +0 -102
- package/src/rules/common/info-description.ts +0 -10
|
@@ -9,12 +9,33 @@ import {
|
|
|
9
9
|
regexFromString,
|
|
10
10
|
} from './utils';
|
|
11
11
|
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
export type AssertionFn = (
|
|
13
|
+
value: any,
|
|
14
|
+
condition: any,
|
|
15
|
+
baseLocation: Location,
|
|
16
|
+
rawValue?: any
|
|
17
|
+
) => AssertResult[];
|
|
16
18
|
|
|
17
|
-
export
|
|
19
|
+
export type Asserts = {
|
|
20
|
+
pattern: AssertionFn;
|
|
21
|
+
enum: AssertionFn;
|
|
22
|
+
defined: AssertionFn;
|
|
23
|
+
required: AssertionFn;
|
|
24
|
+
disallowed: AssertionFn;
|
|
25
|
+
undefined: AssertionFn;
|
|
26
|
+
nonEmpty: AssertionFn;
|
|
27
|
+
minLength: AssertionFn;
|
|
28
|
+
maxLength: AssertionFn;
|
|
29
|
+
casing: AssertionFn;
|
|
30
|
+
sortOrder: AssertionFn;
|
|
31
|
+
mutuallyExclusive: AssertionFn;
|
|
32
|
+
mutuallyRequired: AssertionFn;
|
|
33
|
+
requireAny: AssertionFn;
|
|
34
|
+
ref: AssertionFn;
|
|
35
|
+
const: AssertionFn;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const runOnKeysSet = new Set<keyof Asserts>([
|
|
18
39
|
'mutuallyExclusive',
|
|
19
40
|
'mutuallyRequired',
|
|
20
41
|
'enum',
|
|
@@ -27,8 +48,10 @@ export const runOnKeysSet = new Set([
|
|
|
27
48
|
'required',
|
|
28
49
|
'requireAny',
|
|
29
50
|
'ref',
|
|
51
|
+
'const',
|
|
52
|
+
'defined', // In case if `property` for assertions is not added
|
|
30
53
|
]);
|
|
31
|
-
export const runOnValuesSet = new Set([
|
|
54
|
+
export const runOnValuesSet = new Set<keyof Asserts>([
|
|
32
55
|
'pattern',
|
|
33
56
|
'enum',
|
|
34
57
|
'defined',
|
|
@@ -39,6 +62,7 @@ export const runOnValuesSet = new Set([
|
|
|
39
62
|
'casing',
|
|
40
63
|
'sortOrder',
|
|
41
64
|
'ref',
|
|
65
|
+
'const',
|
|
42
66
|
]);
|
|
43
67
|
|
|
44
68
|
export const asserts: Asserts = {
|
|
@@ -106,6 +130,34 @@ export const asserts: Asserts = {
|
|
|
106
130
|
)
|
|
107
131
|
.filter(isTruthy);
|
|
108
132
|
},
|
|
133
|
+
const: (
|
|
134
|
+
value: string | number | boolean | string[] | number[],
|
|
135
|
+
condition: string | number | boolean,
|
|
136
|
+
baseLocation: Location
|
|
137
|
+
) => {
|
|
138
|
+
if (typeof value === 'undefined') return [];
|
|
139
|
+
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
return value
|
|
142
|
+
.map(
|
|
143
|
+
(_val) =>
|
|
144
|
+
condition !== _val && {
|
|
145
|
+
message: `"${_val}" should be equal ${condition} `,
|
|
146
|
+
location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
.filter(isTruthy);
|
|
150
|
+
} else {
|
|
151
|
+
return value !== condition
|
|
152
|
+
? [
|
|
153
|
+
{
|
|
154
|
+
message: `${value} should be equal ${condition}`,
|
|
155
|
+
location: baseLocation,
|
|
156
|
+
},
|
|
157
|
+
]
|
|
158
|
+
: [];
|
|
159
|
+
}
|
|
160
|
+
},
|
|
109
161
|
undefined: (value: any, condition: boolean = true, baseLocation: Location) => {
|
|
110
162
|
const isUndefined = typeof value === 'undefined';
|
|
111
163
|
const isValid = condition ? isUndefined : !isUndefined;
|
|
@@ -210,7 +262,7 @@ export const asserts: Asserts = {
|
|
|
210
262
|
},
|
|
211
263
|
];
|
|
212
264
|
},
|
|
213
|
-
ref: (_value: any, condition: string | boolean, baseLocation, rawValue: any) => {
|
|
265
|
+
ref: (_value: any, condition: string | boolean, baseLocation: Location, rawValue: any) => {
|
|
214
266
|
if (typeof rawValue === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
|
|
215
267
|
const hasRef = rawValue.hasOwnProperty('$ref');
|
|
216
268
|
if (typeof condition === 'boolean') {
|
|
@@ -237,7 +289,7 @@ export const asserts: Asserts = {
|
|
|
237
289
|
},
|
|
238
290
|
};
|
|
239
291
|
|
|
240
|
-
export function buildAssertCustomFunction(fn: CustomFunction) {
|
|
292
|
+
export function buildAssertCustomFunction(fn: CustomFunction): AssertionFn {
|
|
241
293
|
return (value: string[], options: any, baseLocation: Location) =>
|
|
242
294
|
fn.call(null, value, options, baseLocation);
|
|
243
295
|
}
|
|
@@ -1,65 +1,55 @@
|
|
|
1
|
-
import { asserts,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { asserts, AssertionFn } from './asserts';
|
|
2
|
+
import { buildSubjectVisitor, buildVisitorObject } from './utils';
|
|
3
|
+
import { Oas2Visitor, Oas3Visitor } from '../../../visitors';
|
|
4
|
+
import { RuleSeverity } from '../../../config';
|
|
5
|
+
import { isString } from '../../../utils';
|
|
6
|
+
|
|
7
|
+
export type AssertionLocators = {
|
|
8
|
+
filterInParentKeys?: (string | number)[];
|
|
9
|
+
filterOutParentKeys?: (string | number)[];
|
|
10
|
+
matchParentKeys?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type AssertionDefinition = {
|
|
14
|
+
subject: {
|
|
15
|
+
type: string;
|
|
16
|
+
property?: string | string[];
|
|
17
|
+
} & AssertionLocators;
|
|
18
|
+
assertions: { [name in keyof typeof asserts]?: AssertionFn };
|
|
19
|
+
};
|
|
4
20
|
|
|
5
|
-
export
|
|
6
|
-
|
|
21
|
+
export type RawAssertion = AssertionDefinition & {
|
|
22
|
+
where?: AssertionDefinition[];
|
|
23
|
+
message?: string;
|
|
24
|
+
suggest?: string[];
|
|
25
|
+
severity?: RuleSeverity;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type Assertion = RawAssertion & { assertionId: string };
|
|
29
|
+
|
|
30
|
+
export const Assertions = (opts: Record<string, Assertion>) => {
|
|
31
|
+
const visitors: (Oas2Visitor | Oas3Visitor)[] = [];
|
|
7
32
|
|
|
8
33
|
// As 'Assertions' has an array of asserts,
|
|
9
34
|
// that array spreads into an 'opts' object on init rules phase here
|
|
10
35
|
// https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/config/config.ts#L311
|
|
11
36
|
// that is why we need to iterate through 'opts' values;
|
|
12
37
|
// before - filter only object 'opts' values
|
|
13
|
-
const assertions:
|
|
38
|
+
const assertions: Assertion[] = Object.values(opts).filter(
|
|
14
39
|
(opt: unknown) => typeof opt === 'object' && opt !== null
|
|
15
40
|
);
|
|
16
41
|
|
|
17
42
|
for (const [index, assertion] of assertions.entries()) {
|
|
18
43
|
const assertId =
|
|
19
44
|
(assertion.assertionId && `${assertion.assertionId} assertion`) || `assertion #${index + 1}`;
|
|
20
|
-
if (!assertion.subject) {
|
|
21
|
-
throw new Error(`${assertId}: 'subject' is required`);
|
|
22
|
-
}
|
|
23
45
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
: [assertion.subject];
|
|
27
|
-
|
|
28
|
-
const assertsToApply: AssertToApply[] = Object.keys(asserts)
|
|
29
|
-
.filter((assertName: string) => assertion[assertName] !== undefined)
|
|
30
|
-
.map((assertName: string) => {
|
|
31
|
-
return {
|
|
32
|
-
name: assertName,
|
|
33
|
-
conditions: assertion[assertName],
|
|
34
|
-
runsOnKeys: runOnKeysSet.has(assertName),
|
|
35
|
-
runsOnValues: runOnValuesSet.has(assertName),
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const shouldRunOnKeys: AssertToApply | undefined = assertsToApply.find(
|
|
40
|
-
(assert: AssertToApply) => assert.runsOnKeys && !assert.runsOnValues
|
|
41
|
-
);
|
|
42
|
-
const shouldRunOnValues: AssertToApply | undefined = assertsToApply.find(
|
|
43
|
-
(assert: AssertToApply) => assert.runsOnValues && !assert.runsOnKeys
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
if (shouldRunOnValues && !assertion.property) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
`${shouldRunOnValues.name} can't be used on all keys. Please provide a single property.`
|
|
49
|
-
);
|
|
46
|
+
if (!isString(assertion.subject.type)) {
|
|
47
|
+
throw new Error(`${assertId}: 'type' (String) is required`);
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
for (const subject of subjects) {
|
|
59
|
-
const subjectVisitor = buildSubjectVisitor(assertId, assertion, assertsToApply);
|
|
60
|
-
const visitorObject = buildVisitorObject(subject, assertion.context, subjectVisitor);
|
|
61
|
-
visitors.push(visitorObject);
|
|
62
|
-
}
|
|
50
|
+
const subjectVisitor = buildSubjectVisitor(assertId, assertion);
|
|
51
|
+
const visitorObject = buildVisitorObject(assertion, subjectVisitor);
|
|
52
|
+
visitors.push(visitorObject);
|
|
63
53
|
}
|
|
64
54
|
|
|
65
55
|
return visitors;
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { asserts, runOnKeysSet, runOnValuesSet, Asserts } from './asserts';
|
|
2
2
|
import { colorize } from '../../../logger';
|
|
3
3
|
import { isRef, Location } from '../../../ref-utils';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { isTruthy, keysOf, isString } from '../../../utils';
|
|
5
|
+
import type { AssertResult } from '../../../config';
|
|
6
|
+
import type { Assertion, AssertionDefinition, AssertionLocators } from '.';
|
|
7
|
+
import type {
|
|
8
|
+
Oas2Visitor,
|
|
9
|
+
Oas3Visitor,
|
|
10
|
+
SkipFunctionContext,
|
|
11
|
+
VisitFunction,
|
|
12
|
+
} from '../../../visitors';
|
|
6
13
|
|
|
7
14
|
export type OrderDirection = 'asc' | 'desc';
|
|
8
15
|
|
|
@@ -11,166 +18,236 @@ export type OrderOptions = {
|
|
|
11
18
|
property: string;
|
|
12
19
|
};
|
|
13
20
|
|
|
14
|
-
type Assertion = {
|
|
15
|
-
property: string | string[];
|
|
16
|
-
context?: Record<string, any>[];
|
|
17
|
-
severity?: RuleSeverity;
|
|
18
|
-
suggest?: any[];
|
|
19
|
-
message?: string;
|
|
20
|
-
subject: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
21
|
export type AssertToApply = {
|
|
24
|
-
name:
|
|
22
|
+
name: keyof Asserts;
|
|
25
23
|
conditions: any;
|
|
26
24
|
runsOnKeys: boolean;
|
|
27
25
|
runsOnValues: boolean;
|
|
28
26
|
};
|
|
29
27
|
|
|
28
|
+
type AssertionContext = SkipFunctionContext & {
|
|
29
|
+
node: any;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const assertionMessageTemplates = {
|
|
33
|
+
problems: '{{problems}}',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function getPredicatesFromLocators(
|
|
37
|
+
locators: AssertionLocators
|
|
38
|
+
): ((key: string | number) => boolean)[] {
|
|
39
|
+
const { filterInParentKeys, filterOutParentKeys, matchParentKeys } = locators;
|
|
40
|
+
|
|
41
|
+
const keyMatcher = matchParentKeys && regexFromString(matchParentKeys);
|
|
42
|
+
const matchKeysPredicate =
|
|
43
|
+
keyMatcher && ((key: string | number) => keyMatcher.test(key.toString()));
|
|
44
|
+
|
|
45
|
+
const filterInPredicate =
|
|
46
|
+
Array.isArray(filterInParentKeys) &&
|
|
47
|
+
((key: string | number) => filterInParentKeys.includes(key.toString()));
|
|
48
|
+
|
|
49
|
+
const filterOutPredicate =
|
|
50
|
+
Array.isArray(filterOutParentKeys) &&
|
|
51
|
+
((key: string | number) => !filterOutParentKeys.includes(key.toString()));
|
|
52
|
+
|
|
53
|
+
return [matchKeysPredicate, filterInPredicate, filterOutPredicate].filter(isTruthy);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getAssertsToApply(assertion: AssertionDefinition): AssertToApply[] {
|
|
57
|
+
const assertsToApply = keysOf(asserts)
|
|
58
|
+
.filter((assertName) => assertion.assertions[assertName] !== undefined)
|
|
59
|
+
.map((assertName) => {
|
|
60
|
+
return {
|
|
61
|
+
name: assertName,
|
|
62
|
+
conditions: assertion.assertions[assertName],
|
|
63
|
+
runsOnKeys: runOnKeysSet.has(assertName),
|
|
64
|
+
runsOnValues: runOnValuesSet.has(assertName),
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const shouldRunOnKeys: AssertToApply | undefined = assertsToApply.find(
|
|
69
|
+
(assert: AssertToApply) => assert.runsOnKeys && !assert.runsOnValues
|
|
70
|
+
);
|
|
71
|
+
const shouldRunOnValues: AssertToApply | undefined = assertsToApply.find(
|
|
72
|
+
(assert: AssertToApply) => assert.runsOnValues && !assert.runsOnKeys
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (shouldRunOnValues && !assertion.subject.property) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`${shouldRunOnValues.name} can't be used on all keys. Please provide a single property`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (shouldRunOnKeys && assertion.subject.property) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`${shouldRunOnKeys.name} can't be used on a single property. Please use 'property'.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return assertsToApply;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getAssertionProperties({ subject }: AssertionDefinition): string[] {
|
|
91
|
+
return (Array.isArray(subject.property) ? subject.property : [subject?.property]).filter(
|
|
92
|
+
Boolean
|
|
93
|
+
) as string[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function applyAssertions(
|
|
97
|
+
assertionDefinition: AssertionDefinition,
|
|
98
|
+
asserts: AssertToApply[],
|
|
99
|
+
{ rawLocation, rawNode, resolve, location, node }: AssertionContext
|
|
100
|
+
): AssertResult[] {
|
|
101
|
+
const properties = getAssertionProperties(assertionDefinition);
|
|
102
|
+
const assertResults: Array<AssertResult[]> = [];
|
|
103
|
+
|
|
104
|
+
for (const assert of asserts) {
|
|
105
|
+
const currentLocation = assert.name === 'ref' ? rawLocation : location;
|
|
106
|
+
|
|
107
|
+
if (properties.length) {
|
|
108
|
+
for (const property of properties) {
|
|
109
|
+
// we can have resolvable scalar so need to resolve value here.
|
|
110
|
+
const value = isRef(node[property]) ? resolve(node[property])?.node : node[property];
|
|
111
|
+
assertResults.push(
|
|
112
|
+
runAssertion({
|
|
113
|
+
values: value,
|
|
114
|
+
rawValues: rawNode[property],
|
|
115
|
+
assert,
|
|
116
|
+
location: currentLocation.child(property),
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
const value = assert.name === 'ref' ? rawNode : Object.keys(node);
|
|
122
|
+
assertResults.push(
|
|
123
|
+
runAssertion({
|
|
124
|
+
values: Object.keys(node),
|
|
125
|
+
rawValues: value,
|
|
126
|
+
assert,
|
|
127
|
+
location: currentLocation,
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return assertResults.flat();
|
|
134
|
+
}
|
|
135
|
+
|
|
30
136
|
export function buildVisitorObject(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
137
|
+
assertion: Assertion,
|
|
138
|
+
subjectVisitor: VisitFunction<any>
|
|
139
|
+
): Oas2Visitor | Oas3Visitor {
|
|
140
|
+
const targetVisitorLocatorPredicates = getPredicatesFromLocators(assertion.subject);
|
|
141
|
+
const targetVisitorSkipFunction = targetVisitorLocatorPredicates.length
|
|
142
|
+
? (node: any, key: string | number) =>
|
|
143
|
+
!targetVisitorLocatorPredicates.every((predicate) => predicate(key))
|
|
144
|
+
: undefined;
|
|
145
|
+
const targetVisitor: Oas2Visitor | Oas3Visitor = {
|
|
146
|
+
[assertion.subject.type]: {
|
|
147
|
+
enter: subjectVisitor,
|
|
148
|
+
...(targetVisitorSkipFunction && { skip: targetVisitorSkipFunction }),
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (!Array.isArray(assertion.where)) {
|
|
153
|
+
return targetVisitor;
|
|
37
154
|
}
|
|
38
155
|
|
|
39
156
|
let currentVisitorLevel: Record<string, any> = {};
|
|
40
157
|
const visitor: Record<string, any> = currentVisitorLevel;
|
|
158
|
+
const context = assertion.where;
|
|
41
159
|
|
|
42
160
|
for (let index = 0; index < context.length; index++) {
|
|
43
|
-
const
|
|
44
|
-
if (context.length === index + 1 && node.type === subject) {
|
|
45
|
-
// Visitors don't work properly for the same type nested nodes, so
|
|
46
|
-
// as a workaround for that we don't create separate visitor for the last element
|
|
47
|
-
// which is the same as subject;
|
|
48
|
-
// we will check includes/excludes it in the last visitor.
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
const matchParentKeys = node.matchParentKeys;
|
|
52
|
-
const excludeParentKeys = node.excludeParentKeys;
|
|
161
|
+
const assertionDefinitionNode = context[index];
|
|
53
162
|
|
|
54
|
-
if (
|
|
163
|
+
if (!isString(assertionDefinitionNode.subject?.type)) {
|
|
55
164
|
throw new Error(
|
|
56
|
-
|
|
165
|
+
`${assertion.assertionId} -> where -> [${index}]: 'type' (String) is required`
|
|
57
166
|
);
|
|
58
167
|
}
|
|
59
168
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
169
|
+
const locatorPredicates = getPredicatesFromLocators(assertionDefinitionNode.subject);
|
|
170
|
+
const assertsToApply = getAssertsToApply(assertionDefinitionNode);
|
|
171
|
+
|
|
172
|
+
const skipFunction = (
|
|
173
|
+
node: unknown,
|
|
174
|
+
key: string | number,
|
|
175
|
+
{ location, rawLocation, resolve, rawNode }: SkipFunctionContext
|
|
176
|
+
): boolean =>
|
|
177
|
+
!locatorPredicates.every((predicate) => predicate(key)) ||
|
|
178
|
+
!!applyAssertions(assertionDefinitionNode, assertsToApply, {
|
|
179
|
+
location,
|
|
180
|
+
node,
|
|
181
|
+
rawLocation,
|
|
182
|
+
rawNode,
|
|
183
|
+
resolve,
|
|
184
|
+
}).length;
|
|
185
|
+
|
|
186
|
+
const nodeVisitor = {
|
|
187
|
+
...((locatorPredicates.length || assertsToApply.length) && { skip: skipFunction }),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
assertionDefinitionNode.subject.type === assertion.subject.type &&
|
|
192
|
+
index === context.length - 1
|
|
193
|
+
) {
|
|
194
|
+
// We have to merge the visitors if the last node inside the `where` is the same as the subject.
|
|
195
|
+
targetVisitor[assertion.subject.type] = {
|
|
196
|
+
enter: subjectVisitor,
|
|
197
|
+
...((nodeVisitor.skip && { skip: nodeVisitor.skip }) ||
|
|
198
|
+
(targetVisitorSkipFunction && {
|
|
199
|
+
skip: (
|
|
200
|
+
node,
|
|
201
|
+
key,
|
|
202
|
+
ctx // We may have locators defined on assertion level and on where level for the same node type
|
|
203
|
+
) => !!(nodeVisitor.skip?.(node, key, ctx) || targetVisitorSkipFunction?.(node, key)),
|
|
204
|
+
})),
|
|
70
205
|
};
|
|
71
206
|
} else {
|
|
72
|
-
currentVisitorLevel[
|
|
207
|
+
currentVisitorLevel = currentVisitorLevel[assertionDefinitionNode.subject?.type] =
|
|
208
|
+
nodeVisitor;
|
|
73
209
|
}
|
|
74
|
-
currentVisitorLevel = currentVisitorLevel[node.type];
|
|
75
210
|
}
|
|
76
211
|
|
|
77
|
-
currentVisitorLevel[subject] =
|
|
212
|
+
currentVisitorLevel[assertion.subject.type] = targetVisitor[assertion.subject.type];
|
|
78
213
|
|
|
79
214
|
return visitor;
|
|
80
215
|
}
|
|
81
216
|
|
|
82
|
-
export function buildSubjectVisitor(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
asserts: AssertToApply[]
|
|
86
|
-
) {
|
|
87
|
-
return (
|
|
88
|
-
node: any,
|
|
89
|
-
{ report, location, rawLocation, key, type, resolve, rawNode }: UserContext
|
|
90
|
-
) => {
|
|
91
|
-
let properties = assertion.property;
|
|
92
|
-
// We need to check context's last node if it has the same type as subject node;
|
|
93
|
-
// if yes - that means we didn't create context's last node visitor,
|
|
94
|
-
// so we need to handle 'matchParentKeys' and 'excludeParentKeys' conditions here;
|
|
95
|
-
if (assertion.context) {
|
|
96
|
-
const lastContextNode = assertion.context[assertion.context.length - 1];
|
|
97
|
-
if (lastContextNode.type === type.name) {
|
|
98
|
-
const matchParentKeys = lastContextNode.matchParentKeys;
|
|
99
|
-
const excludeParentKeys = lastContextNode.excludeParentKeys;
|
|
100
|
-
|
|
101
|
-
if (matchParentKeys && !matchParentKeys.includes(key)) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (excludeParentKeys && excludeParentKeys.includes(key)) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (properties) {
|
|
111
|
-
properties = Array.isArray(properties) ? properties : [properties];
|
|
112
|
-
}
|
|
217
|
+
export function buildSubjectVisitor(assertId: string, assertion: Assertion): VisitFunction<any> {
|
|
218
|
+
return (node: any, { report, location, rawLocation, resolve, rawNode }) => {
|
|
219
|
+
const properties = getAssertionProperties(assertion);
|
|
113
220
|
|
|
114
221
|
const defaultMessage = `${colorize.blue(assertId)} failed because the ${colorize.blue(
|
|
115
|
-
assertion.subject
|
|
116
|
-
)}${colorize.blue(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
assertResults.push(
|
|
128
|
-
runAssertion({
|
|
129
|
-
values: value,
|
|
130
|
-
rawValues: rawNode[property],
|
|
131
|
-
assert,
|
|
132
|
-
location: currentLocation.child(property),
|
|
133
|
-
})
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
const value = assert.name === 'ref' ? rawNode : Object.keys(node);
|
|
138
|
-
assertResults.push(
|
|
139
|
-
runAssertion({
|
|
140
|
-
values: Object.keys(node),
|
|
141
|
-
rawValues: value,
|
|
142
|
-
assert,
|
|
143
|
-
location: currentLocation,
|
|
144
|
-
})
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
222
|
+
assertion.subject.type
|
|
223
|
+
)} ${colorize.blue(properties.join(', '))} didn't meet the assertions: ${
|
|
224
|
+
assertionMessageTemplates.problems
|
|
225
|
+
}`.replace(/ +/g, ' ');
|
|
226
|
+
|
|
227
|
+
const problems = applyAssertions(assertion, getAssertsToApply(assertion), {
|
|
228
|
+
rawLocation,
|
|
229
|
+
rawNode,
|
|
230
|
+
resolve,
|
|
231
|
+
location,
|
|
232
|
+
node,
|
|
233
|
+
});
|
|
148
234
|
|
|
149
|
-
const problems = assertResults.flat();
|
|
150
235
|
if (problems.length) {
|
|
151
236
|
const message = assertion.message || defaultMessage;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
237
|
+
for (const problem of problems) {
|
|
238
|
+
const problemMessage = problem.message ? problem.message : defaultMessage;
|
|
239
|
+
report({
|
|
240
|
+
message: message.replace(assertionMessageTemplates.problems, problemMessage),
|
|
241
|
+
location: problem.location || location,
|
|
242
|
+
forceSeverity: assertion.severity || 'error',
|
|
243
|
+
suggest: assertion.suggest || [],
|
|
244
|
+
ruleId: assertId,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
160
247
|
}
|
|
161
248
|
};
|
|
162
249
|
}
|
|
163
250
|
|
|
164
|
-
function getProblemsLocation(problems: AssertResult[]) {
|
|
165
|
-
return problems.length ? problems[0].location : undefined;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function getProblemsMessage(problems: AssertResult[]) {
|
|
169
|
-
return problems.length === 1
|
|
170
|
-
? problems[0].message ?? ''
|
|
171
|
-
: problems.map((problem) => `\n- ${problem.message ?? ''}`).join('');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
251
|
export function getIntersectionLength(keys: string[], properties: string[]): number {
|
|
175
252
|
const props = new Set(properties);
|
|
176
253
|
let count = 0;
|
package/src/rules/common/spec.ts
CHANGED
|
@@ -158,6 +158,13 @@ export const OasSpec: Oas3Rule | Oas2Rule = () => {
|
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
+
|
|
162
|
+
if (propName === 'nullable' && !node.type) {
|
|
163
|
+
report({
|
|
164
|
+
message: 'The `type` field must be defined when the `nullable` field is used.',
|
|
165
|
+
location: location.child([propName]),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
161
168
|
}
|
|
162
169
|
},
|
|
163
170
|
};
|
package/src/rules/oas2/index.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { Oas2Rule } from '../../visitors';
|
|
|
2
2
|
import { OasSpec } from '../common/spec';
|
|
3
3
|
import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
|
|
4
4
|
import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
|
|
5
|
-
import { InfoDescription } from '../common/info-description';
|
|
6
5
|
import { InfoContact } from '../common/info-contact';
|
|
7
6
|
import { InfoLicense } from '../common/info-license';
|
|
8
7
|
import { InfoLicenseUrl } from '../common/info-license-url';
|
|
@@ -45,7 +44,6 @@ export const rules = {
|
|
|
45
44
|
spec: OasSpec as Oas2Rule,
|
|
46
45
|
'no-invalid-schema-examples': NoInvalidSchemaExamples,
|
|
47
46
|
'no-invalid-parameter-examples': NoInvalidParameterExamples,
|
|
48
|
-
'info-description': InfoDescription as Oas2Rule,
|
|
49
47
|
'info-contact': InfoContact as Oas2Rule,
|
|
50
48
|
'info-license': InfoLicense as Oas2Rule,
|
|
51
49
|
'info-license-url': InfoLicenseUrl as Oas2Rule,
|
|
@@ -438,4 +438,36 @@ describe('no-invalid-media-type-examples', () => {
|
|
|
438
438
|
]
|
|
439
439
|
`);
|
|
440
440
|
});
|
|
441
|
+
|
|
442
|
+
it('should not report if allOf used with discriminator', async () => {
|
|
443
|
+
const document = parseYamlToDocument(
|
|
444
|
+
outdent`
|
|
445
|
+
openapi: 3.0.0
|
|
446
|
+
paths:
|
|
447
|
+
/pet:
|
|
448
|
+
get:
|
|
449
|
+
responses:
|
|
450
|
+
'200':
|
|
451
|
+
content:
|
|
452
|
+
application/json:
|
|
453
|
+
schema:
|
|
454
|
+
discriminator:
|
|
455
|
+
propertyName: powerSource
|
|
456
|
+
mapping: {}
|
|
457
|
+
allOf: []
|
|
458
|
+
examples:
|
|
459
|
+
first:
|
|
460
|
+
value: {}
|
|
461
|
+
`,
|
|
462
|
+
'foobar.yaml'
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const results = await lintDocument({
|
|
466
|
+
externalRefResolver: new BaseResolver(),
|
|
467
|
+
document,
|
|
468
|
+
config: await makeConfig({ 'no-invalid-media-type-examples': 'error' }),
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
472
|
+
});
|
|
441
473
|
});
|
package/src/rules/oas3/index.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { OperationIdUrlSafe } from '../common/operation-operationId-url-safe';
|
|
|
15
15
|
import { TagsAlphabetical } from '../common/tags-alphabetical';
|
|
16
16
|
import { NoServerExample } from './no-server-example.com';
|
|
17
17
|
import { NoServerTrailingSlash } from './no-server-trailing-slash';
|
|
18
|
-
import { InfoDescription } from '../common/info-description';
|
|
19
18
|
import { TagDescription } from '../common/tag-description';
|
|
20
19
|
import { InfoContact } from '../common/info-contact';
|
|
21
20
|
import { InfoLicense } from '../common/info-license';
|
|
@@ -53,7 +52,6 @@ import { Operation4xxProblemDetailsRfc7807 } from './operation-4xx-problem-detai
|
|
|
53
52
|
|
|
54
53
|
export const rules = {
|
|
55
54
|
spec: OasSpec,
|
|
56
|
-
'info-description': InfoDescription,
|
|
57
55
|
'info-contact': InfoContact,
|
|
58
56
|
'info-license': InfoLicense,
|
|
59
57
|
'info-license-url': InfoLicenseUrl,
|