@redocly/openapi-core 1.4.1 → 1.6.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 +12 -0
- package/README.md +5 -5
- package/lib/bundle.d.ts +8 -6
- package/lib/bundle.js +48 -14
- package/lib/config/all.js +2 -0
- package/lib/config/config-resolvers.d.ts +9 -1
- package/lib/config/config-resolvers.js +22 -1
- package/lib/config/load.d.ts +11 -3
- package/lib/config/load.js +12 -6
- package/lib/config/minimal.js +2 -0
- package/lib/config/recommended-strict.js +2 -0
- package/lib/config/recommended.js +2 -0
- package/lib/lint.d.ts +6 -3
- package/lib/lint.js +14 -3
- package/lib/rules/oas3/array-parameter-serialization.d.ts +5 -0
- package/lib/rules/oas3/array-parameter-serialization.js +31 -0
- package/lib/rules/oas3/index.js +2 -0
- package/lib/types/portal-config-schema.d.ts +22 -2465
- package/lib/types/portal-config-schema.js +56 -38
- package/lib/types/redocly-yaml.d.ts +1 -1
- package/lib/types/redocly-yaml.js +5 -4
- package/lib/typings/openapi.d.ts +1 -0
- package/lib/visitors.d.ts +5 -5
- package/lib/visitors.js +4 -6
- package/lib/walk.d.ts +3 -3
- package/lib/walk.js +2 -2
- package/package.json +1 -1
- package/src/__tests__/lint.test.ts +2 -2
- package/src/bundle.ts +67 -26
- package/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +4 -0
- package/src/config/__tests__/fixtures/resolve-refs-in-config/config-with-refs.yaml +8 -0
- package/src/config/__tests__/fixtures/resolve-refs-in-config/rules.yaml +2 -0
- package/src/config/__tests__/fixtures/resolve-refs-in-config/seo.yaml +1 -0
- package/src/config/__tests__/load.test.ts +80 -1
- package/src/config/all.ts +2 -0
- package/src/config/config-resolvers.ts +41 -11
- package/src/config/load.ts +36 -19
- package/src/config/minimal.ts +2 -0
- package/src/config/recommended-strict.ts +2 -0
- package/src/config/recommended.ts +2 -0
- package/src/lint.ts +32 -10
- package/src/rules/oas3/__tests__/array-parameter-serialization.test.ts +263 -0
- package/src/rules/oas3/array-parameter-serialization.ts +43 -0
- package/src/rules/oas3/index.ts +2 -0
- package/src/types/portal-config-schema.ts +65 -42
- package/src/types/redocly-yaml.ts +3 -4
- package/src/typings/openapi.ts +1 -0
- package/src/visitors.ts +20 -22
- package/src/walk.ts +8 -8
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import { isAbsoluteUrl } from '../ref-utils';
|
|
3
3
|
import { pickDefined } from '../utils';
|
|
4
|
-
import { BaseResolver } from '../resolve';
|
|
4
|
+
import { resolveDocument, BaseResolver } from '../resolve';
|
|
5
5
|
import { defaultPlugin } from './builtIn';
|
|
6
6
|
import {
|
|
7
7
|
getResolveConfig,
|
|
@@ -11,6 +11,14 @@ import {
|
|
|
11
11
|
prefixRules,
|
|
12
12
|
transformConfig,
|
|
13
13
|
} from './utils';
|
|
14
|
+
import { isBrowser } from '../env';
|
|
15
|
+
import { isNotString, isString, isDefined, parseYaml, keysOf } from '../utils';
|
|
16
|
+
import { Config } from './config';
|
|
17
|
+
import { colorize, logger } from '../logger';
|
|
18
|
+
import { asserts, buildAssertCustomFunction } from '../rules/common/assertions/asserts';
|
|
19
|
+
import { normalizeTypes } from '../types';
|
|
20
|
+
import { ConfigTypes } from '../types/redocly-yaml';
|
|
21
|
+
|
|
14
22
|
import type {
|
|
15
23
|
StyleguideRawConfig,
|
|
16
24
|
ApiStyleguideRawConfig,
|
|
@@ -21,17 +29,39 @@ import type {
|
|
|
21
29
|
RuleConfig,
|
|
22
30
|
DeprecatedInRawConfig,
|
|
23
31
|
} from './types';
|
|
24
|
-
import { isBrowser } from '../env';
|
|
25
|
-
import { isNotString, isString, isDefined, parseYaml, keysOf } from '../utils';
|
|
26
|
-
import { Config } from './config';
|
|
27
|
-
import { colorize, logger } from '../logger';
|
|
28
|
-
import {
|
|
29
|
-
Asserts,
|
|
30
|
-
AssertionFn,
|
|
31
|
-
asserts,
|
|
32
|
-
buildAssertCustomFunction,
|
|
33
|
-
} from '../rules/common/assertions/asserts';
|
|
34
32
|
import type { Assertion, AssertionDefinition, RawAssertion } from '../rules/common/assertions';
|
|
33
|
+
import type { Asserts, AssertionFn } from '../rules/common/assertions/asserts';
|
|
34
|
+
import type { BundleOptions } from '../bundle';
|
|
35
|
+
import type { Document, ResolvedRefMap } from '../resolve';
|
|
36
|
+
|
|
37
|
+
export async function resolveConfigFileAndRefs({
|
|
38
|
+
configPath,
|
|
39
|
+
externalRefResolver = new BaseResolver(),
|
|
40
|
+
base = null,
|
|
41
|
+
}: Omit<BundleOptions, 'config'> & { configPath?: string }): Promise<{
|
|
42
|
+
document: Document;
|
|
43
|
+
resolvedRefMap: ResolvedRefMap;
|
|
44
|
+
}> {
|
|
45
|
+
if (!configPath) {
|
|
46
|
+
throw new Error('Reference to a config is required.\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const document = await externalRefResolver.resolveDocument(base, configPath, true);
|
|
50
|
+
|
|
51
|
+
if (document instanceof Error) {
|
|
52
|
+
throw document;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const types = normalizeTypes(ConfigTypes);
|
|
56
|
+
|
|
57
|
+
const resolvedRefMap = await resolveDocument({
|
|
58
|
+
rootDocument: document,
|
|
59
|
+
rootType: types.ConfigRoot,
|
|
60
|
+
externalRefResolver,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return { document, resolvedRefMap };
|
|
64
|
+
}
|
|
35
65
|
|
|
36
66
|
export async function resolveConfig(rawConfig: RawConfig, configPath?: string): Promise<Config> {
|
|
37
67
|
if (rawConfig.styleguide?.extends?.some(isNotString)) {
|
package/src/config/load.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { RedoclyClient } from '../redocly';
|
|
4
|
-
import { isEmptyObject,
|
|
4
|
+
import { isEmptyObject, doesYamlFileExist } from '../utils';
|
|
5
5
|
import { parseYaml } from '../js-yaml';
|
|
6
6
|
import { Config, DOMAINS } from './config';
|
|
7
7
|
import { ConfigValidationError, transformConfig } from './utils';
|
|
8
|
-
import { resolveConfig } from './config-resolvers';
|
|
8
|
+
import { resolveConfig, resolveConfigFileAndRefs } from './config-resolvers';
|
|
9
|
+
import { bundleConfig } from '../bundle';
|
|
9
10
|
|
|
10
|
-
import type {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
RawUniversalConfig,
|
|
15
|
-
Region,
|
|
16
|
-
} from './types';
|
|
17
|
-
import { RegionalTokenWithValidity } from '../redocly/redocly-client-types';
|
|
11
|
+
import type { Document } from '../resolve';
|
|
12
|
+
import type { RegionalTokenWithValidity } from '../redocly/redocly-client-types';
|
|
13
|
+
import type { RawConfig, RawUniversalConfig, Region } from './types';
|
|
14
|
+
import type { BaseResolver, ResolvedRefMap } from '../resolve';
|
|
18
15
|
|
|
19
16
|
async function addConfigMetadata({
|
|
20
17
|
rawConfig,
|
|
@@ -73,17 +70,30 @@ async function addConfigMetadata({
|
|
|
73
70
|
);
|
|
74
71
|
}
|
|
75
72
|
|
|
73
|
+
export type RawConfigProcessor = (
|
|
74
|
+
rawConfig: Document,
|
|
75
|
+
resolvedRefMap: ResolvedRefMap
|
|
76
|
+
) => void | Promise<void>;
|
|
77
|
+
|
|
76
78
|
export async function loadConfig(
|
|
77
79
|
options: {
|
|
78
80
|
configPath?: string;
|
|
79
81
|
customExtends?: string[];
|
|
80
|
-
processRawConfig?:
|
|
82
|
+
processRawConfig?: RawConfigProcessor;
|
|
83
|
+
externalRefResolver?: BaseResolver;
|
|
81
84
|
files?: string[];
|
|
82
85
|
region?: Region;
|
|
83
86
|
} = {}
|
|
84
87
|
): Promise<Config> {
|
|
85
|
-
const {
|
|
86
|
-
|
|
88
|
+
const {
|
|
89
|
+
configPath = findConfig(),
|
|
90
|
+
customExtends,
|
|
91
|
+
processRawConfig,
|
|
92
|
+
files,
|
|
93
|
+
region,
|
|
94
|
+
externalRefResolver,
|
|
95
|
+
} = options;
|
|
96
|
+
const rawConfig = await getConfig({ configPath, processRawConfig, externalRefResolver });
|
|
87
97
|
|
|
88
98
|
const redoclyClient = new RedoclyClient();
|
|
89
99
|
const tokens = await redoclyClient.getTokens();
|
|
@@ -116,17 +126,24 @@ export function findConfig(dir?: string): string | undefined {
|
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
export async function getConfig(
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
options: {
|
|
130
|
+
configPath?: string;
|
|
131
|
+
processRawConfig?: RawConfigProcessor;
|
|
132
|
+
externalRefResolver?: BaseResolver;
|
|
133
|
+
} = {}
|
|
121
134
|
): Promise<RawConfig> {
|
|
135
|
+
const { configPath = findConfig(), processRawConfig, externalRefResolver } = options;
|
|
122
136
|
if (!configPath || !doesYamlFileExist(configPath)) return {};
|
|
123
137
|
try {
|
|
124
|
-
const
|
|
125
|
-
|
|
138
|
+
const { document, resolvedRefMap } = await resolveConfigFileAndRefs({
|
|
139
|
+
configPath,
|
|
140
|
+
externalRefResolver,
|
|
141
|
+
});
|
|
126
142
|
if (typeof processRawConfig === 'function') {
|
|
127
|
-
await processRawConfig(
|
|
143
|
+
await processRawConfig(document, resolvedRefMap);
|
|
128
144
|
}
|
|
129
|
-
|
|
145
|
+
const bundledConfig = await bundleConfig(document, resolvedRefMap);
|
|
146
|
+
return transformConfig(bundledConfig);
|
|
130
147
|
} catch (e) {
|
|
131
148
|
if (e instanceof ConfigValidationError) {
|
|
132
149
|
throw e;
|
package/src/config/minimal.ts
CHANGED
|
@@ -66,6 +66,7 @@ const minimal: PluginStyleguideConfig<'built-in'> = {
|
|
|
66
66
|
'request-mime-type': 'off',
|
|
67
67
|
'response-contains-property': 'off',
|
|
68
68
|
'response-mime-type': 'off',
|
|
69
|
+
'array-parameter-serialization': 'off',
|
|
69
70
|
},
|
|
70
71
|
oas3_1Rules: {
|
|
71
72
|
'no-invalid-media-type-examples': 'warn',
|
|
@@ -83,6 +84,7 @@ const minimal: PluginStyleguideConfig<'built-in'> = {
|
|
|
83
84
|
'request-mime-type': 'off',
|
|
84
85
|
'response-contains-property': 'off',
|
|
85
86
|
'response-mime-type': 'off',
|
|
87
|
+
'array-parameter-serialization': 'off',
|
|
86
88
|
},
|
|
87
89
|
async2Rules: {
|
|
88
90
|
'channels-kebab-case': 'off',
|
|
@@ -66,6 +66,7 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = {
|
|
|
66
66
|
'request-mime-type': 'off',
|
|
67
67
|
'response-contains-property': 'off',
|
|
68
68
|
'response-mime-type': 'off',
|
|
69
|
+
'array-parameter-serialization': 'off',
|
|
69
70
|
},
|
|
70
71
|
oas3_1Rules: {
|
|
71
72
|
'no-invalid-media-type-examples': 'error',
|
|
@@ -83,6 +84,7 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = {
|
|
|
83
84
|
'request-mime-type': 'off',
|
|
84
85
|
'response-contains-property': 'off',
|
|
85
86
|
'response-mime-type': 'off',
|
|
87
|
+
'array-parameter-serialization': 'off',
|
|
86
88
|
},
|
|
87
89
|
async2Rules: {
|
|
88
90
|
'channels-kebab-case': 'off',
|
|
@@ -66,6 +66,7 @@ const recommended: PluginStyleguideConfig<'built-in'> = {
|
|
|
66
66
|
'request-mime-type': 'off',
|
|
67
67
|
'response-contains-property': 'off',
|
|
68
68
|
'response-mime-type': 'off',
|
|
69
|
+
'array-parameter-serialization': 'off',
|
|
69
70
|
},
|
|
70
71
|
oas3_1Rules: {
|
|
71
72
|
'no-invalid-media-type-examples': 'warn',
|
|
@@ -83,6 +84,7 @@ const recommended: PluginStyleguideConfig<'built-in'> = {
|
|
|
83
84
|
'request-mime-type': 'off',
|
|
84
85
|
'response-contains-property': 'off',
|
|
85
86
|
'response-mime-type': 'off',
|
|
87
|
+
'array-parameter-serialization': 'off',
|
|
86
88
|
},
|
|
87
89
|
async2Rules: {
|
|
88
90
|
'channels-kebab-case': 'off',
|
package/src/lint.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { BaseResolver, resolveDocument,
|
|
1
|
+
import { BaseResolver, resolveDocument, makeDocumentFromString } from './resolve';
|
|
2
2
|
import { normalizeVisitors } from './visitors';
|
|
3
|
-
import {
|
|
4
|
-
import { ProblemSeverity, WalkContext, walkDocument } from './walk';
|
|
3
|
+
import { walkDocument } from './walk';
|
|
5
4
|
import { StyleguideConfig, Config, initRules, defaultPlugin, resolvePlugins } from './config';
|
|
6
5
|
import { normalizeTypes } from './types';
|
|
7
6
|
import { releaseAjvInstance } from './rules/ajv';
|
|
8
7
|
import { SpecVersion, getMajorSpecVersion, detectSpec, getTypes } from './oas-types';
|
|
9
8
|
import { ConfigTypes } from './types/redocly-yaml';
|
|
10
9
|
import { Spec } from './rules/common/spec';
|
|
10
|
+
import { NoUnresolvedRefs } from './rules/no-unresolved-refs';
|
|
11
|
+
|
|
12
|
+
import type { Document, ResolvedRefMap } from './resolve';
|
|
13
|
+
import type { ProblemSeverity, WalkContext } from './walk';
|
|
14
|
+
import type { NodeType } from './types';
|
|
15
|
+
import type { NestedVisitObject, Oas3Visitor, RuleInstanceConfig } from './visitors';
|
|
11
16
|
|
|
12
17
|
export async function lint(opts: {
|
|
13
18
|
ref: string;
|
|
@@ -102,8 +107,13 @@ export async function lintDocument(opts: {
|
|
|
102
107
|
return ctx.problems.map((problem) => config.addProblemToIgnore(problem));
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
export async function lintConfig(opts: {
|
|
106
|
-
|
|
110
|
+
export async function lintConfig(opts: {
|
|
111
|
+
document: Document;
|
|
112
|
+
resolvedRefMap?: ResolvedRefMap;
|
|
113
|
+
severity?: ProblemSeverity;
|
|
114
|
+
externalRefResolver?: BaseResolver;
|
|
115
|
+
}) {
|
|
116
|
+
const { document, severity, externalRefResolver = new BaseResolver() } = opts;
|
|
107
117
|
|
|
108
118
|
const ctx: WalkContext = {
|
|
109
119
|
problems: [],
|
|
@@ -117,21 +127,33 @@ export async function lintConfig(opts: { document: Document; severity?: ProblemS
|
|
|
117
127
|
});
|
|
118
128
|
|
|
119
129
|
const types = normalizeTypes(ConfigTypes, config);
|
|
120
|
-
const rules
|
|
130
|
+
const rules: (RuleInstanceConfig & {
|
|
131
|
+
visitor: NestedVisitObject<unknown, Oas3Visitor | Oas3Visitor[]>;
|
|
132
|
+
})[] = [
|
|
121
133
|
{
|
|
122
134
|
severity: severity || 'error',
|
|
123
135
|
ruleId: 'configuration spec',
|
|
124
136
|
visitor: Spec({ severity: 'error' }),
|
|
125
137
|
},
|
|
138
|
+
{
|
|
139
|
+
severity: severity || 'error',
|
|
140
|
+
ruleId: 'configuration no-unresolved-refs',
|
|
141
|
+
visitor: NoUnresolvedRefs({ severity: 'error' }),
|
|
142
|
+
},
|
|
126
143
|
];
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
144
|
+
const normalizedVisitors = normalizeVisitors(rules, types);
|
|
145
|
+
const resolvedRefMap =
|
|
146
|
+
opts.resolvedRefMap ||
|
|
147
|
+
(await resolveDocument({
|
|
148
|
+
rootDocument: document,
|
|
149
|
+
rootType: types.ConfigRoot,
|
|
150
|
+
externalRefResolver,
|
|
151
|
+
}));
|
|
130
152
|
walkDocument({
|
|
131
153
|
document,
|
|
132
154
|
rootType: types.ConfigRoot,
|
|
133
155
|
normalizedVisitors,
|
|
134
|
-
resolvedRefMap
|
|
156
|
+
resolvedRefMap,
|
|
135
157
|
ctx,
|
|
136
158
|
});
|
|
137
159
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { outdent } from 'outdent';
|
|
2
|
+
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
|
|
3
|
+
import { lintDocument } from '../../../lint';
|
|
4
|
+
import { BaseResolver } from '../../../resolve';
|
|
5
|
+
|
|
6
|
+
describe('oas3 array-parameter-serialization', () => {
|
|
7
|
+
it('should report on array parameter without style and explode', async () => {
|
|
8
|
+
const document = parseYamlToDocument(
|
|
9
|
+
outdent`
|
|
10
|
+
openapi: 3.0.0
|
|
11
|
+
paths:
|
|
12
|
+
'/test':
|
|
13
|
+
parameters:
|
|
14
|
+
- name: a
|
|
15
|
+
in: query
|
|
16
|
+
schema:
|
|
17
|
+
type: array
|
|
18
|
+
items:
|
|
19
|
+
type: string
|
|
20
|
+
- name: b
|
|
21
|
+
in: header
|
|
22
|
+
schema:
|
|
23
|
+
type: array
|
|
24
|
+
items:
|
|
25
|
+
type: string
|
|
26
|
+
`,
|
|
27
|
+
'foobar.yaml'
|
|
28
|
+
);
|
|
29
|
+
const results = await lintDocument({
|
|
30
|
+
externalRefResolver: new BaseResolver(),
|
|
31
|
+
document,
|
|
32
|
+
config: await makeConfig({
|
|
33
|
+
'array-parameter-serialization': { severity: 'error', in: ['query'] },
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
37
|
+
[
|
|
38
|
+
{
|
|
39
|
+
"location": [
|
|
40
|
+
{
|
|
41
|
+
"pointer": "#/paths/~1test/parameters/0",
|
|
42
|
+
"reportOnKey": false,
|
|
43
|
+
"source": "foobar.yaml",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
"message": "Parameter \`a\` should have \`style\` and \`explode \` fields",
|
|
47
|
+
"ruleId": "array-parameter-serialization",
|
|
48
|
+
"severity": "error",
|
|
49
|
+
"suggest": [],
|
|
50
|
+
},
|
|
51
|
+
]
|
|
52
|
+
`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should report on array parameter with style but without explode', async () => {
|
|
56
|
+
const document = parseYamlToDocument(
|
|
57
|
+
outdent`
|
|
58
|
+
openapi: 3.0.0
|
|
59
|
+
paths:
|
|
60
|
+
'/test':
|
|
61
|
+
parameters:
|
|
62
|
+
- name: a
|
|
63
|
+
in: query
|
|
64
|
+
style: form
|
|
65
|
+
schema:
|
|
66
|
+
type: array
|
|
67
|
+
items:
|
|
68
|
+
type: string
|
|
69
|
+
`,
|
|
70
|
+
'foobar.yaml'
|
|
71
|
+
);
|
|
72
|
+
const results = await lintDocument({
|
|
73
|
+
externalRefResolver: new BaseResolver(),
|
|
74
|
+
document,
|
|
75
|
+
config: await makeConfig({
|
|
76
|
+
'array-parameter-serialization': { severity: 'error', in: ['query'] },
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
80
|
+
[
|
|
81
|
+
{
|
|
82
|
+
"location": [
|
|
83
|
+
{
|
|
84
|
+
"pointer": "#/paths/~1test/parameters/0",
|
|
85
|
+
"reportOnKey": false,
|
|
86
|
+
"source": "foobar.yaml",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
"message": "Parameter \`a\` should have \`style\` and \`explode \` fields",
|
|
90
|
+
"ruleId": "array-parameter-serialization",
|
|
91
|
+
"severity": "error",
|
|
92
|
+
"suggest": [],
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should report on parameter without type but with items', async () => {
|
|
99
|
+
const document = parseYamlToDocument(
|
|
100
|
+
outdent`
|
|
101
|
+
openapi: 3.1.0
|
|
102
|
+
paths:
|
|
103
|
+
/test:
|
|
104
|
+
parameters:
|
|
105
|
+
- name: test only type, path level
|
|
106
|
+
in: query
|
|
107
|
+
schema:
|
|
108
|
+
type: array # no items
|
|
109
|
+
get:
|
|
110
|
+
parameters:
|
|
111
|
+
- name: test only items, operation level
|
|
112
|
+
in: header
|
|
113
|
+
items: # no type
|
|
114
|
+
type: string
|
|
115
|
+
components:
|
|
116
|
+
parameters:
|
|
117
|
+
TestParameter:
|
|
118
|
+
in: cookie
|
|
119
|
+
name: test only prefixItems, components level
|
|
120
|
+
prefixItems: # no type or items
|
|
121
|
+
- type: number
|
|
122
|
+
`,
|
|
123
|
+
'foobar.yaml'
|
|
124
|
+
);
|
|
125
|
+
const results = await lintDocument({
|
|
126
|
+
externalRefResolver: new BaseResolver(),
|
|
127
|
+
document,
|
|
128
|
+
config: await makeConfig({
|
|
129
|
+
'array-parameter-serialization': { severity: 'error', in: ['query'] },
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
133
|
+
[
|
|
134
|
+
{
|
|
135
|
+
"location": [
|
|
136
|
+
{
|
|
137
|
+
"pointer": "#/paths/~1test/parameters/0",
|
|
138
|
+
"reportOnKey": false,
|
|
139
|
+
"source": "foobar.yaml",
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
"message": "Parameter \`test only type, path level\` should have \`style\` and \`explode \` fields",
|
|
143
|
+
"ruleId": "array-parameter-serialization",
|
|
144
|
+
"severity": "error",
|
|
145
|
+
"suggest": [],
|
|
146
|
+
},
|
|
147
|
+
]
|
|
148
|
+
`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should not report on array parameter with style and explode', async () => {
|
|
152
|
+
const document = parseYamlToDocument(
|
|
153
|
+
outdent`
|
|
154
|
+
openapi: 3.0.0
|
|
155
|
+
paths:
|
|
156
|
+
'/test':
|
|
157
|
+
parameters:
|
|
158
|
+
- name: a
|
|
159
|
+
in: query
|
|
160
|
+
style: form
|
|
161
|
+
explode: false
|
|
162
|
+
schema:
|
|
163
|
+
type: array
|
|
164
|
+
items:
|
|
165
|
+
type: string
|
|
166
|
+
`,
|
|
167
|
+
'foobar.yaml'
|
|
168
|
+
);
|
|
169
|
+
const results = await lintDocument({
|
|
170
|
+
externalRefResolver: new BaseResolver(),
|
|
171
|
+
document,
|
|
172
|
+
config: await makeConfig({
|
|
173
|
+
'array-parameter-serialization': { severity: 'error', in: ['query'] },
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should not report non-array parameter without style and explode', async () => {
|
|
180
|
+
const document = parseYamlToDocument(
|
|
181
|
+
outdent`
|
|
182
|
+
openapi: 3.0.0
|
|
183
|
+
paths:
|
|
184
|
+
'/test':
|
|
185
|
+
parameters:
|
|
186
|
+
- name: a
|
|
187
|
+
in: query
|
|
188
|
+
schema:
|
|
189
|
+
type: string
|
|
190
|
+
`,
|
|
191
|
+
'foobar.yaml'
|
|
192
|
+
);
|
|
193
|
+
const results = await lintDocument({
|
|
194
|
+
externalRefResolver: new BaseResolver(),
|
|
195
|
+
document,
|
|
196
|
+
config: await makeConfig({
|
|
197
|
+
'array-parameter-serialization': { severity: 'error', in: ['query'] },
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
200
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should report all array parameter without style and explode if property 'in' not defined ", async () => {
|
|
204
|
+
const document = parseYamlToDocument(
|
|
205
|
+
outdent`
|
|
206
|
+
openapi: 3.0.0
|
|
207
|
+
paths:
|
|
208
|
+
'/test':
|
|
209
|
+
parameters:
|
|
210
|
+
- name: a
|
|
211
|
+
in: query
|
|
212
|
+
schema:
|
|
213
|
+
type: array
|
|
214
|
+
items:
|
|
215
|
+
type: string
|
|
216
|
+
- name: b
|
|
217
|
+
in: header
|
|
218
|
+
schema:
|
|
219
|
+
type: array
|
|
220
|
+
items:
|
|
221
|
+
type: string
|
|
222
|
+
`,
|
|
223
|
+
'foobar.yaml'
|
|
224
|
+
);
|
|
225
|
+
const results = await lintDocument({
|
|
226
|
+
externalRefResolver: new BaseResolver(),
|
|
227
|
+
document,
|
|
228
|
+
config: await makeConfig({
|
|
229
|
+
'array-parameter-serialization': { severity: 'error' },
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
233
|
+
[
|
|
234
|
+
{
|
|
235
|
+
"location": [
|
|
236
|
+
{
|
|
237
|
+
"pointer": "#/paths/~1test/parameters/0",
|
|
238
|
+
"reportOnKey": false,
|
|
239
|
+
"source": "foobar.yaml",
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
"message": "Parameter \`a\` should have \`style\` and \`explode \` fields",
|
|
243
|
+
"ruleId": "array-parameter-serialization",
|
|
244
|
+
"severity": "error",
|
|
245
|
+
"suggest": [],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"location": [
|
|
249
|
+
{
|
|
250
|
+
"pointer": "#/paths/~1test/parameters/1",
|
|
251
|
+
"reportOnKey": false,
|
|
252
|
+
"source": "foobar.yaml",
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
"message": "Parameter \`b\` should have \`style\` and \`explode \` fields",
|
|
256
|
+
"ruleId": "array-parameter-serialization",
|
|
257
|
+
"severity": "error",
|
|
258
|
+
"suggest": [],
|
|
259
|
+
},
|
|
260
|
+
]
|
|
261
|
+
`);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Oas3Rule, Oas3Visitor } from '../../visitors';
|
|
2
|
+
import { isRef } from '../../ref-utils';
|
|
3
|
+
import { Oas3_1Schema, Oas3Parameter } from '../../typings/openapi';
|
|
4
|
+
|
|
5
|
+
export type ArrayParameterSerializationOptions = {
|
|
6
|
+
in?: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const ArrayParameterSerialization: Oas3Rule = (
|
|
10
|
+
options: ArrayParameterSerializationOptions
|
|
11
|
+
): Oas3Visitor => {
|
|
12
|
+
return {
|
|
13
|
+
Parameter: {
|
|
14
|
+
leave(node: Oas3Parameter, ctx) {
|
|
15
|
+
if (!node.schema) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const schema = isRef(node.schema)
|
|
19
|
+
? ctx.resolve<Oas3_1Schema>(node.schema).node
|
|
20
|
+
: (node.schema as Oas3_1Schema);
|
|
21
|
+
|
|
22
|
+
if (schema && shouldReportMissingStyleAndExplode(node, schema, options)) {
|
|
23
|
+
ctx.report({
|
|
24
|
+
message: `Parameter \`${node.name}\` should have \`style\` and \`explode \` fields`,
|
|
25
|
+
location: ctx.location,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function shouldReportMissingStyleAndExplode(
|
|
34
|
+
node: Oas3Parameter,
|
|
35
|
+
schema: Oas3_1Schema,
|
|
36
|
+
options: ArrayParameterSerializationOptions
|
|
37
|
+
) {
|
|
38
|
+
return (
|
|
39
|
+
(schema.type === 'array' || schema.items || schema.prefixItems) &&
|
|
40
|
+
(node.style === undefined || node.explode === undefined) &&
|
|
41
|
+
(!options.in || (node.in && options.in?.includes(node.in)))
|
|
42
|
+
);
|
|
43
|
+
}
|
package/src/rules/oas3/index.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { Operation4xxProblemDetailsRfc7807 } from './operation-4xx-problem-detai
|
|
|
52
52
|
import { RequiredStringPropertyMissingMinLength } from '../common/required-string-property-missing-min-length';
|
|
53
53
|
import { SpecStrictRefs } from '../common/spec-strict-refs';
|
|
54
54
|
import { ComponentNameUnique } from './component-name-unique';
|
|
55
|
+
import { ArrayParameterSerialization } from './array-parameter-serialization';
|
|
55
56
|
|
|
56
57
|
export const rules: Oas3RuleSet<'built-in'> = {
|
|
57
58
|
spec: Spec,
|
|
@@ -108,6 +109,7 @@ export const rules: Oas3RuleSet<'built-in'> = {
|
|
|
108
109
|
'required-string-property-missing-min-length': RequiredStringPropertyMissingMinLength,
|
|
109
110
|
'spec-strict-refs': SpecStrictRefs,
|
|
110
111
|
'component-name-unique': ComponentNameUnique,
|
|
112
|
+
'array-parameter-serialization': ArrayParameterSerialization,
|
|
111
113
|
};
|
|
112
114
|
|
|
113
115
|
export const preprocessors = {};
|