@redocly/openapi-core 1.0.0-beta.64 → 1.0.0-beta.68
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/__tests__/__snapshots__/bundle.test.ts.snap +126 -0
- package/__tests__/bundle.test.ts +53 -1
- package/__tests__/fixtures/refs/definitions.yaml +3 -0
- package/__tests__/fixtures/refs/external-request-body.yaml +13 -0
- package/__tests__/fixtures/refs/externalref.yaml +35 -0
- package/__tests__/fixtures/refs/hosted.yaml +35 -0
- package/__tests__/fixtures/refs/rename.yaml +1 -0
- package/__tests__/fixtures/refs/requestBody.yaml +9 -0
- package/__tests__/fixtures/refs/simple.yaml +1 -0
- package/__tests__/fixtures/refs/vendor.schema.yaml +20 -0
- package/lib/bundle.js +16 -4
- package/lib/config/all.js +9 -1
- package/lib/config/config.js +1 -1
- package/lib/config/minimal.js +1 -0
- package/lib/config/recommended.js +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -1
- package/lib/ref-utils.js +1 -2
- package/lib/rules/builtin.d.ts +6 -0
- package/lib/rules/common/info-description-override.d.ts +2 -0
- package/lib/rules/common/info-description-override.js +24 -0
- package/lib/rules/common/info-license-url.js +1 -0
- package/lib/rules/common/no-http-verbs-in-paths.d.ts +2 -0
- package/lib/rules/common/no-http-verbs-in-paths.js +33 -0
- package/lib/rules/common/operation-4xx-response.d.ts +2 -0
- package/lib/rules/common/operation-4xx-response.js +17 -0
- package/lib/rules/common/operation-description-override.d.ts +2 -0
- package/lib/rules/common/operation-description-override.js +29 -0
- package/lib/rules/common/path-excludes-patterns.d.ts +2 -0
- package/lib/rules/common/path-excludes-patterns.js +22 -0
- package/lib/rules/common/path-segment-plural.d.ts +2 -0
- package/lib/rules/common/path-segment-plural.js +32 -0
- package/lib/rules/common/tag-description-override.d.ts +2 -0
- package/lib/rules/common/tag-description-override.js +25 -0
- package/lib/rules/oas2/index.d.ts +9 -0
- package/lib/rules/oas2/index.js +18 -0
- package/lib/rules/oas2/request-mime-type.d.ts +2 -0
- package/lib/rules/oas2/request-mime-type.js +17 -0
- package/lib/rules/oas2/response-mime-type.d.ts +2 -0
- package/lib/rules/oas2/response-mime-type.js +17 -0
- package/lib/rules/oas3/index.d.ts +3 -0
- package/lib/rules/oas3/index.js +19 -1
- package/lib/rules/oas3/no-server-trailing-slash.js +1 -1
- package/lib/rules/oas3/request-mime-type.d.ts +2 -0
- package/lib/rules/oas3/request-mime-type.js +31 -0
- package/lib/rules/oas3/response-mime-type.d.ts +2 -0
- package/lib/rules/oas3/response-mime-type.js +31 -0
- package/lib/types/oas3_1.js +6 -0
- package/lib/utils.d.ts +11 -0
- package/lib/utils.js +69 -1
- package/package.json +4 -2
- package/src/__tests__/utils.test.ts +19 -1
- package/src/bundle.ts +26 -6
- package/src/config/all.ts +9 -1
- package/src/config/config.ts +2 -2
- package/src/config/minimal.ts +1 -0
- package/src/config/recommended.ts +1 -0
- package/src/index.ts +1 -1
- package/src/ref-utils.ts +1 -3
- package/src/rules/common/__tests__/info-license.test.ts +1 -1
- package/src/rules/common/__tests__/operation-4xx-response.test.ts +108 -0
- package/src/rules/common/info-description-override.ts +24 -0
- package/src/rules/common/info-license-url.ts +1 -0
- package/src/rules/common/no-http-verbs-in-paths.ts +36 -0
- package/src/rules/common/operation-4xx-response.ts +17 -0
- package/src/rules/common/operation-description-override.ts +30 -0
- package/src/rules/common/path-excludes-patterns.ts +23 -0
- package/src/rules/common/path-segment-plural.ts +31 -0
- package/src/rules/common/tag-description-override.ts +25 -0
- package/src/rules/oas2/index.ts +18 -0
- package/src/rules/oas2/request-mime-type.ts +17 -0
- package/src/rules/oas2/response-mime-type.ts +17 -0
- package/src/rules/oas3/__tests__/no-server-trailing-slash.test.ts +19 -0
- package/src/rules/oas3/index.ts +20 -3
- package/src/rules/oas3/no-server-trailing-slash.ts +1 -1
- package/src/rules/oas3/request-mime-type.ts +31 -0
- package/src/rules/oas3/response-mime-type.ts +31 -0
- package/src/rules/utils.ts +1 -1
- package/src/types/oas3_1.ts +7 -0
- package/src/utils.ts +79 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
package/src/ref-utils.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { sep as platformDependentSeparator } from 'path';
|
|
2
|
-
|
|
3
1
|
import { Source } from './resolve';
|
|
4
2
|
import { OasRef } from './typings/openapi';
|
|
5
3
|
|
|
@@ -61,7 +59,7 @@ export function pointerBaseName(pointer: string) {
|
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
export function refBaseName(ref: string) {
|
|
64
|
-
const parts = ref.split(
|
|
62
|
+
const parts = ref.split(/[\/\\]/); // split by '\' and '/'
|
|
65
63
|
return parts[parts.length - 1].split('.')[0];
|
|
66
64
|
}
|
|
67
65
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { outdent } from 'outdent';
|
|
2
|
+
import { lintDocument } from '../../../lint';
|
|
3
|
+
import { parseYamlToDocument, replaceSourceWithRef } from '../../../../__tests__/utils';
|
|
4
|
+
import { makeConfig } from '../../__tests__/config';
|
|
5
|
+
import { BaseResolver } from '../../../resolve';
|
|
6
|
+
|
|
7
|
+
describe('Oas3 operation-4xx-response', () => {
|
|
8
|
+
it('should report missing 4xx response', async () => {
|
|
9
|
+
const document = parseYamlToDocument(
|
|
10
|
+
outdent`
|
|
11
|
+
openapi: 3.0.0
|
|
12
|
+
paths:
|
|
13
|
+
'/test':
|
|
14
|
+
put:
|
|
15
|
+
responses:
|
|
16
|
+
200:
|
|
17
|
+
description: ok response
|
|
18
|
+
`,
|
|
19
|
+
'foobar.yaml',
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const results = await lintDocument({
|
|
23
|
+
externalRefResolver: new BaseResolver(),
|
|
24
|
+
document,
|
|
25
|
+
config: makeConfig({ 'operation-4xx-response': 'error' }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
29
|
+
Array [
|
|
30
|
+
Object {
|
|
31
|
+
"location": Array [
|
|
32
|
+
Object {
|
|
33
|
+
"pointer": "#/paths/~1test/put/responses",
|
|
34
|
+
"reportOnKey": true,
|
|
35
|
+
"source": "foobar.yaml",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
"message": "Operation must have at least one \`4xx\` response.",
|
|
39
|
+
"ruleId": "operation-4xx-response",
|
|
40
|
+
"severity": "error",
|
|
41
|
+
"suggest": Array [],
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should not report for present 4xx response', async () => {
|
|
48
|
+
const document = parseYamlToDocument(
|
|
49
|
+
outdent`
|
|
50
|
+
openapi: 3.0.0
|
|
51
|
+
paths:
|
|
52
|
+
'/test/':
|
|
53
|
+
put:
|
|
54
|
+
responses:
|
|
55
|
+
400:
|
|
56
|
+
description: error response
|
|
57
|
+
`,
|
|
58
|
+
'foobar.yaml',
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const results = await lintDocument({
|
|
62
|
+
externalRefResolver: new BaseResolver(),
|
|
63
|
+
document,
|
|
64
|
+
config: makeConfig({ 'operation-4xx-response': 'error' }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should report if default is present but missing 4xx response', async () => {
|
|
71
|
+
const document = parseYamlToDocument(
|
|
72
|
+
outdent`
|
|
73
|
+
openapi: 3.0.0
|
|
74
|
+
paths:
|
|
75
|
+
'/test/':
|
|
76
|
+
put:
|
|
77
|
+
responses:
|
|
78
|
+
default:
|
|
79
|
+
description: some default response
|
|
80
|
+
`,
|
|
81
|
+
'foobar.yaml',
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const results = await lintDocument({
|
|
85
|
+
externalRefResolver: new BaseResolver(),
|
|
86
|
+
document,
|
|
87
|
+
config: makeConfig({ 'operation-4xx-response': 'error' }),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
91
|
+
Array [
|
|
92
|
+
Object {
|
|
93
|
+
"location": Array [
|
|
94
|
+
Object {
|
|
95
|
+
"pointer": "#/paths/~1test~1/put/responses",
|
|
96
|
+
"reportOnKey": true,
|
|
97
|
+
"source": "foobar.yaml",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
"message": "Operation must have at least one \`4xx\` response.",
|
|
101
|
+
"ruleId": "operation-4xx-response",
|
|
102
|
+
"severity": "error",
|
|
103
|
+
"suggest": Array [],
|
|
104
|
+
},
|
|
105
|
+
]
|
|
106
|
+
`);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Oas3Decorator, Oas2Decorator } from '../../visitors';
|
|
2
|
+
import { readFileAsStringSync } from '../../utils';
|
|
3
|
+
import { UserContext } from '../../walk';
|
|
4
|
+
|
|
5
|
+
export const InfoDescriptionOverride: Oas3Decorator | Oas2Decorator = ({ filePath }) => {
|
|
6
|
+
return {
|
|
7
|
+
Info: {
|
|
8
|
+
leave(info, { report, location }: UserContext) {
|
|
9
|
+
if (!filePath)
|
|
10
|
+
throw new Error(
|
|
11
|
+
`Parameter "filePath" is not provided for "info-description-override" rule`,
|
|
12
|
+
);
|
|
13
|
+
try {
|
|
14
|
+
info.description = readFileAsStringSync(filePath);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
report({
|
|
17
|
+
message: `Failed to read markdown override file for "info.description".\n${e.message}`,
|
|
18
|
+
location: location.child('description'),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Oas3Rule, Oas2Rule } from '../../visitors';
|
|
2
|
+
import { Oas2PathItem } from '../../typings/swagger';
|
|
3
|
+
import { Oas3PathItem } from '../../typings/openapi';
|
|
4
|
+
import { UserContext } from '../../walk';
|
|
5
|
+
import { isPathParameter, splitCamelCaseIntoWords } from '../../utils';
|
|
6
|
+
|
|
7
|
+
const httpMethods = ['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'trace'];
|
|
8
|
+
|
|
9
|
+
export const NoHttpVerbsInPaths: Oas3Rule | Oas2Rule = ({ splitIntoWords }) => {
|
|
10
|
+
return {
|
|
11
|
+
PathItem(_path: Oas2PathItem | Oas3PathItem, { key, report, location }: UserContext) {
|
|
12
|
+
const pathKey = key.toString();
|
|
13
|
+
if (!pathKey.startsWith('/')) return;
|
|
14
|
+
const pathSegments = pathKey.split('/');
|
|
15
|
+
|
|
16
|
+
for (const pathSegment of pathSegments) {
|
|
17
|
+
if (!pathSegment || isPathParameter(pathSegment)) continue;
|
|
18
|
+
|
|
19
|
+
const isHttpMethodIncluded = (method: string) => {
|
|
20
|
+
return splitIntoWords
|
|
21
|
+
? splitCamelCaseIntoWords(pathSegment).has(method)
|
|
22
|
+
: pathSegment.toLocaleLowerCase().includes(method);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (const method of httpMethods) {
|
|
26
|
+
if (isHttpMethodIncluded(method)) {
|
|
27
|
+
report({
|
|
28
|
+
message: `path \`${pathKey}\` should not contain http verb ${method}`,
|
|
29
|
+
location: location.key(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Oas3Rule, Oas2Rule } from '../../visitors';
|
|
2
|
+
import { UserContext } from '../../walk';
|
|
3
|
+
|
|
4
|
+
export const Operation4xxResponse: Oas3Rule | Oas2Rule = () => {
|
|
5
|
+
return {
|
|
6
|
+
ResponsesMap(responses: Record<string, object>, { report }: UserContext) {
|
|
7
|
+
const codes = Object.keys(responses);
|
|
8
|
+
|
|
9
|
+
if (!codes.some((code) => /4[Xx0-9]{2}/.test(code))) {
|
|
10
|
+
report({
|
|
11
|
+
message: 'Operation must have at least one `4xx` response.',
|
|
12
|
+
location: { reportOnKey: true },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Oas3Decorator, Oas2Decorator } from '../../visitors';
|
|
2
|
+
import { Oas2Operation } from '../../typings/swagger';
|
|
3
|
+
import { Oas3Operation } from '../../typings/openapi';
|
|
4
|
+
import { readFileAsStringSync } from '../../utils';
|
|
5
|
+
import { UserContext } from '../../walk';
|
|
6
|
+
|
|
7
|
+
export const OperationDescriptionOverride: Oas3Decorator | Oas2Decorator = ({ operationIds }) => {
|
|
8
|
+
return {
|
|
9
|
+
Operation: {
|
|
10
|
+
leave(operation: Oas2Operation | Oas3Operation, { report, location }: UserContext) {
|
|
11
|
+
if (!operation.operationId) return;
|
|
12
|
+
if (!operationIds)
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Parameter "operationIds" is not provided for "operation-description-override" rule`,
|
|
15
|
+
);
|
|
16
|
+
const operationId = operation.operationId;
|
|
17
|
+
if (operationIds[operationId]) {
|
|
18
|
+
try {
|
|
19
|
+
operation.description = readFileAsStringSync(operationIds[operationId]);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
report({
|
|
22
|
+
message: `Failed to read markdown override file for operation "${operationId}".\n${e.message}`,
|
|
23
|
+
location: location.child('operationId').key(),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Oas2Rule, Oas3Rule } from '../../visitors';
|
|
2
|
+
import { Oas2PathItem } from '../../typings/swagger';
|
|
3
|
+
import { Oas3PathItem } from '../../typings/openapi';
|
|
4
|
+
import { UserContext } from '../../walk';
|
|
5
|
+
|
|
6
|
+
export const PathExcludesPatterns: Oas3Rule | Oas2Rule = ({ patterns }) => {
|
|
7
|
+
return {
|
|
8
|
+
PathItem(_path: Oas2PathItem | Oas3PathItem, { report, key, location }: UserContext) {
|
|
9
|
+
if (!patterns)
|
|
10
|
+
throw new Error(`Parameter "patterns" is not provided for "path-excludes-patterns" rule`);
|
|
11
|
+
const pathKey = key.toString();
|
|
12
|
+
if (pathKey.startsWith('/')) {
|
|
13
|
+
const matches = patterns.filter((pattern: string) => pathKey.match(pattern));
|
|
14
|
+
for (const match of matches) {
|
|
15
|
+
report({
|
|
16
|
+
message: `path \`${pathKey}\` should not match regex pattern: \`${match}\``,
|
|
17
|
+
location: location.key(),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Oas3Rule, Oas2Rule } from '../../visitors';
|
|
2
|
+
import { UserContext } from '../../walk';
|
|
3
|
+
import { isPathParameter, isSingular } from '../../utils';
|
|
4
|
+
|
|
5
|
+
export const PathSegmentPlural: Oas3Rule | Oas2Rule = (opts) => {
|
|
6
|
+
const { ignoreLastPathSegment, exceptions } = opts;
|
|
7
|
+
return {
|
|
8
|
+
PathItem: {
|
|
9
|
+
leave(_path: any, { report, key, location }: UserContext) {
|
|
10
|
+
const pathKey = key.toString();
|
|
11
|
+
if (pathKey.startsWith('/')) {
|
|
12
|
+
const pathSegments = pathKey.split('/');
|
|
13
|
+
pathSegments.shift();
|
|
14
|
+
if (ignoreLastPathSegment && pathSegments.length > 1) {
|
|
15
|
+
pathSegments.pop();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for (const pathSegment of pathSegments) {
|
|
19
|
+
if (exceptions && exceptions.includes(pathSegment)) continue;
|
|
20
|
+
if (!isPathParameter(pathSegment) && isSingular(pathSegment)) {
|
|
21
|
+
report({
|
|
22
|
+
message: `path segment \`${pathSegment}\` should be plural.`,
|
|
23
|
+
location: location.key(),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Oas3Decorator, Oas2Decorator } from '../../visitors';
|
|
2
|
+
import { readFileAsStringSync } from '../../utils';
|
|
3
|
+
import { UserContext } from '../../walk';
|
|
4
|
+
|
|
5
|
+
export const TagDescriptionOverride: Oas3Decorator | Oas2Decorator = ({ tagNames }) => {
|
|
6
|
+
return {
|
|
7
|
+
Tag: {
|
|
8
|
+
leave(tag, { report }: UserContext) {
|
|
9
|
+
if (!tagNames)
|
|
10
|
+
throw new Error(
|
|
11
|
+
`Parameter "tagNames" is not provided for "tag-description-override" rule`,
|
|
12
|
+
);
|
|
13
|
+
if (tagNames[tag.name]) {
|
|
14
|
+
try {
|
|
15
|
+
tag.description = readFileAsStringSync(tagNames[tag.name]);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
report({
|
|
18
|
+
message: `Failed to read markdown override file for tag "${tag.name}".\n${e.message}`,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
};
|
package/src/rules/oas2/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { PathsKebabCase } from '../common/paths-kebab-case';
|
|
|
10
10
|
import { NoEnumTypeMismatch } from '../common/no-enum-type-mismatch';
|
|
11
11
|
import { NoPathTrailingSlash } from '../common/no-path-trailing-slash';
|
|
12
12
|
import { Operation2xxResponse } from '../common/operation-2xx-response';
|
|
13
|
+
import { Operation4xxResponse } from '../common/operation-4xx-response';
|
|
13
14
|
import { OperationIdUnique } from '../common/operation-operationId-unique';
|
|
14
15
|
import { OperationParametersUnique } from '../common/operation-parameters-unique';
|
|
15
16
|
import { PathParamsDefined } from '../common/path-params-defined';
|
|
@@ -29,6 +30,14 @@ import { NoIdenticalPaths } from '../common/no-identical-paths';
|
|
|
29
30
|
import { OperationOperationId } from '../common/operation-operationId';
|
|
30
31
|
import { OperationSummary } from '../common/operation-summary';
|
|
31
32
|
import { NoAmbiguousPaths } from '../common/no-ambiguous-paths';
|
|
33
|
+
import { NoHttpVerbsInPaths } from '../common/no-http-verbs-in-paths';
|
|
34
|
+
import { PathExcludesPatterns } from '../common/path-excludes-patterns';
|
|
35
|
+
import { RequestMimeType } from './request-mime-type';
|
|
36
|
+
import { ResponseMimeType } from './response-mime-type';
|
|
37
|
+
import { PathSegmentPlural } from '../common/path-segment-plural';
|
|
38
|
+
import { OperationDescriptionOverride } from '../common/operation-description-override';
|
|
39
|
+
import { TagDescriptionOverride } from '../common/tag-description-override';
|
|
40
|
+
import { InfoDescriptionOverride } from '../common/info-description-override';
|
|
32
41
|
|
|
33
42
|
export const rules = {
|
|
34
43
|
spec: OasSpec as Oas2Rule,
|
|
@@ -43,6 +52,7 @@ export const rules = {
|
|
|
43
52
|
'boolean-parameter-prefixes': BooleanParameterPrefixes as Oas2Rule,
|
|
44
53
|
'no-path-trailing-slash': NoPathTrailingSlash as Oas2Rule,
|
|
45
54
|
'operation-2xx-response': Operation2xxResponse as Oas2Rule,
|
|
55
|
+
'operation-4xx-response': Operation4xxResponse as Oas2Rule,
|
|
46
56
|
'operation-operationId-unique': OperationIdUnique as Oas2Rule,
|
|
47
57
|
'operation-parameters-unique': OperationParametersUnique as Oas2Rule,
|
|
48
58
|
'path-parameters-defined': PathParamsDefined as Oas2Rule,
|
|
@@ -61,9 +71,17 @@ export const rules = {
|
|
|
61
71
|
'no-identical-paths': NoIdenticalPaths as Oas2Rule,
|
|
62
72
|
'no-ambiguous-paths': NoAmbiguousPaths as Oas2Rule,
|
|
63
73
|
'path-http-verbs-order': PathHttpVerbsOrder as Oas2Rule,
|
|
74
|
+
'no-http-verbs-in-paths': NoHttpVerbsInPaths as Oas2Rule,
|
|
75
|
+
'path-excludes-patterns': PathExcludesPatterns as Oas2Rule,
|
|
76
|
+
'request-mime-type': RequestMimeType as Oas2Rule,
|
|
77
|
+
'response-mime-type': ResponseMimeType as Oas2Rule,
|
|
78
|
+
'path-segment-plural': PathSegmentPlural as Oas2Rule,
|
|
64
79
|
};
|
|
65
80
|
|
|
66
81
|
export const preprocessors = {};
|
|
67
82
|
export const decorators = {
|
|
68
83
|
'registry-dependencies': RegistryDependencies as Oas2Decorator,
|
|
84
|
+
'operation-description-override': OperationDescriptionOverride as Oas2Decorator,
|
|
85
|
+
'tag-description-override': TagDescriptionOverride as Oas2Decorator,
|
|
86
|
+
'info-description-override': InfoDescriptionOverride as Oas2Decorator,
|
|
69
87
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Oas2Rule } from '../../visitors';
|
|
2
|
+
import { UserContext } from '../../walk';
|
|
3
|
+
import { Oas2Definition, Oas2Operation } from '../../typings/swagger';
|
|
4
|
+
import { validateMimeType } from '../../utils';
|
|
5
|
+
|
|
6
|
+
export const RequestMimeType: Oas2Rule = ({ allowedValues }) => {
|
|
7
|
+
return {
|
|
8
|
+
DefinitionRoot(root: Oas2Definition, ctx: UserContext) {
|
|
9
|
+
validateMimeType({ type: 'consumes', value: root }, ctx, allowedValues);
|
|
10
|
+
},
|
|
11
|
+
Operation: {
|
|
12
|
+
leave(operation: Oas2Operation, ctx: UserContext) {
|
|
13
|
+
validateMimeType({ type: 'consumes', value: operation }, ctx, allowedValues);
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Oas2Rule } from '../../visitors';
|
|
2
|
+
import { UserContext } from '../../walk';
|
|
3
|
+
import { Oas2Definition, Oas2Operation } from '../../typings/swagger';
|
|
4
|
+
import { validateMimeType } from '../../utils';
|
|
5
|
+
|
|
6
|
+
export const ResponseMimeType: Oas2Rule = ({ allowedValues }) => {
|
|
7
|
+
return {
|
|
8
|
+
DefinitionRoot(root: Oas2Definition, ctx: UserContext) {
|
|
9
|
+
validateMimeType({ type: 'produces', value: root }, ctx, allowedValues);
|
|
10
|
+
},
|
|
11
|
+
Operation: {
|
|
12
|
+
leave(operation: Oas2Operation, ctx: UserContext) {
|
|
13
|
+
validateMimeType({ type: 'produces', value: operation }, ctx, allowedValues);
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -58,4 +58,23 @@ describe('Oas3 oas3-no-server-trailing-slash', () => {
|
|
|
58
58
|
|
|
59
59
|
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
it('oas3-no-server-trailing-slash: should not report on server object with no trailing slash if the url is root', async () => {
|
|
63
|
+
const document = parseYamlToDocument(
|
|
64
|
+
outdent`
|
|
65
|
+
openapi: 3.0.0
|
|
66
|
+
servers:
|
|
67
|
+
- url: /
|
|
68
|
+
`,
|
|
69
|
+
'foobar.yaml',
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const results = await lintDocument({
|
|
73
|
+
externalRefResolver: new BaseResolver(),
|
|
74
|
+
document,
|
|
75
|
+
config: makeConfig({ 'no-server-trailing-slash': 'error' }),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
79
|
+
});
|
|
61
80
|
});
|
package/src/rules/oas3/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Oas3RuleSet } from '../../oas-types';
|
|
2
|
+
import { Oas3Decorator } from '../../visitors';
|
|
2
3
|
import { OasSpec } from '../common/spec';
|
|
3
4
|
import { Operation2xxResponse } from '../common/operation-2xx-response';
|
|
5
|
+
import { Operation4xxResponse } from '../common/operation-4xx-response';
|
|
4
6
|
import { OperationIdUnique } from '../common/operation-operationId-unique';
|
|
5
7
|
import { OperationParametersUnique } from '../common/operation-parameters-unique';
|
|
6
8
|
import { PathParamsDefined } from '../common/path-params-defined';
|
|
@@ -37,8 +39,14 @@ import { OperationOperationId } from '../common/operation-operationId';
|
|
|
37
39
|
import { OperationSummary } from '../common/operation-summary';
|
|
38
40
|
import { NoAmbiguousPaths } from '../common/no-ambiguous-paths';
|
|
39
41
|
import { NoEmptyEnumServers } from './no-servers-empty-enum';
|
|
40
|
-
|
|
41
|
-
import {
|
|
42
|
+
import { NoHttpVerbsInPaths } from '../common/no-http-verbs-in-paths';
|
|
43
|
+
import { RequestMimeType } from './request-mime-type';
|
|
44
|
+
import { ResponseMimeType } from './response-mime-type';
|
|
45
|
+
import { PathSegmentPlural } from '../common/path-segment-plural';
|
|
46
|
+
import { OperationDescriptionOverride } from '../common/operation-description-override';
|
|
47
|
+
import { TagDescriptionOverride } from '../common/tag-description-override';
|
|
48
|
+
import { InfoDescriptionOverride } from '../common/info-description-override';
|
|
49
|
+
import { PathExcludesPatterns } from '../common/path-excludes-patterns';
|
|
42
50
|
|
|
43
51
|
export const rules = {
|
|
44
52
|
spec: OasSpec,
|
|
@@ -47,6 +55,7 @@ export const rules = {
|
|
|
47
55
|
'info-license': InfoLicense,
|
|
48
56
|
'info-license-url': InfoLicenseUrl,
|
|
49
57
|
'operation-2xx-response': Operation2xxResponse,
|
|
58
|
+
'operation-4xx-response': Operation4xxResponse,
|
|
50
59
|
'operation-operationId-unique': OperationIdUnique,
|
|
51
60
|
'operation-parameters-unique': OperationParametersUnique,
|
|
52
61
|
'path-parameters-defined': PathParamsDefined,
|
|
@@ -78,11 +87,19 @@ export const rules = {
|
|
|
78
87
|
'no-identical-paths': NoIdenticalPaths,
|
|
79
88
|
'no-ambiguous-paths': NoAmbiguousPaths,
|
|
80
89
|
'no-undefined-server-variable': NoUndefinedServerVariable,
|
|
81
|
-
'no-servers-empty-enum': NoEmptyEnumServers
|
|
90
|
+
'no-servers-empty-enum': NoEmptyEnumServers,
|
|
91
|
+
'no-http-verbs-in-paths': NoHttpVerbsInPaths,
|
|
92
|
+
'path-excludes-patterns': PathExcludesPatterns,
|
|
93
|
+
'request-mime-type': RequestMimeType,
|
|
94
|
+
'response-mime-type': ResponseMimeType,
|
|
95
|
+
'path-segment-plural': PathSegmentPlural,
|
|
82
96
|
} as Oas3RuleSet;
|
|
83
97
|
|
|
84
98
|
export const preprocessors = {};
|
|
85
99
|
|
|
86
100
|
export const decorators = {
|
|
87
101
|
'registry-dependencies': RegistryDependencies as Oas3Decorator,
|
|
102
|
+
'operation-description-override': OperationDescriptionOverride as Oas3Decorator,
|
|
103
|
+
'tag-description-override': TagDescriptionOverride as Oas3Decorator,
|
|
104
|
+
'info-description-override': InfoDescriptionOverride as Oas3Decorator,
|
|
88
105
|
};
|
|
@@ -4,7 +4,7 @@ export const NoServerTrailingSlash: Oas3Rule = () => {
|
|
|
4
4
|
return {
|
|
5
5
|
Server(server, { report, location }) {
|
|
6
6
|
if (!server.url) return;
|
|
7
|
-
if (server.url.endsWith('/')) {
|
|
7
|
+
if (server.url.endsWith('/') && server.url !== '/') {
|
|
8
8
|
report({
|
|
9
9
|
message: 'Server `url` should not have a trailing slash.',
|
|
10
10
|
location: location.child(['url']),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Oas3Rule } from '../../visitors';
|
|
2
|
+
import { UserContext } from '../../walk';
|
|
3
|
+
import { Oas3RequestBody, Oas3Response } from '../../typings/openapi';
|
|
4
|
+
import { validateMimeTypeOAS3 } from '../../utils';
|
|
5
|
+
|
|
6
|
+
export const RequestMimeType: Oas3Rule = ({ allowedValues }) => {
|
|
7
|
+
return {
|
|
8
|
+
PathMap: {
|
|
9
|
+
RequestBody: {
|
|
10
|
+
leave(requestBody: Oas3RequestBody, ctx: UserContext) {
|
|
11
|
+
validateMimeTypeOAS3({ type: 'consumes', value: requestBody }, ctx, allowedValues);
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
Callback: {
|
|
15
|
+
RequestBody() {},
|
|
16
|
+
Response: {
|
|
17
|
+
leave(response: Oas3Response, ctx: UserContext) {
|
|
18
|
+
validateMimeTypeOAS3({ type: 'consumes', value: response }, ctx, allowedValues);
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
WebhooksMap: {
|
|
24
|
+
Response: {
|
|
25
|
+
leave(response: Oas3Response, ctx: UserContext) {
|
|
26
|
+
validateMimeTypeOAS3({ type: 'consumes', value: response }, ctx, allowedValues);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Oas3Rule } from '../../visitors';
|
|
2
|
+
import { UserContext } from '../../walk';
|
|
3
|
+
import { Oas3RequestBody, Oas3Response } from '../../typings/openapi';
|
|
4
|
+
import { validateMimeTypeOAS3 } from '../../utils';
|
|
5
|
+
|
|
6
|
+
export const ResponseMimeType: Oas3Rule = ({ allowedValues }) => {
|
|
7
|
+
return {
|
|
8
|
+
PathMap: {
|
|
9
|
+
Response: {
|
|
10
|
+
leave(response: Oas3Response, ctx: UserContext) {
|
|
11
|
+
validateMimeTypeOAS3({ type: 'produces', value: response }, ctx, allowedValues);
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
Callback: {
|
|
15
|
+
Response() {},
|
|
16
|
+
RequestBody: {
|
|
17
|
+
leave(requestBody: Oas3RequestBody, ctx: UserContext) {
|
|
18
|
+
validateMimeTypeOAS3({ type: 'produces', value: requestBody }, ctx, allowedValues);
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
WebhooksMap: {
|
|
24
|
+
RequestBody: {
|
|
25
|
+
leave(requestBody: Oas3RequestBody, ctx: UserContext) {
|
|
26
|
+
validateMimeTypeOAS3({ type: 'produces', value: requestBody }, ctx, allowedValues);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
package/src/rules/utils.ts
CHANGED
package/src/types/oas3_1.ts
CHANGED
|
@@ -97,6 +97,13 @@ const Operation: NodeType = {
|
|
|
97
97
|
|
|
98
98
|
const Schema: NodeType = {
|
|
99
99
|
properties: {
|
|
100
|
+
$id: { type: 'string' },
|
|
101
|
+
id: { type: 'string' },
|
|
102
|
+
$schema: { type: 'string' },
|
|
103
|
+
definitions: 'NamedSchemas',
|
|
104
|
+
$defs: 'NamedSchemas',
|
|
105
|
+
$vocabulary: { type: 'string' },
|
|
106
|
+
|
|
100
107
|
externalDocs: 'ExternalDocs',
|
|
101
108
|
discriminator: 'Discriminator',
|
|
102
109
|
myArbitraryKeyword: { type: 'boolean' },
|