@redocly/openapi-core 1.0.0-beta.62 → 1.0.0-beta.66
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__/lint.test.ts +17 -0
- package/__tests__/resolve.test.ts +10 -2
- package/__tests__/utils.ts +3 -5
- package/lib/benchmark/utils.js +2 -2
- package/lib/config/all.js +8 -1
- package/lib/config/config.d.ts +1 -0
- package/lib/config/config.js +4 -4
- package/lib/index.d.ts +2 -1
- package/lib/index.js +5 -1
- package/lib/js-yaml/index.d.ts +3 -0
- package/lib/js-yaml/index.js +19 -0
- package/lib/oas-types.js +3 -0
- package/lib/resolve.d.ts +1 -1
- package/lib/resolve.js +3 -4
- 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-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 +8 -0
- package/lib/rules/oas2/index.js +16 -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 +17 -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/types/redocly-yaml.js +332 -21
- package/lib/utils.d.ts +15 -1
- package/lib/utils.js +82 -3
- package/package.json +6 -4
- package/src/__tests__/js-yaml.test.ts +47 -0
- package/src/__tests__/lint.test.ts +13 -0
- package/src/__tests__/utils.test.ts +74 -0
- package/src/benchmark/utils.ts +2 -2
- package/src/config/all.ts +8 -1
- package/src/config/config.ts +6 -5
- package/src/index.ts +3 -1
- package/src/js-yaml/index.ts +19 -0
- package/src/oas-types.ts +4 -0
- package/src/resolve.ts +5 -5
- package/src/rules/__tests__/no-unresolved-refs.test.ts +2 -2
- package/src/rules/common/__tests__/info-license.test.ts +1 -1
- 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-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 +16 -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/index.ts +18 -3
- 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/types/redocly-yaml.ts +434 -22
- package/src/typings/swagger.ts +0 -1
- package/src/utils.ts +101 -2
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redocly/openapi-core",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.66",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"engines": {
|
|
@@ -30,22 +30,24 @@
|
|
|
30
30
|
"Andriy Leliv <andriy@redoc.ly> (https://redoc.ly/)"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@redocly/ajv": "^8.6.
|
|
33
|
+
"@redocly/ajv": "^8.6.4",
|
|
34
34
|
"@types/node": "^14.11.8",
|
|
35
35
|
"colorette": "^1.2.0",
|
|
36
36
|
"js-levenshtein": "^1.1.6",
|
|
37
|
-
"js-yaml": "^
|
|
37
|
+
"js-yaml": "^4.1.0",
|
|
38
38
|
"lodash.isequal": "^4.5.0",
|
|
39
39
|
"minimatch": "^3.0.4",
|
|
40
40
|
"node-fetch": "^2.6.1",
|
|
41
|
+
"pluralize": "^8.0.0",
|
|
41
42
|
"yaml-ast-parser": "0.0.43"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/js-levenshtein": "^1.1.0",
|
|
45
|
-
"@types/js-yaml": "^
|
|
46
|
+
"@types/js-yaml": "^4.0.3",
|
|
46
47
|
"@types/lodash.isequal": "^4.5.5",
|
|
47
48
|
"@types/minimatch": "^3.0.3",
|
|
48
49
|
"@types/node-fetch": "^2.5.7",
|
|
50
|
+
"@types/pluralize": "^0.0.29",
|
|
49
51
|
"typescript": "^4.0.5"
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parseYaml, stringifyYaml } from '../js-yaml';
|
|
2
|
+
|
|
3
|
+
const yaml = `
|
|
4
|
+
emptyValue:
|
|
5
|
+
spaces in keys: spaces in keys
|
|
6
|
+
numberString: '0123456789'
|
|
7
|
+
number: 1000
|
|
8
|
+
decimal: 12.34
|
|
9
|
+
boolean: true
|
|
10
|
+
date: 2020-01-01
|
|
11
|
+
array:
|
|
12
|
+
- 1
|
|
13
|
+
- 2
|
|
14
|
+
object:
|
|
15
|
+
key1: 1
|
|
16
|
+
key2: 2
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const jsObject = {
|
|
20
|
+
emptyValue: null,
|
|
21
|
+
'spaces in keys': 'spaces in keys',
|
|
22
|
+
numberString: '0123456789',
|
|
23
|
+
number: 1000,
|
|
24
|
+
decimal: 12.34,
|
|
25
|
+
boolean: true,
|
|
26
|
+
date: '2020-01-01',
|
|
27
|
+
array: [1, 2],
|
|
28
|
+
object: { key1: 1, key2: 2 },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('js-yaml', () => {
|
|
32
|
+
test('parseYaml', () => {
|
|
33
|
+
expect(parseYaml(yaml)).toEqual(jsObject);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('parse and stringify', () => {
|
|
37
|
+
expect(parseYaml(stringifyYaml(jsObject))).toEqual(jsObject);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should throw an error for unsupported types', () => {
|
|
41
|
+
expect(() => stringifyYaml({ date: new Date() }))
|
|
42
|
+
.toThrow('unacceptable kind of an object to dump [object Date]');
|
|
43
|
+
|
|
44
|
+
expect(() => stringifyYaml({ foo: () => {} }))
|
|
45
|
+
.toThrow('unacceptable kind of an object to dump [object Function]');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -87,6 +87,19 @@ describe('lint', () => {
|
|
|
87
87
|
"severity": "error",
|
|
88
88
|
"suggest": Array [],
|
|
89
89
|
},
|
|
90
|
+
Object {
|
|
91
|
+
"location": Array [
|
|
92
|
+
Object {
|
|
93
|
+
"pointer": "#/referenceDocs/layout",
|
|
94
|
+
"reportOnKey": false,
|
|
95
|
+
"source": "",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
"message": "Expected type \`string\` but got \`object\`.",
|
|
99
|
+
"ruleId": "spec",
|
|
100
|
+
"severity": "error",
|
|
101
|
+
"suggest": Array [],
|
|
102
|
+
},
|
|
90
103
|
]
|
|
91
104
|
`);
|
|
92
105
|
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { pickObjectProps, omitObjectProps, slash } from '../utils';
|
|
2
|
+
|
|
3
|
+
describe('utils', () => {
|
|
4
|
+
const testObject = {
|
|
5
|
+
a: 'value a',
|
|
6
|
+
b: 'value b',
|
|
7
|
+
c: 'value c',
|
|
8
|
+
d: 'value d',
|
|
9
|
+
e: 'value e',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('pickObjectProps', () => {
|
|
13
|
+
it('returns correct object result', () => {
|
|
14
|
+
expect(pickObjectProps(testObject, ['a', 'b'])).toStrictEqual({ a: 'value a', b: 'value b' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns correct object if passed non existing key', () => {
|
|
18
|
+
expect(pickObjectProps(testObject, ['a', 'b', 'nonExisting'])).toStrictEqual({
|
|
19
|
+
a: 'value a',
|
|
20
|
+
b: 'value b',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns an empty object if no keys are passed', () => {
|
|
25
|
+
expect(pickObjectProps(testObject, [])).toStrictEqual({});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns an empty object if empty target obj passed', () => {
|
|
29
|
+
expect(pickObjectProps({}, ['d', 'e'])).toStrictEqual({});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('omitObjectProps', () => {
|
|
34
|
+
it('returns correct object result', () => {
|
|
35
|
+
expect(omitObjectProps(testObject, ['a', 'b', 'c'])).toStrictEqual({
|
|
36
|
+
d: 'value d',
|
|
37
|
+
e: 'value e',
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns correct object if passed non existing key', () => {
|
|
42
|
+
expect(omitObjectProps(testObject, ['a', 'b', 'c', 'nonExisting'])).toStrictEqual({
|
|
43
|
+
d: 'value d',
|
|
44
|
+
e: 'value e',
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns full object if no keys are passed', () => {
|
|
49
|
+
expect(omitObjectProps(testObject, [])).toStrictEqual(testObject);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns an empty object if empty target obj passed', () => {
|
|
53
|
+
expect(omitObjectProps({}, ['d', 'e'])).toStrictEqual({});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('slash path', () => {
|
|
58
|
+
it('can correctly slash path', () => {
|
|
59
|
+
[
|
|
60
|
+
['foo\\bar', 'foo/bar'],
|
|
61
|
+
['foo/bar', 'foo/bar'],
|
|
62
|
+
['foo\\中文', 'foo/中文'],
|
|
63
|
+
['foo/中文', 'foo/中文'],
|
|
64
|
+
].forEach(([path, expectRes]) => {
|
|
65
|
+
expect(slash(path)).toBe(expectRes);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not modify extended length paths', () => {
|
|
70
|
+
const extended = '\\\\?\\some\\path';
|
|
71
|
+
expect(slash(extended)).toBe(extended);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
package/src/benchmark/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parseYaml } from '../js-yaml';
|
|
2
2
|
import { Document, Source } from '../resolve';
|
|
3
3
|
import { Oas3RuleSet } from '../oas-types';
|
|
4
4
|
import { RuleConfig, LintConfig, Plugin } from '../config/config';
|
|
@@ -6,7 +6,7 @@ import { RuleConfig, LintConfig, Plugin } from '../config/config';
|
|
|
6
6
|
export function parseYamlToDocument(body: string, absoluteRef: string = ''): Document {
|
|
7
7
|
return {
|
|
8
8
|
source: new Source(absoluteRef, body),
|
|
9
|
-
parsed:
|
|
9
|
+
parsed: parseYaml(body, { filename: absoluteRef }),
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
|
package/src/config/all.ts
CHANGED
|
@@ -12,6 +12,7 @@ export default {
|
|
|
12
12
|
'no-identical-paths': 'error',
|
|
13
13
|
'no-ambiguous-paths': 'error',
|
|
14
14
|
'no-path-trailing-slash': 'error',
|
|
15
|
+
'path-segment-plural': 'error',
|
|
15
16
|
'path-declaration-must-exist': 'error',
|
|
16
17
|
'path-not-include-query': 'error',
|
|
17
18
|
'path-parameters-defined': 'error',
|
|
@@ -29,6 +30,12 @@ export default {
|
|
|
29
30
|
'no-enum-type-mismatch': 'error',
|
|
30
31
|
'boolean-parameter-prefixes': 'error',
|
|
31
32
|
'paths-kebab-case': 'error',
|
|
33
|
+
'no-http-verbs-in-paths': 'error',
|
|
34
|
+
'path-excludes-patterns': {
|
|
35
|
+
severity: 'error',
|
|
36
|
+
patterns: [],
|
|
37
|
+
},
|
|
38
|
+
'request-mime-type': 'error',
|
|
32
39
|
spec: 'error',
|
|
33
40
|
},
|
|
34
41
|
oas3_0Rules: {
|
|
@@ -49,5 +56,5 @@ export default {
|
|
|
49
56
|
'no-unused-components': 'error',
|
|
50
57
|
'no-undefined-server-variable': 'error',
|
|
51
58
|
'no-servers-empty-enum': 'error',
|
|
52
|
-
}
|
|
59
|
+
},
|
|
53
60
|
} as LintRawConfig;
|
package/src/config/config.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import * as yaml from 'js-yaml';
|
|
4
3
|
import { dirname } from 'path';
|
|
5
4
|
import { red, blue } from 'colorette';
|
|
6
5
|
|
|
7
|
-
import {
|
|
6
|
+
import { parseYaml, stringifyYaml } from '../js-yaml';
|
|
7
|
+
import { notUndefined, slash } from '../utils';
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
OasVersion,
|
|
@@ -190,7 +190,7 @@ export class LintConfig {
|
|
|
190
190
|
if (fs.hasOwnProperty('existsSync') && fs.existsSync(ignoreFile)) {
|
|
191
191
|
// TODO: parse errors
|
|
192
192
|
this.ignore =
|
|
193
|
-
(
|
|
193
|
+
(parseYaml(fs.readFileSync(ignoreFile, 'utf-8')) as Record<
|
|
194
194
|
string,
|
|
195
195
|
Record<string, Set<string>>
|
|
196
196
|
>) || {};
|
|
@@ -211,12 +211,12 @@ export class LintConfig {
|
|
|
211
211
|
const ignoreFile = path.join(dir, IGNORE_FILE);
|
|
212
212
|
const mapped: Record<string, any> = {};
|
|
213
213
|
for (const absFileName of Object.keys(this.ignore)) {
|
|
214
|
-
const ignoredRules = (mapped[path.relative(dir, absFileName)] = this.ignore[absFileName]);
|
|
214
|
+
const ignoredRules = (mapped[slash(path.relative(dir, absFileName))] = this.ignore[absFileName]);
|
|
215
215
|
for (const ruleId of Object.keys(ignoredRules)) {
|
|
216
216
|
ignoredRules[ruleId] = Array.from(ignoredRules[ruleId]) as any;
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
|
-
fs.writeFileSync(ignoreFile, IGNORE_BANNER +
|
|
219
|
+
fs.writeFileSync(ignoreFile, IGNORE_BANNER + stringifyYaml(mapped));
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
addIgnore(problem: NormalizedProblem) {
|
|
@@ -384,6 +384,7 @@ export class Config {
|
|
|
384
384
|
apiDefinitions: Record<string, string>;
|
|
385
385
|
lint: LintConfig;
|
|
386
386
|
resolve: ResolveConfig;
|
|
387
|
+
licenseKey?: string;
|
|
387
388
|
constructor(public rawConfig: RawConfig, public configFile?: string) {
|
|
388
389
|
this.apiDefinitions = rawConfig.apiDefinitions || {};
|
|
389
390
|
this.lint = new LintConfig(rawConfig.lint || {}, configFile);
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { BundleOutputFormat, readFileFromUrl } from './utils';
|
|
1
|
+
export { BundleOutputFormat, readFileFromUrl, slash } from './utils';
|
|
2
2
|
export { Oas3_1Types } from './types/oas3_1';
|
|
3
3
|
export { Oas3Types } from './types/oas3';
|
|
4
4
|
export { Oas2Types } from './types/oas2';
|
|
@@ -29,9 +29,11 @@ export {
|
|
|
29
29
|
YamlParseError,
|
|
30
30
|
makeDocumentFromString,
|
|
31
31
|
} from './resolve';
|
|
32
|
+
export { parseYaml, stringifyYaml } from './js-yaml';
|
|
32
33
|
export { unescapePointer } from './ref-utils';
|
|
33
34
|
export { detectOpenAPI, OasMajorVersion, openAPIMajor, OasVersion } from './oas-types';
|
|
34
35
|
export { normalizeVisitors } from './visitors';
|
|
36
|
+
|
|
35
37
|
export {
|
|
36
38
|
WalkContext,
|
|
37
39
|
walkDocument,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// TODO: add a type for "types" https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/js-yaml/index.d.ts
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import { JSON_SCHEMA, types, LoadOptions, DumpOptions, load, dump } from 'js-yaml';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SCHEMA_WITHOUT_TIMESTAMP = JSON_SCHEMA.extend({
|
|
6
|
+
implicit: [types.merge],
|
|
7
|
+
explicit: [
|
|
8
|
+
types.binary,
|
|
9
|
+
types.omap,
|
|
10
|
+
types.pairs,
|
|
11
|
+
types.set,
|
|
12
|
+
],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const parseYaml = (str: string, opts?: LoadOptions): unknown =>
|
|
16
|
+
load(str, {schema: DEFAULT_SCHEMA_WITHOUT_TIMESTAMP, ...opts});
|
|
17
|
+
|
|
18
|
+
export const stringifyYaml = (obj: any, opts?: DumpOptions): string =>
|
|
19
|
+
dump(obj, {schema: DEFAULT_SCHEMA_WITHOUT_TIMESTAMP, ...opts});
|
package/src/oas-types.ts
CHANGED
|
@@ -34,6 +34,10 @@ export function detectOpenAPI(root: any): OasVersion {
|
|
|
34
34
|
throw new Error('This doesn’t look like an OpenAPI document.\n');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
if (root.openapi && typeof root.openapi !== 'string') {
|
|
38
|
+
throw new Error(`Invalid OpenAPI version: should be a string but got "${typeof root.openapi}"`);
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
if (root.openapi && root.openapi.startsWith('3.0')) {
|
|
38
42
|
return OasVersion.Version3_0;
|
|
39
43
|
}
|
package/src/resolve.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as url from 'url';
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
import { OasRef } from './typings/openapi';
|
|
6
6
|
import { isRef, joinPointer, escapePointer, parseRef, isAbsoluteUrl } from './ref-utils';
|
|
7
7
|
import type { YAMLNode, LoadOptions } from 'yaml-ast-parser';
|
|
8
8
|
import { NormalizedNodeType, isNamedType } from './types';
|
|
9
|
-
import { readFileFromUrl } from './utils';
|
|
9
|
+
import { readFileFromUrl, parseYaml } from './utils';
|
|
10
10
|
import { ResolveConfig } from './config/config';
|
|
11
11
|
|
|
12
12
|
export type CollectedRefs = Map<string /* absoluteFilePath */, Document>;
|
|
@@ -52,7 +52,7 @@ export class ResolveError extends Error {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const jsYamlErrorLineColRegexp =
|
|
55
|
+
const jsYamlErrorLineColRegexp = /\((\d+):(\d+)\)$/;
|
|
56
56
|
|
|
57
57
|
export class YamlParseError extends Error {
|
|
58
58
|
col: number;
|
|
@@ -79,7 +79,7 @@ export function makeDocumentFromString(sourceString: string, absoluteRef: string
|
|
|
79
79
|
try {
|
|
80
80
|
return {
|
|
81
81
|
source,
|
|
82
|
-
parsed:
|
|
82
|
+
parsed: parseYaml(sourceString, { filename: absoluteRef }),
|
|
83
83
|
};
|
|
84
84
|
} catch (e) {
|
|
85
85
|
throw new YamlParseError(e, source);
|
|
@@ -133,7 +133,7 @@ export class BaseResolver {
|
|
|
133
133
|
try {
|
|
134
134
|
return {
|
|
135
135
|
source,
|
|
136
|
-
parsed:
|
|
136
|
+
parsed: parseYaml(source.body, { filename: source.absoluteRef }),
|
|
137
137
|
};
|
|
138
138
|
} catch (e) {
|
|
139
139
|
throw new YamlParseError(e, source);
|
|
@@ -83,7 +83,7 @@ describe('oas3 boolean-parameter-prefixes', () => {
|
|
|
83
83
|
},
|
|
84
84
|
},
|
|
85
85
|
],
|
|
86
|
-
"message": "Failed to parse: unexpected end of the stream within a single quoted scalar in \\"fixtures/invalid-yaml.yaml\\"
|
|
86
|
+
"message": "Failed to parse: unexpected end of the stream within a single quoted scalar in \\"fixtures/invalid-yaml.yaml\\" (2:1)",
|
|
87
87
|
"ruleId": "no-unresolved-refs",
|
|
88
88
|
"severity": "error",
|
|
89
89
|
"suggest": Array [],
|
|
@@ -96,7 +96,7 @@ describe('oas3 boolean-parameter-prefixes', () => {
|
|
|
96
96
|
"source": "foobar.yaml",
|
|
97
97
|
},
|
|
98
98
|
],
|
|
99
|
-
"message": "Can't resolve $ref: unexpected end of the stream within a single quoted scalar in \\"fixtures/invalid-yaml.yaml\\"
|
|
99
|
+
"message": "Can't resolve $ref: unexpected end of the stream within a single quoted scalar in \\"fixtures/invalid-yaml.yaml\\" (2:1)",
|
|
100
100
|
"ruleId": "no-unresolved-refs",
|
|
101
101
|
"severity": "error",
|
|
102
102
|
"suggest": Array [],
|
|
@@ -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,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
|
@@ -29,6 +29,14 @@ import { NoIdenticalPaths } from '../common/no-identical-paths';
|
|
|
29
29
|
import { OperationOperationId } from '../common/operation-operationId';
|
|
30
30
|
import { OperationSummary } from '../common/operation-summary';
|
|
31
31
|
import { NoAmbiguousPaths } from '../common/no-ambiguous-paths';
|
|
32
|
+
import { NoHttpVerbsInPaths } from '../common/no-http-verbs-in-paths';
|
|
33
|
+
import { PathExcludesPatterns } from '../common/path-excludes-patterns';
|
|
34
|
+
import { RequestMimeType } from './request-mime-type';
|
|
35
|
+
import { ResponseMimeType } from './response-mime-type';
|
|
36
|
+
import { PathSegmentPlural } from '../common/path-segment-plural';
|
|
37
|
+
import { OperationDescriptionOverride } from '../common/operation-description-override';
|
|
38
|
+
import { TagDescriptionOverride } from '../common/tag-description-override';
|
|
39
|
+
import { InfoDescriptionOverride } from '../common/info-description-override';
|
|
32
40
|
|
|
33
41
|
export const rules = {
|
|
34
42
|
spec: OasSpec as Oas2Rule,
|
|
@@ -61,9 +69,17 @@ export const rules = {
|
|
|
61
69
|
'no-identical-paths': NoIdenticalPaths as Oas2Rule,
|
|
62
70
|
'no-ambiguous-paths': NoAmbiguousPaths as Oas2Rule,
|
|
63
71
|
'path-http-verbs-order': PathHttpVerbsOrder as Oas2Rule,
|
|
72
|
+
'no-http-verbs-in-paths': NoHttpVerbsInPaths as Oas2Rule,
|
|
73
|
+
'path-excludes-patterns': PathExcludesPatterns as Oas2Rule,
|
|
74
|
+
'request-mime-type': RequestMimeType as Oas2Rule,
|
|
75
|
+
'response-mime-type': ResponseMimeType as Oas2Rule,
|
|
76
|
+
'path-segment-plural': PathSegmentPlural as Oas2Rule,
|
|
64
77
|
};
|
|
65
78
|
|
|
66
79
|
export const preprocessors = {};
|
|
67
80
|
export const decorators = {
|
|
68
81
|
'registry-dependencies': RegistryDependencies as Oas2Decorator,
|
|
82
|
+
'operation-description-override': OperationDescriptionOverride as Oas2Decorator,
|
|
83
|
+
'tag-description-override': TagDescriptionOverride as Oas2Decorator,
|
|
84
|
+
'info-description-override': InfoDescriptionOverride as Oas2Decorator,
|
|
69
85
|
};
|
|
@@ -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
|
+
};
|