@redocly/openapi-core 1.0.0-beta.109 → 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 (40) hide show
  1. package/README.md +2 -2
  2. package/lib/config/config-resolvers.js +21 -3
  3. package/lib/config/config.d.ts +1 -0
  4. package/lib/config/config.js +1 -0
  5. package/lib/config/load.d.ts +8 -2
  6. package/lib/config/load.js +4 -2
  7. package/lib/config/types.d.ts +10 -0
  8. package/lib/config/utils.js +2 -2
  9. package/lib/rules/ajv.d.ts +1 -1
  10. package/lib/rules/ajv.js +5 -5
  11. package/lib/rules/common/assertions/asserts.d.ts +3 -5
  12. package/lib/rules/common/assertions/asserts.js +137 -97
  13. package/lib/rules/common/assertions/index.js +2 -6
  14. package/lib/rules/common/assertions/utils.d.ts +12 -6
  15. package/lib/rules/common/assertions/utils.js +33 -20
  16. package/lib/rules/utils.js +1 -1
  17. package/lib/types/redocly-yaml.js +16 -1
  18. package/package.json +3 -5
  19. package/src/__tests__/lint.test.ts +88 -0
  20. package/src/config/__tests__/config-resolvers.test.ts +37 -1
  21. package/src/config/__tests__/config.test.ts +5 -0
  22. package/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml +16 -0
  23. package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml +16 -0
  24. package/src/config/__tests__/fixtures/resolve-config/plugin.js +11 -0
  25. package/src/config/__tests__/load.test.ts +1 -1
  26. package/src/config/__tests__/resolve-plugins.test.ts +3 -3
  27. package/src/config/config-resolvers.ts +28 -5
  28. package/src/config/config.ts +2 -0
  29. package/src/config/load.ts +10 -4
  30. package/src/config/types.ts +13 -0
  31. package/src/config/utils.ts +1 -0
  32. package/src/rules/ajv.ts +4 -4
  33. package/src/rules/common/assertions/__tests__/asserts.test.ts +491 -428
  34. package/src/rules/common/assertions/asserts.ts +155 -97
  35. package/src/rules/common/assertions/index.ts +2 -11
  36. package/src/rules/common/assertions/utils.ts +66 -36
  37. package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +51 -2
  38. package/src/rules/utils.ts +2 -1
  39. package/src/types/redocly-yaml.ts +16 -0
  40. package/tsconfig.tsbuildinfo +1 -1
@@ -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 {
@@ -128,7 +128,7 @@ describe('no-invalid-media-type-examples', () => {
128
128
  "source": "foobar.yaml",
129
129
  },
130
130
  ],
131
- "message": "Example value must conform to the schema: must NOT have additional properties \`c\`.",
131
+ "message": "Example value must conform to the schema: must NOT have unevaluated properties \`c\`.",
132
132
  "ruleId": "no-invalid-media-type-examples",
133
133
  "severity": "error",
134
134
  "suggest": Array [],
@@ -137,7 +137,7 @@ describe('no-invalid-media-type-examples', () => {
137
137
  `);
138
138
  });
139
139
 
140
- it('should not on invalid example with allowAdditionalProperties', async () => {
140
+ it('should not report on valid example with allowAdditionalProperties', async () => {
141
141
  const document = parseYamlToDocument(
142
142
  outdent`
143
143
  openapi: 3.0.0
@@ -177,6 +177,55 @@ describe('no-invalid-media-type-examples', () => {
177
177
  expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
178
178
  });
179
179
 
180
+ it('should not report on valid example with allowAdditionalProperties and allOf and $ref', async () => {
181
+ const document = parseYamlToDocument(
182
+ outdent`
183
+ openapi: 3.0.0
184
+ components:
185
+ schemas:
186
+ C:
187
+ properties:
188
+ c:
189
+ type: string
190
+ paths:
191
+ /pet:
192
+ get:
193
+ responses:
194
+ 200:
195
+ content:
196
+ application/json:
197
+ example:
198
+ a: "string"
199
+ b: 13
200
+ c: "string"
201
+ schema:
202
+ type: object
203
+ allOf:
204
+ - $ref: '#/components/schemas/C'
205
+ properties:
206
+ a:
207
+ type: string
208
+ b:
209
+ type: number
210
+
211
+ `,
212
+ 'foobar.yaml'
213
+ );
214
+
215
+ const results = await lintDocument({
216
+ externalRefResolver: new BaseResolver(),
217
+ document,
218
+ config: await makeConfig({
219
+ 'no-invalid-media-type-examples': {
220
+ severity: 'error',
221
+ allowAdditionalProperties: false,
222
+ },
223
+ }),
224
+ });
225
+
226
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
227
+ });
228
+
180
229
  it('should not on invalid examples', async () => {
181
230
  const document = parseYamlToDocument(
182
231
  outdent`
@@ -109,7 +109,8 @@ export function validateExample(
109
109
  message: `Example value must conform to the schema: ${error.message}.`,
110
110
  location: {
111
111
  ...new Location(dataLoc.source, error.instancePath),
112
- reportOnKey: error.keyword === 'additionalProperties',
112
+ reportOnKey:
113
+ error.keyword === 'unevaluatedProperties' || error.keyword === 'additionalProperties',
113
114
  },
114
115
  from: location,
115
116
  suggest: error.suggest,
@@ -165,6 +165,12 @@ const ConfigRoot: NodeType = {
165
165
  doNotResolveExamples: { type: 'boolean' },
166
166
  },
167
167
  },
168
+ files: {
169
+ type: 'array',
170
+ items: {
171
+ type: 'string',
172
+ },
173
+ },
168
174
  },
169
175
  };
170
176
 
@@ -187,6 +193,12 @@ const ConfigApisProperties: NodeType = {
187
193
  ...ConfigStyleguide.properties,
188
194
  'features.openapi': 'ConfigReferenceDocs',
189
195
  'features.mockServer': 'ConfigMockServer',
196
+ files: {
197
+ type: 'array',
198
+ items: {
199
+ type: 'string',
200
+ },
201
+ },
190
202
  },
191
203
  required: ['root'],
192
204
  };
@@ -275,6 +287,10 @@ const Assert: NodeType = {
275
287
  ref: (value: string | boolean) =>
276
288
  typeof value === 'string' ? { type: 'string' } : { type: 'boolean' },
277
289
  },
290
+ additionalProperties: (_value: unknown, key: string) => {
291
+ if (/^\w+\/\w+$/.test(key)) return { type: 'object' };
292
+ return;
293
+ },
278
294
  required: ['subject'],
279
295
  };
280
296