@redocly/openapi-core 1.0.0-beta.108 → 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/benchmark/benches/resolve-with-no-external.bench.js +1 -1
- package/lib/bundle.d.ts +1 -1
- package/lib/bundle.js +4 -4
- package/lib/config/all.js +3 -1
- package/lib/config/config-resolvers.js +22 -4
- 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/minimal.js +3 -1
- package/lib/config/recommended.js +3 -1
- package/lib/config/rules.js +1 -1
- package/lib/config/types.d.ts +17 -0
- package/lib/config/utils.d.ts +2 -2
- package/lib/config/utils.js +44 -6
- package/lib/decorators/common/registry-dependencies.js +1 -1
- package/lib/format/format.d.ts +1 -1
- package/lib/format/format.js +22 -1
- package/lib/lint.js +2 -2
- package/lib/redocly/registry-api.d.ts +0 -1
- package/lib/redocly/registry-api.js +5 -4
- package/lib/resolve.js +3 -1
- 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 +1 -1
- package/lib/rules/common/operation-2xx-response.js +1 -1
- package/lib/rules/common/operation-4xx-response.js +1 -1
- package/lib/rules/common/operation-operationId.js +1 -1
- package/lib/rules/common/operation-tag-defined.js +1 -1
- package/lib/rules/common/path-not-include-query.js +1 -1
- package/lib/rules/common/security-defined.d.ts +2 -0
- package/lib/rules/common/{operation-security-defined.js → security-defined.js} +18 -4
- package/lib/rules/common/spec.js +12 -1
- package/lib/rules/common/tags-alphabetical.js +1 -1
- package/lib/rules/oas2/index.d.ts +1 -1
- package/lib/rules/oas2/index.js +2 -2
- package/lib/rules/oas2/remove-unused-components.js +1 -1
- package/lib/rules/oas2/request-mime-type.js +1 -1
- package/lib/rules/oas2/response-mime-type.js +1 -1
- package/lib/rules/oas3/index.js +6 -2
- package/lib/rules/oas3/no-empty-servers.js +1 -1
- package/lib/rules/oas3/no-server-variables-empty-enum.js +1 -1
- package/lib/rules/oas3/no-unused-components.js +1 -1
- package/lib/rules/oas3/operation-4xx-problem-details-rfc7807.d.ts +5 -0
- package/lib/rules/oas3/operation-4xx-problem-details-rfc7807.js +36 -0
- package/lib/rules/oas3/remove-unused-components.js +1 -1
- package/lib/rules/oas3/request-mime-type.js +1 -1
- package/lib/rules/oas3/response-mime-type.js +1 -1
- package/lib/rules/oas3/spec-components-invalid-map-name.d.ts +2 -0
- package/lib/rules/oas3/spec-components-invalid-map-name.js +46 -0
- package/lib/rules/other/stats.d.ts +2 -2
- package/lib/rules/other/stats.js +2 -2
- package/lib/rules/utils.js +1 -1
- package/lib/types/oas2.js +5 -5
- package/lib/types/oas3.js +27 -20
- package/lib/types/oas3_1.js +3 -3
- package/lib/types/redocly-yaml.js +60 -54
- package/lib/utils.d.ts +3 -3
- package/lib/utils.js +5 -5
- package/lib/visitors.d.ts +11 -11
- package/lib/visitors.js +13 -1
- package/package.json +3 -5
- package/src/__tests__/__snapshots__/bundle.test.ts.snap +3 -3
- package/src/__tests__/fixtures/extension.js +3 -3
- package/src/__tests__/format.test.ts +76 -0
- package/src/__tests__/lint.test.ts +184 -121
- package/src/__tests__/resolve-http.test.ts +1 -1
- package/src/__tests__/resolve.test.ts +9 -9
- package/src/__tests__/walk.test.ts +78 -10
- package/src/benchmark/benches/resolve-with-no-external.bench.ts +1 -1
- package/src/bundle.ts +4 -4
- package/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +6 -2
- package/src/config/__tests__/config-resolvers.test.ts +37 -1
- package/src/config/__tests__/config.test.ts +5 -0
- package/src/config/__tests__/fixtures/plugin-config.yaml +2 -3
- package/src/config/__tests__/fixtures/resolve-config/api/nested-config.yaml +11 -12
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-circular.yaml +7 -8
- 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-file.yaml +18 -19
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml +16 -0
- package/src/config/__tests__/fixtures/resolve-config/local-config.yaml +9 -10
- package/src/config/__tests__/fixtures/resolve-config/plugin.js +11 -0
- package/src/config/__tests__/fixtures/resolve-remote-configs/nested-remote-config.yaml +3 -4
- package/src/config/__tests__/fixtures/resolve-remote-configs/remote-config.yaml +4 -5
- package/src/config/__tests__/load.test.ts +13 -16
- package/src/config/__tests__/resolve-plugins.test.ts +3 -3
- package/src/config/__tests__/utils.test.ts +64 -4
- package/src/config/all.ts +3 -1
- package/src/config/config-resolvers.ts +30 -7
- package/src/config/config.ts +2 -0
- package/src/config/load.ts +13 -6
- package/src/config/minimal.ts +3 -1
- package/src/config/recommended.ts +3 -1
- package/src/config/rules.ts +2 -2
- package/src/config/types.ts +24 -0
- package/src/config/utils.ts +103 -13
- package/src/decorators/common/registry-dependencies.ts +1 -1
- package/src/format/format.ts +32 -2
- package/src/lint.ts +2 -2
- package/src/redocly/registry-api.ts +5 -4
- package/src/resolve.ts +3 -1
- package/src/rules/__tests__/utils.test.ts +1 -1
- package/src/rules/ajv.ts +4 -4
- package/src/rules/common/__tests__/no-enum-type-mismatch.test.ts +1 -0
- package/src/rules/common/__tests__/operation-2xx-response.test.ts +1 -1
- package/src/rules/common/__tests__/operation-4xx-response.test.ts +26 -3
- package/src/rules/common/__tests__/security-defined.test.ts +175 -0
- package/src/rules/common/__tests__/spec.test.ts +79 -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 +1 -1
- package/src/rules/common/operation-2xx-response.ts +1 -1
- package/src/rules/common/operation-4xx-response.ts +1 -1
- package/src/rules/common/operation-operationId.ts +1 -1
- package/src/rules/common/operation-tag-defined.ts +1 -1
- package/src/rules/common/path-not-include-query.ts +1 -1
- package/src/rules/common/{operation-security-defined.ts → security-defined.ts} +19 -4
- package/src/rules/common/spec.ts +15 -1
- package/src/rules/common/tags-alphabetical.ts +1 -1
- package/src/rules/oas2/index.ts +2 -2
- package/src/rules/oas2/remove-unused-components.ts +1 -1
- package/src/rules/oas2/request-mime-type.ts +1 -1
- package/src/rules/oas2/response-mime-type.ts +1 -1
- package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +51 -2
- package/src/rules/oas3/__tests__/operation-4xx-problem-details-rfc7807.test.ts +145 -0
- package/src/rules/oas3/__tests__/spec/spec.test.ts +10 -0
- package/src/rules/oas3/__tests__/spec-components-invalid-map-name.test.ts +217 -0
- package/src/rules/oas3/index.ts +6 -2
- package/src/rules/oas3/no-empty-servers.ts +1 -1
- package/src/rules/oas3/no-server-variables-empty-enum.ts +1 -1
- package/src/rules/oas3/no-unused-components.ts +1 -1
- package/src/rules/oas3/operation-4xx-problem-details-rfc7807.ts +36 -0
- package/src/rules/oas3/remove-unused-components.ts +1 -1
- package/src/rules/oas3/request-mime-type.ts +1 -1
- package/src/rules/oas3/response-mime-type.ts +1 -1
- package/src/rules/oas3/spec-components-invalid-map-name.ts +53 -0
- package/src/rules/other/stats.ts +2 -2
- package/src/rules/utils.ts +2 -1
- package/src/types/index.ts +2 -2
- package/src/types/oas2.ts +5 -5
- package/src/types/oas3.ts +27 -20
- package/src/types/oas3_1.ts +3 -3
- package/src/types/redocly-yaml.ts +66 -38
- package/src/utils.ts +11 -7
- package/src/visitors.ts +29 -13
- package/tsconfig.tsbuildinfo +1 -1
- package/lib/rules/common/operation-security-defined.d.ts +0 -2
- package/src/rules/common/__tests__/operation-security-defined.test.ts +0 -69
|
@@ -70,13 +70,13 @@ describe('Oas3 assertions', () => {
|
|
|
70
70
|
},
|
|
71
71
|
];
|
|
72
72
|
|
|
73
|
-
const visitors = buildVisitorObject('
|
|
73
|
+
const visitors = buildVisitorObject('MediaTypesMap', context, () => {}) as any;
|
|
74
74
|
|
|
75
75
|
expect(visitors).toMatchInlineSnapshot(`
|
|
76
76
|
Object {
|
|
77
77
|
"Operation": Object {
|
|
78
78
|
"ResponsesMap": Object {
|
|
79
|
-
"
|
|
79
|
+
"MediaTypesMap": [Function],
|
|
80
80
|
"skip": [Function],
|
|
81
81
|
},
|
|
82
82
|
"skip": [Function],
|
|
@@ -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
|
+
PathsMap(pathMap: Oas3Paths | Oas2Paths, { report, location }: UserContext) {
|
|
9
9
|
const seenPaths: string[] = [];
|
|
10
10
|
|
|
11
11
|
for (const currentPath of Object.keys(pathMap)) {
|
|
@@ -5,7 +5,7 @@ import { Oas2Paths } from '../../typings/swagger';
|
|
|
5
5
|
|
|
6
6
|
export const NoIdenticalPaths: Oas3Rule | Oas2Rule = () => {
|
|
7
7
|
return {
|
|
8
|
-
|
|
8
|
+
PathsMap(pathMap: Oas3Paths | Oas2Paths, { report, location }: UserContext) {
|
|
9
9
|
const pathsMap = new Map<string, string>();
|
|
10
10
|
for (const pathName of Object.keys(pathMap)) {
|
|
11
11
|
const id = pathName.replace(/{.+?}/g, '{VARIABLE}');
|
|
@@ -7,7 +7,7 @@ export const Operation2xxResponse: Oas3Rule | Oas2Rule = () => {
|
|
|
7
7
|
const codes = Object.keys(responses);
|
|
8
8
|
if (!codes.some((code) => code === 'default' || /2[Xx0-9]{2}/.test(code))) {
|
|
9
9
|
report({
|
|
10
|
-
message: 'Operation must have at least one `
|
|
10
|
+
message: 'Operation must have at least one `2XX` response.',
|
|
11
11
|
location: { reportOnKey: true },
|
|
12
12
|
});
|
|
13
13
|
}
|
|
@@ -8,7 +8,7 @@ export const Operation4xxResponse: Oas3Rule | Oas2Rule = () => {
|
|
|
8
8
|
|
|
9
9
|
if (!codes.some((code) => /4[Xx0-9]{2}/.test(code))) {
|
|
10
10
|
report({
|
|
11
|
-
message: 'Operation must have at least one `
|
|
11
|
+
message: 'Operation must have at least one `4XX` response.',
|
|
12
12
|
location: { reportOnKey: true },
|
|
13
13
|
});
|
|
14
14
|
}
|
|
@@ -6,7 +6,7 @@ import { Oas3Operation } from '../../typings/openapi';
|
|
|
6
6
|
|
|
7
7
|
export const OperationOperationId: Oas3Rule | Oas2Rule = () => {
|
|
8
8
|
return {
|
|
9
|
-
|
|
9
|
+
Root: {
|
|
10
10
|
PathItem: {
|
|
11
11
|
Operation(operation: Oas2Operation | Oas3Operation, ctx: UserContext) {
|
|
12
12
|
validateDefinedAndNonEmpty('operationId', operation, ctx);
|
|
@@ -7,7 +7,7 @@ export const OperationTagDefined: Oas3Rule | Oas2Rule = () => {
|
|
|
7
7
|
let definedTags: Set<string>;
|
|
8
8
|
|
|
9
9
|
return {
|
|
10
|
-
|
|
10
|
+
Root(root: Oas2Definition | Oas3Definition) {
|
|
11
11
|
definedTags = new Set((root.tags ?? []).map((t) => t.name));
|
|
12
12
|
},
|
|
13
13
|
Operation(operation: Oas2Operation | Oas3Operation, { report, location }: UserContext) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Oas3Rule, Oas2Rule } from '../../visitors';
|
|
2
2
|
import { Location } from '../../ref-utils';
|
|
3
3
|
import { UserContext } from '../../walk';
|
|
4
|
-
import { Oas2SecurityScheme } from '../../typings/swagger';
|
|
5
|
-
import { Oas3SecurityScheme } from '../../typings/openapi';
|
|
4
|
+
import { Oas2Definition, Oas2Operation, Oas2SecurityScheme } from '../../typings/swagger';
|
|
5
|
+
import { Oas3Definition, Oas3Operation, Oas3SecurityScheme } from '../../typings/openapi';
|
|
6
6
|
|
|
7
|
-
export const
|
|
7
|
+
export const SecurityDefined: Oas3Rule | Oas2Rule = () => {
|
|
8
8
|
const referencedSchemes = new Map<
|
|
9
9
|
string,
|
|
10
10
|
{
|
|
@@ -13,9 +13,11 @@ export const OperationSecurityDefined: Oas3Rule | Oas2Rule = () => {
|
|
|
13
13
|
}
|
|
14
14
|
>();
|
|
15
15
|
|
|
16
|
+
let eachOperationHasSecurity: boolean = true;
|
|
17
|
+
|
|
16
18
|
return {
|
|
17
19
|
DefinitionRoot: {
|
|
18
|
-
leave(
|
|
20
|
+
leave(root: Oas2Definition | Oas3Definition, { report }: UserContext) {
|
|
19
21
|
for (const [name, scheme] of referencedSchemes.entries()) {
|
|
20
22
|
if (scheme.defined) continue;
|
|
21
23
|
for (const reportedFromLocation of scheme.from) {
|
|
@@ -25,6 +27,14 @@ export const OperationSecurityDefined: Oas3Rule | Oas2Rule = () => {
|
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
29
|
}
|
|
30
|
+
|
|
31
|
+
if (root.security || eachOperationHasSecurity) {
|
|
32
|
+
return;
|
|
33
|
+
} else {
|
|
34
|
+
report({
|
|
35
|
+
message: `Every API should have security defined on the root level or for each operation.`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
28
38
|
},
|
|
29
39
|
},
|
|
30
40
|
SecurityScheme(_securityScheme: Oas2SecurityScheme | Oas3SecurityScheme, { key }: UserContext) {
|
|
@@ -41,5 +51,10 @@ export const OperationSecurityDefined: Oas3Rule | Oas2Rule = () => {
|
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
},
|
|
54
|
+
Operation(operation: Oas2Operation | Oas3Operation) {
|
|
55
|
+
if (!operation?.security) {
|
|
56
|
+
eachOperationHasSecurity = false;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
44
59
|
};
|
|
45
60
|
};
|