@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.
Files changed (159) hide show
  1. package/README.md +2 -2
  2. package/lib/benchmark/benches/resolve-with-no-external.bench.js +1 -1
  3. package/lib/bundle.d.ts +1 -1
  4. package/lib/bundle.js +4 -4
  5. package/lib/config/all.js +3 -1
  6. package/lib/config/config-resolvers.js +22 -4
  7. package/lib/config/config.d.ts +1 -0
  8. package/lib/config/config.js +1 -0
  9. package/lib/config/load.d.ts +8 -2
  10. package/lib/config/load.js +4 -2
  11. package/lib/config/minimal.js +3 -1
  12. package/lib/config/recommended.js +3 -1
  13. package/lib/config/rules.js +1 -1
  14. package/lib/config/types.d.ts +17 -0
  15. package/lib/config/utils.d.ts +2 -2
  16. package/lib/config/utils.js +44 -6
  17. package/lib/decorators/common/registry-dependencies.js +1 -1
  18. package/lib/format/format.d.ts +1 -1
  19. package/lib/format/format.js +22 -1
  20. package/lib/lint.js +2 -2
  21. package/lib/redocly/registry-api.d.ts +0 -1
  22. package/lib/redocly/registry-api.js +5 -4
  23. package/lib/resolve.js +3 -1
  24. package/lib/rules/ajv.d.ts +1 -1
  25. package/lib/rules/ajv.js +5 -5
  26. package/lib/rules/common/assertions/asserts.d.ts +3 -5
  27. package/lib/rules/common/assertions/asserts.js +137 -97
  28. package/lib/rules/common/assertions/index.js +2 -6
  29. package/lib/rules/common/assertions/utils.d.ts +12 -6
  30. package/lib/rules/common/assertions/utils.js +33 -20
  31. package/lib/rules/common/no-ambiguous-paths.js +1 -1
  32. package/lib/rules/common/no-identical-paths.js +1 -1
  33. package/lib/rules/common/operation-2xx-response.js +1 -1
  34. package/lib/rules/common/operation-4xx-response.js +1 -1
  35. package/lib/rules/common/operation-operationId.js +1 -1
  36. package/lib/rules/common/operation-tag-defined.js +1 -1
  37. package/lib/rules/common/path-not-include-query.js +1 -1
  38. package/lib/rules/common/security-defined.d.ts +2 -0
  39. package/lib/rules/common/{operation-security-defined.js → security-defined.js} +18 -4
  40. package/lib/rules/common/spec.js +12 -1
  41. package/lib/rules/common/tags-alphabetical.js +1 -1
  42. package/lib/rules/oas2/index.d.ts +1 -1
  43. package/lib/rules/oas2/index.js +2 -2
  44. package/lib/rules/oas2/remove-unused-components.js +1 -1
  45. package/lib/rules/oas2/request-mime-type.js +1 -1
  46. package/lib/rules/oas2/response-mime-type.js +1 -1
  47. package/lib/rules/oas3/index.js +6 -2
  48. package/lib/rules/oas3/no-empty-servers.js +1 -1
  49. package/lib/rules/oas3/no-server-variables-empty-enum.js +1 -1
  50. package/lib/rules/oas3/no-unused-components.js +1 -1
  51. package/lib/rules/oas3/operation-4xx-problem-details-rfc7807.d.ts +5 -0
  52. package/lib/rules/oas3/operation-4xx-problem-details-rfc7807.js +36 -0
  53. package/lib/rules/oas3/remove-unused-components.js +1 -1
  54. package/lib/rules/oas3/request-mime-type.js +1 -1
  55. package/lib/rules/oas3/response-mime-type.js +1 -1
  56. package/lib/rules/oas3/spec-components-invalid-map-name.d.ts +2 -0
  57. package/lib/rules/oas3/spec-components-invalid-map-name.js +46 -0
  58. package/lib/rules/other/stats.d.ts +2 -2
  59. package/lib/rules/other/stats.js +2 -2
  60. package/lib/rules/utils.js +1 -1
  61. package/lib/types/oas2.js +5 -5
  62. package/lib/types/oas3.js +27 -20
  63. package/lib/types/oas3_1.js +3 -3
  64. package/lib/types/redocly-yaml.js +60 -54
  65. package/lib/utils.d.ts +3 -3
  66. package/lib/utils.js +5 -5
  67. package/lib/visitors.d.ts +11 -11
  68. package/lib/visitors.js +13 -1
  69. package/package.json +3 -5
  70. package/src/__tests__/__snapshots__/bundle.test.ts.snap +3 -3
  71. package/src/__tests__/fixtures/extension.js +3 -3
  72. package/src/__tests__/format.test.ts +76 -0
  73. package/src/__tests__/lint.test.ts +184 -121
  74. package/src/__tests__/resolve-http.test.ts +1 -1
  75. package/src/__tests__/resolve.test.ts +9 -9
  76. package/src/__tests__/walk.test.ts +78 -10
  77. package/src/benchmark/benches/resolve-with-no-external.bench.ts +1 -1
  78. package/src/bundle.ts +4 -4
  79. package/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +6 -2
  80. package/src/config/__tests__/config-resolvers.test.ts +37 -1
  81. package/src/config/__tests__/config.test.ts +5 -0
  82. package/src/config/__tests__/fixtures/plugin-config.yaml +2 -3
  83. package/src/config/__tests__/fixtures/resolve-config/api/nested-config.yaml +11 -12
  84. package/src/config/__tests__/fixtures/resolve-config/local-config-with-circular.yaml +7 -8
  85. package/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml +16 -0
  86. package/src/config/__tests__/fixtures/resolve-config/local-config-with-file.yaml +18 -19
  87. package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml +16 -0
  88. package/src/config/__tests__/fixtures/resolve-config/local-config.yaml +9 -10
  89. package/src/config/__tests__/fixtures/resolve-config/plugin.js +11 -0
  90. package/src/config/__tests__/fixtures/resolve-remote-configs/nested-remote-config.yaml +3 -4
  91. package/src/config/__tests__/fixtures/resolve-remote-configs/remote-config.yaml +4 -5
  92. package/src/config/__tests__/load.test.ts +13 -16
  93. package/src/config/__tests__/resolve-plugins.test.ts +3 -3
  94. package/src/config/__tests__/utils.test.ts +64 -4
  95. package/src/config/all.ts +3 -1
  96. package/src/config/config-resolvers.ts +30 -7
  97. package/src/config/config.ts +2 -0
  98. package/src/config/load.ts +13 -6
  99. package/src/config/minimal.ts +3 -1
  100. package/src/config/recommended.ts +3 -1
  101. package/src/config/rules.ts +2 -2
  102. package/src/config/types.ts +24 -0
  103. package/src/config/utils.ts +103 -13
  104. package/src/decorators/common/registry-dependencies.ts +1 -1
  105. package/src/format/format.ts +32 -2
  106. package/src/lint.ts +2 -2
  107. package/src/redocly/registry-api.ts +5 -4
  108. package/src/resolve.ts +3 -1
  109. package/src/rules/__tests__/utils.test.ts +1 -1
  110. package/src/rules/ajv.ts +4 -4
  111. package/src/rules/common/__tests__/no-enum-type-mismatch.test.ts +1 -0
  112. package/src/rules/common/__tests__/operation-2xx-response.test.ts +1 -1
  113. package/src/rules/common/__tests__/operation-4xx-response.test.ts +26 -3
  114. package/src/rules/common/__tests__/security-defined.test.ts +175 -0
  115. package/src/rules/common/__tests__/spec.test.ts +79 -0
  116. package/src/rules/common/assertions/__tests__/asserts.test.ts +491 -428
  117. package/src/rules/common/assertions/__tests__/utils.test.ts +2 -2
  118. package/src/rules/common/assertions/asserts.ts +155 -97
  119. package/src/rules/common/assertions/index.ts +2 -11
  120. package/src/rules/common/assertions/utils.ts +66 -36
  121. package/src/rules/common/no-ambiguous-paths.ts +1 -1
  122. package/src/rules/common/no-identical-paths.ts +1 -1
  123. package/src/rules/common/operation-2xx-response.ts +1 -1
  124. package/src/rules/common/operation-4xx-response.ts +1 -1
  125. package/src/rules/common/operation-operationId.ts +1 -1
  126. package/src/rules/common/operation-tag-defined.ts +1 -1
  127. package/src/rules/common/path-not-include-query.ts +1 -1
  128. package/src/rules/common/{operation-security-defined.ts → security-defined.ts} +19 -4
  129. package/src/rules/common/spec.ts +15 -1
  130. package/src/rules/common/tags-alphabetical.ts +1 -1
  131. package/src/rules/oas2/index.ts +2 -2
  132. package/src/rules/oas2/remove-unused-components.ts +1 -1
  133. package/src/rules/oas2/request-mime-type.ts +1 -1
  134. package/src/rules/oas2/response-mime-type.ts +1 -1
  135. package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +51 -2
  136. package/src/rules/oas3/__tests__/operation-4xx-problem-details-rfc7807.test.ts +145 -0
  137. package/src/rules/oas3/__tests__/spec/spec.test.ts +10 -0
  138. package/src/rules/oas3/__tests__/spec-components-invalid-map-name.test.ts +217 -0
  139. package/src/rules/oas3/index.ts +6 -2
  140. package/src/rules/oas3/no-empty-servers.ts +1 -1
  141. package/src/rules/oas3/no-server-variables-empty-enum.ts +1 -1
  142. package/src/rules/oas3/no-unused-components.ts +1 -1
  143. package/src/rules/oas3/operation-4xx-problem-details-rfc7807.ts +36 -0
  144. package/src/rules/oas3/remove-unused-components.ts +1 -1
  145. package/src/rules/oas3/request-mime-type.ts +1 -1
  146. package/src/rules/oas3/response-mime-type.ts +1 -1
  147. package/src/rules/oas3/spec-components-invalid-map-name.ts +53 -0
  148. package/src/rules/other/stats.ts +2 -2
  149. package/src/rules/utils.ts +2 -1
  150. package/src/types/index.ts +2 -2
  151. package/src/types/oas2.ts +5 -5
  152. package/src/types/oas3.ts +27 -20
  153. package/src/types/oas3_1.ts +3 -3
  154. package/src/types/redocly-yaml.ts +66 -38
  155. package/src/utils.ts +11 -7
  156. package/src/visitors.ts +29 -13
  157. package/tsconfig.tsbuildinfo +1 -1
  158. package/lib/rules/common/operation-security-defined.d.ts +0 -2
  159. 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('MediaTypeMap', context, () => {}) as any;
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
- "MediaTypeMap": [Function],
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 { isValid: true }; // property doesn't exist, no need to lint it with this assert
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
- for (const _val of values) {
50
- if (!regx?.test(_val)) {
51
- return { isValid: false, location: runOnValue(value) ? baseLocation : baseLocation.key() };
52
- }
53
- }
54
- return { isValid: true };
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 { isValid: true }; // property doesn't exist, no need to lint it with this assert
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
- for (const _val of values) {
60
- if (!condition.includes(_val)) {
61
- return {
62
- isValid: false,
63
- location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
64
- };
65
- }
66
- }
67
- return { isValid: true };
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
- return { isValid: condition ? isDefined : !isDefined, location: baseLocation };
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
- for (const requiredKey of keys) {
75
- if (!value.includes(requiredKey)) {
76
- return { isValid: false, location: baseLocation.key() };
77
- }
78
- }
79
- return { isValid: true };
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 { isValid: true }; // property doesn't exist, no need to lint it with this assert
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
- for (const _val of values) {
85
- if (condition.includes(_val)) {
86
- return {
87
- isValid: false,
88
- location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
89
- };
90
- }
91
- }
92
- return { isValid: true };
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
- return { isValid: condition ? isUndefined : !isUndefined, location: baseLocation };
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
- return { isValid: condition ? !isEmpty : isEmpty, location: baseLocation };
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') return { isValid: true }; // property doesn't exist, no need to lint it with this assert
108
- return { isValid: value.length >= condition, location: baseLocation };
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') return { isValid: true }; // property doesn't exist, no need to lint it with this assert
112
- return { isValid: value.length <= condition, location: baseLocation };
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 { isValid: true }; // property doesn't exist, no need to lint it with this assert
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
- for (const _val of values) {
118
- let matchCase = false;
119
- switch (condition) {
120
- case 'camelCase':
121
- matchCase = !!_val.match(/^[a-z][a-zA-Z0-9]+$/g);
122
- break;
123
- case 'kebab-case':
124
- matchCase = !!_val.match(/^([a-z][a-z0-9]*)(-[a-z0-9]+)*$/g);
125
- break;
126
- case 'snake_case':
127
- matchCase = !!_val.match(/^([a-z][a-z0-9]*)(_[a-z0-9]+)*$/g);
128
- break;
129
- case 'PascalCase':
130
- matchCase = !!_val.match(/^[A-Z][a-zA-Z0-9]+$/g);
131
- break;
132
- case 'MACRO_CASE':
133
- matchCase = !!_val.match(/^([A-Z][A-Z0-9]*)(_[A-Z0-9]+)*$/g);
134
- break;
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') return { isValid: true };
153
- return { isValid: isOrdered(value, condition), location: baseLocation };
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
- return { isValid: getIntersectionLength(value, condition) < 2, location: baseLocation.key() };
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
- return {
160
- isValid:
161
- getIntersectionLength(value, condition) > 0
162
- ? getIntersectionLength(value, condition) === condition.length
163
- : true,
164
- location: baseLocation.key(),
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 { isValid: getIntersectionLength(value, condition) >= 1, location: baseLocation.key() };
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 { isValid: true }; // property doesn't exist, no need to lint it with this assert
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
- return {
175
- isValid: condition ? hasRef : !hasRef,
176
- location: hasRef ? baseLocation : baseLocation.key(),
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
- return {
181
- isValid: hasRef && regex?.test(rawValue['$ref']),
182
- location: hasRef ? baseLocation : baseLocation.key(),
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/master/packages/core/src/config/config.ts#L311
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 { Problem, ProblemSeverity, UserContext } from '../../../walk';
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
- properties: string | string[],
77
- asserts: AssertToApply[],
78
- context?: Record<string, any>[]
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
- runAssertion({
113
- values: value,
114
- rawValues: rawNode[property],
115
- assert,
116
- location: currentLocation.child(property),
117
- report,
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
- runAssertion({
123
- values: Object.keys(node),
124
- rawValues: value,
125
- assert,
126
- location: currentLocation,
127
- report,
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, report }: RunAssertionParams) {
177
- const lintResult = asserts[assert.name](values, assert.conditions, location, rawValues);
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
- PathMap(pathMap: Oas3Paths | Oas2Paths, { report, location }: UserContext) {
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
- PathMap(pathMap: Oas3Paths | Oas2Paths, { report, location }: UserContext) {
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 `2xx` response.',
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 `4xx` response.',
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
- DefinitionRoot: {
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
- DefinitionRoot(root: Oas2Definition | Oas3Definition) {
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) {
@@ -3,7 +3,7 @@ import { UserContext } from '../../walk';
3
3
 
4
4
  export const PathNotIncludeQuery: Oas3Rule | Oas2Rule = () => {
5
5
  return {
6
- PathMap: {
6
+ PathsMap: {
7
7
  PathItem(_operation: object, { report, key }: UserContext) {
8
8
  if (key.toString().includes('?')) {
9
9
  report({
@@ -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 OperationSecurityDefined: Oas3Rule | Oas2Rule = () => {
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(_: object, { report }: UserContext) {
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
  };