@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.
Files changed (88) hide show
  1. package/README.md +2 -2
  2. package/lib/config/config-resolvers.js +44 -25
  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/common/no-ambiguous-paths.js +1 -1
  17. package/lib/rules/common/no-identical-paths.js +4 -4
  18. package/lib/rules/common/operation-2xx-response.js +2 -2
  19. package/lib/rules/common/operation-4xx-response.js +2 -2
  20. package/lib/rules/common/path-not-include-query.js +1 -1
  21. package/lib/rules/common/path-params-defined.js +7 -2
  22. package/lib/rules/common/response-contains-header.js +2 -2
  23. package/lib/rules/common/security-defined.js +10 -5
  24. package/lib/rules/common/spec.js +14 -12
  25. package/lib/rules/oas3/request-mime-type.js +1 -1
  26. package/lib/rules/oas3/response-mime-type.js +1 -1
  27. package/lib/rules/other/stats.d.ts +1 -1
  28. package/lib/rules/other/stats.js +1 -1
  29. package/lib/rules/utils.d.ts +1 -0
  30. package/lib/rules/utils.js +18 -2
  31. package/lib/types/oas2.js +6 -6
  32. package/lib/types/oas3.js +11 -11
  33. package/lib/types/oas3_1.js +3 -3
  34. package/lib/types/redocly-yaml.js +30 -5
  35. package/lib/utils.d.ts +1 -0
  36. package/lib/utils.js +13 -1
  37. package/lib/visitors.d.ts +7 -6
  38. package/lib/visitors.js +11 -3
  39. package/package.json +3 -5
  40. package/src/__tests__/__snapshots__/bundle.test.ts.snap +1 -1
  41. package/src/__tests__/lint.test.ts +88 -0
  42. package/src/__tests__/utils.test.ts +11 -0
  43. package/src/__tests__/walk.test.ts +2 -2
  44. package/src/config/__tests__/config-resolvers.test.ts +62 -1
  45. package/src/config/__tests__/config.test.ts +5 -0
  46. package/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml +16 -0
  47. package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml +16 -0
  48. package/src/config/__tests__/fixtures/resolve-config/plugin.js +11 -0
  49. package/src/config/__tests__/load.test.ts +1 -1
  50. package/src/config/__tests__/resolve-plugins.test.ts +3 -3
  51. package/src/config/config-resolvers.ts +30 -6
  52. package/src/config/config.ts +2 -0
  53. package/src/config/load.ts +10 -4
  54. package/src/config/types.ts +13 -0
  55. package/src/config/utils.ts +1 -0
  56. package/src/rules/ajv.ts +4 -4
  57. package/src/rules/common/__tests__/operation-2xx-response.test.ts +37 -0
  58. package/src/rules/common/__tests__/operation-4xx-response.test.ts +37 -0
  59. package/src/rules/common/__tests__/path-params-defined.test.ts +69 -0
  60. package/src/rules/common/__tests__/security-defined.test.ts +6 -6
  61. package/src/rules/common/__tests__/spec.test.ts +125 -0
  62. package/src/rules/common/assertions/__tests__/asserts.test.ts +491 -428
  63. package/src/rules/common/assertions/__tests__/utils.test.ts +2 -2
  64. package/src/rules/common/assertions/asserts.ts +155 -97
  65. package/src/rules/common/assertions/index.ts +2 -11
  66. package/src/rules/common/assertions/utils.ts +66 -36
  67. package/src/rules/common/no-ambiguous-paths.ts +1 -1
  68. package/src/rules/common/no-identical-paths.ts +4 -4
  69. package/src/rules/common/operation-2xx-response.ts +2 -2
  70. package/src/rules/common/operation-4xx-response.ts +2 -2
  71. package/src/rules/common/path-not-include-query.ts +1 -1
  72. package/src/rules/common/path-params-defined.ts +9 -2
  73. package/src/rules/common/response-contains-header.ts +6 -1
  74. package/src/rules/common/security-defined.ts +10 -5
  75. package/src/rules/common/spec.ts +15 -11
  76. package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +51 -2
  77. package/src/rules/oas3/__tests__/response-contains-header.test.ts +116 -0
  78. package/src/rules/oas3/request-mime-type.ts +1 -1
  79. package/src/rules/oas3/response-mime-type.ts +1 -1
  80. package/src/rules/other/stats.ts +1 -1
  81. package/src/rules/utils.ts +24 -1
  82. package/src/types/oas2.ts +6 -6
  83. package/src/types/oas3.ts +11 -11
  84. package/src/types/oas3_1.ts +3 -3
  85. package/src/types/redocly-yaml.ts +30 -4
  86. package/src/utils.ts +13 -0
  87. package/src/visitors.ts +25 -10
  88. 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 PathsMap",
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 PathsMap",
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/master/packages/core/src/config/__tests__/fixtures/resolve-remote-configs/remote-config.yaml',
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
@@ -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(undefined, undefined, mockFn);
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: Record<string, RuleConfig> | undefined
397
- ): Record<string, RuleConfig> | undefined {
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/', ''),
@@ -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
  }
@@ -62,11 +62,17 @@ async function addConfigMetadata({
62
62
  }
63
63
 
64
64
  export async function loadConfig(
65
- configPath: string | undefined = findConfig(),
66
- customExtends?: string[],
67
- processRawConfig?: (rawConfig: RawConfig) => void | Promise<void>
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 rawConfig = await getConfig(configPath);
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
  }
@@ -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'> &
@@ -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
- defaultAdditionalProperties: allowAdditionalProperties,
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": false,
84
+ "pointer": "#/paths/~1pets/get",
85
+ "reportOnKey": true,
86
86
  "source": "foobar.yaml",
87
87
  },
88
88
  ],
89
- "message": "Every API should have security defined on the root level or for each operation.",
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": false,
136
+ "pointer": "#/paths/~1cats/get",
137
+ "reportOnKey": true,
138
138
  "source": "foobar.yaml",
139
139
  },
140
140
  ],
141
- "message": "Every API should have security defined on the root level or for each operation.",
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 [],