@redocly/openapi-core 1.0.2 → 1.2.0
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/CHANGELOG.md +22 -0
- package/lib/bundle.d.ts +2 -2
- package/lib/bundle.js +24 -22
- package/lib/config/builtIn.js +4 -0
- package/lib/config/config-resolvers.js +19 -6
- package/lib/config/config.d.ts +9 -9
- package/lib/config/config.js +32 -17
- package/lib/config/rules.d.ts +2 -2
- package/lib/config/types.d.ts +9 -3
- package/lib/format/codeframes.d.ts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +7 -6
- package/lib/lint.js +10 -16
- package/lib/oas-types.d.ts +16 -10
- package/lib/oas-types.js +52 -26
- package/lib/rules/async2/channels-kebab-case.d.ts +2 -0
- package/lib/rules/async2/channels-kebab-case.js +19 -0
- package/lib/rules/async2/index.d.ts +12 -0
- package/lib/rules/async2/index.js +22 -0
- package/lib/rules/async2/no-channel-trailing-slash.d.ts +2 -0
- package/lib/rules/async2/no-channel-trailing-slash.js +16 -0
- package/lib/rules/common/no-path-trailing-slash.js +2 -2
- package/lib/rules/common/scalar-property-missing-example.js +1 -1
- package/lib/rules/common/spec.d.ts +2 -2
- package/lib/rules/common/spec.js +3 -3
- package/lib/rules/common/tags-alphabetical.js +5 -2
- package/lib/rules/oas2/index.js +1 -1
- package/lib/rules/oas2/remove-unused-components.js +4 -1
- package/lib/rules/oas3/index.js +1 -1
- package/lib/rules/oas3/remove-unused-components.js +4 -1
- package/lib/rules/utils.d.ts +1 -1
- package/lib/types/asyncapi.d.ts +2 -0
- package/lib/types/asyncapi.js +1027 -0
- package/lib/types/portal-config-schema.d.ts +2533 -0
- package/lib/types/portal-config-schema.js +304 -0
- package/lib/types/redocly-yaml.js +26 -20
- package/lib/types/{config-external-schemas.d.ts → theme-config.d.ts} +140 -575
- package/lib/types/{config-external-schemas.js → theme-config.js} +55 -261
- package/lib/typings/asyncapi.d.ts +21 -0
- package/lib/typings/asyncapi.js +2 -0
- package/lib/visitors.d.ts +12 -0
- package/lib/walk.d.ts +3 -3
- package/package.json +3 -2
- package/src/__tests__/lint.test.ts +38 -6
- package/src/bundle.ts +27 -28
- package/src/config/__tests__/__snapshots__/config.test.ts.snap +24 -0
- package/src/config/__tests__/config.test.ts +15 -4
- package/src/config/builtIn.ts +4 -0
- package/src/config/config-resolvers.ts +25 -6
- package/src/config/config.ts +51 -27
- package/src/config/rules.ts +2 -2
- package/src/config/types.ts +14 -4
- package/src/index.ts +7 -1
- package/src/lint.ts +13 -22
- package/src/oas-types.ts +59 -21
- package/src/rules/async2/__tests__/channels-kebab-case.test.ts +141 -0
- package/src/rules/async2/__tests__/no-channel-trailing-slash.test.ts +97 -0
- package/src/rules/async2/channels-kebab-case.ts +18 -0
- package/src/rules/async2/index.ts +22 -0
- package/src/rules/async2/no-channel-trailing-slash.ts +15 -0
- package/src/rules/common/__tests__/no-path-trailing-slash.test.ts +41 -0
- package/src/rules/common/__tests__/tags-alphabetical.test.ts +58 -0
- package/src/rules/common/no-path-trailing-slash.ts +2 -2
- package/src/rules/common/scalar-property-missing-example.ts +2 -2
- package/src/rules/common/spec.ts +2 -2
- package/src/rules/common/tags-alphabetical.ts +8 -4
- package/src/rules/oas2/__tests__/remove-unused-components.test.ts +155 -0
- package/src/rules/oas2/index.ts +2 -2
- package/src/rules/oas2/remove-unused-components.ts +6 -1
- package/src/rules/oas3/__tests__/remove-unused-components.test.ts +171 -0
- package/src/rules/oas3/index.ts +2 -2
- package/src/rules/oas3/remove-unused-components.ts +6 -1
- package/src/types/asyncapi.ts +1136 -0
- package/src/types/portal-config-schema.ts +343 -0
- package/src/types/redocly-yaml.ts +23 -34
- package/src/types/{config-external-schemas.ts → theme-config.ts} +60 -294
- package/src/typings/asyncapi.ts +26 -0
- package/src/visitors.ts +22 -0
- package/src/walk.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
package/src/lint.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { BaseResolver, resolveDocument, Document, makeDocumentFromString } from './resolve';
|
|
2
2
|
import { normalizeVisitors } from './visitors';
|
|
3
|
-
import { Oas3_1Types } from './types/oas3_1';
|
|
4
|
-
import { Oas3Types } from './types/oas3';
|
|
5
|
-
import { Oas2Types } from './types/oas2';
|
|
6
3
|
import { NodeType } from './types';
|
|
7
4
|
import { ProblemSeverity, WalkContext, walkDocument } from './walk';
|
|
8
5
|
import { StyleguideConfig, Config, initRules, defaultPlugin, resolvePlugins } from './config';
|
|
9
6
|
import { normalizeTypes } from './types';
|
|
10
7
|
import { releaseAjvInstance } from './rules/ajv';
|
|
11
|
-
import {
|
|
8
|
+
import { Oas3RuleSet, SpecVersion, getMajorSpecVersion, detectSpec, getTypes } from './oas-types';
|
|
12
9
|
import { ConfigTypes } from './types/redocly-yaml';
|
|
13
|
-
import {
|
|
10
|
+
import { Spec } from './rules/common/spec';
|
|
14
11
|
|
|
15
12
|
export async function lint(opts: {
|
|
16
13
|
ref: string;
|
|
@@ -54,29 +51,22 @@ export async function lintDocument(opts: {
|
|
|
54
51
|
releaseAjvInstance(); // FIXME: preprocessors can modify nodes which are then cached to ajv-instance by absolute path
|
|
55
52
|
|
|
56
53
|
const { document, customTypes, externalRefResolver, config } = opts;
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const rules = config.getRulesForOasVersion(
|
|
54
|
+
const specVersion = detectSpec(document.parsed);
|
|
55
|
+
const specMajorVersion = getMajorSpecVersion(specVersion);
|
|
56
|
+
const rules = config.getRulesForOasVersion(specMajorVersion);
|
|
60
57
|
const types = normalizeTypes(
|
|
61
|
-
config.extendTypes(
|
|
62
|
-
customTypes ?? oasMajorVersion === OasMajorVersion.Version3
|
|
63
|
-
? oasVersion === OasVersion.Version3_1
|
|
64
|
-
? Oas3_1Types
|
|
65
|
-
: Oas3Types
|
|
66
|
-
: Oas2Types,
|
|
67
|
-
oasVersion
|
|
68
|
-
),
|
|
58
|
+
config.extendTypes(customTypes ?? getTypes(specVersion), specVersion),
|
|
69
59
|
config
|
|
70
60
|
);
|
|
71
61
|
|
|
72
62
|
const ctx: WalkContext = {
|
|
73
63
|
problems: [],
|
|
74
|
-
oasVersion:
|
|
64
|
+
oasVersion: specVersion,
|
|
75
65
|
visitorsData: {},
|
|
76
66
|
};
|
|
77
67
|
|
|
78
|
-
const preprocessors = initRules(rules as any, config, 'preprocessors',
|
|
79
|
-
const regularRules = initRules(rules as Oas3RuleSet[], config, 'rules',
|
|
68
|
+
const preprocessors = initRules(rules as any, config, 'preprocessors', specVersion);
|
|
69
|
+
const regularRules = initRules(rules as Oas3RuleSet[], config, 'rules', specVersion);
|
|
80
70
|
|
|
81
71
|
let resolvedRefMap = await resolveDocument({
|
|
82
72
|
rootDocument: document,
|
|
@@ -117,7 +107,7 @@ export async function lintConfig(opts: { document: Document; severity?: ProblemS
|
|
|
117
107
|
|
|
118
108
|
const ctx: WalkContext = {
|
|
119
109
|
problems: [],
|
|
120
|
-
oasVersion:
|
|
110
|
+
oasVersion: SpecVersion.OAS3_0,
|
|
121
111
|
visitorsData: {},
|
|
122
112
|
};
|
|
123
113
|
const plugins = resolvePlugins([defaultPlugin]);
|
|
@@ -131,10 +121,11 @@ export async function lintConfig(opts: { document: Document; severity?: ProblemS
|
|
|
131
121
|
{
|
|
132
122
|
severity: severity || 'error',
|
|
133
123
|
ruleId: 'configuration spec',
|
|
134
|
-
visitor:
|
|
124
|
+
visitor: Spec({ severity: 'error' }),
|
|
135
125
|
},
|
|
136
126
|
];
|
|
137
|
-
|
|
127
|
+
// TODO: check why any is needed
|
|
128
|
+
const normalizedVisitors = normalizeVisitors(rules as any, types);
|
|
138
129
|
|
|
139
130
|
walkDocument({
|
|
140
131
|
document,
|
package/src/oas-types.ts
CHANGED
|
@@ -1,57 +1,95 @@
|
|
|
1
|
-
import {
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
|
56
|
+
return SpecVersion.OAS3_0;
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
if (root.openapi && root.openapi.startsWith('3.1')) {
|
|
41
|
-
return
|
|
60
|
+
return SpecVersion.OAS3_1;
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
if (root.swagger && root.swagger === '2.0') {
|
|
45
|
-
return
|
|
64
|
+
return SpecVersion.OAS2;
|
|
46
65
|
}
|
|
47
66
|
|
|
48
|
-
|
|
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
|
|
52
|
-
if (version ===
|
|
53
|
-
return
|
|
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
|
|
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
|
+
};
|
|
@@ -41,6 +41,47 @@ describe('no-path-trailing-slash', () => {
|
|
|
41
41
|
`);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
it('should report on trailing slash in path on key when referencing', async () => {
|
|
45
|
+
const document = parseYamlToDocument(
|
|
46
|
+
outdent`
|
|
47
|
+
openapi: 3.0.0
|
|
48
|
+
paths:
|
|
49
|
+
'/bad/':
|
|
50
|
+
$ref: '#/components/pathItems/MyItem'
|
|
51
|
+
components:
|
|
52
|
+
pathItems:
|
|
53
|
+
MyItem:
|
|
54
|
+
get:
|
|
55
|
+
summary: List all pets
|
|
56
|
+
`,
|
|
57
|
+
'foobar.yaml'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const results = await lintDocument({
|
|
61
|
+
externalRefResolver: new BaseResolver(),
|
|
62
|
+
document,
|
|
63
|
+
config: await makeConfig({ 'no-path-trailing-slash': 'error' }),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
67
|
+
Array [
|
|
68
|
+
Object {
|
|
69
|
+
"location": Array [
|
|
70
|
+
Object {
|
|
71
|
+
"pointer": "#/paths/~1bad~1",
|
|
72
|
+
"reportOnKey": true,
|
|
73
|
+
"source": "foobar.yaml",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
"message": "\`/bad/\` should not have a trailing slash.",
|
|
77
|
+
"ruleId": "no-path-trailing-slash",
|
|
78
|
+
"severity": "error",
|
|
79
|
+
"suggest": Array [],
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
`);
|
|
83
|
+
});
|
|
84
|
+
|
|
44
85
|
it('should not report on if no trailing slash in path', async () => {
|
|
45
86
|
const document = parseYamlToDocument(
|
|
46
87
|
outdent`
|
|
@@ -61,4 +61,62 @@ describe('Oas3 tags-alphabetical', () => {
|
|
|
61
61
|
|
|
62
62
|
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
it('should report on tags object if not sorted alphabetically not ignoring case', async () => {
|
|
66
|
+
const document = parseYamlToDocument(
|
|
67
|
+
outdent`
|
|
68
|
+
openapi: 3.0.0
|
|
69
|
+
paths: {}
|
|
70
|
+
tags:
|
|
71
|
+
- name: a
|
|
72
|
+
- name: B
|
|
73
|
+
`,
|
|
74
|
+
'foobar.yaml'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const results = await lintDocument({
|
|
78
|
+
externalRefResolver: new BaseResolver(),
|
|
79
|
+
document,
|
|
80
|
+
config: await makeConfig({ 'tags-alphabetical': 'error' }),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
84
|
+
Array [
|
|
85
|
+
Object {
|
|
86
|
+
"location": Array [
|
|
87
|
+
Object {
|
|
88
|
+
"pointer": "#/tags/0",
|
|
89
|
+
"reportOnKey": false,
|
|
90
|
+
"source": "foobar.yaml",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
"message": "The \`tags\` array should be in alphabetical order.",
|
|
94
|
+
"ruleId": "tags-alphabetical",
|
|
95
|
+
"severity": "error",
|
|
96
|
+
"suggest": Array [],
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should not report on tags object if sorted alphabetically ignoring case', async () => {
|
|
103
|
+
const document = parseYamlToDocument(
|
|
104
|
+
outdent`
|
|
105
|
+
openapi: 3.0.0
|
|
106
|
+
paths: {}
|
|
107
|
+
tags:
|
|
108
|
+
- name: a
|
|
109
|
+
- name: B
|
|
110
|
+
`,
|
|
111
|
+
'foobar.yaml'
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const results = await lintDocument({
|
|
115
|
+
externalRefResolver: new BaseResolver(),
|
|
116
|
+
document,
|
|
117
|
+
config: await makeConfig({ 'tags-alphabetical': { severity: 'error', ignoreCase: true } }),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
121
|
+
});
|
|
64
122
|
});
|
|
@@ -3,11 +3,11 @@ import { UserContext } from '../../walk';
|
|
|
3
3
|
|
|
4
4
|
export const NoPathTrailingSlash: Oas3Rule | Oas2Rule = () => {
|
|
5
5
|
return {
|
|
6
|
-
PathItem(_path: any, { report, key,
|
|
6
|
+
PathItem(_path: any, { report, key, rawLocation }: UserContext) {
|
|
7
7
|
if ((key as string).endsWith('/') && key !== '/') {
|
|
8
8
|
report({
|
|
9
9
|
message: `\`${key}\` should not have a trailing slash.`,
|
|
10
|
-
location:
|
|
10
|
+
location: rawLocation.key(),
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
13
|
},
|
|
@@ -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 {
|
|
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 ===
|
|
28
|
+
oasVersion === SpecVersion.OAS3_1 ? ' or "examples"' : ''
|
|
29
29
|
} defined.`,
|
|
30
30
|
location: location.child(propName).key(),
|
|
31
31
|
});
|
package/src/rules/common/spec.ts
CHANGED
|
@@ -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
|
|
8
|
+
export const Spec: Oas3Rule | Oas2Rule | Async2Rule = () => {
|
|
9
9
|
return {
|
|
10
10
|
any(
|
|
11
11
|
node: any,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Oas3Rule, Oas2Rule } from '../../visitors';
|
|
2
|
-
import { Oas2Definition } from '../../typings/swagger';
|
|
3
|
-
import { Oas3Definition } from '../../typings/openapi';
|
|
2
|
+
import { Oas2Definition, Oas2Tag } from '../../typings/swagger';
|
|
3
|
+
import { Oas3Definition, Oas3Tag } from '../../typings/openapi';
|
|
4
4
|
import { UserContext } from '../../walk';
|
|
5
5
|
|
|
6
|
-
export const TagsAlphabetical: Oas3Rule | Oas2Rule = () => {
|
|
6
|
+
export const TagsAlphabetical: Oas3Rule | Oas2Rule = ({ ignoreCase = false }) => {
|
|
7
7
|
return {
|
|
8
8
|
Root(root: Oas2Definition | Oas3Definition, { report, location }: UserContext) {
|
|
9
9
|
if (!root.tags) return;
|
|
10
10
|
for (let i = 0; i < root.tags.length - 1; i++) {
|
|
11
|
-
if (root.tags[i]
|
|
11
|
+
if (getTagName(root.tags[i], ignoreCase) > getTagName(root.tags[i + 1], ignoreCase)) {
|
|
12
12
|
report({
|
|
13
13
|
message: 'The `tags` array should be in alphabetical order.',
|
|
14
14
|
location: location.child(['tags', i]),
|
|
@@ -18,3 +18,7 @@ export const TagsAlphabetical: Oas3Rule | Oas2Rule = () => {
|
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
};
|
|
21
|
+
|
|
22
|
+
function getTagName(tag: Oas2Tag | Oas3Tag, ignoreCase: boolean): string {
|
|
23
|
+
return ignoreCase ? tag.name.toLowerCase() : tag.name;
|
|
24
|
+
}
|