@redocly/openapi-core 1.0.0-beta.110 → 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.
Files changed (93) hide show
  1. package/lib/config/all.js +0 -1
  2. package/lib/config/config-resolvers.js +42 -34
  3. package/lib/config/load.d.ts +1 -1
  4. package/lib/config/load.js +5 -5
  5. package/lib/config/minimal.js +0 -1
  6. package/lib/config/recommended.js +0 -1
  7. package/lib/rules/common/assertions/asserts.d.ts +22 -5
  8. package/lib/rules/common/assertions/asserts.js +25 -0
  9. package/lib/rules/common/assertions/index.d.ts +27 -2
  10. package/lib/rules/common/assertions/index.js +6 -29
  11. package/lib/rules/common/assertions/utils.d.ts +7 -14
  12. package/lib/rules/common/assertions/utils.js +129 -97
  13. package/lib/rules/common/no-ambiguous-paths.js +1 -1
  14. package/lib/rules/common/no-identical-paths.js +4 -4
  15. package/lib/rules/common/operation-2xx-response.js +2 -2
  16. package/lib/rules/common/operation-4xx-response.js +2 -2
  17. package/lib/rules/common/path-not-include-query.js +1 -1
  18. package/lib/rules/common/path-params-defined.js +7 -2
  19. package/lib/rules/common/response-contains-header.js +2 -2
  20. package/lib/rules/common/security-defined.js +10 -5
  21. package/lib/rules/common/spec.js +14 -12
  22. package/lib/rules/oas2/index.d.ts +0 -1
  23. package/lib/rules/oas2/index.js +0 -2
  24. package/lib/rules/oas3/index.js +0 -2
  25. package/lib/rules/oas3/request-mime-type.js +1 -1
  26. package/lib/rules/oas3/response-mime-type.js +1 -1
  27. package/lib/rules/other/stats.d.ts +1 -1
  28. package/lib/rules/other/stats.js +1 -1
  29. package/lib/rules/utils.d.ts +1 -0
  30. package/lib/rules/utils.js +17 -1
  31. package/lib/types/oas2.js +6 -6
  32. package/lib/types/oas3.js +11 -11
  33. package/lib/types/oas3_1.js +3 -3
  34. package/lib/types/redocly-yaml.js +58 -31
  35. package/lib/utils.d.ts +2 -0
  36. package/lib/utils.js +19 -1
  37. package/lib/visitors.d.ts +9 -7
  38. package/lib/visitors.js +12 -3
  39. package/lib/walk.js +7 -1
  40. package/package.json +1 -1
  41. package/src/__tests__/__snapshots__/bundle.test.ts.snap +1 -1
  42. package/src/__tests__/lint.test.ts +24 -5
  43. package/src/__tests__/utils.test.ts +11 -0
  44. package/src/__tests__/walk.test.ts +2 -2
  45. package/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +1 -3
  46. package/src/config/__tests__/config-resolvers.test.ts +30 -5
  47. package/src/config/__tests__/fixtures/load-redocly.yaml +4 -0
  48. package/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml +6 -4
  49. package/src/config/__tests__/load.test.ts +4 -1
  50. package/src/config/all.ts +0 -1
  51. package/src/config/config-resolvers.ts +44 -20
  52. package/src/config/load.ts +8 -5
  53. package/src/config/minimal.ts +0 -1
  54. package/src/config/recommended.ts +0 -1
  55. package/src/rules/common/__tests__/operation-2xx-response.test.ts +37 -0
  56. package/src/rules/common/__tests__/operation-4xx-response.test.ts +37 -0
  57. package/src/rules/common/__tests__/path-params-defined.test.ts +69 -0
  58. package/src/rules/common/__tests__/security-defined.test.ts +6 -6
  59. package/src/rules/common/__tests__/spec.test.ts +125 -0
  60. package/src/rules/common/assertions/__tests__/asserts.test.ts +7 -3
  61. package/src/rules/common/assertions/__tests__/index.test.ts +41 -20
  62. package/src/rules/common/assertions/__tests__/utils.test.ts +44 -18
  63. package/src/rules/common/assertions/asserts.ts +60 -8
  64. package/src/rules/common/assertions/index.ts +36 -46
  65. package/src/rules/common/assertions/utils.ts +204 -127
  66. package/src/rules/common/no-ambiguous-paths.ts +1 -1
  67. package/src/rules/common/no-identical-paths.ts +4 -4
  68. package/src/rules/common/operation-2xx-response.ts +2 -2
  69. package/src/rules/common/operation-4xx-response.ts +2 -2
  70. package/src/rules/common/path-not-include-query.ts +1 -1
  71. package/src/rules/common/path-params-defined.ts +9 -2
  72. package/src/rules/common/response-contains-header.ts +6 -1
  73. package/src/rules/common/security-defined.ts +10 -5
  74. package/src/rules/common/spec.ts +15 -11
  75. package/src/rules/oas2/index.ts +0 -2
  76. package/src/rules/oas3/__tests__/response-contains-header.test.ts +116 -0
  77. package/src/rules/oas3/index.ts +0 -2
  78. package/src/rules/oas3/request-mime-type.ts +1 -1
  79. package/src/rules/oas3/response-mime-type.ts +1 -1
  80. package/src/rules/other/stats.ts +1 -1
  81. package/src/rules/utils.ts +22 -0
  82. package/src/types/oas2.ts +6 -6
  83. package/src/types/oas3.ts +11 -11
  84. package/src/types/oas3_1.ts +3 -3
  85. package/src/types/redocly-yaml.ts +58 -33
  86. package/src/utils.ts +18 -0
  87. package/src/visitors.ts +32 -11
  88. package/src/walk.ts +8 -1
  89. package/tsconfig.tsbuildinfo +1 -1
  90. package/lib/rules/common/info-description.d.ts +0 -2
  91. package/lib/rules/common/info-description.js +0 -12
  92. package/src/rules/common/__tests__/info-description.test.ts +0 -102
  93. package/src/rules/common/info-description.ts +0 -10
