@redocly/openapi-core 1.0.0-beta.91 → 1.0.0-beta.94

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 (45) hide show
  1. package/__tests__/normalizeVisitors.test.ts +1 -1
  2. package/__tests__/walk.test.ts +6 -6
  3. package/lib/config/all.js +1 -0
  4. package/lib/config/config.js +1 -1
  5. package/lib/config/minimal.js +1 -0
  6. package/lib/config/recommended.js +1 -0
  7. package/lib/config/rules.d.ts +1 -1
  8. package/lib/config/rules.js +10 -2
  9. package/lib/rules/common/assertions/asserts.d.ts +5 -0
  10. package/lib/rules/common/assertions/asserts.js +143 -0
  11. package/lib/rules/common/assertions/index.d.ts +2 -0
  12. package/lib/rules/common/assertions/index.js +52 -0
  13. package/lib/rules/common/assertions/utils.d.ts +20 -0
  14. package/lib/rules/common/assertions/utils.js +123 -0
  15. package/lib/rules/oas2/index.d.ts +1 -0
  16. package/lib/rules/oas2/index.js +2 -0
  17. package/lib/rules/oas3/index.js +2 -0
  18. package/lib/types/index.js +2 -2
  19. package/lib/types/oas3_1.js +31 -5
  20. package/lib/visitors.d.ts +2 -2
  21. package/lib/walk.d.ts +1 -0
  22. package/lib/walk.js +1 -1
  23. package/package.json +1 -1
  24. package/src/bundle.ts +1 -1
  25. package/src/config/__tests__/config.test.ts +256 -0
  26. package/src/config/all.ts +1 -0
  27. package/src/config/config.ts +15 -12
  28. package/src/config/minimal.ts +1 -0
  29. package/src/config/recommended.ts +1 -0
  30. package/src/config/rules.ts +11 -2
  31. package/src/lint.ts +3 -3
  32. package/src/rules/common/assertions/__tests__/asserts.test.ts +231 -0
  33. package/src/rules/common/assertions/__tests__/index.test.ts +65 -0
  34. package/src/rules/common/assertions/__tests__/utils.test.ts +89 -0
  35. package/src/rules/common/assertions/asserts.ts +137 -0
  36. package/src/rules/common/assertions/index.ts +75 -0
  37. package/src/rules/common/assertions/utils.ts +164 -0
  38. package/src/rules/oas2/index.ts +2 -0
  39. package/src/rules/oas3/__tests__/spec/servers.test.ts +1 -1
  40. package/src/rules/oas3/index.ts +2 -0
  41. package/src/types/index.ts +2 -2
  42. package/src/types/oas3_1.ts +32 -7
  43. package/src/visitors.ts +2 -2
  44. package/src/walk.ts +2 -1
  45. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,89 @@
