@redocly/openapi-core 1.0.0-beta.109 → 1.0.0-beta.111
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 +44 -25
- 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/common/no-ambiguous-paths.js +1 -1
- package/lib/rules/common/no-identical-paths.js +4 -4
- package/lib/rules/common/operation-2xx-response.js +2 -2
- package/lib/rules/common/operation-4xx-response.js +2 -2
- package/lib/rules/common/path-not-include-query.js +1 -1
- package/lib/rules/common/path-params-defined.js +7 -2
- package/lib/rules/common/response-contains-header.js +2 -2
- package/lib/rules/common/security-defined.js +10 -5
- package/lib/rules/common/spec.js +14 -12
- package/lib/rules/oas3/request-mime-type.js +1 -1
- package/lib/rules/oas3/response-mime-type.js +1 -1
- package/lib/rules/other/stats.d.ts +1 -1
- package/lib/rules/other/stats.js +1 -1
- package/lib/rules/utils.d.ts +1 -0
- package/lib/rules/utils.js +18 -2
- package/lib/types/oas2.js +6 -6
- package/lib/types/oas3.js +11 -11
- package/lib/types/oas3_1.js +3 -3
- package/lib/types/redocly-yaml.js +30 -5
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +13 -1
- package/lib/visitors.d.ts +7 -6
- package/lib/visitors.js +11 -3
- package/package.json +3 -5
- package/src/__tests__/__snapshots__/bundle.test.ts.snap +1 -1
- package/src/__tests__/lint.test.ts +88 -0
- package/src/__tests__/utils.test.ts +11 -0
- package/src/__tests__/walk.test.ts +2 -2
- package/src/config/__tests__/config-resolvers.test.ts +62 -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 +30 -6
- 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/__tests__/operation-2xx-response.test.ts +37 -0
- package/src/rules/common/__tests__/operation-4xx-response.test.ts +37 -0
- package/src/rules/common/__tests__/path-params-defined.test.ts +69 -0
- package/src/rules/common/__tests__/security-defined.test.ts +6 -6
- package/src/rules/common/__tests__/spec.test.ts +125 -0
- package/src/rules/common/assertions/__tests__/asserts.test.ts +491 -428
- package/src/rules/common/assertions/__tests__/utils.test.ts +2 -2
- 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/common/no-ambiguous-paths.ts +1 -1
- package/src/rules/common/no-identical-paths.ts +4 -4
- package/src/rules/common/operation-2xx-response.ts +2 -2
- package/src/rules/common/operation-4xx-response.ts +2 -2
- package/src/rules/common/path-not-include-query.ts +1 -1
- package/src/rules/common/path-params-defined.ts +9 -2
- package/src/rules/common/response-contains-header.ts +6 -1
- package/src/rules/common/security-defined.ts +10 -5
- package/src/rules/common/spec.ts +15 -11
- package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +51 -2
- package/src/rules/oas3/__tests__/response-contains-header.test.ts +116 -0
- package/src/rules/oas3/request-mime-type.ts +1 -1
- package/src/rules/oas3/response-mime-type.ts +1 -1
- package/src/rules/other/stats.ts +1 -1
- package/src/rules/utils.ts +24 -1
- package/src/types/oas2.ts +6 -6
- package/src/types/oas3.ts +11 -11
- package/src/types/oas3_1.ts +3 -3
- package/src/types/redocly-yaml.ts +30 -4
- package/src/utils.ts +13 -0
- package/src/visitors.ts +25 -10
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -65,7 +65,7 @@ describe('Oas3 assertions', () => {
|
|
|
65
65
|
matchParentKeys: ['put'],
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
|
-
type: '
|
|
68
|
+
type: 'Responses',
|
|
69
69
|
matchParentKeys: [201, 200],
|
|
70
70
|
},
|
|
71
71
|
];
|
|
@@ -75,7 +75,7 @@ describe('Oas3 assertions', () => {
|
|
|
75
75
|
expect(visitors).toMatchInlineSnapshot(`
|
|
76
76
|
Object {
|
|
77
77
|
"Operation": Object {
|
|
78
|
-
"
|
|
78
|
+
"Responses": Object {
|
|
79
79
|
"MediaTypesMap": [Function],
|
|
80
80
|
"skip": [Function],
|
|
81
81
|
},
|
|
@@ -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 {
|
|
@@ -5,7 +5,7 @@ import { Oas2Paths } from '../../typings/swagger';
|
|
|
5
5
|
|
|
6
6
|
export const NoAmbiguousPaths: Oas3Rule | Oas2Rule = () => {
|
|
7
7
|
return {
|
|
8
|
-
|
|
8
|
+
Paths(pathMap: Oas3Paths | Oas2Paths, { report, location }: UserContext) {
|
|
9
9
|
const seenPaths: string[] = [];
|
|
10
10
|
|
|
11
11
|
for (const currentPath of Object.keys(pathMap)) {
|
|
@@ -5,18 +5,18 @@ import { Oas2Paths } from '../../typings/swagger';
|
|
|
5
5
|
|
|
6
6
|
export const NoIdenticalPaths: Oas3Rule | Oas2Rule = () => {
|
|
7
7
|
return {
|
|
8
|
-
|
|
9
|
-
const
|
|
8
|
+
Paths(pathMap: Oas3Paths | Oas2Paths, { report, location }: UserContext) {
|
|
9
|
+
const Paths = new Map<string, string>();
|
|
10
10
|
for (const pathName of Object.keys(pathMap)) {
|
|
11
11
|
const id = pathName.replace(/{.+?}/g, '{VARIABLE}');
|
|
12
|
-
const existingSamePath =
|
|
12
|
+
const existingSamePath = Paths.get(id);
|
|
13
13
|
if (existingSamePath) {
|
|
14
14
|
report({
|
|
15
15
|
message: `The path already exists which differs only by path parameter name(s): \`${existingSamePath}\` and \`${pathName}\`.`,
|
|
16
16
|
location: location.child([pathName]).key(),
|
|
17
17
|
});
|
|
18
18
|
} else {
|
|
19
|
-
|
|
19
|
+
Paths.set(id, pathName);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
},
|
|
@@ -3,8 +3,8 @@ import { UserContext } from '../../walk';
|
|
|
3
3
|
|
|
4
4
|
export const Operation2xxResponse: Oas3Rule | Oas2Rule = () => {
|
|
5
5
|
return {
|
|
6
|
-
|
|
7
|
-
const codes = Object.keys(responses);
|
|
6
|
+
Responses(responses: Record<string, object>, { report }: UserContext) {
|
|
7
|
+
const codes = Object.keys(responses || {});
|
|
8
8
|
if (!codes.some((code) => code === 'default' || /2[Xx0-9]{2}/.test(code))) {
|
|
9
9
|
report({
|
|
10
10
|
message: 'Operation must have at least one `2XX` response.',
|
|
@@ -3,8 +3,8 @@ import { UserContext } from '../../walk';
|
|
|
3
3
|
|
|
4
4
|
export const Operation4xxResponse: Oas3Rule | Oas2Rule = () => {
|
|
5
5
|
return {
|
|
6
|
-
|
|
7
|
-
const codes = Object.keys(responses);
|
|
6
|
+
Responses(responses: Record<string, object>, { report }: UserContext) {
|
|
7
|
+
const codes = Object.keys(responses || {});
|
|
8
8
|
|
|
9
9
|
if (!codes.some((code) => /4[Xx0-9]{2}/.test(code))) {
|
|
10
10
|
report({
|
|
@@ -9,6 +9,7 @@ export const PathParamsDefined: Oas3Rule | Oas2Rule = () => {
|
|
|
9
9
|
let pathTemplateParams: Set<string>;
|
|
10
10
|
let definedPathParams: Set<string>;
|
|
11
11
|
let currentPath: string;
|
|
12
|
+
let definedOperationParams: Set<string>;
|
|
12
13
|
|
|
13
14
|
return {
|
|
14
15
|
PathItem: {
|
|
@@ -31,9 +32,15 @@ export const PathParamsDefined: Oas3Rule | Oas2Rule = () => {
|
|
|
31
32
|
}
|
|
32
33
|
},
|
|
33
34
|
Operation: {
|
|
35
|
+
enter() {
|
|
36
|
+
definedOperationParams = new Set();
|
|
37
|
+
},
|
|
34
38
|
leave(_op: object, { report, location }: UserContext) {
|
|
35
39
|
for (const templateParam of Array.from(pathTemplateParams.keys())) {
|
|
36
|
-
if (
|
|
40
|
+
if (
|
|
41
|
+
!definedOperationParams.has(templateParam) &&
|
|
42
|
+
!definedPathParams.has(templateParam)
|
|
43
|
+
) {
|
|
37
44
|
report({
|
|
38
45
|
message: `The operation does not define the path parameter \`{${templateParam}}\` expected by path \`${currentPath}\`.`,
|
|
39
46
|
location: location.child(['parameters']).key(), // report on operation
|
|
@@ -43,7 +50,7 @@ export const PathParamsDefined: Oas3Rule | Oas2Rule = () => {
|
|
|
43
50
|
},
|
|
44
51
|
Parameter(parameter: Oas2Parameter | Oas3Parameter, { report, location }: UserContext) {
|
|
45
52
|
if (parameter.in === 'path' && parameter.name) {
|
|
46
|
-
|
|
53
|
+
definedOperationParams.add(parameter.name);
|
|
47
54
|
if (!pathTemplateParams.has(parameter.name)) {
|
|
48
55
|
report({
|
|
49
56
|
message: `Path parameter \`${parameter.name}\` is not used in the path \`${currentPath}\`.`,
|
|
@@ -16,7 +16,12 @@ export const ResponseContainsHeader: Oas3Rule | Oas2Rule = (options) => {
|
|
|
16
16
|
names[getMatchingStatusCodeRange(key).toLowerCase()] ||
|
|
17
17
|
[];
|
|
18
18
|
for (const expectedHeader of expectedHeaders) {
|
|
19
|
-
if (
|
|
19
|
+
if (
|
|
20
|
+
!response?.headers ||
|
|
21
|
+
!Object.keys(response?.headers).some(
|
|
22
|
+
(header) => header.toLowerCase() === expectedHeader.toLowerCase()
|
|
23
|
+
)
|
|
24
|
+
) {
|
|
20
25
|
report({
|
|
21
26
|
message: `Response object must contain a "${expectedHeader}" header.`,
|
|
22
27
|
location: location.child('headers').key(),
|