@redocly/openapi-core 1.0.0-beta.109 → 1.0.0-beta.111
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/config/config-resolvers.js +44 -25
- package/lib/config/config.d.ts +1 -0
- package/lib/config/config.js +1 -0
- package/lib/config/load.d.ts +8 -2
- package/lib/config/load.js +4 -2
- package/lib/config/types.d.ts +10 -0
- package/lib/config/utils.js +2 -2
- package/lib/rules/ajv.d.ts +1 -1
- package/lib/rules/ajv.js +5 -5
- package/lib/rules/common/assertions/asserts.d.ts +3 -5
- package/lib/rules/common/assertions/asserts.js +137 -97
- package/lib/rules/common/assertions/index.js +2 -6
- package/lib/rules/common/assertions/utils.d.ts +12 -6
- package/lib/rules/common/assertions/utils.js +33 -20
- package/lib/rules/common/no-ambiguous-paths.js +1 -1
- package/lib/rules/common/no-identical-paths.js +4 -4
- package/lib/rules/common/operation-2xx-response.js +2 -2
- package/lib/rules/common/operation-4xx-response.js +2 -2
- package/lib/rules/common/path-not-include-query.js +1 -1
- package/lib/rules/common/path-params-defined.js +7 -2
- package/lib/rules/common/response-contains-header.js +2 -2
- package/lib/rules/common/security-defined.js +10 -5
- package/lib/rules/common/spec.js +14 -12
- package/lib/rules/oas3/request-mime-type.js +1 -1
- package/lib/rules/oas3/response-mime-type.js +1 -1
- package/lib/rules/other/stats.d.ts +1 -1
- package/lib/rules/other/stats.js +1 -1
- package/lib/rules/utils.d.ts +1 -0
- package/lib/rules/utils.js +18 -2
- package/lib/types/oas2.js +6 -6
- package/lib/types/oas3.js +11 -11
- package/lib/types/oas3_1.js +3 -3
- package/lib/types/redocly-yaml.js +30 -5
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +13 -1
- package/lib/visitors.d.ts +7 -6
- package/lib/visitors.js +11 -3
- package/package.json +3 -5
- package/src/__tests__/__snapshots__/bundle.test.ts.snap +1 -1
- package/src/__tests__/lint.test.ts +88 -0
- package/src/__tests__/utils.test.ts +11 -0
- package/src/__tests__/walk.test.ts +2 -2
- package/src/config/__tests__/config-resolvers.test.ts +62 -1
- package/src/config/__tests__/config.test.ts +5 -0
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml +16 -0
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml +16 -0
- package/src/config/__tests__/fixtures/resolve-config/plugin.js +11 -0
- package/src/config/__tests__/load.test.ts +1 -1
- package/src/config/__tests__/resolve-plugins.test.ts +3 -3
- package/src/config/config-resolvers.ts +30 -6
- package/src/config/config.ts +2 -0
- package/src/config/load.ts +10 -4
- package/src/config/types.ts +13 -0
- package/src/config/utils.ts +1 -0
- package/src/rules/ajv.ts +4 -4
- package/src/rules/common/__tests__/operation-2xx-response.test.ts +37 -0
- package/src/rules/common/__tests__/operation-4xx-response.test.ts +37 -0
- package/src/rules/common/__tests__/path-params-defined.test.ts +69 -0
- package/src/rules/common/__tests__/security-defined.test.ts +6 -6
- package/src/rules/common/__tests__/spec.test.ts +125 -0
- package/src/rules/common/assertions/__tests__/asserts.test.ts +491 -428
- package/src/rules/common/assertions/__tests__/utils.test.ts +2 -2
- package/src/rules/common/assertions/asserts.ts +155 -97
- package/src/rules/common/assertions/index.ts +2 -11
- package/src/rules/common/assertions/utils.ts +66 -36
- package/src/rules/common/no-ambiguous-paths.ts +1 -1
- package/src/rules/common/no-identical-paths.ts +4 -4
- package/src/rules/common/operation-2xx-response.ts +2 -2
- package/src/rules/common/operation-4xx-response.ts +2 -2
- package/src/rules/common/path-not-include-query.ts +1 -1
- package/src/rules/common/path-params-defined.ts +9 -2
- package/src/rules/common/response-contains-header.ts +6 -1
- package/src/rules/common/security-defined.ts +10 -5
- package/src/rules/common/spec.ts +15 -11
- package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +51 -2
- package/src/rules/oas3/__tests__/response-contains-header.test.ts +116 -0
- package/src/rules/oas3/request-mime-type.ts +1 -1
- package/src/rules/oas3/response-mime-type.ts +1 -1
- package/src/rules/other/stats.ts +1 -1
- package/src/rules/utils.ts +24 -1
- package/src/types/oas2.ts +6 -6
- package/src/types/oas3.ts +11 -11
- package/src/types/oas3_1.ts +3 -3
- package/src/types/redocly-yaml.ts +30 -4
- package/src/utils.ts +13 -0
- package/src/visitors.ts +25 -10
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
slash,
|
|
5
5
|
getMatchingStatusCodeRange,
|
|
6
6
|
doesYamlFileExist,
|
|
7
|
+
pickDefined,
|
|
7
8
|
} from '../utils';
|
|
8
9
|
import { isBrowser } from '../env';
|
|
9
10
|
import * as fs from 'fs';
|
|
@@ -81,6 +82,16 @@ describe('utils', () => {
|
|
|
81
82
|
});
|
|
82
83
|
});
|
|
83
84
|
|
|
85
|
+
describe('pickDefined', () => {
|
|
86
|
+
it('returns undefined for undefined', () => {
|
|
87
|
+
expect(pickDefined(undefined)).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('picks only defined values', () => {
|
|
91
|
+
expect(pickDefined({ a: 1, b: undefined, c: 3 })).toStrictEqual({ a: 1, c: 3 });
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
84
95
|
describe('getMatchingStatusCodeRange', () => {
|
|
85
96
|
it('should get the generalized form of status codes', () => {
|
|
86
97
|
expect(getMatchingStatusCodeRange('202')).toEqual('2XX');
|
|
@@ -1115,7 +1115,7 @@ describe('walk order', () => {
|
|
|
1115
1115
|
expect(calls).toMatchInlineSnapshot(`
|
|
1116
1116
|
Array [
|
|
1117
1117
|
"enter Root",
|
|
1118
|
-
"enter
|
|
1118
|
+
"enter Paths",
|
|
1119
1119
|
"enter PathItem",
|
|
1120
1120
|
"enter ParameterList",
|
|
1121
1121
|
"enter Parameter",
|
|
@@ -1134,7 +1134,7 @@ describe('walk order', () => {
|
|
|
1134
1134
|
"leave ParameterList",
|
|
1135
1135
|
"leave Operation",
|
|
1136
1136
|
"leave PathItem",
|
|
1137
|
-
"leave
|
|
1137
|
+
"leave Paths",
|
|
1138
1138
|
"enter Components",
|
|
1139
1139
|
"enter NamedParameters",
|
|
1140
1140
|
"leave NamedParameters",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { colorize } from '../../logger';
|
|
2
|
+
import { asserts } from '../../rules/common/assertions/asserts';
|
|
1
3
|
import { resolveStyleguideConfig, resolveApis, resolveConfig } from '../config-resolvers';
|
|
2
4
|
const path = require('path');
|
|
3
5
|
|
|
@@ -130,6 +132,40 @@ describe('resolveStyleguideConfig', () => {
|
|
|
130
132
|
expect(styleguide).toMatchSnapshot();
|
|
131
133
|
});
|
|
132
134
|
|
|
135
|
+
it('should resolve custom assertion from plugin', async () => {
|
|
136
|
+
const styleguideConfig = {
|
|
137
|
+
extends: ['local-config-with-custom-function.yaml'],
|
|
138
|
+
};
|
|
139
|
+
const { plugins } = await resolveStyleguideConfig({
|
|
140
|
+
styleguideConfig,
|
|
141
|
+
configPath,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(plugins).toBeDefined();
|
|
145
|
+
expect(plugins?.length).toBe(2);
|
|
146
|
+
expect(asserts['test-plugin/checkWordsCount']).toBeDefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should throw error when custom assertion load not exist plugin', async () => {
|
|
150
|
+
const styleguideConfig = {
|
|
151
|
+
extends: ['local-config-with-wrong-custom-function.yaml'],
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
await resolveStyleguideConfig({
|
|
155
|
+
styleguideConfig,
|
|
156
|
+
configPath,
|
|
157
|
+
});
|
|
158
|
+
} catch (e) {
|
|
159
|
+
expect(e.message.toString()).toContain(
|
|
160
|
+
`Plugin ${colorize.red(
|
|
161
|
+
'test-plugin'
|
|
162
|
+
)} doesn't export assertions function with name ${colorize.red('checkWordsCount2')}.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
expect(asserts['test-plugin/checkWordsCount']).toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
133
169
|
it('should correctly merge assertions from nested config', async () => {
|
|
134
170
|
const styleguideConfig = {
|
|
135
171
|
extends: ['local-config-with-file.yaml'],
|
|
@@ -165,7 +201,7 @@ describe('resolveStyleguideConfig', () => {
|
|
|
165
201
|
const styleguideConfig = {
|
|
166
202
|
// This points to ./fixtures/resolve-remote-configs/remote-config.yaml
|
|
167
203
|
extends: [
|
|
168
|
-
'https://raw.githubusercontent.com/Redocly/redocly-cli/
|
|
204
|
+
'https://raw.githubusercontent.com/Redocly/redocly-cli/main/packages/core/src/config/__tests__/fixtures/resolve-remote-configs/remote-config.yaml',
|
|
169
205
|
],
|
|
170
206
|
};
|
|
171
207
|
|
|
@@ -428,4 +464,29 @@ describe('resolveConfig', () => {
|
|
|
428
464
|
delete apis['petstore'].styleguide.pluginPaths;
|
|
429
465
|
expect(apis['petstore'].styleguide).toMatchSnapshot();
|
|
430
466
|
});
|
|
467
|
+
|
|
468
|
+
it('should default to the extends from the main config if no extends defined', async () => {
|
|
469
|
+
const rawConfig: RawConfig = {
|
|
470
|
+
apis: {
|
|
471
|
+
petstore: {
|
|
472
|
+
root: 'some/path',
|
|
473
|
+
styleguide: {
|
|
474
|
+
rules: {
|
|
475
|
+
'operation-4xx-response': 'error',
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
styleguide: {
|
|
481
|
+
extends: ['minimal'],
|
|
482
|
+
rules: {
|
|
483
|
+
'operation-2xx-response': 'warn',
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const { apis } = await resolveConfig(rawConfig, configPath);
|
|
489
|
+
expect(apis['petstore'].styleguide.rules).toBeDefined();
|
|
490
|
+
expect(apis['petstore'].styleguide.rules?.['operation-2xx-response']).toEqual('warn'); // from minimal ruleset
|
|
491
|
+
});
|
|
431
492
|
});
|
|
@@ -48,6 +48,7 @@ const testConfig: Config = {
|
|
|
48
48
|
'features.mockServer': {},
|
|
49
49
|
resolve: { http: { headers: [] } },
|
|
50
50
|
organization: 'redocly-test',
|
|
51
|
+
files: [],
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
describe('getMergedConfig', () => {
|
|
@@ -67,6 +68,7 @@ describe('getMergedConfig', () => {
|
|
|
67
68
|
"configFile": "redocly.yaml",
|
|
68
69
|
"features.mockServer": Object {},
|
|
69
70
|
"features.openapi": Object {},
|
|
71
|
+
"files": Array [],
|
|
70
72
|
"organization": "redocly-test",
|
|
71
73
|
"rawConfig": Object {
|
|
72
74
|
"apis": Object {
|
|
@@ -81,6 +83,7 @@ describe('getMergedConfig', () => {
|
|
|
81
83
|
},
|
|
82
84
|
"features.mockServer": Object {},
|
|
83
85
|
"features.openapi": Object {},
|
|
86
|
+
"files": Array [],
|
|
84
87
|
"organization": "redocly-test",
|
|
85
88
|
"styleguide": Object {
|
|
86
89
|
"extendPaths": Array [],
|
|
@@ -163,6 +166,7 @@ describe('getMergedConfig', () => {
|
|
|
163
166
|
"configFile": "redocly.yaml",
|
|
164
167
|
"features.mockServer": Object {},
|
|
165
168
|
"features.openapi": Object {},
|
|
169
|
+
"files": Array [],
|
|
166
170
|
"organization": "redocly-test",
|
|
167
171
|
"rawConfig": Object {
|
|
168
172
|
"apis": Object {
|
|
@@ -177,6 +181,7 @@ describe('getMergedConfig', () => {
|
|
|
177
181
|
},
|
|
178
182
|
"features.mockServer": Object {},
|
|
179
183
|
"features.openapi": Object {},
|
|
184
|
+
"files": Array [],
|
|
180
185
|
"organization": "redocly-test",
|
|
181
186
|
"styleguide": Object {
|
|
182
187
|
"extendPaths": Array [],
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
lint:
|
|
2
|
+
rules:
|
|
3
|
+
no-invalid-media-type-examples: warn
|
|
4
|
+
operation-4xx-response: off
|
|
5
|
+
assert/tag-description:
|
|
6
|
+
subject: Tag
|
|
7
|
+
property: description
|
|
8
|
+
message: Tag description must have at least 3 words.
|
|
9
|
+
severity: error
|
|
10
|
+
test-plugin/checkWordsCount:
|
|
11
|
+
min: 3
|
|
12
|
+
plugins:
|
|
13
|
+
- plugin.js
|
|
14
|
+
extends:
|
|
15
|
+
- recommended
|
|
16
|
+
- test-plugin/all
|
package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
lint:
|
|
2
|
+
rules:
|
|
3
|
+
no-invalid-media-type-examples: warn
|
|
4
|
+
operation-4xx-response: off
|
|
5
|
+
assert/tag-description:
|
|
6
|
+
subject: Tag
|
|
7
|
+
property: description
|
|
8
|
+
message: Tag description must have at least 3 words.
|
|
9
|
+
severity: error
|
|
10
|
+
test-plugin/checkWordsCount2:
|
|
11
|
+
min: 3
|
|
12
|
+
plugins:
|
|
13
|
+
- plugin.js
|
|
14
|
+
extends:
|
|
15
|
+
- recommended
|
|
16
|
+
- test-plugin/all
|
|
@@ -51,6 +51,16 @@ const decorators = {
|
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
const assertions = {
|
|
55
|
+
checkWordsCount: (value, opts, location) => {
|
|
56
|
+
const words = value.split(' ');
|
|
57
|
+
if (words.length >= opts.min) {
|
|
58
|
+
return { isValid: true };
|
|
59
|
+
}
|
|
60
|
+
return { isValid: false, location };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
54
64
|
const configs = {
|
|
55
65
|
all: {
|
|
56
66
|
rules: {
|
|
@@ -66,4 +76,5 @@ module.exports = {
|
|
|
66
76
|
rules,
|
|
67
77
|
decorators,
|
|
68
78
|
configs,
|
|
79
|
+
assertions,
|
|
69
80
|
};
|
|
@@ -49,7 +49,7 @@ describe('loadConfig', () => {
|
|
|
49
49
|
|
|
50
50
|
it('should call callback if such passed', async () => {
|
|
51
51
|
const mockFn = jest.fn();
|
|
52
|
-
await loadConfig(
|
|
52
|
+
await loadConfig({ processRawConfig: mockFn });
|
|
53
53
|
expect(mockFn).toHaveBeenCalled();
|
|
54
54
|
});
|
|
55
55
|
});
|
|
@@ -5,21 +5,21 @@ describe('resolving a plugin', () => {
|
|
|
5
5
|
const configPath = path.join(__dirname, 'fixtures/plugin-config.yaml');
|
|
6
6
|
|
|
7
7
|
it('should prefix rule names with the plugin id', async () => {
|
|
8
|
-
const config = await loadConfig(configPath);
|
|
8
|
+
const config = await loadConfig({ configPath });
|
|
9
9
|
const plugin = config.styleguide.plugins[0];
|
|
10
10
|
|
|
11
11
|
expect(plugin.rules?.oas3).toHaveProperty('test-plugin/openid-connect-url-well-known');
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
it('should prefix preprocessor names with the plugin id', async () => {
|
|
15
|
-
const config = await loadConfig(configPath);
|
|
15
|
+
const config = await loadConfig({ configPath });
|
|
16
16
|
const plugin = config.styleguide.plugins[0];
|
|
17
17
|
|
|
18
18
|
expect(plugin.preprocessors?.oas2).toHaveProperty('test-plugin/description-preprocessor');
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
it('should prefix decorator names with the plugin id', async () => {
|
|
22
|
-
const config = await loadConfig(configPath);
|
|
22
|
+
const config = await loadConfig({ configPath });
|
|
23
23
|
const plugin = config.styleguide.plugins[0];
|
|
24
24
|
|
|
25
25
|
expect(plugin.decorators?.oas3).toHaveProperty('test-plugin/inject-x-stats');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import { isAbsoluteUrl } from '../ref-utils';
|
|
3
|
+
import { pickDefined } from '../utils';
|
|
3
4
|
import { BaseResolver } from '../resolve';
|
|
4
5
|
import { defaultPlugin } from './builtIn';
|
|
5
6
|
import {
|
|
@@ -24,6 +25,7 @@ import { isBrowser } from '../env';
|
|
|
24
25
|
import { isNotString, isString, isDefined, parseYaml } from '../utils';
|
|
25
26
|
import { Config } from './config';
|
|
26
27
|
import { colorize, logger } from '../logger';
|
|
28
|
+
import { asserts, buildAssertCustomFunction } from '../rules/common/assertions/asserts';
|
|
27
29
|
|
|
28
30
|
export async function resolveConfig(rawConfig: RawConfig, configPath?: string): Promise<Config> {
|
|
29
31
|
if (rawConfig.styleguide?.extends?.some(isNotString)) {
|
|
@@ -175,6 +177,10 @@ export function resolvePlugins(
|
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
if (pluginModule.assertions) {
|
|
181
|
+
plugin.assertions = pluginModule.assertions;
|
|
182
|
+
}
|
|
183
|
+
|
|
178
184
|
return plugin;
|
|
179
185
|
})
|
|
180
186
|
.filter(isDefined);
|
|
@@ -299,8 +305,7 @@ export async function resolveStyleguideConfig(
|
|
|
299
305
|
return {
|
|
300
306
|
...resolvedStyleguideConfig,
|
|
301
307
|
rules:
|
|
302
|
-
resolvedStyleguideConfig.rules &&
|
|
303
|
-
groupStyleguideAssertionRules(resolvedStyleguideConfig.rules),
|
|
308
|
+
resolvedStyleguideConfig.rules && groupStyleguideAssertionRules(resolvedStyleguideConfig),
|
|
304
309
|
};
|
|
305
310
|
}
|
|
306
311
|
|
|
@@ -351,7 +356,7 @@ function getMergedRawStyleguideConfig(
|
|
|
351
356
|
) {
|
|
352
357
|
const resultLint = {
|
|
353
358
|
...rootStyleguideConfig,
|
|
354
|
-
...apiStyleguideConfig,
|
|
359
|
+
...pickDefined(apiStyleguideConfig),
|
|
355
360
|
rules: { ...rootStyleguideConfig?.rules, ...apiStyleguideConfig?.rules },
|
|
356
361
|
oas2Rules: { ...rootStyleguideConfig?.oas2Rules, ...apiStyleguideConfig?.oas2Rules },
|
|
357
362
|
oas3_0Rules: { ...rootStyleguideConfig?.oas3_0Rules, ...apiStyleguideConfig?.oas3_0Rules },
|
|
@@ -392,9 +397,10 @@ function getMergedRawStyleguideConfig(
|
|
|
392
397
|
return resultLint;
|
|
393
398
|
}
|
|
394
399
|
|
|
395
|
-
function groupStyleguideAssertionRules(
|
|
396
|
-
rules
|
|
397
|
-
|
|
400
|
+
function groupStyleguideAssertionRules({
|
|
401
|
+
rules,
|
|
402
|
+
plugins,
|
|
403
|
+
}: ResolvedStyleguideConfig): Record<string, RuleConfig> | undefined {
|
|
398
404
|
if (!rules) {
|
|
399
405
|
return rules;
|
|
400
406
|
}
|
|
@@ -407,6 +413,24 @@ function groupStyleguideAssertionRules(
|
|
|
407
413
|
for (const [ruleKey, rule] of Object.entries(rules)) {
|
|
408
414
|
if (ruleKey.startsWith('assert/') && typeof rule === 'object' && rule !== null) {
|
|
409
415
|
const assertion = rule;
|
|
416
|
+
if (plugins) {
|
|
417
|
+
for (const field of Object.keys(assertion)) {
|
|
418
|
+
const [pluginId, fn] = field.split('/');
|
|
419
|
+
if (!pluginId || !fn) continue;
|
|
420
|
+
const plugin = plugins.find((plugin) => plugin.id === pluginId);
|
|
421
|
+
if (!plugin) {
|
|
422
|
+
throw Error(colorize.red(`Plugin ${colorize.blue(pluginId)} isn't found.`));
|
|
423
|
+
}
|
|
424
|
+
if (!plugin.assertions || !plugin.assertions[fn]) {
|
|
425
|
+
throw Error(
|
|
426
|
+
`Plugin ${colorize.red(
|
|
427
|
+
pluginId
|
|
428
|
+
)} doesn't export assertions function with name ${colorize.red(fn)}.`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
asserts[field] = buildAssertCustomFunction(plugin.assertions[fn]);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
410
434
|
assertions.push({
|
|
411
435
|
...assertion,
|
|
412
436
|
assertionId: ruleKey.replace('assert/', ''),
|
package/src/config/config.ts
CHANGED
|
@@ -306,6 +306,7 @@ export class Config {
|
|
|
306
306
|
'features.openapi': Record<string, any>;
|
|
307
307
|
'features.mockServer'?: Record<string, any>;
|
|
308
308
|
organization?: string;
|
|
309
|
+
files: string[];
|
|
309
310
|
constructor(public rawConfig: ResolvedConfig, public configFile?: string) {
|
|
310
311
|
this.apis = rawConfig.apis || {};
|
|
311
312
|
this.styleguide = new StyleguideConfig(rawConfig.styleguide || {}, configFile);
|
|
@@ -314,5 +315,6 @@ export class Config {
|
|
|
314
315
|
this.resolve = getResolveConfig(rawConfig?.resolve);
|
|
315
316
|
this.region = rawConfig.region;
|
|
316
317
|
this.organization = rawConfig.organization;
|
|
318
|
+
this.files = rawConfig.files || [];
|
|
317
319
|
}
|
|
318
320
|
}
|
package/src/config/load.ts
CHANGED
|
@@ -62,11 +62,17 @@ async function addConfigMetadata({
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export async function loadConfig(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
options: {
|
|
66
|
+
configPath?: string;
|
|
67
|
+
customExtends?: string[];
|
|
68
|
+
processRawConfig?: (rawConfig: RawConfig) => void | Promise<void>;
|
|
69
|
+
files?: string[];
|
|
70
|
+
region?: Region;
|
|
71
|
+
} = {}
|
|
68
72
|
): Promise<Config> {
|
|
69
|
-
const
|
|
73
|
+
const { configPath = findConfig(), customExtends, processRawConfig, files, region } = options;
|
|
74
|
+
const config = await getConfig(configPath);
|
|
75
|
+
const rawConfig = { ...config, files: files ?? config.files, region: region ?? config.region };
|
|
70
76
|
if (typeof processRawConfig === 'function') {
|
|
71
77
|
await processRawConfig(rawConfig);
|
|
72
78
|
}
|
package/src/config/types.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
OasVersion,
|
|
11
11
|
} from '../oas-types';
|
|
12
12
|
import type { NodeType } from '../types';
|
|
13
|
+
import { Location } from '../ref-utils';
|
|
13
14
|
|
|
14
15
|
export type RuleSeverity = ProblemSeverity | 'off';
|
|
15
16
|
|
|
@@ -83,6 +84,15 @@ export type CustomRulesConfig = {
|
|
|
83
84
|
oas2?: Oas2RuleSet;
|
|
84
85
|
};
|
|
85
86
|
|
|
87
|
+
export type AssertResult = { message?: string; location?: Location };
|
|
88
|
+
export type CustomFunction = (
|
|
89
|
+
value: any,
|
|
90
|
+
options: unknown,
|
|
91
|
+
baseLocation: Location
|
|
92
|
+
) => AssertResult[];
|
|
93
|
+
|
|
94
|
+
export type AssertionsConfig = Record<string, CustomFunction>;
|
|
95
|
+
|
|
86
96
|
export type Plugin = {
|
|
87
97
|
id: string;
|
|
88
98
|
configs?: Record<string, PluginStyleguideConfig>;
|
|
@@ -90,6 +100,7 @@ export type Plugin = {
|
|
|
90
100
|
preprocessors?: PreprocessorsConfig;
|
|
91
101
|
decorators?: DecoratorsConfig;
|
|
92
102
|
typeExtension?: TypeExtensionsConfig;
|
|
103
|
+
assertions?: AssertionsConfig;
|
|
93
104
|
};
|
|
94
105
|
|
|
95
106
|
export type PluginStyleguideConfig = Omit<StyleguideRawConfig, 'plugins' | 'extends'>;
|
|
@@ -145,6 +156,7 @@ export type DeprecatedInApi = {
|
|
|
145
156
|
|
|
146
157
|
export type ResolvedApi = Omit<Api, 'styleguide'> & {
|
|
147
158
|
styleguide: ResolvedStyleguideConfig;
|
|
159
|
+
files?: string[];
|
|
148
160
|
};
|
|
149
161
|
|
|
150
162
|
export type RawConfig = {
|
|
@@ -153,6 +165,7 @@ export type RawConfig = {
|
|
|
153
165
|
resolve?: RawResolveConfig;
|
|
154
166
|
region?: Region;
|
|
155
167
|
organization?: string;
|
|
168
|
+
files?: string[];
|
|
156
169
|
} & FeaturesConfig;
|
|
157
170
|
|
|
158
171
|
export type FlatApi = Omit<Api, 'styleguide'> &
|
package/src/config/utils.ts
CHANGED
|
@@ -233,6 +233,7 @@ export function getMergedConfig(config: Config, apiName?: string): Config {
|
|
|
233
233
|
...config['features.mockServer'],
|
|
234
234
|
...config.apis[apiName]?.['features.mockServer'],
|
|
235
235
|
},
|
|
236
|
+
files: [...config.files, ...(config.apis?.[apiName]?.files ?? [])],
|
|
236
237
|
// TODO: merge everything else here
|
|
237
238
|
},
|
|
238
239
|
config.configFile
|
package/src/rules/ajv.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Ajv, { ValidateFunction, ErrorObject } from '@redocly/ajv';
|
|
1
|
+
import Ajv, { ValidateFunction, ErrorObject } from '@redocly/ajv/dist/2020';
|
|
2
2
|
import { Location, escapePointer } from '../ref-utils';
|
|
3
3
|
import { ResolveFn } from '../walk';
|
|
4
4
|
|
|
@@ -20,7 +20,7 @@ function getAjv(resolve: ResolveFn, allowAdditionalProperties: boolean) {
|
|
|
20
20
|
discriminator: true,
|
|
21
21
|
allowUnionTypes: true,
|
|
22
22
|
validateFormats: false, // TODO: fix it
|
|
23
|
-
|
|
23
|
+
defaultUnevaluatedProperties: allowAdditionalProperties,
|
|
24
24
|
loadSchemaSync(base: string, $ref: string) {
|
|
25
25
|
const resolvedRef = resolve({ $ref }, base.split('#')[0]);
|
|
26
26
|
if (!resolvedRef || !resolvedRef.location) return false;
|
|
@@ -87,8 +87,8 @@ export function validateJsonSchema(
|
|
|
87
87
|
if (propName) {
|
|
88
88
|
message = `\`${propName}\` property ${message}`;
|
|
89
89
|
}
|
|
90
|
-
if (error.keyword === 'additionalProperties') {
|
|
91
|
-
const property = error.params.additionalProperty;
|
|
90
|
+
if (error.keyword === 'additionalProperties' || error.keyword === 'unevaluatedProperties') {
|
|
91
|
+
const property = error.params.additionalProperty || error.params.unevaluatedProperty;
|
|
92
92
|
message = `${message} \`${property}\``;
|
|
93
93
|
error.instancePath += '/' + escapePointer(property);
|
|
94
94
|
}
|
|
@@ -88,4 +88,41 @@ describe('Oas3 operation-2xx-response', () => {
|
|
|
88
88
|
|
|
89
89
|
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
it('should report even if the responses are null', async () => {
|
|
93
|
+
const document = parseYamlToDocument(
|
|
94
|
+
outdent`
|
|
95
|
+
openapi: 3.0.0
|
|
96
|
+
paths:
|
|
97
|
+
'/test/':
|
|
98
|
+
put:
|
|
99
|
+
responses: null
|
|
100
|
+
`,
|
|
101
|
+
'foobar.yaml'
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const results = await lintDocument({
|
|
105
|
+
externalRefResolver: new BaseResolver(),
|
|
106
|
+
document,
|
|
107
|
+
config: await makeConfig({ 'operation-2xx-response': 'error' }),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
111
|
+
Array [
|
|
112
|
+
Object {
|
|
113
|
+
"location": Array [
|
|
114
|
+
Object {
|
|
115
|
+
"pointer": "#/paths/~1test~1/put/responses",
|
|
116
|
+
"reportOnKey": true,
|
|
117
|
+
"source": "foobar.yaml",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
"message": "Operation must have at least one \`2XX\` response.",
|
|
121
|
+
"ruleId": "operation-2xx-response",
|
|
122
|
+
"severity": "error",
|
|
123
|
+
"suggest": Array [],
|
|
124
|
+
},
|
|
125
|
+
]
|
|
126
|
+
`);
|
|
127
|
+
});
|
|
91
128
|
});
|
|
@@ -127,4 +127,41 @@ describe('Oas3 operation-4xx-response', () => {
|
|
|
127
127
|
]
|
|
128
128
|
`);
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
it('should report even if the responses are null', async () => {
|
|
132
|
+
const document = parseYamlToDocument(
|
|
133
|
+
outdent`
|
|
134
|
+
openapi: 3.0.0
|
|
135
|
+
paths:
|
|
136
|
+
'/test/':
|
|
137
|
+
put:
|
|
138
|
+
responses: null
|
|
139
|
+
`,
|
|
140
|
+
'foobar.yaml'
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const results = await lintDocument({
|
|
144
|
+
externalRefResolver: new BaseResolver(),
|
|
145
|
+
document,
|
|
146
|
+
config: await makeConfig({ 'operation-2xx-response': 'error' }),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
150
|
+
Array [
|
|
151
|
+
Object {
|
|
152
|
+
"location": Array [
|
|
153
|
+
Object {
|
|
154
|
+
"pointer": "#/paths/~1test~1/put/responses",
|
|
155
|
+
"reportOnKey": true,
|
|
156
|
+
"source": "foobar.yaml",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
"message": "Operation must have at least one \`2XX\` response.",
|
|
160
|
+
"ruleId": "operation-2xx-response",
|
|
161
|
+
"severity": "error",
|
|
162
|
+
"suggest": Array [],
|
|
163
|
+
},
|
|
164
|
+
]
|
|
165
|
+
`);
|
|
166
|
+
});
|
|
130
167
|
});
|
|
@@ -130,4 +130,73 @@ describe('Oas3 path-params-defined', () => {
|
|
|
130
130
|
]
|
|
131
131
|
`);
|
|
132
132
|
});
|
|
133
|
+
|
|
134
|
+
it('should fail cause POST has no parameters', async () => {
|
|
135
|
+
const document = parseYamlToDocument(
|
|
136
|
+
outdent`
|
|
137
|
+
openapi: 3.0.0
|
|
138
|
+
paths:
|
|
139
|
+
/pets/{a}:
|
|
140
|
+
get:
|
|
141
|
+
parameters:
|
|
142
|
+
- name: a
|
|
143
|
+
in: path
|
|
144
|
+
post:
|
|
145
|
+
description: without parameters
|
|
146
|
+
`,
|
|
147
|
+
'foobar.yaml'
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const results = await lintDocument({
|
|
151
|
+
externalRefResolver: new BaseResolver(),
|
|
152
|
+
document,
|
|
153
|
+
config: await makeConfig({ 'path-params-defined': 'error' }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
157
|
+
Array [
|
|
158
|
+
Object {
|
|
159
|
+
"location": Array [
|
|
160
|
+
Object {
|
|
161
|
+
"pointer": "#/paths/~1pets~1{a}/post/parameters",
|
|
162
|
+
"reportOnKey": true,
|
|
163
|
+
"source": "foobar.yaml",
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
"message": "The operation does not define the path parameter \`{a}\` expected by path \`/pets/{a}\`.",
|
|
167
|
+
"ruleId": "path-params-defined",
|
|
168
|
+
"severity": "error",
|
|
169
|
+
"suggest": Array [],
|
|
170
|
+
},
|
|
171
|
+
]
|
|
172
|
+
`);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should apply parameters for POST operation from path parameters', async () => {
|
|
176
|
+
const document = parseYamlToDocument(
|
|
177
|
+
outdent`
|
|
178
|
+
openapi: 3.0.0
|
|
179
|
+
paths:
|
|
180
|
+
/pets/{a}:
|
|
181
|
+
parameters:
|
|
182
|
+
- name: a
|
|
183
|
+
in: path
|
|
184
|
+
get:
|
|
185
|
+
parameters:
|
|
186
|
+
- name: a
|
|
187
|
+
in: path
|
|
188
|
+
post:
|
|
189
|
+
description: without parameters
|
|
190
|
+
`,
|
|
191
|
+
'foobar.yaml'
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const results = await lintDocument({
|
|
195
|
+
externalRefResolver: new BaseResolver(),
|
|
196
|
+
document,
|
|
197
|
+
config: await makeConfig({ 'path-params-defined': 'error' }),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
201
|
+
});
|
|
133
202
|
});
|
|
@@ -81,12 +81,12 @@ describe('Oas3 security-defined', () => {
|
|
|
81
81
|
Object {
|
|
82
82
|
"location": Array [
|
|
83
83
|
Object {
|
|
84
|
-
"pointer": "#/",
|
|
85
|
-
"reportOnKey":
|
|
84
|
+
"pointer": "#/paths/~1pets/get",
|
|
85
|
+
"reportOnKey": true,
|
|
86
86
|
"source": "foobar.yaml",
|
|
87
87
|
},
|
|
88
88
|
],
|
|
89
|
-
"message": "Every
|
|
89
|
+
"message": "Every operation should have security defined on it or on the root level.",
|
|
90
90
|
"ruleId": "security-defined",
|
|
91
91
|
"severity": "error",
|
|
92
92
|
"suggest": Array [],
|
|
@@ -133,12 +133,12 @@ describe('Oas3 security-defined', () => {
|
|
|
133
133
|
Object {
|
|
134
134
|
"location": Array [
|
|
135
135
|
Object {
|
|
136
|
-
"pointer": "#/",
|
|
137
|
-
"reportOnKey":
|
|
136
|
+
"pointer": "#/paths/~1cats/get",
|
|
137
|
+
"reportOnKey": true,
|
|
138
138
|
"source": "foobar.yaml",
|
|
139
139
|
},
|
|
140
140
|
],
|
|
141
|
-
"message": "Every
|
|
141
|
+
"message": "Every operation should have security defined on it or on the root level.",
|
|
142
142
|
"ruleId": "security-defined",
|
|
143
143
|
"severity": "error",
|
|
144
144
|
"suggest": Array [],
|