1
+ import { isOrdered, buildVisitorObject, getIntersectionLength } from '../utils';
2
+
3
+ describe('Oas3 assertions', () => {
4
+ describe('Utils', () => {
5
+ describe('getCounts', () => {
6
+ it('should return the right counts', () => {
7
+ const arr = ['foo', 'bar', 'baz'];
8
+ expect(getIntersectionLength(arr, ['foo'])).toBe(1);
9
+ expect(getIntersectionLength(arr, ['foo', 'bar', 'baz'])).toBe(3);
10
+ expect(getIntersectionLength(arr, ['foo', 'test', 'baz'])).toBe(2);
11
+ expect(getIntersectionLength(arr, ['example', 'test'])).toBe(0);
12
+ });
13
+ });
14
+
15
+ describe('isOrdered', () => {
16
+ it('should say if array is ordered or not in specific direction', () => {
17
+ expect(isOrdered(['example', 'foo', 'test'], 'asc')).toBeTruthy();
18
+ expect(isOrdered(['example'], 'asc')).toBeTruthy();
19
+ expect(isOrdered(['test', 'foo', 'example'], 'desc')).toBeTruthy();
20
+ expect(isOrdered(['example'], 'desc')).toBeTruthy();
21
+ expect(isOrdered(['example', 'test', 'foo'], 'asc')).toBeFalsy();
22
+ expect(isOrdered(['example', 'foo', 'test'], 'desc')).toBeFalsy();
23
+ });
24
+ });
25
+
26
+ describe('buildVisitorObject', () => {
27
+ it('should return a consistent visitor structure', () => {
28
+ const context = [
29
+ {
30
+ type: 'Foo',
31
+ matchParentKeys: ['test'],
32
+ },
33
+ {
34
+ type: 'Bar',
35
+ matchParentKeys: ['test'],
36
+ },
37
+ {
38
+ type: 'Roof',
39
+ matchParentKeys: ['test'],
40
+ },
41
+ ];
42
+
43
+ const visitors = buildVisitorObject('Bar', context, () => {}) as any;
44
+
45
+ expect(visitors).toMatchInlineSnapshot(`
46
+ Object {
47
+ "Foo": Object {
48
+ "Bar": Object {
49
+ "Roof": Object {
50
+ "Bar": [Function],
51
+ "skip": [Function],
52
+ },
53
+ "skip": [Function],
54
+ },
55
+ "skip": [Function],
56
+ },
57
+ }
58
+ `);
59
+ });
60
+
61
+ it('should return the right visitor structure', () => {
62
+ const context = [
63
+ {
64
+ type: 'Operation',
65
+ matchParentKeys: ['put'],
66
+ },
67
+ {
68
+ type: 'ResponsesMap',
69
+ matchParentKeys: [201, 200],
70
+ },
71
+ ];
72
+
73
+ const visitors = buildVisitorObject('MediaTypeMap', context, () => {}) as any;
74
+
75
+ expect(visitors).toMatchInlineSnapshot(`
76
+ Object {
77
+ "Operation": Object {
78
+ "ResponsesMap": Object {
79
+ "MediaTypeMap": [Function],
80
+ "skip": [Function],
81
+ },
82
+ "skip": [Function],
83
+ },
84
+ }
85
+ `);
86
+ });
87
+ });
88
+ });
89
+ });
@@ -0,0 +1,137 @@
1
+ import { OrderOptions, OrderDirection, isOrdered, getIntersectionLength } from './utils';
2
+
3
+ type Asserts = Record<string, (value: any, condition: any) => boolean>;
4
+
5
+ export const runOnKeysSet = new Set([
6
+ 'mutuallyExclusive',
7
+ 'mutuallyRequired',
8
+ 'enum',
9
+ 'pattern',
10
+ 'minLength',
11
+ 'maxLength',
12
+ 'casing',
13
+ 'sortOrder',
14
+ 'disallowed',
15
+ 'required',
16
+ ]);
17
+ export const runOnValuesSet = new Set([
18
+ 'pattern',
19
+ 'enum',
20
+ 'defined',
21
+ 'undefined',
22
+ 'nonEmpty',
23
+ 'minLength',
24
+ 'maxLength',
25
+ 'casing',
26
+ 'sortOrder',
27
+ ]);
28
+
29
+ export const asserts: Asserts = {
30
+ pattern: (value: string | string[], condition: string): boolean => {
31
+ if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert
32
+ const values = typeof value === 'string' ? [value] : value;
33
+ const regexOptions = condition.match(/(\b\/\b)(.+)/g) || ['/'];
34
+ condition = condition.slice(1).replace(regexOptions[0], '');
35
+ const regx = new RegExp(condition, regexOptions[0].slice(1));
36
+ for (let _val of values) {
37
+ if (!_val.match(regx)) {
38
+ return false;
39
+ }
40
+ }
41
+ return true;
42
+ },
43
+ enum: (value: string | string[], condition: string[]): boolean => {
44
+ if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert
45
+ const values = typeof value === 'string' ? [value] : value;
46
+ for (let _val of values) {
47
+ if (!condition.includes(_val)) {
48
+ return false;
49
+ }
50
+ }
51
+ return true;
52
+ },
53
+ defined: (value: string | undefined, condition: boolean = true): boolean => {
54
+ const isDefined = typeof value !== 'undefined';
55
+ return condition ? isDefined : !isDefined;
56
+ },
57
+ required: (value: string[], keys: string[]): boolean => {
58
+ for (const requiredKey of keys) {
59
+ if (!value.includes(requiredKey)) {
60
+ return false;
61
+ }
62
+ }
63
+ return true;
64
+ },
65
+ disallowed: (value: string | string[], condition: string[]): boolean => {
66
+ if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert
67
+ const values = typeof value === 'string' ? [value] : value;
68
+ for (let _val of values) {
69
+ if (condition.includes(_val)) {
70
+ return false;
71
+ }
72
+ }
73
+ return true;
74
+ },
75
+ undefined: (value: any, condition: boolean = true): boolean => {
76
+ const isUndefined = typeof value === 'undefined';
77
+ return condition ? isUndefined : !isUndefined;
78
+ },
79
+ nonEmpty: (value: string | undefined | null, condition: boolean = true): boolean => {
80
+ const isEmpty = typeof value === 'undefined' || value === null || value === '';
81
+ return condition ? !isEmpty : isEmpty;
82
+ },
83
+ minLength: (value: string | any[], condition: number): boolean => {
84
+ if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert
85
+ return value.length >= condition;
86
+ },
87
+ maxLength: (value: string | any[], condition: number): boolean => {
88
+ if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert
89
+ return value.length <= condition;
90
+ },
91
+ casing: (value: string | string[], condition: string): boolean => {
92
+ if (typeof value === 'undefined') return true; // property doesn't exist, no need to lint it with this assert
93
+ const values = typeof value === 'string' ? [value] : value;
94
+ for (let _val of values) {
95
+ let matchCase = false;
96
+ switch (condition) {
97
+ case 'camelCase':
98
+ matchCase = !!_val.match(/^[a-z][a-zA-Z0-9]+$/g);
99
+ break;
100
+ case 'kebab-case':
101
+ matchCase = !!_val.match(/^([a-z][a-z0-9]*)(-[a-z0-9]+)*$/g);
102
+ break;
103
+ case 'snake_case':
104
+ matchCase = !!_val.match(/^([a-z][a-z0-9]*)(_[a-z0-9]+)*$/g);
105
+ break;
106
+ case 'PascalCase':
107
+ matchCase = !!_val.match(/^[A-Z][a-zA-Z0-9]+$/g);
108
+ break;
109
+ case 'MACRO_CASE':
110
+ matchCase = !!_val.match(/^([A-Z][A-Z0-9]*)(_[A-Z0-9]+)*$/g);
111
+ break;
112
+ case 'COBOL-CASE':
113
+ matchCase = !!_val.match(/^([A-Z][A-Z0-9]*)(-[A-Z0-9]+)*$/g);
114
+ break;
115
+ case 'flatcase':
116
+ matchCase = !!_val.match(/^[a-z][a-z0-9]+$/g);
117
+ break;
118
+ }
119
+ if (!matchCase) {
120
+ return false;
121
+ }
122
+ }
123
+ return true;
124
+ },
125
+ sortOrder: (value: any[], condition: OrderOptions | OrderDirection): boolean => {
126
+ if (typeof value === 'undefined') return true;
127
+ return isOrdered(value, condition);
128
+ },
129
+ mutuallyExclusive: (value: string[], condition: string[]): boolean => {
130
+ return getIntersectionLength(value, condition) < 2;
131
+ },
132
+ mutuallyRequired: (value: string[], condition: string[]): boolean => {
133
+ return getIntersectionLength(value, condition) > 0
134
+ ? getIntersectionLength(value, condition) === condition.length
135
+ : true;
136
+ },
137
+ };
@@ -0,0 +1,75 @@
1
+ import { asserts, runOnKeysSet, runOnValuesSet } from './asserts';
2
+ import { AssertToApply, buildSubjectVisitor, buildVisitorObject } from './utils';
3
+ import { Oas2Rule, Oas3Rule } from '../../../visitors';
4
+
5
+ export const Assertions: Oas3Rule | Oas2Rule = (opts: object) => {
6
+ let visitors: any[] = [];
7
+
8
+ // As 'Assertions' has an array of asserts,
9
+ // that array spreads into an 'opts' object on init rules phase here
10
+ // https://github.com/Redocly/openapi-cli/blob/master/packages/core/src/config/config.ts#L311
11
+ // that is why we need to iterate through 'opts' values;
12
+ // before - filter only object 'opts' values
13
+ const assertions: any[] = Object.values(opts).filter(
14
+ (opt: unknown) => typeof opt === 'object' && opt !== null,
15
+ );
16
+
17
+ for (const [index, assertion] of assertions.entries()) {
18
+ const assertId =
19
+ (assertion.assertionId && `${assertion.assertionId} assertion`) || `assertion #${index + 1}`;
20
+
21
+ if (!assertion.subject) {
22
+ throw new Error(`${assertId}: 'subject' is required`);
23
+ }
24
+
25
+ const subjects: string[] = Array.isArray(assertion.subject)
26
+ ? assertion.subject
27
+ : [assertion.subject];
28
+
29
+ const assertsToApply: AssertToApply[] = Object.keys(asserts)
30
+ .filter((assertName: string) => assertion[assertName] !== undefined)
31
+ .map((assertName: string) => {
32
+ return {
33
+ assertId,
34
+ name: assertName,
35
+ conditions: assertion[assertName],
36
+ message: assertion.message,
37
+ severity: assertion.severity || 'error',
38
+ suggest: assertion.suggest || [],
39
+ runsOnKeys: runOnKeysSet.has(assertName),
40
+ runsOnValues: runOnValuesSet.has(assertName),
41
+ };
42
+ });
43
+
44
+ const shouldRunOnKeys: AssertToApply | undefined = assertsToApply.find(
45
+ (assert: AssertToApply) => assert.runsOnKeys && !assert.runsOnValues,
46
+ );
47
+ const shouldRunOnValues: AssertToApply | undefined = assertsToApply.find(
48
+ (assert: AssertToApply) => assert.runsOnValues && !assert.runsOnKeys,
49
+ );
50
+
51
+ if (shouldRunOnValues && !assertion.property) {
52
+ throw new Error(
53
+ `${shouldRunOnValues.name} can't be used on all keys. Please provide a single property.`,
54
+ );
55
+ }
56
+
57
+ if (shouldRunOnKeys && assertion.property) {
58
+ throw new Error(
59
+ `${shouldRunOnKeys.name} can't be used on a single property. Please use 'property'.`,
60
+ );
61
+ }
62
+
63
+ for (const subject of subjects) {
64
+ const subjectVisitor = buildSubjectVisitor(
65
+ assertion.property,
66
+ assertsToApply,
67
+ assertion.context,
68
+ );
69
+ const visitorObject = buildVisitorObject(subject, assertion.context, subjectVisitor);
70
+ visitors.push(visitorObject);
71
+ }
72
+ }
73
+
74
+ return visitors;
75
+ };
@@ -0,0 +1,164 @@
1
+ import { Problem, ProblemSeverity, UserContext } from '../../../walk';
2
+ import { asserts } from './asserts';
3
+
4
+ export type OrderDirection = 'asc' | 'desc';
5
+
6
+ export type OrderOptions = {
7
+ direction: OrderDirection;
8
+ property: string;
9
+ };
10
+
11
+ export type AssertToApply = {
12
+ name: string;
13
+ assertId?: string;
14
+ conditions: any;
15
+ message?: string;
16
+ severity?: ProblemSeverity;
17
+ suggest?: string[];
18
+ runsOnKeys: boolean;
19
+ runsOnValues: boolean;
20
+ };
21
+
22
+ export function buildVisitorObject(
23
+ subject: string,
24
+ context: Record<string, any>[],
25
+ subjectVisitor: any,
26
+ ) {
27
+ if (!context) {
28
+ return { [subject]: subjectVisitor };
29
+ }
30
+
31
+ let currentVisitorLevel: Record<string, any> = {};
32
+ const visitor: Record<string, any> = currentVisitorLevel;
33
+
34
+ for (let index = 0; index < context.length; index++) {
35
+ const node = context[index];
36
+ if (context.length === index + 1 && node.type === subject) {
37
+ // Visitors don't work properly for the same type nested nodes, so
38
+ // as a workaround for that we don't create separate visitor for the last element
39
+ // which is the same as subject;
40
+ // we will check includes/excludes it in the last visitor.
41
+ continue;
42
+ }
43
+ const matchParentKeys = node.matchParentKeys;
44
+ const excludeParentKeys = node.excludeParentKeys;
45
+
46
+ if (matchParentKeys && excludeParentKeys) {
47
+ throw new Error(
48
+ `Both 'matchParentKeys' and 'excludeParentKeys' can't be under one context item`,
49
+ );
50
+ }
51
+
52
+ if (matchParentKeys || excludeParentKeys) {
53
+ currentVisitorLevel[node.type] = {
54
+ skip: (_value: any, key: string) => {
55
+ if (matchParentKeys) {
56
+ return !matchParentKeys.includes(key);
57
+ }
58
+ if (excludeParentKeys) {
59
+ return excludeParentKeys.includes(key);
60
+ }
61
+ },
62
+ };
63
+ } else {
64
+ currentVisitorLevel[node.type] = {};
65
+ }
66
+ currentVisitorLevel = currentVisitorLevel[node.type];
67
+ }
68
+
69
+ currentVisitorLevel[subject] = subjectVisitor;
70
+
71
+ return visitor;
72
+ }
73
+
74
+ export function buildSubjectVisitor(
75
+ properties: string | string[],
76
+ asserts: AssertToApply[],
77
+ context?: Record<string, any>[],
78
+ ) {
79
+ return function (node: any, { report, location, key, type }: UserContext) {
80
+ // We need to check context's last node if it has the same type as subject node;
81
+ // if yes - that means we didn't create context's last node visitor,
82
+ // so we need to handle 'matchParentKeys' and 'excludeParentKeys' conditions here;
83
+ if (context) {
84
+ const lastContextNode = context[context.length - 1];
85
+ if (lastContextNode.type === type.name) {
86
+ const matchParentKeys = lastContextNode.matchParentKeys;
87
+ const excludeParentKeys = lastContextNode.excludeParentKeys;
88
+
89
+ if (matchParentKeys && !matchParentKeys.includes(key)) {
90
+ return;
91
+ }
92
+ if (excludeParentKeys && excludeParentKeys.includes(key)) {
93
+ return;
94
+ }
95
+ }
96
+ }
97
+
98
+ if (properties) {
99
+ properties = Array.isArray(properties) ? properties : [properties];
100
+ }
101
+
102
+ for (const assert of asserts) {
103
+ if (properties) {
104
+ for (const property of properties) {
105
+ runAssertion(node[property], assert, location.child(property), report);
106
+ }
107
+ } else {
108
+ runAssertion(Object.keys(node), assert, location.key(), report);
109
+ }
110
+ }
111
+ };
112
+ }
113
+
114
+ export function getIntersectionLength(keys: string[], properties: string[]): number {
115
+ const props = new Set(properties);
116
+ let count = 0;
117
+ for (const key of keys) {
118
+ if (props.has(key)) {
119
+ count++;
120
+ }
121
+ }
122
+ return count;
123
+ }
124
+
125
+ export function isOrdered(value: any[], options: OrderOptions | OrderDirection): boolean {
126
+ const direction = (options as OrderOptions).direction || (options as OrderDirection);
127
+ const property = (options as OrderOptions).property;
128
+ for (let i = 1; i < value.length; i++) {
129
+ let currValue = value[i];
130
+ let prevVal = value[i - 1];
131
+
132
+ if (property) {
133
+ if (!value[i][property] || !value[i - 1][property]) {
134
+ return false; // property doesn't exist, so collection is not ordered
135
+ }
136
+ currValue = value[i][property];
137
+ prevVal = value[i - 1][property];
138
+ }
139
+
140
+ const result = direction === 'asc' ? currValue >= prevVal : currValue <= prevVal;
141
+ if (!result) {
142
+ return false;
143
+ }
144
+ }
145
+ return true;
146
+ }
147
+
148
+ function runAssertion(
149
+ values: string | string[],
150
+ assert: AssertToApply,
151
+ location: any,
152
+ report: (problem: Problem) => void,
153
+ ) {
154
+ const lintResult = asserts[assert.name](values, assert.conditions);
155
+ if (!lintResult) {
156
+ report({
157
+ message: assert.message || `The ${assert.assertId} doesn't meet required conditions`,
158
+ location,
159
+ forceSeverity: assert.severity,
160
+ suggest: assert.suggest,
161
+ ruleId: assert.assertId,
162
+ });
163
+ }
164
+ }
@@ -14,6 +14,7 @@ import { NoEnumTypeMismatch } from '../common/no-enum-type-mismatch';
14
14
  import { NoPathTrailingSlash } from '../common/no-path-trailing-slash';
