@redocly/openapi-core 1.1.0 → 1.2.1

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 (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +250 -7
  3. package/lib/bundle.d.ts +12 -6
  4. package/lib/bundle.js +34 -24
  5. package/lib/config/builtIn.js +4 -0
  6. package/lib/config/config-resolvers.js +19 -6
  7. package/lib/config/config.d.ts +9 -9
  8. package/lib/config/config.js +32 -17
  9. package/lib/config/load.d.ts +3 -2
  10. package/lib/config/load.js +2 -2
  11. package/lib/config/rules.d.ts +2 -2
  12. package/lib/config/types.d.ts +10 -3
  13. package/lib/index.d.ts +4 -3
  14. package/lib/index.js +10 -6
  15. package/lib/lint.js +10 -16
  16. package/lib/oas-types.d.ts +16 -10
  17. package/lib/oas-types.js +52 -26
  18. package/lib/rules/async2/channels-kebab-case.d.ts +2 -0
  19. package/lib/rules/async2/channels-kebab-case.js +19 -0
  20. package/lib/rules/async2/index.d.ts +12 -0
  21. package/lib/rules/async2/index.js +22 -0
  22. package/lib/rules/async2/no-channel-trailing-slash.d.ts +2 -0
  23. package/lib/rules/async2/no-channel-trailing-slash.js +16 -0
  24. package/lib/rules/common/assertions/asserts.js +12 -4
  25. package/lib/rules/common/scalar-property-missing-example.js +1 -1
  26. package/lib/rules/common/spec.d.ts +2 -2
  27. package/lib/rules/common/spec.js +3 -3
  28. package/lib/rules/oas2/index.js +1 -1
  29. package/lib/rules/oas3/index.js +1 -1
  30. package/lib/rules/oas3/no-server-example.com.js +3 -2
  31. package/lib/types/asyncapi.d.ts +2 -0
  32. package/lib/types/asyncapi.js +1034 -0
  33. package/lib/types/oas3_1.js +8 -1
  34. package/lib/types/redocly-yaml.js +3 -0
  35. package/lib/typings/asyncapi.d.ts +21 -0
  36. package/lib/typings/asyncapi.js +2 -0
  37. package/lib/visitors.d.ts +12 -0
  38. package/lib/walk.d.ts +3 -3
  39. package/package.json +4 -2
  40. package/src/__tests__/__snapshots__/bundle.test.ts.snap +1 -1
  41. package/src/__tests__/bundle.test.ts +65 -25
  42. package/src/__tests__/fixtures/lint/openapi.yaml +10 -0
  43. package/src/__tests__/fixtures/redocly.yaml +2 -0
  44. package/src/__tests__/lint.test.ts +67 -8
  45. package/src/bundle.ts +62 -35
  46. package/src/config/__tests__/__snapshots__/config.test.ts.snap +24 -0
  47. package/src/config/__tests__/config.test.ts +15 -4
  48. package/src/config/__tests__/load.test.ts +35 -2
  49. package/src/config/builtIn.ts +4 -0
  50. package/src/config/config-resolvers.ts +25 -6
  51. package/src/config/config.ts +51 -27
  52. package/src/config/load.ts +11 -4
  53. package/src/config/rules.ts +2 -2
  54. package/src/config/types.ts +17 -4
  55. package/src/index.ts +10 -2
  56. package/src/lint.ts +13 -22
  57. package/src/oas-types.ts +59 -21
  58. package/src/rules/async2/__tests__/channels-kebab-case.test.ts +141 -0
  59. package/src/rules/async2/__tests__/no-channel-trailing-slash.test.ts +97 -0
  60. package/src/rules/async2/channels-kebab-case.ts +18 -0
  61. package/src/rules/async2/index.ts +22 -0
  62. package/src/rules/async2/no-channel-trailing-slash.ts +15 -0
  63. package/src/rules/common/assertions/asserts.ts +16 -4
  64. package/src/rules/common/scalar-property-missing-example.ts +2 -2
  65. package/src/rules/common/spec.ts +2 -2
  66. package/src/rules/oas2/index.ts +2 -2
  67. package/src/rules/oas3/__tests__/no-server-example.com.test.ts +36 -1
  68. package/src/rules/oas3/__tests__/spec/spec.test.ts +1 -1
  69. package/src/rules/oas3/index.ts +2 -2
  70. package/src/rules/oas3/no-server-example.com.ts +3 -2
  71. package/src/types/asyncapi.ts +1142 -0
  72. package/src/types/oas3_1.ts +7 -1
  73. package/src/types/redocly-yaml.ts +3 -0
  74. package/src/typings/asyncapi.ts +26 -0
  75. package/src/visitors.ts +22 -0
  76. package/src/walk.ts +3 -3
  77. package/tsconfig.tsbuildinfo +1 -1
package/src/oas-types.ts CHANGED
@@ -1,57 +1,95 @@
1
- import { Oas3Rule, Oas3Preprocessor, Oas2Rule, Oas2Preprocessor } from './visitors';
1
+ import {
2
+ Oas3Rule,
3
+ Oas3Preprocessor,
4
+ Oas2Rule,
5
+ Oas2Preprocessor,
6
+ Async2Preprocessor,
7
+ Async2Rule,
8
+ } from './visitors';
9
+ import { Oas2Types } from './types/oas2';
10
+ import { Oas3Types } from './types/oas3';
11
+ import { Oas3_1Types } from './types/oas3_1';
12
+ import { AsyncApi2Types } from './types/asyncapi';
2
13
 
3
14
  export type RuleSet<T> = Record<string, T>;
4
15
 
5
- export enum OasVersion {
6
- Version2 = 'oas2',
7
- Version3_0 = 'oas3_0',
8
- Version3_1 = 'oas3_1',
16
+ export enum SpecVersion {
17
+ OAS2 = 'oas2',
18
+ OAS3_0 = 'oas3_0',
19
+ OAS3_1 = 'oas3_1',
20
+ Async2 = 'async2', // todo split into 2.x maybe?
9
21
  }
10
22
 
11
- export enum OasMajorVersion {
12
- Version2 = 'oas2',
13
- Version3 = 'oas3',
23
+ export enum SpecMajorVersion {
24
+ OAS2 = 'oas2',
25
+ OAS3 = 'oas3',
26
+ Async2 = 'async2',
14
27
  }
15
28
 
29
+ const typesMap = {
30
+ [SpecVersion.OAS2]: Oas2Types,
31
+ [SpecVersion.OAS3_0]: Oas3Types,
32
+ [SpecVersion.OAS3_1]: Oas3_1Types,
33
+ [SpecVersion.Async2]: AsyncApi2Types,
34
+ };
35
+
16
36
  export type Oas3RuleSet = Record<string, Oas3Rule>;
17
37
  export type Oas2RuleSet = Record<string, Oas2Rule>;
38
+ export type Async2RuleSet = Record<string, Async2Rule>;
18
39
  export type Oas3PreprocessorsSet = Record<string, Oas3Preprocessor>;
19
40
  export type Oas2PreprocessorsSet = Record<string, Oas2Preprocessor>;
41
+ export type Async2PreprocessorsSet = Record<string, Async2Preprocessor>;
20
42
  export type Oas3DecoratorsSet = Record<string, Oas3Preprocessor>;
21
43
  export type Oas2DecoratorsSet = Record<string, Oas2Preprocessor>;
44
+ export type Async2DecoratorsSet = Record<string, Async2Preprocessor>;
22
45
 
23
- export function detectOpenAPI(root: any): OasVersion {
46
+ export function detectSpec(root: any): SpecVersion {
24
47
  if (typeof root !== 'object') {
25
48
  throw new Error(`Document must be JSON object, got ${typeof root}`);
26
49
  }
27
50
 
28
- if (!(root.openapi || root.swagger)) {
29
- throw new Error('This doesn’t look like an OpenAPI document.\n');
30
- }
31
-
32
51
  if (root.openapi && typeof root.openapi !== 'string') {
33
52
  throw new Error(`Invalid OpenAPI version: should be a string but got "${typeof root.openapi}"`);
34
53
  }
35
54
 
36
55
  if (root.openapi && root.openapi.startsWith('3.0')) {
37
- return OasVersion.Version3_0;
56
+ return SpecVersion.OAS3_0;
38
57
  }
39
58
 
40
59
  if (root.openapi && root.openapi.startsWith('3.1')) {
41
- return OasVersion.Version3_1;
60
+ return SpecVersion.OAS3_1;
42
61
  }
43
62
 
44
63
  if (root.swagger && root.swagger === '2.0') {
45
- return OasVersion.Version2;
64
+ return SpecVersion.OAS2;
46
65
  }
47
66
 
48
- throw new Error(`Unsupported OpenAPI Version: ${root.openapi || root.swagger}`);
67
+ // if not detected yet
68
+ if (root.openapi || root.swagger) {
69
+ throw new Error(`Unsupported OpenAPI version: ${root.openapi || root.swagger}`);
70
+ }
71
+
72
+ if (root.asyncapi && root.asyncapi.startsWith('2.')) {
73
+ return SpecVersion.Async2;
74
+ }
75
+
76
+ if (root.asyncapi) {
77
+ throw new Error(`Unsupported AsyncAPI version: ${root.asyncapi}`);
78
+ }
79
+
80
+ throw new Error(`Unsupported specification`);
49
81
  }
50
82
 
51
- export function openAPIMajor(version: OasVersion): OasMajorVersion {
52
- if (version === OasVersion.Version2) {
53
- return OasMajorVersion.Version2;
83
+ export function getMajorSpecVersion(version: SpecVersion): SpecMajorVersion {
84
+ if (version === SpecVersion.OAS2) {
85
+ return SpecMajorVersion.OAS2;
86
+ } else if (version === SpecVersion.Async2) {
87
+ return SpecMajorVersion.Async2;
54
88
  } else {
55
- return OasMajorVersion.Version3;
89
+ return SpecMajorVersion.OAS3;
56
90
  }
57
91
  }
92
+
93
+ export function getTypes(spec: SpecVersion) {
94
+ return typesMap[spec];
95
+ }
@@ -0,0 +1,141 @@
1
+ import { outdent } from 'outdent';
2
+ import { lintDocument } from '../../../lint';
3
+ import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
4
+ import { BaseResolver } from '../../../resolve';
5
+
6
+ describe('Async2 channels-kebab-case', () => {
7
+ it('should report on no kebab-case channel path', async () => {
8
+ const document = parseYamlToDocument(
9
+ outdent`
10
+ asyncapi: '2.6.0'
11
+ info:
12
+ title: Cool API
13
+ version: 1.0.0
14
+ channels:
15
+ NOT_A_KEBAB:
16
+ subscribe:
17
+ message:
18
+ messageId: Message1
19
+ `,
20
+ 'asyncapi.yaml'
21
+ );
22
+
23
+ const results = await lintDocument({
24
+ externalRefResolver: new BaseResolver(),
25
+ document,
26
+ config: await makeConfig({ 'channels-kebab-case': 'error' }),
27
+ });
28
+
29
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
30
+ Array [
31
+ Object {
32
+ "location": Array [
33
+ Object {
34
+ "pointer": "#/channels/NOT_A_KEBAB",
35
+ "reportOnKey": true,
36
+ "source": "asyncapi.yaml",
37
+ },
38
+ ],
39
+ "message": "\`NOT_A_KEBAB\` does not use kebab-case.",
40
+ "ruleId": "channels-kebab-case",
41
+ "severity": "error",
42
+ "suggest": Array [],
43
+ },
44
+ ]
45
+ `);
46
+ });
47
+
48
+ it('should report on snake_case in channel path', async () => {
49
+ const document = parseYamlToDocument(
50
+ outdent`
51
+ asyncapi: '2.6.0'
52
+ info:
53
+ title: Cool API
54
+ version: 1.0.0
55
+ channels:
56
+ snake_kebab:
57
+ subscribe:
58
+ message:
59
+ messageId: Message1
60
+ `,
61
+ 'asyncapi.yaml'
62
+ );
63
+
64
+ const results = await lintDocument({
65
+ externalRefResolver: new BaseResolver(),
66
+ document,
67
+ config: await makeConfig({ 'channels-kebab-case': 'error' }),
68
+ });
69
+
70
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
71
+ Array [
72
+ Object {
73
+ "location": Array [
74
+ Object {
75
+ "pointer": "#/channels/snake_kebab",
76
+ "reportOnKey": true,
77
+ "source": "asyncapi.yaml",
78
+ },
79
+ ],
80
+ "message": "\`snake_kebab\` does not use kebab-case.",
81
+ "ruleId": "channels-kebab-case",
82
+ "severity": "error",
83
+ "suggest": Array [],
84
+ },
85
+ ]
86
+ `);
87
+ });
88
+
89
+ it('should allow trailing slash in channel path with "channels-kebab-case" rule', async () => {
90
+ const document = parseYamlToDocument(
91
+ outdent`
92
+ asyncapi: '2.6.0'
93
+ info:
94
+ title: Cool API
95
+ version: 1.0.0
96
+ channels:
97
+ kebab/:
98
+ subscribe:
99
+ message:
100
+ messageId: Message1
101
+ `,
102
+ 'asyncapi.yaml'
103
+ );
104
+
105
+ const results = await lintDocument({
106
+ externalRefResolver: new BaseResolver(),
107
+ document,
108
+ config: await makeConfig({
109
+ 'paths-kebab-case': 'error',
110
+ 'no-path-trailing-slash': 'off',
111
+ }),
112
+ });
113
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
114
+ });
115
+
116
+ it('words with hyphens are allowed with "channels-kebab-case" rule', async () => {
117
+ const document = parseYamlToDocument(
118
+ outdent`
119
+ asyncapi: '2.6.0'
120
+ info:
121
+ title: Cool API
122
+ version: 1.0.0
123
+ channels:
124
+ kebab-with-longer-channel-path:
125
+ subscribe:
126
+ message:
127
+ messageId: Message1
128
+ `,
129
+ 'asyncapi.yaml'
130
+ );
131
+
132
+ const results = await lintDocument({
133
+ externalRefResolver: new BaseResolver(),
134
+ document,
135
+ config: await makeConfig({
136
+ 'paths-kebab-case': 'error',
137
+ }),
138
+ });
139
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
140
+ });
141
+ });
@@ -0,0 +1,97 @@
1
+ import { outdent } from 'outdent';
2
+ import { lintDocument } from '../../../lint';
3
+ import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
4
+ import { BaseResolver } from '../../../resolve';
5
+
6
+ describe('no-channel-trailing-slash', () => {
7
+ it('should report on trailing slash in a channel path', async () => {
8
+ const document = parseYamlToDocument(
9
+ outdent`
10
+ asyncapi: '2.6.0'
11
+ info:
12
+ title: Excellent API
13
+ version: 1.0.0
14
+ channels:
15
+ /trailing/:
16
+ subscribe:
17
+ message:
18
+ messageId: Message1
19
+ `,
20
+ 'asyncapi.yaml'
21
+ );
22
+
23
+ const results = await lintDocument({
24
+ externalRefResolver: new BaseResolver(),
25
+ document,
26
+ config: await makeConfig({ 'no-channel-trailing-slash': 'error' }),
27
+ });
28
+
29
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
30
+ Array [
31
+ Object {
32
+ "location": Array [
33
+ Object {
34
+ "pointer": "#/channels/~1trailing~1",
35
+ "reportOnKey": true,
36
+ "source": "asyncapi.yaml",
37
+ },
38
+ ],
39
+ "message": "\`/trailing/\` should not have a trailing slash.",
40
+ "ruleId": "no-channel-trailing-slash",
41
+ "severity": "error",
42
+ "suggest": Array [],
43
+ },
44
+ ]
45
+ `);
46
+ });
47
+
48
+ it('should not report on if no trailing slash in path', async () => {
49
+ const document = parseYamlToDocument(
50
+ outdent`
51
+ asyncapi: '2.6.0'
52
+ info:
53
+ title: Excellent API
54
+ version: 1.0.0
55
+ channels:
56
+ /expected:
57
+ subscribe:
58
+ message:
59
+ messageId: Message1
60
+ `,
61
+ 'asyncapi.yaml'
62
+ );
63
+
64
+ const results = await lintDocument({
65
+ externalRefResolver: new BaseResolver(),
66
+ document,
67
+ config: await makeConfig({ 'no-channel-trailing-slash': 'error' }),
68
+ });
69
+
70
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
71
+ });
72
+
73
+ it('should not report on trailing slash in path if the path is root', async () => {
74
+ const document = parseYamlToDocument(
75
+ outdent`
76
+ asyncapi: '2.6.0'
77
+ info:
78
+ title: Excellent API
79
+ version: 1.0.0
80
+ channels:
81
+ /:
82
+ subscribe:
83
+ message:
84
+ messageId: Message1
85
+ `,
86
+ 'foobar.yaml'
87
+ );
88
+
89
+ const results = await lintDocument({
90
+ externalRefResolver: new BaseResolver(),
91
+ document,
92
+ config: await makeConfig({ 'no-channel-trailing-slash': 'error' }),
93
+ });
94
+
95
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
96
+ });
97
+ });
@@ -0,0 +1,18 @@
1
+ import { Async2Rule } from '../../visitors';
2
+ import { UserContext } from '../../walk';
3
+
4
+ export const ChannelsKebabCase: Async2Rule = () => {
5
+ return {
6
+ Channel(_channel: object, { report, key }: UserContext) {
7
+ const segments = (key as string)
8
+ .split(/[/.:]/) // split on / or : as likely channel namespacers
9
+ .filter((s) => s !== ''); // filter out empty segments
10
+ if (!segments.every((segment) => /^{.+}$/.test(segment) || /^[a-z0-9-.]+$/.test(segment))) {
11
+ report({
12
+ message: `\`${key}\` does not use kebab-case.`,
13
+ location: { reportOnKey: true },
14
+ });
15
+ }
16
+ },
17
+ };
18
+ };
@@ -0,0 +1,22 @@
1
+ import { Async2Rule } from '../../visitors';
2
+ import { Assertions } from '../common/assertions';
3
+ import { Spec } from '../common/spec';
4
+ import { InfoContact } from '../common/info-contact';
5
+ import { OperationOperationId } from '../common/operation-operationId';
6
+ import { TagDescription } from '../common/tag-description';
7
+ import { TagsAlphabetical } from '../common/tags-alphabetical';
8
+ import { ChannelsKebabCase } from './channels-kebab-case';
9
+ import { NoChannelTrailingSlash } from './no-channel-trailing-slash';
10
+
11
+ export const rules = {
12
+ spec: Spec as Async2Rule,
13
+ assertions: Assertions,
14
+ 'info-contact': InfoContact,
15
+ 'operation-operationId': OperationOperationId,
16
+ 'channels-kebab-case': ChannelsKebabCase,
17
+ 'no-channel-trailing-slash': NoChannelTrailingSlash,
18
+ 'tag-description': TagDescription,
19
+ 'tags-alphabetical': TagsAlphabetical,
20
+ };
21
+
22
+ export const preprocessors = {};
@@ -0,0 +1,15 @@
1
+ import { Async2Rule } from '../../visitors';
2
+ import { UserContext } from '../../walk';
3
+
4
+ export const NoChannelTrailingSlash: Async2Rule = () => {
5
+ return {
6
+ Channel(_channel: any, { report, key, location }: UserContext) {
7
+ if ((key as string).endsWith('/') && key !== '/') {
8
+ report({
9
+ message: `\`${key}\` should not have a trailing slash.`,
10
+ location: location.key(),
11
+ });
12
+ }
13
+ },
14
+ };
15
+ };
@@ -66,7 +66,11 @@ export const runOnValuesSet = new Set<keyof Asserts>([
66
66
  ]);
67
67
 
68
68
  export const asserts: Asserts = {
69
- pattern: (value: string | string[], condition: string, { baseLocation }: AssertionFnContext) => {
69
+ pattern: (
70
+ value: string | string[],
71
+ condition: string,
72
+ { baseLocation, rawValue }: AssertionFnContext
73
+ ) => {
70
74
  if (typeof value === 'undefined' || isPlainObject(value)) return []; // property doesn't exist or is an object, no need to lint it with this assert
71
75
  const values = Array.isArray(value) ? value : [value];
72
76
  const regex = regexFromString(condition);
@@ -76,7 +80,11 @@ export const asserts: Asserts = {
76
80
  (_val) =>
77
81
  !regex?.test(_val) && {
78
82
  message: `"${_val}" should match a regex ${condition}`,
79
- location: runOnValue(value) ? baseLocation : baseLocation.key(),
83
+ location: runOnValue(value)
84
+ ? baseLocation
85
+ : isPlainObject(rawValue)
86
+ ? baseLocation.child(_val).key()
87
+ : baseLocation.key(),
80
88
  }
81
89
  )
82
90
  .filter(isTruthy);
@@ -84,7 +92,7 @@ export const asserts: Asserts = {
84
92
  notPattern: (
85
93
  value: string | string[],
86
94
  condition: string,
87
- { baseLocation }: AssertionFnContext
95
+ { baseLocation, rawValue }: AssertionFnContext
88
96
  ) => {
89
97
  if (typeof value === 'undefined' || isPlainObject(value)) return []; // property doesn't exist or is an object, no need to lint it with this assert
90
98
  const values = Array.isArray(value) ? value : [value];
@@ -95,7 +103,11 @@ export const asserts: Asserts = {
95
103
  (_val) =>
96
104
  regex?.test(_val) && {
97
105
  message: `"${_val}" should not match a regex ${condition}`,
98
- location: runOnValue(value) ? baseLocation : baseLocation.key(),
106
+ location: runOnValue(value)
107
+ ? baseLocation
108
+ : isPlainObject(rawValue)
109
+ ? baseLocation.child(_val).key()
110
+ : baseLocation.key(),
99
111
  }
100
112
  )
101
113
  .filter(isTruthy);
@@ -2,7 +2,7 @@ import type { Oas2Rule, Oas3Rule } from '../../visitors';
2
2
  import type { UserContext } from '../../walk';
3
3
  import type { Oas2Schema } from '../../typings/swagger';
4
4
  import type { Oas3Schema, Oas3_1Schema } from '../../typings/openapi';
5
- import { OasVersion } from '../../oas-types';
5
+ import { SpecVersion } from '../../oas-types';
6
6
 
7
7
  const SCALAR_TYPES = ['string', 'integer', 'number', 'boolean', 'null'];
8
8
 
@@ -25,7 +25,7 @@ export const ScalarPropertyMissingExample: Oas3Rule | Oas2Rule = () => {
25
25
  ) {
26
26
  report({
27
27
  message: `Scalar property should have "example"${
28
- oasVersion === OasVersion.Version3_1 ? ' or "examples"' : ''
28
+ oasVersion === SpecVersion.OAS3_1 ? ' or "examples"' : ''
29
29
  } defined.`,
30
30
  location: location.child(propName).key(),
31
31
  });
@@ -1,11 +1,11 @@
1
- import type { Oas3Rule, Oas2Rule } from '../../visitors';
1
+ import type { Oas3Rule, Oas2Rule, Async2Rule } from '../../visitors';
2
2
  import { isNamedType, SpecExtension } from '../../types';
3
3
  import { oasTypeOf, matchesJsonSchemaType, getSuggest, validateSchemaEnumType } from '../utils';
4
4
  import { isRef } from '../../ref-utils';
5
5
  import { isPlainObject } from '../../utils';
6
6
  import { UserContext } from '../../walk';
7
7
 
8
- export const OasSpec: Oas3Rule | Oas2Rule = () => {
8
+ export const Spec: Oas3Rule | Oas2Rule | Async2Rule = () => {
9
9
  return {
10
10
  any(
11
11
  node: any,
@@ -1,5 +1,5 @@
1
1
  import { Oas2Rule } from '../../visitors';
2
- import { OasSpec } from '../common/spec';
2
+ import { Spec } from '../common/spec';
3
3
  import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
4
4
  import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
5
5
  import { InfoContact } from '../common/info-contact';
@@ -43,7 +43,7 @@ import { RequiredStringPropertyMissingMinLength } from '../common/required-strin
43
43
  import { SpecStrictRefs } from '../common/spec-strict-refs';
44
44
 
45
45
  export const rules = {
46
- spec: OasSpec as Oas2Rule,
46
+ spec: Spec as Oas2Rule,
47
47
  'no-invalid-schema-examples': NoInvalidSchemaExamples,
48
48
  'no-invalid-parameter-examples': NoInvalidParameterExamples,
49
49
  'info-contact': InfoContact as Oas2Rule,
@@ -30,7 +30,7 @@ describe('Oas3 oas3-no-server-example.com', () => {
30
30
  "source": "foobar.yaml",
31
31
  },
32
32
  ],
33
- "message": "Server \`url\` should not point at example.com.",
33
+ "message": "Server \`url\` should not point to example.com or localhost.",
34
34
  "ruleId": "no-server-example.com",
35
35
  "severity": "error",
36
36
  "suggest": Array [],
@@ -57,4 +57,39 @@ describe('Oas3 oas3-no-server-example.com', () => {
57
57
 
58
58
  expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
59
59
  });
60
+
61
+ it('oas3-no-server-example.com: should report on server object with "foo.example.com" url', async () => {
62
+ const document = parseYamlToDocument(
63
+ outdent`
64
+ openapi: 3.0.0
65
+ servers:
66
+ - url: foo.example.com
67
+ `,
68
+ 'foobar.yaml'
69
+ );
70
+
71
+ const results = await lintDocument({
72
+ externalRefResolver: new BaseResolver(),
73
+ document,
74
+ config: await makeConfig({ 'no-server-example.com': 'error' }),
75
+ });
76
+
77
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
78
+ Array [
79
+ Object {
80
+ "location": Array [
81
+ Object {
82
+ "pointer": "#/servers/0/url",
83
+ "reportOnKey": false,
84
+ "source": "foobar.yaml",
85
+ },
86
+ ],
87
+ "message": "Server \`url\` should not point to example.com or localhost.",
88
+ "ruleId": "no-server-example.com",
89
+ "severity": "error",
90
+ "suggest": Array [],
91
+ },
92
+ ]
93
+ `);
94
+ });
60
95
  });
@@ -17,7 +17,7 @@ describe('Oas3 Structural visitor basic', () => {
17
17
  - 25.3
18
18
  - test
19
19
  servers:
20
- - url: 'http://example.com'
20
+ - url: 'http://redocly-example.com'
21
21
  variables:
22
22
  a:
23
23
  default: test
@@ -1,5 +1,5 @@
1
1
  import { Oas3RuleSet } from '../../oas-types';
2
- import { OasSpec } from '../common/spec';
2
+ import { Spec } from '../common/spec';
3
3
  import { Operation2xxResponse } from '../common/operation-2xx-response';
4
4
  import { Operation4xxResponse } from '../common/operation-4xx-response';
5
5
  import { Assertions } from '../common/assertions';
@@ -54,7 +54,7 @@ import { SpecStrictRefs } from '../common/spec-strict-refs';
54
54
  import { ComponentNameUnique } from './component-name-unique';
55
55
 
56
56
  export const rules = {
57
- spec: OasSpec,
57
+ spec: Spec,
58
58
  'info-contact': InfoContact,
59
59
  'info-license': InfoLicense,
60
60
  'info-license-url': InfoLicenseUrl,
@@ -3,9 +3,10 @@ import { Oas3Rule } from '../../visitors';
3
3
  export const NoServerExample: Oas3Rule = () => {
4
4
  return {
5
5
  Server(server, { report, location }) {
6
- if (['example.com', 'localhost'].indexOf(server.url) !== -1) {
6
+ const pattern = /^(.*[\/.])?(example\.com|localhost)([\/:?].*|$)/;
7
+ if (server.url && pattern.test(server.url)) {
7
8
  report({
8
- message: 'Server `url` should not point at example.com.',
9
+ message: 'Server `url` should not point to example.com or localhost.',
9
10
  location: location.child(['url']),
10
11
  });
11
12
  }