@redocly/openapi-core 1.0.0-beta.66 → 1.0.0-beta.70
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/__tests__/lint.test.ts +1 -1
- package/__tests__/login.test.ts +17 -0
- package/lib/bundle.d.ts +4 -0
- package/lib/bundle.js +25 -7
- package/lib/config/all.js +3 -0
- package/lib/config/config.d.ts +10 -0
- package/lib/config/config.js +7 -1
- package/lib/config/load.js +17 -8
- package/lib/config/minimal.js +1 -0
- package/lib/config/recommended.js +1 -0
- package/lib/index.d.ts +2 -2
- package/lib/lint.js +2 -0
- package/lib/redocly/index.d.ts +25 -20
- package/lib/redocly/index.js +77 -214
- package/lib/redocly/registry-api-types.d.ts +28 -0
- package/lib/redocly/registry-api-types.js +2 -0
- package/lib/redocly/registry-api.d.ts +14 -0
- package/lib/redocly/registry-api.js +105 -0
- package/lib/ref-utils.js +1 -2
- package/lib/rules/common/no-invalid-parameter-examples.d.ts +1 -0
- package/lib/rules/common/no-invalid-parameter-examples.js +25 -0
- package/lib/rules/common/no-invalid-schema-examples.d.ts +1 -0
- package/lib/rules/common/no-invalid-schema-examples.js +23 -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/paths-kebab-case.js +1 -1
- package/lib/rules/common/registry-dependencies.js +4 -7
- package/lib/rules/oas2/index.d.ts +3 -0
- package/lib/rules/oas2/index.js +6 -0
- package/lib/rules/oas3/index.js +6 -0
- package/lib/rules/oas3/no-invalid-media-type-examples.js +5 -26
- package/lib/rules/oas3/no-server-trailing-slash.js +1 -1
- package/lib/rules/utils.d.ts +3 -0
- package/lib/rules/utils.js +26 -1
- package/lib/typings/openapi.d.ts +3 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +5 -1
- package/lib/walk.d.ts +2 -0
- package/lib/walk.js +7 -0
- package/package.json +1 -1
- package/src/bundle.ts +51 -9
- package/src/config/__tests__/load.test.ts +35 -0
- package/src/config/all.ts +3 -0
- package/src/config/config.ts +11 -0
- package/src/config/load.ts +20 -9
- package/src/config/minimal.ts +1 -0
- package/src/config/recommended.ts +1 -0
- package/src/index.ts +2 -8
- package/src/lint.ts +2 -0
- package/src/redocly/__tests__/redocly-client.test.ts +114 -0
- package/src/redocly/index.ts +90 -227
- package/src/redocly/registry-api-types.ts +31 -0
- package/src/redocly/registry-api.ts +110 -0
- package/src/ref-utils.ts +1 -3
- package/src/rules/common/__tests__/operation-4xx-response.test.ts +108 -0
- package/src/rules/common/__tests__/paths-kebab-case.test.ts +23 -0
- package/src/rules/common/no-invalid-parameter-examples.ts +36 -0
- package/src/rules/common/no-invalid-schema-examples.ts +27 -0
- package/src/rules/common/operation-4xx-response.ts +17 -0
- package/src/rules/common/paths-kebab-case.ts +1 -1
- package/src/rules/common/registry-dependencies.ts +6 -8
- package/src/rules/oas2/index.ts +6 -0
- package/src/rules/oas3/__tests__/no-server-trailing-slash.test.ts +19 -0
- package/src/rules/oas3/index.ts +6 -0
- package/src/rules/oas3/no-invalid-media-type-examples.ts +16 -36
- package/src/rules/oas3/no-server-trailing-slash.ts +1 -1
- package/src/rules/utils.ts +43 -2
- package/src/typings/openapi.ts +4 -0
- package/src/utils.ts +5 -1
- package/src/walk.ts +10 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/lib/redocly/query.d.ts +0 -4
- package/lib/redocly/query.js +0 -44
- package/src/redocly/query.ts +0 -38
|
@@ -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
|
+
});
|
|
@@ -83,4 +83,27 @@ describe('Oas3 paths-kebab-case', () => {
|
|
|
83
83
|
]
|
|
84
84
|
`);
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
it('should allow trailing slash in path with "paths-kebab-case" rule', async () => {
|
|
88
|
+
const document = parseYamlToDocument(
|
|
89
|
+
outdent`
|
|
90
|
+
openapi: 3.0.0
|
|
91
|
+
paths:
|
|
92
|
+
/some/:
|
|
93
|
+
get:
|
|
94
|
+
summary: List all pets
|
|
95
|
+
`,
|
|
96
|
+
'foobar.yaml',
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const results = await lintDocument({
|
|
100
|
+
externalRefResolver: new BaseResolver(),
|
|
101
|
+
document,
|
|
102
|
+
config: makeConfig({
|
|
103
|
+
'paths-kebab-case': 'error',
|
|
104
|
+
'no-path-trailing-slash': 'off',
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
108
|
+
});
|
|
86
109
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { UserContext } from '../../walk';
|
|
2
|
+
import { Oas3Parameter } from '../../typings/openapi';
|
|
3
|
+
import { validateExample } from '../utils';
|
|
4
|
+
|
|
5
|
+
export const NoInvalidParameterExamples: any = (opts: any) => {
|
|
6
|
+
const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true;
|
|
7
|
+
return {
|
|
8
|
+
Parameter: {
|
|
9
|
+
leave(parameter: Oas3Parameter, ctx: UserContext) {
|
|
10
|
+
if (parameter.example) {
|
|
11
|
+
validateExample(
|
|
12
|
+
parameter.example,
|
|
13
|
+
parameter.schema!,
|
|
14
|
+
ctx.location.child('example'),
|
|
15
|
+
ctx,
|
|
16
|
+
disallowAdditionalProperties,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (parameter.examples) {
|
|
21
|
+
for (const [key, example] of Object.entries(parameter.examples)) {
|
|
22
|
+
if ('value' in example) {
|
|
23
|
+
validateExample(
|
|
24
|
+
example.value,
|
|
25
|
+
parameter.schema!,
|
|
26
|
+
ctx.location.child(['examples', key]),
|
|
27
|
+
ctx,
|
|
28
|
+
false,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { UserContext } from '../../walk';
|
|
2
|
+
import { Oas3_1Schema } from '../../typings/openapi';
|
|
3
|
+
import { validateExample } from '../utils';
|
|
4
|
+
|
|
5
|
+
export const NoInvalidSchemaExamples: any = (opts: any) => {
|
|
6
|
+
const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true;
|
|
7
|
+
return {
|
|
8
|
+
Schema: {
|
|
9
|
+
leave(schema: Oas3_1Schema, ctx: UserContext) {
|
|
10
|
+
if (schema.examples) {
|
|
11
|
+
for (const example of schema.examples) {
|
|
12
|
+
validateExample(
|
|
13
|
+
example,
|
|
14
|
+
schema,
|
|
15
|
+
ctx.location.child(['examples', schema.examples.indexOf(example)]),
|
|
16
|
+
ctx,
|
|
17
|
+
disallowAdditionalProperties,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (schema.example) {
|
|
22
|
+
validateExample(schema.example, schema, ctx.location.child('example'), ctx, false);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -4,7 +4,7 @@ import { UserContext } from '../../walk';
|
|
|
4
4
|
export const PathsKebabCase: Oas3Rule | Oas2Rule = () => {
|
|
5
5
|
return {
|
|
6
6
|
PathItem(_path: object, { report, key }: UserContext) {
|
|
7
|
-
const segments = (key as string).substr(1).split('/');
|
|
7
|
+
const segments = (key as string).substr(1).split('/').filter(s => s !== ''); // filter out empty segments
|
|
8
8
|
if (!segments.every((segment) => /^{.+}$/.test(segment) || /^[a-z0-9-.]+$/.test(segment))) {
|
|
9
9
|
report({
|
|
10
10
|
message: `\`${key}\` does not use kebab-case.`,
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UserContext } from '../../walk';
|
|
2
|
+
import { isRedoclyRegistryURL } from '../../redocly';
|
|
2
3
|
|
|
3
4
|
import { Oas3Decorator, Oas2Decorator } from '../../visitors';
|
|
4
5
|
|
|
5
6
|
export const RegistryDependencies: Oas3Decorator | Oas2Decorator = () => {
|
|
6
|
-
let redoclyClient: RedoclyClient;
|
|
7
7
|
let registryDependencies = new Set<string>();
|
|
8
8
|
|
|
9
9
|
return {
|
|
10
10
|
DefinitionRoot: {
|
|
11
|
-
leave() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
redoclyClient.updateDependencies(Array.from(registryDependencies.keys()));
|
|
15
|
-
}
|
|
11
|
+
leave(_: any, ctx: UserContext) {
|
|
12
|
+
const data = ctx.getVisitorData();
|
|
13
|
+
data.links = Array.from(registryDependencies);
|
|
16
14
|
},
|
|
17
15
|
},
|
|
18
16
|
ref(node) {
|
|
19
17
|
if (node.$ref) {
|
|
20
18
|
const link = node.$ref.split('#/')[0];
|
|
21
|
-
if (
|
|
19
|
+
if (isRedoclyRegistryURL(link)) {
|
|
22
20
|
registryDependencies.add(link);
|
|
23
21
|
}
|
|
24
22
|
}
|
package/src/rules/oas2/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { OasSpec } from '../common/spec';
|
|
2
|
+
import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
|
|
3
|
+
import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
|
|
2
4
|
import { InfoDescription } from '../common/info-description';
|
|
3
5
|
import { InfoContact } from '../common/info-contact';
|
|
4
6
|
import { InfoLicense } from '../common/info-license-url';
|
|
@@ -10,6 +12,7 @@ import { PathsKebabCase } from '../common/paths-kebab-case';
|
|
|
10
12
|
import { NoEnumTypeMismatch } from '../common/no-enum-type-mismatch';
|
|
11
13
|
import { NoPathTrailingSlash } from '../common/no-path-trailing-slash';
|
|
12
14
|
import { Operation2xxResponse } from '../common/operation-2xx-response';
|
|
15
|
+
import { Operation4xxResponse } from '../common/operation-4xx-response';
|
|
13
16
|
import { OperationIdUnique } from '../common/operation-operationId-unique';
|
|
14
17
|
import { OperationParametersUnique } from '../common/operation-parameters-unique';
|
|
15
18
|
import { PathParamsDefined } from '../common/path-params-defined';
|
|
@@ -40,6 +43,8 @@ import { InfoDescriptionOverride } from '../common/info-description-override';
|
|
|
40
43
|
|
|
41
44
|
export const rules = {
|
|
42
45
|
spec: OasSpec as Oas2Rule,
|
|
46
|
+
'no-invalid-schema-examples': NoInvalidSchemaExamples,
|
|
47
|
+
'no-invalid-parameter-examples': NoInvalidParameterExamples,
|
|
43
48
|
'info-description': InfoDescription as Oas2Rule,
|
|
44
49
|
'info-contact': InfoContact as Oas2Rule,
|
|
45
50
|
'info-license': InfoLicense as Oas2Rule,
|
|
@@ -51,6 +56,7 @@ export const rules = {
|
|
|
51
56
|
'boolean-parameter-prefixes': BooleanParameterPrefixes as Oas2Rule,
|
|
52
57
|
'no-path-trailing-slash': NoPathTrailingSlash as Oas2Rule,
|
|
53
58
|
'operation-2xx-response': Operation2xxResponse as Oas2Rule,
|
|
59
|
+
'operation-4xx-response': Operation4xxResponse as Oas2Rule,
|
|
54
60
|
'operation-operationId-unique': OperationIdUnique as Oas2Rule,
|
|
55
61
|
'operation-parameters-unique': OperationParametersUnique as Oas2Rule,
|
|
56
62
|
'path-parameters-defined': PathParamsDefined as Oas2Rule,
|
|
@@ -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
|
@@ -2,6 +2,7 @@ import { Oas3RuleSet } from '../../oas-types';
|
|
|
2
2
|
import { Oas3Decorator } from '../../visitors';
|
|
3
3
|
import { OasSpec } from '../common/spec';
|
|
4
4
|
import { Operation2xxResponse } from '../common/operation-2xx-response';
|
|
5
|
+
import { Operation4xxResponse } from '../common/operation-4xx-response';
|
|
5
6
|
import { OperationIdUnique } from '../common/operation-operationId-unique';
|
|
6
7
|
import { OperationParametersUnique } from '../common/operation-parameters-unique';
|
|
7
8
|
import { PathParamsDefined } from '../common/path-params-defined';
|
|
@@ -46,6 +47,8 @@ import { OperationDescriptionOverride } from '../common/operation-description-ov
|
|
|
46
47
|
import { TagDescriptionOverride } from '../common/tag-description-override';
|
|
47
48
|
import { InfoDescriptionOverride } from '../common/info-description-override';
|
|
48
49
|
import { PathExcludesPatterns } from '../common/path-excludes-patterns';
|
|
50
|
+
import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
|
|
51
|
+
import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
|
|
49
52
|
|
|
50
53
|
export const rules = {
|
|
51
54
|
spec: OasSpec,
|
|
@@ -54,6 +57,7 @@ export const rules = {
|
|
|
54
57
|
'info-license': InfoLicense,
|
|
55
58
|
'info-license-url': InfoLicenseUrl,
|
|
56
59
|
'operation-2xx-response': Operation2xxResponse,
|
|
60
|
+
'operation-4xx-response': Operation4xxResponse,
|
|
57
61
|
'operation-operationId-unique': OperationIdUnique,
|
|
58
62
|
'operation-parameters-unique': OperationParametersUnique,
|
|
59
63
|
'path-parameters-defined': PathParamsDefined,
|
|
@@ -91,6 +95,8 @@ export const rules = {
|
|
|
91
95
|
'request-mime-type': RequestMimeType,
|
|
92
96
|
'response-mime-type': ResponseMimeType,
|
|
93
97
|
'path-segment-plural': PathSegmentPlural,
|
|
98
|
+
'no-invalid-schema-examples': NoInvalidSchemaExamples,
|
|
99
|
+
'no-invalid-parameter-examples': NoInvalidParameterExamples,
|
|
94
100
|
} as Oas3RuleSet;
|
|
95
101
|
|
|
96
102
|
export const preprocessors = {};
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { Oas3Rule } from '../../visitors';
|
|
2
|
-
import { validateJsonSchema } from '../ajv';
|
|
3
2
|
import { Location, isRef } from '../../ref-utils';
|
|
4
3
|
import { Oas3Example } from '../../typings/openapi';
|
|
4
|
+
import { validateExample } from '../utils';
|
|
5
|
+
import { UserContext } from '../../walk';
|
|
5
6
|
|
|
6
7
|
export const ValidContentExamples: Oas3Rule = (opts) => {
|
|
7
8
|
const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true;
|
|
8
9
|
|
|
9
10
|
return {
|
|
10
11
|
MediaType: {
|
|
11
|
-
leave(mediaType,
|
|
12
|
+
leave(mediaType, ctx: UserContext) {
|
|
13
|
+
const { location, resolve } = ctx;
|
|
12
14
|
if (!mediaType.schema) return;
|
|
13
|
-
|
|
14
15
|
if (mediaType.example) {
|
|
15
|
-
validateExample(
|
|
16
|
+
validateExample(
|
|
17
|
+
mediaType.example,
|
|
18
|
+
mediaType.schema,
|
|
19
|
+
location.child('example'),
|
|
20
|
+
ctx,
|
|
21
|
+
disallowAdditionalProperties,
|
|
22
|
+
);
|
|
16
23
|
} else if (mediaType.examples) {
|
|
17
24
|
for (const exampleName of Object.keys(mediaType.examples)) {
|
|
18
25
|
let example = mediaType.examples[exampleName];
|
|
@@ -23,40 +30,13 @@ export const ValidContentExamples: Oas3Rule = (opts) => {
|
|
|
23
30
|
dataLoc = resolved.location.child('value');
|
|
24
31
|
example = resolved.node;
|
|
25
32
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
function validateExample(example: any, dataLoc: Location) {
|
|
32
|
-
try {
|
|
33
|
-
const { valid, errors } = validateJsonSchema(
|
|
34
|
-
example,
|
|
35
|
-
mediaType.schema!,
|
|
36
|
-
location.child('schema'),
|
|
37
|
-
dataLoc.pointer,
|
|
38
|
-
resolve,
|
|
33
|
+
validateExample(
|
|
34
|
+
example.value,
|
|
35
|
+
mediaType.schema,
|
|
36
|
+
dataLoc,
|
|
37
|
+
ctx,
|
|
39
38
|
disallowAdditionalProperties,
|
|
40
39
|
);
|
|
41
|
-
if (!valid) {
|
|
42
|
-
for (let error of errors) {
|
|
43
|
-
report({
|
|
44
|
-
message: `Example value must conform to the schema: ${error.message}.`,
|
|
45
|
-
location: {
|
|
46
|
-
...new Location(dataLoc.source, error.instancePath),
|
|
47
|
-
reportOnKey: error.keyword === 'additionalProperties',
|
|
48
|
-
},
|
|
49
|
-
from: location,
|
|
50
|
-
suggest: error.suggest,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
} catch(e) {
|
|
55
|
-
report({
|
|
56
|
-
message: `Example validation errored: ${e.message}.`,
|
|
57
|
-
location: location.child('schema'),
|
|
58
|
-
from: location
|
|
59
|
-
});
|
|
60
40
|
}
|
|
61
41
|
}
|
|
62
42
|
},
|
|
@@ -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']),
|
package/src/rules/utils.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import levenshtein = require('js-levenshtein');
|
|
2
2
|
import { UserContext } from '../walk';
|
|
3
|
+
import { Location } from '../ref-utils';
|
|
4
|
+
import { validateJsonSchema } from './ajv';
|
|
5
|
+
import { Oas3Schema, Referenced } from '../typings/openapi';
|
|
3
6
|
|
|
4
7
|
export function oasTypeOf(value: unknown) {
|
|
5
8
|
if (Array.isArray(value)) {
|
|
@@ -20,7 +23,7 @@ export function oasTypeOf(value: unknown) {
|
|
|
20
23
|
*/
|
|
21
24
|
export function matchesJsonSchemaType(value: unknown, type: string, nullable: boolean): boolean {
|
|
22
25
|
if (nullable && value === null) {
|
|
23
|
-
return value === null
|
|
26
|
+
return value === null;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
switch (type) {
|
|
@@ -79,4 +82,42 @@ export function getSuggest(given: string, variants: string[]): string[] {
|
|
|
79
82
|
|
|
80
83
|
// if (bestMatch.distance <= 4) return bestMatch.string;
|
|
81
84
|
return distances.map((d) => d.variant);
|
|
82
|
-
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function validateExample(
|
|
88
|
+
example: any,
|
|
89
|
+
schema: Referenced<Oas3Schema>,
|
|
90
|
+
dataLoc: Location,
|
|
91
|
+
{ resolve, location, report }: UserContext,
|
|
92
|
+
disallowAdditionalProperties: boolean,
|
|
93
|
+
) {
|
|
94
|
+
try {
|
|
95
|
+
const { valid, errors } = validateJsonSchema(
|
|
96
|
+
example,
|
|
97
|
+
schema,
|
|
98
|
+
location.child('schema'),
|
|
99
|
+
dataLoc.pointer,
|
|
100
|
+
resolve,
|
|
101
|
+
disallowAdditionalProperties,
|
|
102
|
+
);
|
|
103
|
+
if (!valid) {
|
|
104
|
+
for (let error of errors) {
|
|
105
|
+
report({
|
|
106
|
+
message: `Example value must conform to the schema: ${error.message}.`,
|
|
107
|
+
location: {
|
|
108
|
+
...new Location(dataLoc.source, error.instancePath),
|
|
109
|
+
reportOnKey: error.keyword === 'additionalProperties',
|
|
110
|
+
},
|
|
111
|
+
from: location,
|
|
112
|
+
suggest: error.suggest,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
report({
|
|
118
|
+
message: `Example validation errored: ${e.message}.`,
|
|
119
|
+
location: location.child('schema'),
|
|
120
|
+
from: location,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/typings/openapi.ts
CHANGED
|
@@ -150,6 +150,10 @@ export interface Oas3Schema {
|
|
|
150
150
|
xml?: Oas3Xml;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
export interface Oas3_1Schema extends Oas3Schema {
|
|
154
|
+
examples?: any[];
|
|
155
|
+
}
|
|
156
|
+
|
|
153
157
|
export interface Oas3Discriminator {
|
|
154
158
|
propertyName: string;
|
|
155
159
|
mapping?: { [name: string]: string };
|
package/src/utils.ts
CHANGED
package/src/walk.ts
CHANGED
|
@@ -33,6 +33,7 @@ export type UserContext = {
|
|
|
33
33
|
key: string | number;
|
|
34
34
|
parent: any;
|
|
35
35
|
oasVersion: OasVersion;
|
|
36
|
+
getVisitorData: () => Record<string, unknown>;
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
export type Loc = {
|
|
@@ -77,6 +78,7 @@ export type NormalizedProblem = {
|
|
|
77
78
|
export type WalkContext = {
|
|
78
79
|
problems: NormalizedProblem[];
|
|
79
80
|
oasVersion: OasVersion;
|
|
81
|
+
visitorsData: Record<string, Record<string, unknown>>; // custom data store that visitors can use for various purposes
|
|
80
82
|
refTypes?: Map<string, NormalizedNodeType>;
|
|
81
83
|
};
|
|
82
84
|
|
|
@@ -141,6 +143,7 @@ export function walkDocument<T>(opts: {
|
|
|
141
143
|
key,
|
|
142
144
|
parentLocations: {},
|
|
143
145
|
oasVersion: ctx.oasVersion,
|
|
146
|
+
getVisitorData: getVisitorDataFn.bind(undefined, ruleId)
|
|
144
147
|
},
|
|
145
148
|
{ node: resolvedNode, location: resolvedLocation, error },
|
|
146
149
|
);
|
|
@@ -325,6 +328,7 @@ export function walkDocument<T>(opts: {
|
|
|
325
328
|
key,
|
|
326
329
|
parentLocations: {},
|
|
327
330
|
oasVersion: ctx.oasVersion,
|
|
331
|
+
getVisitorData: getVisitorDataFn.bind(undefined, ruleId)
|
|
328
332
|
},
|
|
329
333
|
{ node: resolvedNode, location: resolvedLocation, error },
|
|
330
334
|
);
|
|
@@ -357,6 +361,7 @@ export function walkDocument<T>(opts: {
|
|
|
357
361
|
ignoreNextVisitorsOnNode: () => {
|
|
358
362
|
ignoreNextVisitorsOnNode = true;
|
|
359
363
|
},
|
|
364
|
+
getVisitorData: getVisitorDataFn.bind(undefined, ruleId),
|
|
360
365
|
},
|
|
361
366
|
collectParents(context),
|
|
362
367
|
context,
|
|
@@ -408,5 +413,10 @@ export function walkDocument<T>(opts: {
|
|
|
408
413
|
}),
|
|
409
414
|
});
|
|
410
415
|
}
|
|
416
|
+
|
|
417
|
+
function getVisitorDataFn(ruleId: string) {
|
|
418
|
+
ctx.visitorsData[ruleId] = ctx.visitorsData[ruleId] || {};
|
|
419
|
+
return ctx.visitorsData[ruleId];
|
|
420
|
+
}
|
|
411
421
|
}
|
|
412
422
|
}
|