15
15
  import { Operation2xxResponse } from '../common/operation-2xx-response';
16
16
  import { Operation4xxResponse } from '../common/operation-4xx-response';
17
+ import { Assertions } from '../common/assertions';
17
18
  import { OperationIdUnique } from '../common/operation-operationId-unique';
18
19
  import { OperationParametersUnique } from '../common/operation-parameters-unique';
19
20
  import { PathParamsDefined } from '../common/path-params-defined';
@@ -53,6 +54,7 @@ export const rules = {
53
54
  'no-path-trailing-slash': NoPathTrailingSlash as Oas2Rule,
54
55
  'operation-2xx-response': Operation2xxResponse as Oas2Rule,
55
56
  'operation-4xx-response': Operation4xxResponse as Oas2Rule,
57
+ 'assertions': Assertions as Oas2Rule,
56
58
  'operation-operationId-unique': OperationIdUnique as Oas2Rule,
57
59
  'operation-parameters-unique': OperationParametersUnique as Oas2Rule,
58
60
  'path-parameters-defined': PathParamsDefined as Oas2Rule,
@@ -176,7 +176,7 @@ describe('OpenAPI Schema', () => {
176
176
  Array [
177
177
  Object {
178
178
  "location": "#/servers",
179
- "message": "Expected type \`Server_List\` (array) but got \`object\`",
179
+ "message": "Expected type \`ServerList\` (array) but got \`object\`",
180
180
  },
181
181
  ]
182
182
  `);
@@ -2,6 +2,7 @@ import { Oas3RuleSet } from '../../oas-types';
2
2
  import { OasSpec } from '../common/spec';
3
3
  import { Operation2xxResponse } from '../common/operation-2xx-response';
4
4
  import { Operation4xxResponse } from '../common/operation-4xx-response';
5
+ import { Assertions } from '../common/assertions';
5
6
  import { OperationIdUnique } from '../common/operation-operationId-unique';
6
7
  import { OperationParametersUnique } from '../common/operation-parameters-unique';
7
8
  import { PathParamsDefined } from '../common/path-params-defined';
@@ -53,6 +54,7 @@ export const rules = {
53
54
  'info-license-url': InfoLicenseUrl,
54
55
  'operation-2xx-response': Operation2xxResponse,
55
56
  'operation-4xx-response': Operation4xxResponse,
57
+ 'assertions': Assertions,
56
58
  'operation-operationId-unique': OperationIdUnique,
57
59
  'operation-parameters-unique': OperationParametersUnique,
58
60
  'path-parameters-defined': PathParamsDefined,
@@ -49,7 +49,7 @@ type NormalizedResolveTypeFn = (
49
49
 
50
50
  export function listOf(typeName: string) {
51
51
  return {
52
- name: typeName + '_List',
52
+ name: `${typeName}List`,
53
53
  properties: {},
54
54
  items: typeName,
55
55
  };
@@ -57,7 +57,7 @@ export function listOf(typeName: string) {
57
57
 
58
58
  export function mapOf(typeName: string) {
59
59
  return {
60
- name: typeName + '_Map',
60
+ name: `${typeName}Map`,
61
61
  properties: {},
62
62
  additionalProperties: () => typeName,
63
63
  };
@@ -1,5 +1,5 @@
1
1
  import { NodeType, listOf, mapOf } from '.';
2
- import { Oas3Types } from './oas3'
2
+ import { Oas3Types } from './oas3';
3
3
 
4
4
  const DefinitionRoot: NodeType = {
5
5
  properties: {
@@ -15,7 +15,7 @@ const DefinitionRoot: NodeType = {
15
15
  jsonSchemaDialect: { type: 'string' },
16
16
  },
17
17
  required: ['openapi', 'info'],
18
- requiredOneOf: ['paths', 'components', 'webhooks']
18
+ requiredOneOf: ['paths', 'components', 'webhooks'],
19
19
  };
20
20
 
21
21
  const License: NodeType = {
@@ -106,11 +106,14 @@ const Schema: NodeType = {
106
106
  enum: { type: 'array' },
107
107
  type: (value: any) => {
108
108
  if (Array.isArray(value)) {
109
- return { type: 'array', items: { enum: ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'] } }
109
+ return {
110
+ type: 'array',
111
+ items: { enum: ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'] },
112
+ };
110
113
  } else {
111
114
  return {
112
115
  enum: ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'],
113
- }
116
+ };
114
117
  }
115
118
  },
116
119
  allOf: listOf('Schema'),
@@ -126,7 +129,13 @@ const Schema: NodeType = {
126
129
  patternProperties: { type: 'object' },
127
130
  propertyNames: 'Schema',
128
131
  unevaluatedItems: 'Schema',
129
- unevaluatedProperties: 'Schema',
132
+ unevaluatedProperties: (value: unknown) => {
133
+ if (typeof value === 'boolean') {
134
+ return { type: 'boolean' };
135
+ } else {
136
+ return 'Schema';
137
+ }
138
+ },
130
139
  summary: { type: 'string' },
131
140
  properties: 'SchemaProperties',
132
141
  items: (value: any) => {
@@ -196,9 +205,25 @@ const SecurityScheme: NodeType = {
196
205
  case 'clientCredentials':
197
206
  return ['type', 'flows', 'tokenUrl', 'refreshUrl', 'description', 'scopes'];
198
207
  case 'authorizationCode':
199
- return ['type', 'flows', 'authorizationUrl', 'refreshUrl', 'tokenUrl', 'description', 'scopes'];
208
+ return [
209
+ 'type',
210
+ 'flows',
211
+ 'authorizationUrl',
212
+ 'refreshUrl',
213
+ 'tokenUrl',
214
+ 'description',
215
+ 'scopes',
216
+ ];
200
217
  default:
201
- return ['type', 'flows', 'authorizationUrl', 'refreshUrl', 'tokenUrl', 'description', 'scopes'];
218
+ return [
219
+ 'type',
220
+ 'flows',
221
+ 'authorizationUrl',
222
+ 'refreshUrl',
223
+ 'tokenUrl',
224
+ 'description',
225
+ 'scopes',
226
+ ];
202
227
  }
203
228
  case 'openIdConnect':
204
229
  return ['type', 'openIdConnectUrl', 'description'];
package/src/visitors.ts CHANGED
@@ -244,8 +244,8 @@ export type NormalizedOasVisitors<T extends BaseVisitor> = {
244
244
  };
245
245
  };
246
246
 
247
- export type Oas3Rule = (options: Record<string, any>) => Oas3Visitor;
248
- export type Oas2Rule = (options: Record<string, any>) => Oas2Visitor;
247
+ export type Oas3Rule = (options: Record<string, any>) => Oas3Visitor | Oas3Visitor[];
248
+ export type Oas2Rule = (options: Record<string, any>) => Oas2Visitor | Oas2Visitor[];
249
249
  export type Oas3Preprocessor = (options: Record<string, any>) => Oas3TransformVisitor;
250
250
  export type Oas2Preprocessor = (options: Record<string, any>) => Oas2TransformVisitor;
251
251
  export type Oas3Decorator = (options: Record<string, any>) => Oas3TransformVisitor;
package/src/walk.ts CHANGED
@@ -63,6 +63,7 @@ export type Problem = {
63
63
  location?: Partial<LocationObject> | Array<Partial<LocationObject>>;
64
64
  from?: LocationObject;
65
65
  forceSeverity?: ProblemSeverity;
66
+ ruleId?: string;
66
67
  };
67
68
 
68
69
  export type NormalizedProblem = {
@@ -397,7 +398,7 @@ export function walkDocument<T>(opts: {
397
398
  : [{ ...currentLocation, reportOnKey: false }];
398
399
 
399
400
  ctx.problems.push({
400
- ruleId,
401
+ ruleId: opts.ruleId || ruleId,
401
402
  severity: opts.forceSeverity || severity,
402
403
  ...opts,
403
404
  suggest: opts.suggest || [],