@redocly/openapi-core 1.0.0-beta.111 → 1.0.0-beta.112
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 +19 -12
- package/lib/config/load.d.ts +1 -1
- package/lib/config/load.js +5 -5
- package/lib/config/minimal.js +0 -1
- package/lib/config/recommended.js +0 -1
- 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/oas2/index.d.ts +0 -1
- package/lib/rules/oas2/index.js +0 -2
- package/lib/rules/oas3/index.js +0 -2
- package/lib/types/redocly-yaml.js +44 -27
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +7 -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__/lint.test.ts +24 -5
- package/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +1 -3
- package/src/config/__tests__/config-resolvers.test.ts +5 -5
- package/src/config/__tests__/fixtures/load-redocly.yaml +4 -0
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml +6 -4
- package/src/config/__tests__/load.test.ts +4 -1
- package/src/config/all.ts +0 -1
- package/src/config/config-resolvers.ts +42 -19
- package/src/config/load.ts +8 -5
- package/src/config/minimal.ts +0 -1
- package/src/config/recommended.ts +0 -1
- 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/oas2/index.ts +0 -2
- package/src/rules/oas3/index.ts +0 -2
- package/src/types/redocly-yaml.ts +44 -29
- package/src/utils.ts +5 -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
|
@@ -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/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,
|
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,
|
|
@@ -3,7 +3,6 @@ import { omitObjectProps, pickObjectProps, isCustomRuleId } from '../utils';
|
|
|
3
3
|
|
|
4
4
|
const builtInRulesList = [
|
|
5
5
|
'spec',
|
|
6
|
-
'info-description',
|
|
7
6
|
'info-contact',
|
|
8
7
|
'info-license',
|
|
9
8
|
'info-license-url',
|
|
@@ -147,16 +146,8 @@ const ConfigRoot: NodeType = {
|
|
|
147
146
|
properties: {
|
|
148
147
|
organization: { type: 'string' },
|
|
149
148
|
apis: 'ConfigApis',
|
|
150
|
-
apiDefinitions: {
|
|
151
|
-
type: 'object',
|
|
152
|
-
properties: {},
|
|
153
|
-
additionalProperties: { properties: { type: 'string' } },
|
|
154
|
-
}, // deprecated
|
|
155
149
|
...RootConfigStyleguide.properties,
|
|
156
|
-
styleguide: 'RootConfigStyleguide', // deprecated
|
|
157
|
-
lint: 'RootConfigStyleguide', // deprecated
|
|
158
150
|
'features.openapi': 'ConfigReferenceDocs',
|
|
159
|
-
referenceDocs: 'ConfigReferenceDocs', // deprecated
|
|
160
151
|
'features.mockServer': 'ConfigMockServer',
|
|
161
152
|
region: { enum: ['us', 'eu'] },
|
|
162
153
|
resolve: {
|
|
@@ -239,15 +230,9 @@ const ObjectRule: NodeType = {
|
|
|
239
230
|
required: ['severity'],
|
|
240
231
|
};
|
|
241
232
|
|
|
242
|
-
const
|
|
233
|
+
const AssertionDefinitionSubject: NodeType = {
|
|
243
234
|
properties: {
|
|
244
|
-
|
|
245
|
-
if (Array.isArray(value)) {
|
|
246
|
-
return { type: 'array', items: { enum: nodeTypesList } };
|
|
247
|
-
} else {
|
|
248
|
-
return { enum: nodeTypesList };
|
|
249
|
-
}
|
|
250
|
-
},
|
|
235
|
+
type: { enum: nodeTypesList },
|
|
251
236
|
property: (value: unknown) => {
|
|
252
237
|
if (Array.isArray(value)) {
|
|
253
238
|
return { type: 'array', items: { type: 'string' } };
|
|
@@ -257,10 +242,15 @@ const Assert: NodeType = {
|
|
|
257
242
|
return { type: 'string' };
|
|
258
243
|
}
|
|
259
244
|
},
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
245
|
+
filterInParentKeys: { type: 'array', items: { type: 'string' } },
|
|
246
|
+
filterOutParentKeys: { type: 'array', items: { type: 'string' } },
|
|
247
|
+
matchParentKeys: { type: 'string' },
|
|
248
|
+
},
|
|
249
|
+
required: ['type'],
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const AssertionDefinitionAssertions: NodeType = {
|
|
253
|
+
properties: {
|
|
264
254
|
enum: { type: 'array', items: { type: 'string' } },
|
|
265
255
|
pattern: { type: 'string' },
|
|
266
256
|
casing: {
|
|
@@ -280,27 +270,50 @@ const Assert: NodeType = {
|
|
|
280
270
|
requireAny: { type: 'array', items: { type: 'string' } },
|
|
281
271
|
disallowed: { type: 'array', items: { type: 'string' } },
|
|
282
272
|
defined: { type: 'boolean' },
|
|
283
|
-
undefined: { type: 'boolean' },
|
|
273
|
+
// undefined: { type: 'boolean' }, // TODO: Remove `undefined` assertion from codebase overall
|
|
284
274
|
nonEmpty: { type: 'boolean' },
|
|
285
275
|
minLength: { type: 'integer' },
|
|
286
276
|
maxLength: { type: 'integer' },
|
|
287
277
|
ref: (value: string | boolean) =>
|
|
288
278
|
typeof value === 'string' ? { type: 'string' } : { type: 'boolean' },
|
|
279
|
+
const: (value: string | boolean | number) => {
|
|
280
|
+
if (typeof value === 'string') {
|
|
281
|
+
return { type: 'string' };
|
|
282
|
+
}
|
|
283
|
+
if (typeof value === 'number') {
|
|
284
|
+
return { type: 'number' };
|
|
285
|
+
}
|
|
286
|
+
if (typeof value === 'boolean') {
|
|
287
|
+
return { type: 'boolean' };
|
|
288
|
+
} else {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
},
|
|
289
292
|
},
|
|
290
293
|
additionalProperties: (_value: unknown, key: string) => {
|
|
291
294
|
if (/^\w+\/\w+$/.test(key)) return { type: 'object' };
|
|
292
295
|
return;
|
|
293
296
|
},
|
|
294
|
-
required: ['subject'],
|
|
295
297
|
};
|
|
296
298
|
|
|
297
|
-
const
|
|
299
|
+
const AssertDefinition: NodeType = {
|
|
298
300
|
properties: {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
excludeParentKeys: { type: 'array', items: { type: 'string' } },
|
|
301
|
+
subject: 'AssertionDefinitionSubject',
|
|
302
|
+
assertions: 'AssertionDefinitionAssertions',
|
|
302
303
|
},
|
|
303
|
-
required: ['
|
|
304
|
+
required: ['subject', 'assertions'],
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const Assert: NodeType = {
|
|
308
|
+
properties: {
|
|
309
|
+
subject: 'AssertionDefinitionSubject',
|
|
310
|
+
assertions: 'AssertionDefinitionAssertions',
|
|
311
|
+
where: listOf('AssertDefinition'),
|
|
312
|
+
message: { type: 'string' },
|
|
313
|
+
suggest: { type: 'array', items: { type: 'string' } },
|
|
314
|
+
severity: { enum: ['error', 'warn', 'off'] },
|
|
315
|
+
},
|
|
316
|
+
required: ['subject', 'assertions'],
|
|
304
317
|
};
|
|
305
318
|
|
|
306
319
|
const ConfigLanguage: NodeType = {
|
|
@@ -926,7 +939,7 @@ export const ConfigTypes: Record<string, NodeType> = {
|
|
|
926
939
|
ConfigSidebarLinks,
|
|
927
940
|
CommonConfigSidebarLinks,
|
|
928
941
|
ConfigTheme,
|
|
929
|
-
|
|
942
|
+
AssertDefinition,
|
|
930
943
|
ThemeColors,
|
|
931
944
|
CommonThemeColors,
|
|
932
945
|
BorderThemeColors,
|
|
@@ -973,4 +986,6 @@ export const ConfigTypes: Record<string, NodeType> = {
|
|
|
973
986
|
Sidebar,
|
|
974
987
|
Heading,
|
|
975
988
|
Typography,
|
|
989
|
+
AssertionDefinitionAssertions,
|
|
990
|
+
AssertionDefinitionSubject,
|
|
976
991
|
};
|
package/src/utils.ts
CHANGED
|
@@ -232,6 +232,11 @@ export function identity<T>(value: T): T {
|
|
|
232
232
|
return value;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
export function keysOf<T>(obj: T) {
|
|
236
|
+
if (!obj) return [];
|
|
237
|
+
return Object.keys(obj) as (keyof T)[];
|
|
238
|
+
}
|
|
239
|
+
|
|
235
240
|
export function pickDefined<T extends Record<string, unknown>>(
|
|
236
241
|
obj?: T
|
|
237
242
|
): Record<string, unknown> | undefined {
|
package/src/visitors.ts
CHANGED
|
@@ -50,6 +50,11 @@ import type { Stack } from './utils';
|
|
|
50
50
|
import type { UserContext, ResolveResult, ProblemSeverity } from './walk';
|
|
51
51
|
import type { Location } from './ref-utils';
|
|
52
52
|
|
|
53
|
+
export type SkipFunctionContext = Pick<
|
|
54
|
+
UserContext,
|
|
55
|
+
'location' | 'rawNode' | 'resolve' | 'rawLocation'
|
|
56
|
+
>;
|
|
57
|
+
|
|
53
58
|
export type VisitFunction<T> = (
|
|
54
59
|
node: T,
|
|
55
60
|
ctx: UserContext & { ignoreNextVisitorsOnNode: () => void },
|
|
@@ -59,7 +64,7 @@ export type VisitFunction<T> = (
|
|
|
59
64
|
|
|
60
65
|
type VisitRefFunction = (node: OasRef, ctx: UserContext, resolved: ResolveResult<any>) => void;
|
|
61
66
|
|
|
62
|
-
type SkipFunction<T> = (node: T, key: string | number) => boolean;
|
|
67
|
+
type SkipFunction<T> = (node: T, key: string | number, ctx: SkipFunctionContext) => boolean;
|
|
63
68
|
|
|
64
69
|
type VisitObject<T> = {
|
|
65
70
|
enter?: VisitFunction<T>;
|
|
@@ -210,6 +215,7 @@ const legacyTypesMap = {
|
|
|
210
215
|
HeadersMap: 'HeaderMap',
|
|
211
216
|
LinksMap: 'LinkMap',
|
|
212
217
|
OAuth2Flows: 'SecuritySchemeFlows',
|
|
218
|
+
Responses: 'ResponsesMap',
|
|
213
219
|
};
|
|
214
220
|
|
|
215
221
|
type Oas3NestedVisitor = {
|
package/src/walk.ts
CHANGED
|
@@ -226,7 +226,14 @@ export function walkDocument<T>(opts: {
|
|
|
226
226
|
nextLevelTypeActivated: null,
|
|
227
227
|
withParentNode: context.parent?.activatedOn?.value.node,
|
|
228
228
|
skipped:
|
|
229
|
-
(context.parent?.activatedOn?.value.skipped ||
|
|
229
|
+
(context.parent?.activatedOn?.value.skipped ||
|
|
230
|
+
skip?.(resolvedNode, key, {
|
|
231
|
+
location,
|
|
232
|
+
rawLocation,
|
|
233
|
+
resolve,
|
|
234
|
+
rawNode: node,
|
|
235
|
+
})) ??
|
|
236
|
+
false,
|
|
230
237
|
};
|
|
231
238
|
|
|
232
239
|
context.activatedOn = pushStack<any>(context.activatedOn, activatedOn);
|