@@ -1,8 +1,15 @@
1
- import type { AssertResult, RuleSeverity } from '../../../config';
1
+ import { asserts, runOnKeysSet, runOnValuesSet, Asserts } from './asserts';
2
2
  import { colorize } from '../../../logger';
3
3
  import { isRef, Location } from '../../../ref-utils';
4
- import { UserContext } from '../../../walk';
5
- import { asserts } from './asserts';
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: string;
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
- subject: string,
32
- context: Record<string, any>[],
33
- subjectVisitor: any
34
- ) {
35
- if (!context) {
36
- return { [subject]: subjectVisitor };
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 node = context[index];
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 (matchParentKeys && excludeParentKeys) {
163
+ if (!isString(assertionDefinitionNode.subject?.type)) {
55
164
  throw new Error(
56
- `Both 'matchParentKeys' and 'excludeParentKeys' can't be under one context item`
165
+ `${assertion.assertionId} -> where -> [${index}]: 'type' (String) is required`
57
166
  );
58
167
  }
59
168
 
60
- if (matchParentKeys || excludeParentKeys) {
61
- currentVisitorLevel[node.type] = {
62
- skip: (_value: any, key: string) => {
63
- if (matchParentKeys) {
64
- return !matchParentKeys.includes(key);
65
- }
66
- if (excludeParentKeys) {
67
- return excludeParentKeys.includes(key);
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[node.type] = {};
207
+ currentVisitorLevel = currentVisitorLevel[assertionDefinitionNode.subject?.type] =
208
+ nodeVisitor;
73
209
  }
74
- currentVisitorLevel = currentVisitorLevel[node.type];
75
210
  }
76
211
 
77
- currentVisitorLevel[subject] = subjectVisitor;
212
+ currentVisitorLevel[assertion.subject.type] = targetVisitor[assertion.subject.type];
78
213
 
79
214
  return visitor;
80
215
  }
81
216
 
82
- export function buildSubjectVisitor(
83
- assertId: string,
84
- assertion: Assertion,
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
- properties ? ` ${(properties as string[]).join(', ')}` : ''
118
- )} didn't meet the assertions: {{problems}}`;
119
-
120
- const assertResults: Array<AssertResult[]> = [];
121
- for (const assert of asserts) {
122
- const currentLocation = assert.name === 'ref' ? rawLocation : location;
123
- if (properties) {
124
- for (const property of properties) {
125
- // we can have resolvable scalar so need to resolve value here.
126
- const value = isRef(node[property]) ? resolve(node[property])?.node : node[property];
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
- 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
- });
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;
@@ -5,7 +5,7 @@ import { Oas2Paths } from '../../typings/swagger';
5
5
 
6
6
  export const NoAmbiguousPaths: Oas3Rule | Oas2Rule = () => {
7
7
  return {
8
- PathsMap(pathMap: Oas3Paths | Oas2Paths, { report, location }: UserContext) {
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
- PathsMap(pathMap: Oas3Paths | Oas2Paths, { report, location }: UserContext) {
9
- const pathsMap = new Map<string, string>();
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 = pathsMap.get(id);
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
- pathsMap.set(id, pathName);
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
- ResponsesMap(responses: Record<string, object>, { report }: UserContext) {
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
- ResponsesMap(responses: Record<string, object>, { report }: UserContext) {
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({
@@ -3,7 +3,7 @@ import { UserContext } from '../../walk';
3
3
 
4
4
  export const PathNotIncludeQuery: Oas3Rule | Oas2Rule = () => {
5
5
  return {
6
- PathsMap: {
6
+ Paths: {
7
7
  PathItem(_operation: object, { report, key }: UserContext) {
8
8
  if (key.toString().includes('?')) {
9
9
  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 (!definedPathParams.has(templateParam)) {
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
- definedPathParams.add(parameter.name);
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 (!response.headers?.[expectedHeader]) {
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(),
@@ -13,10 +13,11 @@ export const SecurityDefined: Oas3Rule | Oas2Rule = () => {
13
13
  }
14
14
  >();
15
15
 
16
+ const operationsWithoutSecurity: Location[] = [];
16
17
  let eachOperationHasSecurity: boolean = true;
17
18
 
18
19
  return {
19
- DefinitionRoot: {
20
+ Root: {
20
21
  leave(root: Oas2Definition | Oas3Definition, { report }: UserContext) {
21
22
  for (const [name, scheme] of referencedSchemes.entries()) {
22
23
  if (scheme.defined) continue;
@@ -31,9 +32,12 @@ export const SecurityDefined: Oas3Rule | Oas2Rule = () => {
31
32
  if (root.security || eachOperationHasSecurity) {
32
33
  return;
33
34
  } else {
34
- report({
35
- message: `Every API should have security defined on the root level or for each operation.`,
36
- });
35
+ for (const operationLocation of operationsWithoutSecurity) {
36
+ report({
37
+ message: `Every operation should have security defined on it or on the root level.`,
38
+ location: operationLocation.key(),
39
+ });
40
+ }
37
41
  }
38
42
  },
39
43
  },
@@ -51,9 +55,10 @@ export const SecurityDefined: Oas3Rule | Oas2Rule = () => {
51
55
  }
52
56
  }
53
57
  },
54
- Operation(operation: Oas2Operation | Oas3Operation) {
58
+ Operation(operation: Oas2Operation | Oas3Operation, { location }: UserContext) {
55
59
  if (!operation?.security) {
56
60
  eachOperationHasSecurity = false;
61
+ operationsWithoutSecurity.push(location);
57
62
  }
58
63
  },
59
64
  };
@@ -1,8 +1,9 @@
1
1
  import type { Oas3Rule, Oas2Rule } from '../../visitors';
2
2
  import { isNamedType } from '../../types';
3
- import { oasTypeOf, matchesJsonSchemaType, getSuggest } from '../utils';
3
+ import { oasTypeOf, matchesJsonSchemaType, getSuggest, validateSchemaEnumType } from '../utils';
4
4
  import { isRef } from '../../ref-utils';
5
5
  import { isPlainObject } from '../../utils';
6
+ import { UserContext } from '../../walk';
6
7
 
7
8
  export const OasSpec: Oas3Rule | Oas2Rule = () => {
8
9
  return {
@@ -114,17 +115,20 @@ export const OasSpec: Oas3Rule | Oas2Rule = () => {
114
115
  propValue = resolve(propValue).node;
115
116
  }
116
117
 
117
- if (propSchema.enum) {
118
- if (!propSchema.enum.includes(propValue)) {
119
- report({
120
- location: propLocation,
121
- message: `\`${propName}\` can be one of the following only: ${propSchema.enum
122
- .map((i) => `"${i}"`)
123
- .join(', ')}.`,
124
- from: refLocation,
125
- suggest: getSuggest(propValue, propSchema.enum),
126
- });
118
+ if (propSchema.items && propSchema.items?.enum && Array.isArray(propValue)) {
119
+ for (let i = 0; i < propValue.length; i++) {
120
+ validateSchemaEnumType(propSchema.items?.enum, propValue[i], propName, refLocation, {
121
+ report,
122
+ location: location.child([propName, i]),
123
+ } as UserContext);
127
124
  }
125
+ }
126
+
127
+ if (propSchema.enum) {
128
+ validateSchemaEnumType(propSchema.enum, propValue, propName, refLocation, {
129
+ report,
130
+ location: location.child([propName]),
131
+ } as UserContext);
128
132
  } else if (propSchema.type && !matchesJsonSchemaType(propValue, propSchema.type, false)) {
129
133
  report({
130
134
  message: `Expected type \`${propSchema.type}\` but got \`${propValueType}\`.`,
@@ -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,