@redocly/openapi-core 1.0.0-beta.112 → 1.0.0-beta.113
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/lib/config/config-resolvers.js +3 -6
- package/lib/config/config.d.ts +4 -10
- package/lib/config/config.js +1 -1
- package/lib/config/load.js +6 -6
- package/lib/config/rules.d.ts +6 -3
- package/lib/config/rules.js +3 -2
- package/lib/config/types.d.ts +3 -0
- package/lib/ref-utils.d.ts +1 -0
- package/lib/ref-utils.js +5 -1
- package/lib/resolve.js +19 -0
- package/lib/rules/common/spec.js +6 -0
- package/lib/rules/utils.js +3 -0
- package/lib/types/oas2.js +11 -7
- package/lib/types/oas3.js +15 -10
- package/lib/types/oas3_1.js +1 -0
- package/lib/types/redocly-yaml.js +5 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +7 -1
- package/package.json +1 -1
- package/src/__tests__/bundle.test.ts +46 -0
- package/src/benchmark/benches/rebilly.yaml +36 -28
- package/src/config/__tests__/config-resolvers.test.ts +1 -2
- package/src/config/__tests__/fixtures/load-redocly.yaml +0 -2
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml +0 -1
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml +0 -1
- package/src/config/config-resolvers.ts +2 -12
- package/src/config/config.ts +6 -5
- package/src/config/load.ts +12 -5
- package/src/config/rules.ts +11 -3
- package/src/config/types.ts +2 -0
- package/src/ref-utils.ts +4 -0
- package/src/resolve.ts +25 -3
- package/src/rules/common/__tests__/spec.test.ts +170 -0
- package/src/rules/common/spec.ts +7 -0
- package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +32 -0
- package/src/rules/utils.ts +4 -0
- package/src/types/oas2.ts +11 -7
- package/src/types/oas3.ts +15 -10
- package/src/types/oas3_1.ts +1 -0
- package/src/types/redocly-yaml.ts +5 -0
- package/src/utils.ts +6 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -342,7 +342,7 @@ describe('resolveApis', () => {
|
|
|
342
342
|
});
|
|
343
343
|
|
|
344
344
|
describe('resolveConfig', () => {
|
|
345
|
-
it('should add recommended to top level by default', async () => {
|
|
345
|
+
it('should NOT add recommended to top level by default IF there is a config file', async () => {
|
|
346
346
|
const rawConfig: RawConfig = {
|
|
347
347
|
apis: {
|
|
348
348
|
petstore: {
|
|
@@ -373,7 +373,6 @@ describe('resolveConfig', () => {
|
|
|
373
373
|
expect(apis['petstore'].styleguide.pluginPaths!.map(removeAbsolutePath)).toEqual([]);
|
|
374
374
|
|
|
375
375
|
expect(apis['petstore'].styleguide.rules).toEqual({
|
|
376
|
-
...(await recommendedStyleguidePreset).rules,
|
|
377
376
|
'operation-2xx-response': 'warn',
|
|
378
377
|
'operation-4xx-response': 'error',
|
|
379
378
|
});
|
|
@@ -41,25 +41,15 @@ export async function resolveConfig(rawConfig: RawConfig, configPath?: string):
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const resolver = new BaseResolver(getResolveConfig(rawConfig.resolve));
|
|
44
|
-
const configExtends = rawConfig?.styleguide?.extends ?? ['recommended'];
|
|
45
|
-
const recommendedFallback = !rawConfig?.styleguide?.extends;
|
|
46
|
-
const styleguideConfig = {
|
|
47
|
-
...rawConfig?.styleguide,
|
|
48
|
-
extends: configExtends,
|
|
49
|
-
recommendedFallback,
|
|
50
|
-
};
|
|
51
44
|
|
|
52
45
|
const apis = await resolveApis({
|
|
53
|
-
rawConfig
|
|
54
|
-
...rawConfig,
|
|
55
|
-
styleguide: styleguideConfig,
|
|
56
|
-
},
|
|
46
|
+
rawConfig,
|
|
57
47
|
configPath,
|
|
58
48
|
resolver,
|
|
59
49
|
});
|
|
60
50
|
|
|
61
51
|
const styleguide = await resolveStyleguideConfig({
|
|
62
|
-
styleguideConfig,
|
|
52
|
+
styleguideConfig: rawConfig.styleguide,
|
|
63
53
|
configPath,
|
|
64
54
|
resolver,
|
|
65
55
|
});
|
package/src/config/config.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { parseYaml, stringifyYaml } from '../js-yaml';
|
|
|
4
4
|
import { slash, doesYamlFileExist } from '../utils';
|
|
5
5
|
import { NormalizedProblem } from '../walk';
|
|
6
6
|
import { OasVersion, OasMajorVersion, Oas2RuleSet, Oas3RuleSet } from '../oas-types';
|
|
7
|
-
import { env } from '../env';
|
|
7
|
+
import { isBrowser, env } from '../env';
|
|
8
8
|
|
|
9
9
|
import type { NodeType } from '../types';
|
|
10
10
|
import type {
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
ResolvedConfig,
|
|
18
18
|
ResolvedStyleguideConfig,
|
|
19
19
|
RuleConfig,
|
|
20
|
+
RuleSettings,
|
|
20
21
|
} from './types';
|
|
21
22
|
import { getResolveConfig } from './utils';
|
|
22
23
|
|
|
@@ -50,7 +51,7 @@ function getIgnoreFilePath(configFile?: string): string | undefined {
|
|
|
50
51
|
? path.join(path.dirname(configFile), IGNORE_FILE)
|
|
51
52
|
: path.join(configFile, IGNORE_FILE);
|
|
52
53
|
} else {
|
|
53
|
-
return
|
|
54
|
+
return isBrowser ? undefined : path.join(process.cwd(), IGNORE_FILE);
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -183,7 +184,7 @@ export class StyleguideConfig {
|
|
|
183
184
|
return extendedTypes;
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
getRuleSettings(ruleId: string, oasVersion: OasVersion) {
|
|
187
|
+
getRuleSettings(ruleId: string, oasVersion: OasVersion): RuleSettings {
|
|
187
188
|
this._usedRules.add(ruleId);
|
|
188
189
|
this._usedVersions.add(oasVersion);
|
|
189
190
|
const settings = this.rules[oasVersion][ruleId] || 'off';
|
|
@@ -196,7 +197,7 @@ export class StyleguideConfig {
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
getPreprocessorSettings(ruleId: string, oasVersion: OasVersion) {
|
|
200
|
+
getPreprocessorSettings(ruleId: string, oasVersion: OasVersion): RuleSettings {
|
|
200
201
|
this._usedRules.add(ruleId);
|
|
201
202
|
this._usedVersions.add(oasVersion);
|
|
202
203
|
|
|
@@ -210,7 +211,7 @@ export class StyleguideConfig {
|
|
|
210
211
|
}
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
getDecoratorSettings(ruleId: string, oasVersion: OasVersion) {
|
|
214
|
+
getDecoratorSettings(ruleId: string, oasVersion: OasVersion): RuleSettings {
|
|
214
215
|
this._usedRules.add(ruleId);
|
|
215
216
|
this._usedVersions.add(oasVersion);
|
|
216
217
|
const settings = this.decorators[oasVersion][ruleId] || 'off';
|
package/src/config/load.ts
CHANGED
|
@@ -15,18 +15,21 @@ async function addConfigMetadata({
|
|
|
15
15
|
customExtends,
|
|
16
16
|
configPath,
|
|
17
17
|
tokens,
|
|
18
|
+
files,
|
|
19
|
+
region,
|
|
18
20
|
}: {
|
|
19
21
|
rawConfig: RawConfig;
|
|
20
22
|
customExtends?: string[];
|
|
21
23
|
configPath?: string;
|
|
22
24
|
tokens?: RegionalTokenWithValidity[];
|
|
25
|
+
files?: string[];
|
|
26
|
+
region?: Region;
|
|
23
27
|
}): Promise<Config> {
|
|
24
28
|
if (customExtends !== undefined) {
|
|
25
29
|
rawConfig.styleguide = rawConfig.styleguide || {};
|
|
26
30
|
rawConfig.styleguide.extends = customExtends;
|
|
27
31
|
} else if (isEmptyObject(rawConfig)) {
|
|
28
|
-
|
|
29
|
-
// rawConfig.styleguide = { extends: ['recommended'], recommendedFallback: true };
|
|
32
|
+
rawConfig.styleguide = { extends: ['recommended'], recommendedFallback: true };
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
if (tokens?.length) {
|
|
@@ -58,7 +61,10 @@ async function addConfigMetadata({
|
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
return resolveConfig(
|
|
64
|
+
return resolveConfig(
|
|
65
|
+
{ ...rawConfig, files: files ?? rawConfig.files, region: region ?? rawConfig.region },
|
|
66
|
+
configPath
|
|
67
|
+
);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
export async function loadConfig(
|
|
@@ -71,8 +77,7 @@ export async function loadConfig(
|
|
|
71
77
|
} = {}
|
|
72
78
|
): Promise<Config> {
|
|
73
79
|
const { configPath = findConfig(), customExtends, processRawConfig, files, region } = options;
|
|
74
|
-
const
|
|
75
|
-
const rawConfig = { ...config, files: files ?? config.files, region: region ?? config.region };
|
|
80
|
+
const rawConfig = await getConfig(configPath, processRawConfig);
|
|
76
81
|
|
|
77
82
|
const redoclyClient = new RedoclyClient();
|
|
78
83
|
const tokens = await redoclyClient.getTokens();
|
|
@@ -82,6 +87,8 @@ export async function loadConfig(
|
|
|
82
87
|
customExtends,
|
|
83
88
|
configPath,
|
|
84
89
|
tokens,
|
|
90
|
+
files,
|
|
91
|
+
region,
|
|
85
92
|
});
|
|
86
93
|
}
|
|
87
94
|
|
package/src/config/rules.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { RuleSet, OasVersion } from '../oas-types';
|
|
2
2
|
import { StyleguideConfig } from './config';
|
|
3
3
|
import { isDefined } from '../utils';
|
|
4
|
+
import type { ProblemSeverity } from '../walk';
|
|
5
|
+
|
|
6
|
+
type InitializedRule = {
|
|
7
|
+
severity: ProblemSeverity;
|
|
8
|
+
ruleId: string;
|
|
9
|
+
visitor: any;
|
|
10
|
+
};
|
|
4
11
|
|
|
5
12
|
export function initRules<T extends Function, P extends RuleSet<T>>(
|
|
6
13
|
rules: P[],
|
|
7
14
|
config: StyleguideConfig,
|
|
8
15
|
type: 'rules' | 'preprocessors' | 'decorators',
|
|
9
16
|
oasVersion: OasVersion
|
|
10
|
-
) {
|
|
17
|
+
): InitializedRule[] {
|
|
11
18
|
return rules
|
|
12
19
|
.flatMap((ruleset) =>
|
|
13
20
|
Object.keys(ruleset).map((ruleId) => {
|
|
@@ -23,19 +30,20 @@ export function initRules<T extends Function, P extends RuleSet<T>>(
|
|
|
23
30
|
if (ruleSettings.severity === 'off') {
|
|
24
31
|
return undefined;
|
|
25
32
|
}
|
|
33
|
+
const severity: ProblemSeverity = ruleSettings.severity;
|
|
26
34
|
|
|
27
35
|
const visitors = rule(ruleSettings);
|
|
28
36
|
|
|
29
37
|
if (Array.isArray(visitors)) {
|
|
30
38
|
return visitors.map((visitor: any) => ({
|
|
31
|
-
severity
|
|
39
|
+
severity,
|
|
32
40
|
ruleId,
|
|
33
41
|
visitor: visitor,
|
|
34
42
|
}));
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
return {
|
|
38
|
-
severity
|
|
46
|
+
severity,
|
|
39
47
|
ruleId,
|
|
40
48
|
visitor: visitors, // note: actually it is only one visitor object
|
|
41
49
|
};
|
package/src/config/types.ts
CHANGED
package/src/ref-utils.ts
CHANGED
package/src/resolve.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { OasRef } from './typings/openapi';
|
|
4
|
-
import { isRef, joinPointer, escapePointer, parseRef, isAbsoluteUrl } from './ref-utils';
|
|
4
|
+
import { isRef, joinPointer, escapePointer, parseRef, isAbsoluteUrl, isAnchor } from './ref-utils';
|
|
5
5
|
import type { YAMLNode, LoadOptions } from 'yaml-ast-parser';
|
|
6
6
|
import { NormalizedNodeType, isNamedType } from './types';
|
|
7
|
-
import { readFileFromUrl, parseYaml } from './utils';
|
|
7
|
+
import { readFileFromUrl, parseYaml, nextTick } from './utils';
|
|
8
8
|
import { ResolveConfig } from './config/types';
|
|
9
9
|
|
|
10
10
|
export type CollectedRefs = Map<string /* absoluteFilePath */, Document>;
|
|
@@ -237,6 +237,7 @@ export async function resolveDocument(opts: {
|
|
|
237
237
|
type: any
|
|
238
238
|
) {
|
|
239
239
|
const rootNodeDocAbsoluteRef = rootNodeDocument.source.absoluteRef;
|
|
240
|
+
const anchorRefsMap: Map<string, any> = new Map();
|
|
240
241
|
|
|
241
242
|
walk(rootNode, type, rootNodeDocAbsoluteRef + rootNodePointer);
|
|
242
243
|
|
|
@@ -252,6 +253,11 @@ export async function resolveDocument(opts: {
|
|
|
252
253
|
|
|
253
254
|
seedNodes.add(nodeId);
|
|
254
255
|
|
|
256
|
+
const [_, anchor] = Object.entries(node).find(([key]) => key === '$anchor') || [];
|
|
257
|
+
if (anchor) {
|
|
258
|
+
anchorRefsMap.set(`#${anchor}`, node);
|
|
259
|
+
}
|
|
260
|
+
|
|
255
261
|
if (Array.isArray(node)) {
|
|
256
262
|
const itemsType = type.items;
|
|
257
263
|
// we continue resolving unknown types, but stop early on known scalars
|
|
@@ -313,6 +319,22 @@ export async function resolveDocument(opts: {
|
|
|
313
319
|
if (hasRef(refStack.prev, ref)) {
|
|
314
320
|
throw new Error('Self-referencing circular pointer');
|
|
315
321
|
}
|
|
322
|
+
|
|
323
|
+
if (isAnchor(ref.$ref)) {
|
|
324
|
+
// Wait for all anchors in the document to be collected firstly.
|
|
325
|
+
await nextTick();
|
|
326
|
+
const resolvedRef: ResolvedRef = {
|
|
327
|
+
resolved: true,
|
|
328
|
+
isRemote: false,
|
|
329
|
+
node: anchorRefsMap.get(ref.$ref),
|
|
330
|
+
document,
|
|
331
|
+
nodePointer: ref.$ref,
|
|
332
|
+
};
|
|
333
|
+
const refId = makeRefId(document.source.absoluteRef, ref.$ref);
|
|
334
|
+
resolvedRefMap.set(refId, resolvedRef);
|
|
335
|
+
return resolvedRef;
|
|
336
|
+
}
|
|
337
|
+
|
|
316
338
|
const { uri, pointer } = parseRef(ref.$ref);
|
|
317
339
|
const isRemote = uri !== null;
|
|
318
340
|
let targetDoc: Document;
|
|
@@ -336,7 +358,7 @@ export async function resolveDocument(opts: {
|
|
|
336
358
|
}
|
|
337
359
|
|
|
338
360
|
let resolvedRef: ResolvedRef = {
|
|
339
|
-
resolved: true
|
|
361
|
+
resolved: true,
|
|
340
362
|
document: targetDoc,
|
|
341
363
|
isRemote,
|
|
342
364
|
node: document.parsed,
|
|
@@ -138,6 +138,176 @@ describe('Oas3 spec', () => {
|
|
|
138
138
|
]
|
|
139
139
|
`);
|
|
140
140
|
});
|
|
141
|
+
|
|
142
|
+
it('should report on nullable without type', async () => {
|
|
143
|
+
const document = parseYamlToDocument(
|
|
144
|
+
outdent`
|
|
145
|
+
openapi: 3.0.0
|
|
146
|
+
components:
|
|
147
|
+
requestBodies:
|
|
148
|
+
TestRequestBody:
|
|
149
|
+
content:
|
|
150
|
+
application/json:
|
|
151
|
+
schema:
|
|
152
|
+
nullable: true
|
|
153
|
+
`,
|
|
154
|
+
'foobar.yaml'
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const results = await lintDocument({
|
|
158
|
+
externalRefResolver: new BaseResolver(),
|
|
159
|
+
document,
|
|
160
|
+
config: await makeConfig({ spec: 'error' }),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
164
|
+
Array [
|
|
165
|
+
Object {
|
|
166
|
+
"from": undefined,
|
|
167
|
+
"location": Array [
|
|
168
|
+
Object {
|
|
169
|
+
"pointer": "#/",
|
|
170
|
+
"reportOnKey": true,
|
|
171
|
+
"source": "foobar.yaml",
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
"message": "The field \`paths\` must be present on this level.",
|
|
175
|
+
"ruleId": "spec",
|
|
176
|
+
"severity": "error",
|
|
177
|
+
"suggest": Array [],
|
|
178
|
+
},
|
|
179
|
+
Object {
|
|
180
|
+
"from": undefined,
|
|
181
|
+
"location": Array [
|
|
182
|
+
Object {
|
|
183
|
+
"pointer": "#/",
|
|
184
|
+
"reportOnKey": true,
|
|
185
|
+
"source": "foobar.yaml",
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
"message": "The field \`info\` must be present on this level.",
|
|
189
|
+
"ruleId": "spec",
|
|
190
|
+
"severity": "error",
|
|
191
|
+
"suggest": Array [],
|
|
192
|
+
},
|
|
193
|
+
Object {
|
|
194
|
+
"location": Array [
|
|
195
|
+
Object {
|
|
196
|
+
"pointer": "#/components/requestBodies/TestRequestBody/content/application~1json/schema/nullable",
|
|
197
|
+
"reportOnKey": false,
|
|
198
|
+
"source": "foobar.yaml",
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
"message": "The \`type\` field must be defined when the \`nullable\` field is used.",
|
|
202
|
+
"ruleId": "spec",
|
|
203
|
+
"severity": "error",
|
|
204
|
+
"suggest": Array [],
|
|
205
|
+
},
|
|
206
|
+
]
|
|
207
|
+
`);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should report on nullable with type defined in allOf', async () => {
|
|
211
|
+
const document = parseYamlToDocument(
|
|
212
|
+
outdent`
|
|
213
|
+
openapi: 3.0.0
|
|
214
|
+
components:
|
|
215
|
+
requestBodies:
|
|
216
|
+
TestRequestBody:
|
|
217
|
+
content:
|
|
218
|
+
application/json:
|
|
219
|
+
schema:
|
|
220
|
+
nullable: true
|
|
221
|
+
allOf:
|
|
222
|
+
- $ref: "#/components/requestBodies/TestSchema"
|
|
223
|
+
schemas:
|
|
224
|
+
TestSchema:
|
|
225
|
+
title: TestSchema
|
|
226
|
+
type: object
|
|
227
|
+
`,
|
|
228
|
+
'foobar.yaml'
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const results = await lintDocument({
|
|
232
|
+
externalRefResolver: new BaseResolver(),
|
|
233
|
+
document,
|
|
234
|
+
config: await makeConfig({ spec: 'error' }),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
238
|
+
Array [
|
|
239
|
+
Object {
|
|
240
|
+
"from": undefined,
|
|
241
|
+
"location": Array [
|
|
242
|
+
Object {
|
|
243
|
+
"pointer": "#/",
|
|
244
|
+
"reportOnKey": true,
|
|
245
|
+
"source": "foobar.yaml",
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
"message": "The field \`paths\` must be present on this level.",
|
|
249
|
+
"ruleId": "spec",
|
|
250
|
+
"severity": "error",
|
|
251
|
+
"suggest": Array [],
|
|
252
|
+
},
|
|
253
|
+
Object {
|
|
254
|
+
"from": undefined,
|
|
255
|
+
"location": Array [
|
|
256
|
+
Object {
|
|
257
|
+
"pointer": "#/",
|
|
258
|
+
"reportOnKey": true,
|
|
259
|
+
"source": "foobar.yaml",
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
"message": "The field \`info\` must be present on this level.",
|
|
263
|
+
"ruleId": "spec",
|
|
264
|
+
"severity": "error",
|
|
265
|
+
"suggest": Array [],
|
|
266
|
+
},
|
|
267
|
+
Object {
|
|
268
|
+
"location": Array [
|
|
269
|
+
Object {
|
|
270
|
+
"pointer": "#/components/requestBodies/TestRequestBody/content/application~1json/schema/nullable",
|
|
271
|
+
"reportOnKey": false,
|
|
272
|
+
"source": "foobar.yaml",
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
"message": "The \`type\` field must be defined when the \`nullable\` field is used.",
|
|
276
|
+
"ruleId": "spec",
|
|
277
|
+
"severity": "error",
|
|
278
|
+
"suggest": Array [],
|
|
279
|
+
},
|
|
280
|
+
Object {
|
|
281
|
+
"from": undefined,
|
|
282
|
+
"location": Array [
|
|
283
|
+
Object {
|
|
284
|
+
"pointer": "#/components/requestBodies/schemas",
|
|
285
|
+
"reportOnKey": true,
|
|
286
|
+
"source": "foobar.yaml",
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
"message": "The field \`content\` must be present on this level.",
|
|
290
|
+
"ruleId": "spec",
|
|
291
|
+
"severity": "error",
|
|
292
|
+
"suggest": Array [],
|
|
293
|
+
},
|
|
294
|
+
Object {
|
|
295
|
+
"from": undefined,
|
|
296
|
+
"location": Array [
|
|
297
|
+
Object {
|
|
298
|
+
"pointer": "#/components/requestBodies/schemas/TestSchema",
|
|
299
|
+
"reportOnKey": true,
|
|
300
|
+
"source": "foobar.yaml",
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
"message": "Property \`TestSchema\` is not expected here.",
|
|
304
|
+
"ruleId": "spec",
|
|
305
|
+
"severity": "error",
|
|
306
|
+
"suggest": Array [],
|
|
307
|
+
},
|
|
308
|
+
]
|
|
309
|
+
`);
|
|
310
|
+
});
|
|
141
311
|
});
|
|
142
312
|
|
|
143
313
|
describe('Oas3.1 spec', () => {
|
package/src/rules/common/spec.ts
CHANGED
|
@@ -158,6 +158,13 @@ export const OasSpec: Oas3Rule | Oas2Rule = () => {
|
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
+
|
|
162
|
+
if (propName === 'nullable' && !node.type) {
|
|
163
|
+
report({
|
|
164
|
+
message: 'The `type` field must be defined when the `nullable` field is used.',
|
|
165
|
+
location: location.child([propName]),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
161
168
|
}
|
|
162
169
|
},
|
|
163
170
|
};
|
|
@@ -438,4 +438,36 @@ describe('no-invalid-media-type-examples', () => {
|
|
|
438
438
|
]
|
|
439
439
|
`);
|
|
440
440
|
});
|
|
441
|
+
|
|
442
|
+
it('should not report if allOf used with discriminator', async () => {
|
|
443
|
+
const document = parseYamlToDocument(
|
|
444
|
+
outdent`
|
|
445
|
+
openapi: 3.0.0
|
|
446
|
+
paths:
|
|
447
|
+
/pet:
|
|
448
|
+
get:
|
|
449
|
+
responses:
|
|
450
|
+
'200':
|
|
451
|
+
content:
|
|
452
|
+
application/json:
|
|
453
|
+
schema:
|
|
454
|
+
discriminator:
|
|
455
|
+
propertyName: powerSource
|
|
456
|
+
mapping: {}
|
|
457
|
+
allOf: []
|
|
458
|
+
examples:
|
|
459
|
+
first:
|
|
460
|
+
value: {}
|
|
461
|
+
`,
|
|
462
|
+
'foobar.yaml'
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const results = await lintDocument({
|
|
466
|
+
externalRefResolver: new BaseResolver(),
|
|
467
|
+
document,
|
|
468
|
+
config: await makeConfig({ 'no-invalid-media-type-examples': 'error' }),
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
472
|
+
});
|
|
441
473
|
});
|
package/src/rules/utils.ts
CHANGED
|
@@ -118,6 +118,10 @@ export function validateExample(
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
} catch (e) {
|
|
121
|
+
if (e.message === 'discriminator: requires oneOf or anyOf composite keyword') {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
121
125
|
report({
|
|
122
126
|
message: `Example validation errored: ${e.message}.`,
|
|
123
127
|
location: location.child('schema'),
|
package/src/types/oas2.ts
CHANGED
|
@@ -16,8 +16,8 @@ const Root: NodeType = {
|
|
|
16
16
|
parameters: 'NamedParameters',
|
|
17
17
|
responses: 'NamedResponses',
|
|
18
18
|
securityDefinitions: 'NamedSecuritySchemes',
|
|
19
|
-
security:
|
|
20
|
-
tags:
|
|
19
|
+
security: 'SecurityRequirementList',
|
|
20
|
+
tags: 'TagList',
|
|
21
21
|
externalDocs: 'ExternalDocs',
|
|
22
22
|
},
|
|
23
23
|
required: ['swagger', 'paths', 'info'],
|
|
@@ -60,7 +60,7 @@ const Paths: NodeType = {
|
|
|
60
60
|
const PathItem: NodeType = {
|
|
61
61
|
properties: {
|
|
62
62
|
$ref: { type: 'string' }, // TODO: verify special $ref handling for Path Item
|
|
63
|
-
parameters:
|
|
63
|
+
parameters: 'ParameterList',
|
|
64
64
|
|
|
65
65
|
get: 'Operation',
|
|
66
66
|
put: 'Operation',
|
|
@@ -81,13 +81,13 @@ const Operation: NodeType = {
|
|
|
81
81
|
operationId: { type: 'string' },
|
|
82
82
|
consumes: { type: 'array', items: { type: 'string' } },
|
|
83
83
|
produces: { type: 'array', items: { type: 'string' } },
|
|
84
|
-
parameters:
|
|
84
|
+
parameters: 'ParameterList',
|
|
85
85
|
responses: 'Responses',
|
|
86
86
|
schemes: { type: 'array', items: { type: 'string' } },
|
|
87
87
|
deprecated: { type: 'boolean' },
|
|
88
|
-
security:
|
|
89
|
-
'x-codeSamples':
|
|
90
|
-
'x-code-samples':
|
|
88
|
+
security: 'SecurityRequirementList',
|
|
89
|
+
'x-codeSamples': 'XCodeSampleList',
|
|
90
|
+
'x-code-samples': 'XCodeSampleList', // deprecated
|
|
91
91
|
'x-hideTryItPanel': { type: 'boolean' },
|
|
92
92
|
},
|
|
93
93
|
required: ['responses'],
|
|
@@ -371,8 +371,10 @@ const SecurityRequirement: NodeType = {
|
|
|
371
371
|
export const Oas2Types: Record<string, NodeType> = {
|
|
372
372
|
Root,
|
|
373
373
|
Tag,
|
|
374
|
+
TagList: listOf('Tag'),
|
|
374
375
|
ExternalDocs,
|
|
375
376
|
SecurityRequirement,
|
|
377
|
+
SecurityRequirementList: listOf('SecurityRequirement'),
|
|
376
378
|
Info,
|
|
377
379
|
Contact,
|
|
378
380
|
License,
|
|
@@ -380,6 +382,7 @@ export const Oas2Types: Record<string, NodeType> = {
|
|
|
380
382
|
PathItem,
|
|
381
383
|
Parameter,
|
|
382
384
|
ParameterItems,
|
|
385
|
+
ParameterList: listOf('Parameter'),
|
|
383
386
|
Operation,
|
|
384
387
|
Examples,
|
|
385
388
|
Header,
|
|
@@ -394,4 +397,5 @@ export const Oas2Types: Record<string, NodeType> = {
|
|
|
394
397
|
NamedSecuritySchemes: mapOf('SecurityScheme'),
|
|
395
398
|
SecurityScheme,
|
|
396
399
|
XCodeSample,
|
|
400
|
+
XCodeSampleList: listOf('XCodeSample'),
|
|
397
401
|
};
|
package/src/types/oas3.ts
CHANGED
|
@@ -6,9 +6,9 @@ const Root: NodeType = {
|
|
|
6
6
|
properties: {
|
|
7
7
|
openapi: null,
|
|
8
8
|
info: 'Info',
|
|
9
|
-
servers:
|
|
10
|
-
security:
|
|
11
|
-
tags:
|
|
9
|
+
servers: 'ServerList',
|
|
10
|
+
security: 'SecurityRequirementList',
|
|
11
|
+
tags: 'TagList',
|
|
12
12
|
externalDocs: 'ExternalDocs',
|
|
13
13
|
paths: 'Paths',
|
|
14
14
|
components: 'Components',
|
|
@@ -102,8 +102,8 @@ const WebhooksMap: NodeType = {
|
|
|
102
102
|
const PathItem: NodeType = {
|
|
103
103
|
properties: {
|
|
104
104
|
$ref: { type: 'string' }, // TODO: verify special $ref handling for Path Item
|
|
105
|
-
servers:
|
|
106
|
-
parameters:
|
|
105
|
+
servers: 'ServerList',
|
|
106
|
+
parameters: 'ParameterList',
|
|
107
107
|
summary: { type: 'string' },
|
|
108
108
|
description: { type: 'string' },
|
|
109
109
|
get: 'Operation',
|
|
@@ -149,15 +149,15 @@ const Operation: NodeType = {
|
|
|
149
149
|
description: { type: 'string' },
|
|
150
150
|
externalDocs: 'ExternalDocs',
|
|
151
151
|
operationId: { type: 'string' },
|
|
152
|
-
parameters:
|
|
153
|
-
security:
|
|
154
|
-
servers:
|
|
152
|
+
parameters: 'ParameterList',
|
|
153
|
+
security: 'SecurityRequirementList',
|
|
154
|
+
servers: 'ServerList',
|
|
155
155
|
requestBody: 'RequestBody',
|
|
156
156
|
responses: 'Responses',
|
|
157
157
|
deprecated: { type: 'boolean' },
|
|
158
158
|
callbacks: 'CallbacksMap',
|
|
159
|
-
'x-codeSamples':
|
|
160
|
-
'x-code-samples':
|
|
159
|
+
'x-codeSamples': 'XCodeSampleList',
|
|
160
|
+
'x-code-samples': 'XCodeSampleList', // deprecated
|
|
161
161
|
'x-hideTryItPanel': { type: 'boolean' },
|
|
162
162
|
},
|
|
163
163
|
required: ['responses'],
|
|
@@ -462,17 +462,21 @@ const SecurityScheme: NodeType = {
|
|
|
462
462
|
export const Oas3Types: Record<string, NodeType> = {
|
|
463
463
|
Root,
|
|
464
464
|
Tag,
|
|
465
|
+
TagList: listOf('Tag'),
|
|
465
466
|
ExternalDocs,
|
|
466
467
|
Server,
|
|
468
|
+
ServerList: listOf('Server'),
|
|
467
469
|
ServerVariable,
|
|
468
470
|
ServerVariablesMap: mapOf('ServerVariable'),
|
|
469
471
|
SecurityRequirement,
|
|
472
|
+
SecurityRequirementList: listOf('SecurityRequirement'),
|
|
470
473
|
Info,
|
|
471
474
|
Contact,
|
|
472
475
|
License,
|
|
473
476
|
Paths,
|
|
474
477
|
PathItem,
|
|
475
478
|
Parameter,
|
|
479
|
+
ParameterList: listOf('Parameter'),
|
|
476
480
|
Operation,
|
|
477
481
|
Callback: mapOf('PathItem'),
|
|
478
482
|
CallbacksMap: mapOf('Callback'),
|
|
@@ -511,5 +515,6 @@ export const Oas3Types: Record<string, NodeType> = {
|
|
|
511
515
|
OAuth2Flows,
|
|
512
516
|
SecurityScheme,
|
|
513
517
|
XCodeSample,
|
|
518
|
+
XCodeSampleList: listOf('XCodeSample'),
|
|
514
519
|
WebhooksMap,
|
|
515
520
|
};
|
package/src/types/oas3_1.ts
CHANGED