@redocly/openapi-core 1.0.0-beta.109 → 1.0.0-beta.110
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/README.md +2 -2
- package/lib/config/config-resolvers.js +21 -3
- package/lib/config/config.d.ts +1 -0
- package/lib/config/config.js +1 -0
- package/lib/config/load.d.ts +8 -2
- package/lib/config/load.js +4 -2
- package/lib/config/types.d.ts +10 -0
- package/lib/config/utils.js +2 -2
- package/lib/rules/ajv.d.ts +1 -1
- package/lib/rules/ajv.js +5 -5
- package/lib/rules/common/assertions/asserts.d.ts +3 -5
- package/lib/rules/common/assertions/asserts.js +137 -97
- package/lib/rules/common/assertions/index.js +2 -6
- package/lib/rules/common/assertions/utils.d.ts +12 -6
- package/lib/rules/common/assertions/utils.js +33 -20
- package/lib/rules/utils.js +1 -1
- package/lib/types/redocly-yaml.js +16 -1
- package/package.json +3 -5
- package/src/__tests__/lint.test.ts +88 -0
- package/src/config/__tests__/config-resolvers.test.ts +37 -1
- package/src/config/__tests__/config.test.ts +5 -0
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml +16 -0
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml +16 -0
- package/src/config/__tests__/fixtures/resolve-config/plugin.js +11 -0
- package/src/config/__tests__/load.test.ts +1 -1
- package/src/config/__tests__/resolve-plugins.test.ts +3 -3
- package/src/config/config-resolvers.ts +28 -5
- package/src/config/config.ts +2 -0
- package/src/config/load.ts +10 -4
- package/src/config/types.ts +13 -0
- package/src/config/utils.ts +1 -0
- package/src/rules/ajv.ts +4 -4
- package/src/rules/common/assertions/__tests__/asserts.test.ts +491 -428
- package/src/rules/common/assertions/asserts.ts +155 -97
- package/src/rules/common/assertions/index.ts +2 -11
- package/src/rules/common/assertions/utils.ts +66 -36
- package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +51 -2
- package/src/rules/utils.ts +2 -1
- package/src/types/redocly-yaml.ts +16 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { AssertResult, CustomFunction } from 'core/src/config/types';
|
|
1
2
|
import { Location } from '../../../ref-utils';
|
|
2
|
-
import { isString as runOnValue } from '../../../utils';
|
|
3
|
+
import { isString as runOnValue, isTruthy } from '../../../utils';
|
|
3
4
|
import {
|
|
4
5
|
OrderOptions,
|
|
5
6
|
OrderDirection,
|
|
@@ -8,10 +9,9 @@ import {
|
|
|
8
9
|
regexFromString,
|
|
9
10
|
} from './utils';
|
|
10
11
|
|
|
11
|
-
type AssertResult = { isValid: boolean; location?: Location };
|
|
12
12
|
type Asserts = Record<
|
|
13
13
|
string,
|
|
14
|
-
(value: any, condition: any, baseLocation: Location, rawValue?: any) => AssertResult
|
|
14
|
+
(value: any, condition: any, baseLocation: Location, rawValue?: any) => AssertResult[]
|
|
15
15
|
>;
|
|
16
16
|
|
|
17
17
|
export const runOnKeysSet = new Set([
|
|
@@ -43,57 +43,80 @@ export const runOnValuesSet = new Set([
|
|
|
43
43
|
|
|
44
44
|
export const asserts: Asserts = {
|
|
45
45
|
pattern: (value: string | string[], condition: string, baseLocation: Location) => {
|
|
46
|
-
if (typeof value === 'undefined') return
|
|
46
|
+
if (typeof value === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
|
|
47
47
|
const values = runOnValue(value) ? [value] : value;
|
|
48
48
|
const regx = regexFromString(condition);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
|
|
50
|
+
return values
|
|
51
|
+
.map(
|
|
52
|
+
(_val) =>
|
|
53
|
+
!regx?.test(_val) && {
|
|
54
|
+
message: `"${_val}" should match a regex ${condition}`,
|
|
55
|
+
location: runOnValue(value) ? baseLocation : baseLocation.key(),
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
.filter(isTruthy);
|
|
55
59
|
},
|
|
56
60
|
enum: (value: string | string[], condition: string[], baseLocation: Location) => {
|
|
57
|
-
if (typeof value === 'undefined') return
|
|
61
|
+
if (typeof value === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
|
|
58
62
|
const values = runOnValue(value) ? [value] : value;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
return values
|
|
64
|
+
.map(
|
|
65
|
+
(_val) =>
|
|
66
|
+
!condition.includes(_val) && {
|
|
67
|
+
message: `"${_val}" should be one of the predefined values`,
|
|
68
|
+
location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
.filter(isTruthy);
|
|
68
72
|
},
|
|
69
73
|
defined: (value: string | undefined, condition: boolean = true, baseLocation: Location) => {
|
|
70
74
|
const isDefined = typeof value !== 'undefined';
|
|
71
|
-
|
|
75
|
+
const isValid = condition ? isDefined : !isDefined;
|
|
76
|
+
return isValid
|
|
77
|
+
? []
|
|
78
|
+
: [
|
|
79
|
+
{
|
|
80
|
+
message: condition ? `Should be defined` : 'Should be not defined',
|
|
81
|
+
location: baseLocation,
|
|
82
|
+
},
|
|
83
|
+
];
|
|
72
84
|
},
|
|
73
85
|
required: (value: string[], keys: string[], baseLocation: Location) => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
return keys
|
|
87
|
+
.map(
|
|
88
|
+
(requiredKey) =>
|
|
89
|
+
!value.includes(requiredKey) && {
|
|
90
|
+
message: `${requiredKey} is required`,
|
|
91
|
+
location: baseLocation.key(),
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
.filter(isTruthy);
|
|
80
95
|
},
|
|
81
96
|
disallowed: (value: string | string[], condition: string[], baseLocation: Location) => {
|
|
82
|
-
if (typeof value === 'undefined') return
|
|
97
|
+
if (typeof value === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
|
|
83
98
|
const values = runOnValue(value) ? [value] : value;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
return values
|
|
100
|
+
.map(
|
|
101
|
+
(_val) =>
|
|
102
|
+
condition.includes(_val) && {
|
|
103
|
+
message: `"${_val}" is disallowed`,
|
|
104
|
+
location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
.filter(isTruthy);
|
|
93
108
|
},
|
|
94
109
|
undefined: (value: any, condition: boolean = true, baseLocation: Location) => {
|
|
95
110
|
const isUndefined = typeof value === 'undefined';
|
|
96
|
-
|
|
111
|
+
const isValid = condition ? isUndefined : !isUndefined;
|
|
112
|
+
return isValid
|
|
113
|
+
? []
|
|
114
|
+
: [
|
|
115
|
+
{
|
|
116
|
+
message: condition ? `Should not be defined` : 'Should be defined',
|
|
117
|
+
location: baseLocation,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
97
120
|
},
|
|
98
121
|
nonEmpty: (
|
|
99
122
|
value: string | undefined | null,
|
|
@@ -101,85 +124,120 @@ export const asserts: Asserts = {
|
|
|
101
124
|
baseLocation: Location
|
|
102
125
|
) => {
|
|
103
126
|
const isEmpty = typeof value === 'undefined' || value === null || value === '';
|
|
104
|
-
|
|
127
|
+
const isValid = condition ? !isEmpty : isEmpty;
|
|
128
|
+
return isValid
|
|
129
|
+
? []
|
|
130
|
+
: [
|
|
131
|
+
{
|
|
132
|
+
message: condition ? `Should not be empty` : 'Should be empty',
|
|
133
|
+
location: baseLocation,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
105
136
|
},
|
|
106
137
|
minLength: (value: string | any[], condition: number, baseLocation: Location) => {
|
|
107
|
-
if (typeof value === 'undefined'
|
|
108
|
-
return {
|
|
138
|
+
if (typeof value === 'undefined' || value.length >= condition) return []; // property doesn't exist, no need to lint it with this assert
|
|
139
|
+
return [{ message: `Should have at least ${condition} characters`, location: baseLocation }];
|
|
109
140
|
},
|
|
110
141
|
maxLength: (value: string | any[], condition: number, baseLocation: Location) => {
|
|
111
|
-
if (typeof value === 'undefined'
|
|
112
|
-
return {
|
|
142
|
+
if (typeof value === 'undefined' || value.length <= condition) return []; // property doesn't exist, no need to lint it with this assert
|
|
143
|
+
return [{ message: `Should have at most ${condition} characters`, location: baseLocation }];
|
|
113
144
|
},
|
|
114
145
|
casing: (value: string | string[], condition: string, baseLocation: Location) => {
|
|
115
|
-
if (typeof value === 'undefined') return
|
|
146
|
+
if (typeof value === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
|
|
116
147
|
const values: string[] = runOnValue(value) ? [value] : value;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
case 'COBOL-CASE':
|
|
136
|
-
matchCase = !!_val.match(/^([A-Z][A-Z0-9]*)(-[A-Z0-9]+)*$/g);
|
|
137
|
-
break;
|
|
138
|
-
case 'flatcase':
|
|
139
|
-
matchCase = !!_val.match(/^[a-z][a-z0-9]+$/g);
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
if (!matchCase) {
|
|
143
|
-
return {
|
|
144
|
-
isValid: false,
|
|
145
|
-
location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return { isValid: true };
|
|
148
|
+
const casingRegexes: Record<string, RegExp> = {
|
|
149
|
+
camelCase: /^[a-z][a-zA-Z0-9]+$/g,
|
|
150
|
+
'kebab-case': /^([a-z][a-z0-9]*)(-[a-z0-9]+)*$/g,
|
|
151
|
+
snake_case: /^([a-z][a-z0-9]*)(_[a-z0-9]+)*$/g,
|
|
152
|
+
PascalCase: /^[A-Z][a-zA-Z0-9]+$/g,
|
|
153
|
+
MACRO_CASE: /^([A-Z][A-Z0-9]*)(_[A-Z0-9]+)*$/g,
|
|
154
|
+
'COBOL-CASE': /^([A-Z][A-Z0-9]*)(-[A-Z0-9]+)*$/g,
|
|
155
|
+
flatcase: /^[a-z][a-z0-9]+$/g,
|
|
156
|
+
};
|
|
157
|
+
return values
|
|
158
|
+
.map(
|
|
159
|
+
(_val) =>
|
|
160
|
+
!_val.match(casingRegexes[condition]) && {
|
|
161
|
+
message: `"${_val}" should use ${condition}`,
|
|
162
|
+
location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
.filter(isTruthy);
|
|
150
166
|
},
|
|
151
167
|
sortOrder: (value: any[], condition: OrderOptions | OrderDirection, baseLocation: Location) => {
|
|
152
|
-
if (typeof value === 'undefined'
|
|
153
|
-
|
|
168
|
+
if (typeof value === 'undefined' || isOrdered(value, condition)) return [];
|
|
169
|
+
const direction = (condition as OrderOptions).direction || (condition as OrderDirection);
|
|
170
|
+
const property = (condition as OrderOptions).property;
|
|
171
|
+
return [
|
|
172
|
+
{
|
|
173
|
+
message: `Should be sorted in ${
|
|
174
|
+
direction === 'asc' ? 'an ascending' : 'a descending'
|
|
175
|
+
} order${property ? ` by property ${property}` : ''}`,
|
|
176
|
+
location: baseLocation,
|
|
177
|
+
},
|
|
178
|
+
];
|
|
154
179
|
},
|
|
155
180
|
mutuallyExclusive: (value: string[], condition: string[], baseLocation: Location) => {
|
|
156
|
-
|
|
181
|
+
if (getIntersectionLength(value, condition) < 2) return [];
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
message: `${condition.join(', ')} keys should be mutually exclusive`,
|
|
185
|
+
location: baseLocation.key(),
|
|
186
|
+
},
|
|
187
|
+
];
|
|
157
188
|
},
|
|
158
189
|
mutuallyRequired: (value: string[], condition: string[], baseLocation: Location) => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
getIntersectionLength(value, condition)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
190
|
+
const isValid =
|
|
191
|
+
getIntersectionLength(value, condition) > 0
|
|
192
|
+
? getIntersectionLength(value, condition) === condition.length
|
|
193
|
+
: true;
|
|
194
|
+
return isValid
|
|
195
|
+
? []
|
|
196
|
+
: [
|
|
197
|
+
{
|
|
198
|
+
message: `Properties ${condition.join(', ')} are mutually required`,
|
|
199
|
+
location: baseLocation.key(),
|
|
200
|
+
},
|
|
201
|
+
];
|
|
166
202
|
},
|
|
167
203
|
requireAny: (value: string[], condition: string[], baseLocation: Location) => {
|
|
168
|
-
return
|
|
204
|
+
return getIntersectionLength(value, condition) >= 1
|
|
205
|
+
? []
|
|
206
|
+
: [
|
|
207
|
+
{
|
|
208
|
+
message: `Should have any of ${condition.join(', ')}`,
|
|
209
|
+
location: baseLocation.key(),
|
|
210
|
+
},
|
|
211
|
+
];
|
|
169
212
|
},
|
|
170
213
|
ref: (_value: any, condition: string | boolean, baseLocation, rawValue: any) => {
|
|
171
|
-
if (typeof rawValue === 'undefined') return
|
|
214
|
+
if (typeof rawValue === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
|
|
172
215
|
const hasRef = rawValue.hasOwnProperty('$ref');
|
|
173
216
|
if (typeof condition === 'boolean') {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
217
|
+
const isValid = condition ? hasRef : !hasRef;
|
|
218
|
+
return isValid
|
|
219
|
+
? []
|
|
220
|
+
: [
|
|
221
|
+
{
|
|
222
|
+
message: condition ? `should use $ref` : 'should not use $ref',
|
|
223
|
+
location: hasRef ? baseLocation : baseLocation.key(),
|
|
224
|
+
},
|
|
225
|
+
];
|
|
178
226
|
}
|
|
179
227
|
const regex = regexFromString(condition);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
228
|
+
const isValid = hasRef && regex?.test(rawValue['$ref']);
|
|
229
|
+
return isValid
|
|
230
|
+
? []
|
|
231
|
+
: [
|
|
232
|
+
{
|
|
233
|
+
message: `$ref value should match ${condition}`,
|
|
234
|
+
location: hasRef ? baseLocation : baseLocation.key(),
|
|
235
|
+
},
|
|
236
|
+
];
|
|
184
237
|
},
|
|
185
238
|
};
|
|
239
|
+
|
|
240
|
+
export function buildAssertCustomFunction(fn: CustomFunction) {
|
|
241
|
+
return (value: string[], options: any, baseLocation: Location) =>
|
|
242
|
+
fn.call(null, value, options, baseLocation);
|
|
243
|
+
}
|
|
@@ -7,7 +7,7 @@ export const Assertions: Oas3Rule | Oas2Rule = (opts: object) => {
|
|
|
7
7
|
|
|
8
8
|
// As 'Assertions' has an array of asserts,
|
|
9
9
|
// that array spreads into an 'opts' object on init rules phase here
|
|
10
|
-
// https://github.com/Redocly/redocly-cli/blob/
|
|
10
|
+
// https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/config/config.ts#L311
|
|
11
11
|
// that is why we need to iterate through 'opts' values;
|
|
12
12
|
// before - filter only object 'opts' values
|
|
13
13
|
const assertions: any[] = Object.values(opts).filter(
|
|
@@ -17,7 +17,6 @@ export const Assertions: Oas3Rule | Oas2Rule = (opts: object) => {
|
|
|
17
17
|
for (const [index, assertion] of assertions.entries()) {
|
|
18
18
|
const assertId =
|
|
19
19
|
(assertion.assertionId && `${assertion.assertionId} assertion`) || `assertion #${index + 1}`;
|
|
20
|
-
|
|
21
20
|
if (!assertion.subject) {
|
|
22
21
|
throw new Error(`${assertId}: 'subject' is required`);
|
|
23
22
|
}
|
|
@@ -30,12 +29,8 @@ export const Assertions: Oas3Rule | Oas2Rule = (opts: object) => {
|
|
|
30
29
|
.filter((assertName: string) => assertion[assertName] !== undefined)
|
|
31
30
|
.map((assertName: string) => {
|
|
32
31
|
return {
|
|
33
|
-
assertId,
|
|
34
32
|
name: assertName,
|
|
35
33
|
conditions: assertion[assertName],
|
|
36
|
-
message: assertion.message,
|
|
37
|
-
severity: assertion.severity || 'error',
|
|
38
|
-
suggest: assertion.suggest || [],
|
|
39
34
|
runsOnKeys: runOnKeysSet.has(assertName),
|
|
40
35
|
runsOnValues: runOnValuesSet.has(assertName),
|
|
41
36
|
};
|
|
@@ -61,11 +56,7 @@ export const Assertions: Oas3Rule | Oas2Rule = (opts: object) => {
|
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
for (const subject of subjects) {
|
|
64
|
-
const subjectVisitor = buildSubjectVisitor(
|
|
65
|
-
assertion.property,
|
|
66
|
-
assertsToApply,
|
|
67
|
-
assertion.context
|
|
68
|
-
);
|
|
59
|
+
const subjectVisitor = buildSubjectVisitor(assertId, assertion, assertsToApply);
|
|
69
60
|
const visitorObject = buildVisitorObject(subject, assertion.context, subjectVisitor);
|
|
70
61
|
visitors.push(visitorObject);
|
|
71
62
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { AssertResult, RuleSeverity } from '../../../config';
|
|
2
|
+
import { colorize } from '../../../logger';
|
|
1
3
|
import { isRef, Location } from '../../../ref-utils';
|
|
2
|
-
import {
|
|
4
|
+
import { UserContext } from '../../../walk';
|
|
3
5
|
import { asserts } from './asserts';
|
|
4
6
|
|
|
5
7
|
export type OrderDirection = 'asc' | 'desc';
|
|
@@ -9,13 +11,18 @@ export type OrderOptions = {
|
|
|
9
11
|
property: string;
|
|
10
12
|
};
|
|
11
13
|
|
|
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
|
+
|
|
12
23
|
export type AssertToApply = {
|
|
13
24
|
name: string;
|
|
14
|
-
assertId?: string;
|
|
15
25
|
conditions: any;
|
|
16
|
-
message?: string;
|
|
17
|
-
severity?: ProblemSeverity;
|
|
18
|
-
suggest?: string[];
|
|
19
26
|
runsOnKeys: boolean;
|
|
20
27
|
runsOnValues: boolean;
|
|
21
28
|
};
|
|
@@ -73,19 +80,20 @@ export function buildVisitorObject(
|
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
export function buildSubjectVisitor(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
assertId: string,
|
|
84
|
+
assertion: Assertion,
|
|
85
|
+
asserts: AssertToApply[]
|
|
79
86
|
) {
|
|
80
87
|
return (
|
|
81
88
|
node: any,
|
|
82
89
|
{ report, location, rawLocation, key, type, resolve, rawNode }: UserContext
|
|
83
90
|
) => {
|
|
91
|
+
let properties = assertion.property;
|
|
84
92
|
// We need to check context's last node if it has the same type as subject node;
|
|
85
93
|
// if yes - that means we didn't create context's last node visitor,
|
|
86
94
|
// so we need to handle 'matchParentKeys' and 'excludeParentKeys' conditions here;
|
|
87
|
-
if (context) {
|
|
88
|
-
const lastContextNode = context[context.length - 1];
|
|
95
|
+
if (assertion.context) {
|
|
96
|
+
const lastContextNode = assertion.context[assertion.context.length - 1];
|
|
89
97
|
if (lastContextNode.type === type.name) {
|
|
90
98
|
const matchParentKeys = lastContextNode.matchParentKeys;
|
|
91
99
|
const excludeParentKeys = lastContextNode.excludeParentKeys;
|
|
@@ -103,34 +111,66 @@ export function buildSubjectVisitor(
|
|
|
103
111
|
properties = Array.isArray(properties) ? properties : [properties];
|
|
104
112
|
}
|
|
105
113
|
|
|
114
|
+
const defaultMessage = `${colorize.blue(assertId)} failed because the ${colorize.blue(
|
|
115
|
+
assertion.subject
|
|
116
|
+
)}${colorize.blue(
|
|
117
|
+
properties ? ` ${(properties as string[]).join(', ')}` : ''
|
|
118
|
+
)} didn't meet the assertions: {{problems}}`;
|
|
119
|
+
|
|
120
|
+
const assertResults: Array<AssertResult[]> = [];
|
|
106
121
|
for (const assert of asserts) {
|
|
107
122
|
const currentLocation = assert.name === 'ref' ? rawLocation : location;
|
|
108
123
|
if (properties) {
|
|
109
124
|
for (const property of properties) {
|
|
110
125
|
// we can have resolvable scalar so need to resolve value here.
|
|
111
126
|
const value = isRef(node[property]) ? resolve(node[property])?.node : node[property];
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
assertResults.push(
|
|
128
|
+
runAssertion({
|
|
129
|
+
values: value,
|
|
130
|
+
rawValues: rawNode[property],
|
|
131
|
+
assert,
|
|
132
|
+
location: currentLocation.child(property),
|
|
133
|
+
})
|
|
134
|
+
);
|
|
119
135
|
}
|
|
120
136
|
} else {
|
|
121
137
|
const value = assert.name === 'ref' ? rawNode : Object.keys(node);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
assertResults.push(
|
|
139
|
+
runAssertion({
|
|
140
|
+
values: Object.keys(node),
|
|
141
|
+
rawValues: value,
|
|
142
|
+
assert,
|
|
143
|
+
location: currentLocation,
|
|
144
|
+
})
|
|
145
|
+
);
|
|
129
146
|
}
|
|
130
147
|
}
|
|
148
|
+
|
|
149
|
+
const problems = assertResults.flat();
|
|
150
|
+
if (problems.length) {
|
|
151
|
+
const message = assertion.message || defaultMessage;
|
|
152
|
+
|
|
153
|
+
report({
|
|
154
|
+
message: message.replace('{{problems}}', getProblemsMessage(problems)),
|
|
155
|
+
location: getProblemsLocation(problems) || location,
|
|
156
|
+
forceSeverity: assertion.severity || 'error',
|
|
157
|
+
suggest: assertion.suggest || [],
|
|
158
|
+
ruleId: assertId,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
131
161
|
};
|
|
132
162
|
}
|
|
133
163
|
|
|
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
|
+
|
|
134
174
|
export function getIntersectionLength(keys: string[], properties: string[]): number {
|
|
135
175
|
const props = new Set(properties);
|
|
136
176
|
let count = 0;
|
|
@@ -170,20 +210,10 @@ type RunAssertionParams = {
|
|
|
170
210
|
rawValues: any;
|
|
171
211
|
assert: AssertToApply;
|
|
172
212
|
location: Location;
|
|
173
|
-
report: (problem: Problem) => void;
|
|
174
213
|
};
|
|
175
214
|
|
|
176
|
-
function runAssertion({ values, rawValues, assert, location
|
|
177
|
-
|
|
178
|
-
if (!lintResult.isValid) {
|
|
179
|
-
report({
|
|
180
|
-
message: assert.message || `The ${assert.assertId} doesn't meet required conditions`,
|
|
181
|
-
location: lintResult.location || location,
|
|
182
|
-
forceSeverity: assert.severity,
|
|
183
|
-
suggest: assert.suggest,
|
|
184
|
-
ruleId: assert.assertId,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
215
|
+
function runAssertion({ values, rawValues, assert, location }: RunAssertionParams): AssertResult[] {
|
|
216
|
+
return asserts[assert.name](values, assert.conditions, location, rawValues);
|
|
187
217
|
}
|
|
188
218
|
|
|
189
219
|
export function regexFromString(input: string): RegExp | null {
|
|
@@ -128,7 +128,7 @@ describe('no-invalid-media-type-examples', () => {
|
|
|
128
128
|
"source": "foobar.yaml",
|
|
129
129
|
},
|
|
130
130
|
],
|
|
131
|
-
"message": "Example value must conform to the schema: must NOT have
|
|
131
|
+
"message": "Example value must conform to the schema: must NOT have unevaluated properties \`c\`.",
|
|
132
132
|
"ruleId": "no-invalid-media-type-examples",
|
|
133
133
|
"severity": "error",
|
|
134
134
|
"suggest": Array [],
|
|
@@ -137,7 +137,7 @@ describe('no-invalid-media-type-examples', () => {
|
|
|
137
137
|
`);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
it('should not on
|
|
140
|
+
it('should not report on valid example with allowAdditionalProperties', async () => {
|
|
141
141
|
const document = parseYamlToDocument(
|
|
142
142
|
outdent`
|
|
143
143
|
openapi: 3.0.0
|
|
@@ -177,6 +177,55 @@ describe('no-invalid-media-type-examples', () => {
|
|
|
177
177
|
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
it('should not report on valid example with allowAdditionalProperties and allOf and $ref', async () => {
|
|
181
|
+
const document = parseYamlToDocument(
|
|
182
|
+
outdent`
|
|
183
|
+
openapi: 3.0.0
|
|
184
|
+
components:
|
|
185
|
+
schemas:
|
|
186
|
+
C:
|
|
187
|
+
properties:
|
|
188
|
+
c:
|
|
189
|
+
type: string
|
|
190
|
+
paths:
|
|
191
|
+
/pet:
|
|
192
|
+
get:
|
|
193
|
+
responses:
|
|
194
|
+
200:
|
|
195
|
+
content:
|
|
196
|
+
application/json:
|
|
197
|
+
example:
|
|
198
|
+
a: "string"
|
|
199
|
+
b: 13
|
|
200
|
+
c: "string"
|
|
201
|
+
schema:
|
|
202
|
+
type: object
|
|
203
|
+
allOf:
|
|
204
|
+
- $ref: '#/components/schemas/C'
|
|
205
|
+
properties:
|
|
206
|
+
a:
|
|
207
|
+
type: string
|
|
208
|
+
b:
|
|
209
|
+
type: number
|
|
210
|
+
|
|
211
|
+
`,
|
|
212
|
+
'foobar.yaml'
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const results = await lintDocument({
|
|
216
|
+
externalRefResolver: new BaseResolver(),
|
|
217
|
+
document,
|
|
218
|
+
config: await makeConfig({
|
|
219
|
+
'no-invalid-media-type-examples': {
|
|
220
|
+
severity: 'error',
|
|
221
|
+
allowAdditionalProperties: false,
|
|
222
|
+
},
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
227
|
+
});
|
|
228
|
+
|
|
180
229
|
it('should not on invalid examples', async () => {
|
|
181
230
|
const document = parseYamlToDocument(
|
|
182
231
|
outdent`
|
package/src/rules/utils.ts
CHANGED
|
@@ -109,7 +109,8 @@ export function validateExample(
|
|
|
109
109
|
message: `Example value must conform to the schema: ${error.message}.`,
|
|
110
110
|
location: {
|
|
111
111
|
...new Location(dataLoc.source, error.instancePath),
|
|
112
|
-
reportOnKey:
|
|
112
|
+
reportOnKey:
|
|
113
|
+
error.keyword === 'unevaluatedProperties' || error.keyword === 'additionalProperties',
|
|
113
114
|
},
|
|
114
115
|
from: location,
|
|
115
116
|
suggest: error.suggest,
|
|
@@ -165,6 +165,12 @@ const ConfigRoot: NodeType = {
|
|
|
165
165
|
doNotResolveExamples: { type: 'boolean' },
|
|
166
166
|
},
|
|
167
167
|
},
|
|
168
|
+
files: {
|
|
169
|
+
type: 'array',
|
|
170
|
+
items: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
168
174
|
},
|
|
169
175
|
};
|
|
170
176
|
|
|
@@ -187,6 +193,12 @@ const ConfigApisProperties: NodeType = {
|
|
|
187
193
|
...ConfigStyleguide.properties,
|
|
188
194
|
'features.openapi': 'ConfigReferenceDocs',
|
|
189
195
|
'features.mockServer': 'ConfigMockServer',
|
|
196
|
+
files: {
|
|
197
|
+
type: 'array',
|
|
198
|
+
items: {
|
|
199
|
+
type: 'string',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
190
202
|
},
|
|
191
203
|
required: ['root'],
|
|
192
204
|
};
|
|
@@ -275,6 +287,10 @@ const Assert: NodeType = {
|
|
|
275
287
|
ref: (value: string | boolean) =>
|
|
276
288
|
typeof value === 'string' ? { type: 'string' } : { type: 'boolean' },
|
|
277
289
|
},
|
|
290
|
+
additionalProperties: (_value: unknown, key: string) => {
|
|
291
|
+
if (/^\w+\/\w+$/.test(key)) return { type: 'object' };
|
|
292
|
+
return;
|
|
293
|
+
},
|
|
278
294
|
required: ['subject'],
|
|
279
295
|
};
|
|
280
296
|
|