@amritk/lint 0.3.2 → 0.3.3
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/dist/core/document.js +12 -10
- package/dist/core/formats.js +10 -13
- package/dist/core/glob.js +96 -118
- package/dist/core/index.js +31 -11
- package/dist/core/jsonpath.js +487 -561
- package/dist/core/lint.js +78 -85
- package/dist/core/plugin.js +21 -29
- package/dist/core/pointers.js +107 -151
- package/dist/core/ruleset.js +0 -0
- package/dist/core/runner.js +214 -272
- package/dist/core/types.js +4 -1
- package/dist/core/validate-ruleset.js +98 -112
- package/dist/fix/apply.js +58 -96
- package/dist/fix/index.js +7 -2
- package/dist/fix/plugin.js +15 -20
- package/dist/functions/alphabetical.js +39 -50
- package/dist/functions/casing.js +39 -45
- package/dist/functions/defined.js +7 -5
- package/dist/functions/enumeration.js +15 -25
- package/dist/functions/falsy.js +7 -5
- package/dist/functions/index.js +60 -44
- package/dist/functions/length.js +22 -32
- package/dist/functions/or.js +18 -24
- package/dist/functions/pattern.js +39 -49
- package/dist/functions/schema.js +93 -119
- package/dist/functions/truthy.js +7 -5
- package/dist/functions/typed-enum.js +33 -36
- package/dist/functions/undefined.js +7 -8
- package/dist/functions/unreferenced-reusable-object.js +35 -47
- package/dist/functions/xor.js +13 -16
- package/dist/index.js +114 -164
- package/dist/parsers/edit-model.js +316 -444
- package/dist/parsers/index.js +22 -19
- package/dist/parsers/json.js +39 -37
- package/dist/parsers/lines.js +23 -26
- package/dist/parsers/types.js +9 -7
- package/dist/parsers/yaml.js +137 -204
- package/dist/rules/openapi/fixers.js +166 -221
- package/dist/rules/openapi/formats.js +26 -27
- package/dist/rules/openapi/functions/example-validation.js +98 -138
- package/dist/rules/openapi/functions/helpers.js +8 -10
- package/dist/rules/openapi/functions/index.js +98 -72
- package/dist/rules/openapi/functions/oas-additional-operations.js +16 -22
- package/dist/rules/openapi/functions/oas-discriminator.js +24 -22
- package/dist/rules/openapi/functions/oas-example-external-value.js +13 -21
- package/dist/rules/openapi/functions/oas-example-value.js +24 -27
- package/dist/rules/openapi/functions/oas-mutually-exclusive.js +15 -19
- package/dist/rules/openapi/functions/oas-no-nullable.js +13 -21
- package/dist/rules/openapi/functions/oas-op-form-data-consume-check.js +21 -19
- package/dist/rules/openapi/functions/oas-op-id-unique.js +27 -27
- package/dist/rules/openapi/functions/oas-op-params.js +36 -42
- package/dist/rules/openapi/functions/oas-op-security-defined.js +40 -39
- package/dist/rules/openapi/functions/oas-op-success-response.js +11 -13
- package/dist/rules/openapi/functions/oas-path-param.js +75 -96
- package/dist/rules/openapi/functions/oas-schema-example-deprecated.js +33 -40
- package/dist/rules/openapi/functions/oas-schema.js +9 -14
- package/dist/rules/openapi/functions/oas-server-name-unique.js +21 -19
- package/dist/rules/openapi/functions/oas-server-variables.js +45 -49
- package/dist/rules/openapi/functions/oas-tag-defined.js +20 -20
- package/dist/rules/openapi/functions/oas-tag-kind.js +16 -16
- package/dist/rules/openapi/functions/oas-tag-parent-defined.js +40 -43
- package/dist/rules/openapi/functions/oas-tags-unique.js +18 -16
- package/dist/rules/openapi/functions/oas-unused-component.js +48 -58
- package/dist/rules/openapi/functions/ref-siblings.js +13 -11
- package/dist/rules/openapi/index.js +99 -117
- package/dist/rules/openapi/oas.js +524 -536
- package/dist/rules/openapi/schemas/index.js +17 -33
- package/dist/rules/openapi/schemas/oas20.json +1 -1592
- package/dist/rules/openapi/schemas/oas30.json +1 -1651
- package/dist/rules/openapi/schemas/oas31.json +1 -1412
- package/dist/rules/openapi/schemas/oas32.json +1 -1684
- package/package.json +5 -5
|
@@ -1,31 +1,28 @@
|
|
|
1
|
-
import { isObject } from
|
|
2
|
-
// OpenAPI 3.2 Example Object exclusivity (MUST-level, per the spec's field
|
|
3
|
-
// table). The `value`/`externalValue` pair is intentionally omitted here — the
|
|
4
|
-
// 3.x-wide `oas3-examples-value-or-externalValue` rule already covers it, so we
|
|
5
|
-
// only police the constraints introduced with the new `dataValue` /
|
|
6
|
-
// `serializedValue` fields to avoid double-reporting.
|
|
1
|
+
import { isObject } from "./helpers.js";
|
|
7
2
|
const EXAMPLE_EXCLUSIONS = [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
// dataValue MUST NOT accompany value.
|
|
4
|
+
{ field: "dataValue", conflicts: ["value"] },
|
|
5
|
+
// serializedValue MUST NOT accompany value or externalValue.
|
|
6
|
+
{ field: "serializedValue", conflicts: ["value", "externalValue"] }
|
|
12
7
|
];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
}
|
|
8
|
+
const oasExampleValue = (example, _options, context) => {
|
|
9
|
+
if (!isObject(example))
|
|
10
|
+
return [];
|
|
11
|
+
const results = [];
|
|
12
|
+
for (const { field, conflicts } of EXAMPLE_EXCLUSIONS) {
|
|
13
|
+
if (example[field] === void 0)
|
|
14
|
+
continue;
|
|
15
|
+
for (const other of conflicts) {
|
|
16
|
+
if (example[other] !== void 0) {
|
|
17
|
+
results.push({
|
|
18
|
+
message: `"${field}" must not be used together with "${other}"`,
|
|
19
|
+
path: [...context.path, other]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
29
22
|
}
|
|
30
|
-
|
|
23
|
+
}
|
|
24
|
+
return results;
|
|
25
|
+
};
|
|
26
|
+
export {
|
|
27
|
+
oasExampleValue
|
|
31
28
|
};
|
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import { isObject } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return conflicts.map((property) => ({
|
|
17
|
-
message: `"${property}" must not be used together with "${anchor}" (mutually exclusive)`,
|
|
18
|
-
path: [...context.path, property],
|
|
19
|
-
}));
|
|
1
|
+
import { isObject } from "./helpers.js";
|
|
2
|
+
const oasMutuallyExclusive = (input, options, context) => {
|
|
3
|
+
if (!isObject(input))
|
|
4
|
+
return [];
|
|
5
|
+
const present = (options?.properties ?? []).filter((property) => input[property] !== void 0);
|
|
6
|
+
if (present.length <= 1)
|
|
7
|
+
return [];
|
|
8
|
+
const [anchor, ...conflicts] = present;
|
|
9
|
+
return conflicts.map((property) => ({
|
|
10
|
+
message: `"${property}" must not be used together with "${anchor}" (mutually exclusive)`,
|
|
11
|
+
path: [...context.path, property]
|
|
12
|
+
}));
|
|
13
|
+
};
|
|
14
|
+
export {
|
|
15
|
+
oasMutuallyExclusive
|
|
20
16
|
};
|
|
@@ -1,22 +1,14 @@
|
|
|
1
|
-
import { isObject } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!isObject(parent) || typeof parent['nullable'] !== 'boolean')
|
|
15
|
-
return [];
|
|
16
|
-
return [
|
|
17
|
-
{
|
|
18
|
-
message: 'nullable is not available in OpenAPI 3.1 or later; use a "null" type instead.',
|
|
19
|
-
path: [...context.path, 'nullable'],
|
|
20
|
-
},
|
|
21
|
-
];
|
|
1
|
+
import { isObject } from "./helpers.js";
|
|
2
|
+
const oasNoNullable = (parent, _options, context) => {
|
|
3
|
+
if (!isObject(parent) || typeof parent["nullable"] !== "boolean")
|
|
4
|
+
return [];
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
message: 'nullable is not available in OpenAPI 3.1 or later; use a "null" type instead.',
|
|
8
|
+
path: [...context.path, "nullable"]
|
|
9
|
+
}
|
|
10
|
+
];
|
|
11
|
+
};
|
|
12
|
+
export {
|
|
13
|
+
oasNoNullable
|
|
22
14
|
};
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import { isObject } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
if (!isObject(operation))
|
|
5
|
-
return [];
|
|
6
|
-
const params = Array.isArray(operation['parameters']) ? operation['parameters'] : [];
|
|
7
|
-
const hasFormData = params.some((param) => isObject(param) && param['in'] === 'formData');
|
|
8
|
-
if (!hasFormData)
|
|
9
|
-
return [];
|
|
10
|
-
const consumes = Array.isArray(operation['consumes']) ? operation['consumes'] : [];
|
|
11
|
-
const ok = consumes.some((type) => type === 'application/x-www-form-urlencoded' || type === 'multipart/form-data');
|
|
12
|
-
if (!ok) {
|
|
13
|
-
return [
|
|
14
|
-
{
|
|
15
|
-
message: 'Operations with formData parameters must consume application/x-www-form-urlencoded or multipart/form-data',
|
|
16
|
-
path: [...context.path, 'consumes'],
|
|
17
|
-
},
|
|
18
|
-
];
|
|
19
|
-
}
|
|
1
|
+
import { isObject } from "./helpers.js";
|
|
2
|
+
const oasOpFormDataConsumeCheck = (operation, _options, context) => {
|
|
3
|
+
if (!isObject(operation))
|
|
20
4
|
return [];
|
|
5
|
+
const params = Array.isArray(operation["parameters"]) ? operation["parameters"] : [];
|
|
6
|
+
const hasFormData = params.some((param) => isObject(param) && param["in"] === "formData");
|
|
7
|
+
if (!hasFormData)
|
|
8
|
+
return [];
|
|
9
|
+
const consumes = Array.isArray(operation["consumes"]) ? operation["consumes"] : [];
|
|
10
|
+
const ok = consumes.some((type) => type === "application/x-www-form-urlencoded" || type === "multipart/form-data");
|
|
11
|
+
if (!ok) {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
message: "Operations with formData parameters must consume application/x-www-form-urlencoded or multipart/form-data",
|
|
15
|
+
path: [...context.path, "consumes"]
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
};
|
|
21
|
+
export {
|
|
22
|
+
oasOpFormDataConsumeCheck
|
|
21
23
|
};
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import { isObject, OPERATION_METHODS } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
seen.add(id);
|
|
26
|
-
}
|
|
1
|
+
import { isObject, OPERATION_METHODS } from "./helpers.js";
|
|
2
|
+
const oasOpIdUnique = (paths, _options, context) => {
|
|
3
|
+
if (!isObject(paths))
|
|
4
|
+
return [];
|
|
5
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6
|
+
const results = [];
|
|
7
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
8
|
+
if (!isObject(item))
|
|
9
|
+
continue;
|
|
10
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
11
|
+
if (!OPERATION_METHODS.has(method) || !isObject(operation))
|
|
12
|
+
continue;
|
|
13
|
+
const id = operation["operationId"];
|
|
14
|
+
if (typeof id !== "string")
|
|
15
|
+
continue;
|
|
16
|
+
if (seen.has(id)) {
|
|
17
|
+
results.push({
|
|
18
|
+
message: `operationId "${id}" must be unique`,
|
|
19
|
+
path: [...context.path, path, method, "operationId"]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
seen.add(id);
|
|
27
23
|
}
|
|
28
|
-
|
|
24
|
+
}
|
|
25
|
+
return results;
|
|
26
|
+
};
|
|
27
|
+
export {
|
|
28
|
+
oasOpIdUnique
|
|
29
29
|
};
|
|
@@ -1,46 +1,40 @@
|
|
|
1
|
-
import { isObject } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const formDataIndices = [];
|
|
16
|
-
params.forEach((param, index) => {
|
|
17
|
-
if (!isObject(param) || '$ref' in param)
|
|
18
|
-
return;
|
|
19
|
-
if (param['name'] !== undefined && param['in'] !== undefined) {
|
|
20
|
-
const key = `${String(param['in'])}:${String(param['name'])}`;
|
|
21
|
-
if (seen.has(key)) {
|
|
22
|
-
results.push({
|
|
23
|
-
message: `Duplicate parameter "${String(param['name'])}" in "${String(param['in'])}"`,
|
|
24
|
-
path: [...context.path, index],
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
seen.add(key);
|
|
28
|
-
}
|
|
29
|
-
if (param['in'] === 'body')
|
|
30
|
-
bodyIndices.push(index);
|
|
31
|
-
else if (param['in'] === 'formData')
|
|
32
|
-
formDataIndices.push(index);
|
|
33
|
-
});
|
|
34
|
-
// OAS2 allows at most one body parameter, and body and formData are mutually
|
|
35
|
-
// exclusive within a single operation.
|
|
36
|
-
if (bodyIndices.length > 0 && formDataIndices.length > 0) {
|
|
37
|
-
results.push({ message: 'Operation must not have both "in:body" and "in:formData" parameters' });
|
|
38
|
-
}
|
|
39
|
-
for (let i = 1; i < bodyIndices.length; i++) {
|
|
1
|
+
import { isObject } from "./helpers.js";
|
|
2
|
+
const oasOpParams = (params, _options, context) => {
|
|
3
|
+
if (!Array.isArray(params))
|
|
4
|
+
return [];
|
|
5
|
+
const results = [];
|
|
6
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7
|
+
const bodyIndices = [];
|
|
8
|
+
const formDataIndices = [];
|
|
9
|
+
params.forEach((param, index) => {
|
|
10
|
+
if (!isObject(param) || "$ref" in param)
|
|
11
|
+
return;
|
|
12
|
+
if (param["name"] !== void 0 && param["in"] !== void 0) {
|
|
13
|
+
const key = `${String(param["in"])}:${String(param["name"])}`;
|
|
14
|
+
if (seen.has(key)) {
|
|
40
15
|
results.push({
|
|
41
|
-
|
|
42
|
-
|
|
16
|
+
message: `Duplicate parameter "${String(param["name"])}" in "${String(param["in"])}"`,
|
|
17
|
+
path: [...context.path, index]
|
|
43
18
|
});
|
|
19
|
+
}
|
|
20
|
+
seen.add(key);
|
|
44
21
|
}
|
|
45
|
-
|
|
22
|
+
if (param["in"] === "body")
|
|
23
|
+
bodyIndices.push(index);
|
|
24
|
+
else if (param["in"] === "formData")
|
|
25
|
+
formDataIndices.push(index);
|
|
26
|
+
});
|
|
27
|
+
if (bodyIndices.length > 0 && formDataIndices.length > 0) {
|
|
28
|
+
results.push({ message: 'Operation must not have both "in:body" and "in:formData" parameters' });
|
|
29
|
+
}
|
|
30
|
+
for (let i = 1; i < bodyIndices.length; i++) {
|
|
31
|
+
results.push({
|
|
32
|
+
message: 'Operation must not have more than a single instance of the "in:body" parameter',
|
|
33
|
+
path: [...context.path, bodyIndices[i]]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
};
|
|
38
|
+
export {
|
|
39
|
+
oasOpParams
|
|
46
40
|
};
|
|
@@ -1,44 +1,45 @@
|
|
|
1
|
-
import { isObject, OPERATION_METHODS } from
|
|
2
|
-
/** Walks `path` into `root`, returning the nested value or undefined. */
|
|
1
|
+
import { isObject, OPERATION_METHODS } from "./helpers.js";
|
|
3
2
|
const getIn = (root, path) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
let current = root;
|
|
4
|
+
for (const key of path) {
|
|
5
|
+
if (!isObject(current))
|
|
6
|
+
return void 0;
|
|
7
|
+
current = current[key];
|
|
8
|
+
}
|
|
9
|
+
return current;
|
|
11
10
|
};
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
results.push({ message: `Security scheme "${name}" is not defined`, path: [...path, index, name] });
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
};
|
|
32
|
-
check(root['security'], [...context.path, 'security']);
|
|
33
|
-
const paths = isObject(root['paths']) ? root['paths'] : {};
|
|
34
|
-
for (const [path, item] of Object.entries(paths)) {
|
|
35
|
-
if (!isObject(item))
|
|
36
|
-
continue;
|
|
37
|
-
for (const [method, operation] of Object.entries(item)) {
|
|
38
|
-
if (OPERATION_METHODS.has(method) && isObject(operation)) {
|
|
39
|
-
check(operation['security'], [...context.path, 'paths', path, method, 'security']);
|
|
40
|
-
}
|
|
11
|
+
const oasOpSecurityDefined = (root, options, context) => {
|
|
12
|
+
if (!isObject(root))
|
|
13
|
+
return [];
|
|
14
|
+
const schemes = getIn(root, options?.schemesPath ?? []);
|
|
15
|
+
const defined = new Set(isObject(schemes) ? Object.keys(schemes) : []);
|
|
16
|
+
const results = [];
|
|
17
|
+
const check = (security, path) => {
|
|
18
|
+
if (!Array.isArray(security))
|
|
19
|
+
return;
|
|
20
|
+
security.forEach((requirement, index) => {
|
|
21
|
+
if (!isObject(requirement))
|
|
22
|
+
return;
|
|
23
|
+
for (const name of Object.keys(requirement)) {
|
|
24
|
+
if (!defined.has(name)) {
|
|
25
|
+
results.push({ message: `Security scheme "${name}" is not defined`, path: [...path, index, name] });
|
|
41
26
|
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
check(root["security"], [...context.path, "security"]);
|
|
31
|
+
const paths = isObject(root["paths"]) ? root["paths"] : {};
|
|
32
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
33
|
+
if (!isObject(item))
|
|
34
|
+
continue;
|
|
35
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
36
|
+
if (OPERATION_METHODS.has(method) && isObject(operation)) {
|
|
37
|
+
check(operation["security"], [...context.path, "paths", path, method, "security"]);
|
|
38
|
+
}
|
|
42
39
|
}
|
|
43
|
-
|
|
40
|
+
}
|
|
41
|
+
return results;
|
|
42
|
+
};
|
|
43
|
+
export {
|
|
44
|
+
oasOpSecurityDefined
|
|
44
45
|
};
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import { isObject } from
|
|
2
|
-
// A success response is any 2xx/3xx status code, or a `2XX`/`3XX` wildcard (3.x).
|
|
3
|
-
// `default` is intentionally NOT counted — it can carry any status, including an
|
|
4
|
-
// error, so it does not prove the operation has a success response (this matches
|
|
5
|
-
// Spectral, which counts only codes in the [200, 400) range plus the wildcards).
|
|
1
|
+
import { isObject } from "./helpers.js";
|
|
6
2
|
const SUCCESS_CODE = /^[23](\d\d|XX)$/;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (!isObject(responses))
|
|
10
|
-
return [];
|
|
11
|
-
const hasSuccess = Object.keys(responses).some((code) => SUCCESS_CODE.test(code));
|
|
12
|
-
if (!hasSuccess) {
|
|
13
|
-
return [{ message: 'Operation must define at least one 2xx or 3xx response' }];
|
|
14
|
-
}
|
|
3
|
+
const oasOpSuccessResponse = (responses) => {
|
|
4
|
+
if (!isObject(responses))
|
|
15
5
|
return [];
|
|
6
|
+
const hasSuccess = Object.keys(responses).some((code) => SUCCESS_CODE.test(code));
|
|
7
|
+
if (!hasSuccess) {
|
|
8
|
+
return [{ message: "Operation must define at least one 2xx or 3xx response" }];
|
|
9
|
+
}
|
|
10
|
+
return [];
|
|
11
|
+
};
|
|
12
|
+
export {
|
|
13
|
+
oasOpSuccessResponse
|
|
16
14
|
};
|
|
@@ -1,106 +1,85 @@
|
|
|
1
|
-
import { isObject, OPERATION_METHODS } from
|
|
2
|
-
// Path template parameters, e.g. `{id}`. Matches Spectral's grammar so matrix
|
|
3
|
-
// (`{;id}`), optional (`{id?}`), and explode (`{id*}`) markers are stripped off
|
|
4
|
-
// the captured name before comparison.
|
|
1
|
+
import { isObject, OPERATION_METHODS } from "./helpers.js";
|
|
5
2
|
const PATH_TEMPLATE = /(\{;?\??[a-zA-Z0-9_-]+\*?\})/g;
|
|
6
|
-
const namedPathParam = (param) => param[
|
|
7
|
-
/**
|
|
8
|
-
* Records a single `in: path` definition, emitting Spectral's `required: true`
|
|
9
|
-
* and duplicate-definition findings. Returns the parameter name when it is the
|
|
10
|
-
* first definition seen (so the caller registers it as usable), else undefined.
|
|
11
|
-
*/
|
|
3
|
+
const namedPathParam = (param) => param["in"] === "path" && typeof param["name"] === "string" ? param["name"] : void 0;
|
|
12
4
|
const recordPathParam = (param, definitionPath, seen, results) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
const name = namedPathParam(param);
|
|
6
|
+
if (name === void 0)
|
|
7
|
+
return void 0;
|
|
8
|
+
if (param["required"] !== true) {
|
|
9
|
+
results.push({
|
|
10
|
+
message: `Path parameter "${name}" must have a "required" property that is set to "true"`,
|
|
11
|
+
path: definitionPath
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
if (name in seen) {
|
|
15
|
+
results.push({ message: `Path parameter "${name}" must not be defined multiple times`, path: definitionPath });
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
return name;
|
|
19
|
+
};
|
|
20
|
+
const oasPathParam = (paths, _options, context) => {
|
|
21
|
+
if (!isObject(paths))
|
|
22
|
+
return [];
|
|
23
|
+
const results = [];
|
|
24
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
25
|
+
if (!isObject(item))
|
|
26
|
+
continue;
|
|
27
|
+
const templates = [];
|
|
28
|
+
for (const match of path.matchAll(PATH_TEMPLATE)) {
|
|
29
|
+
const name = match[0].replace(/[{}?*;]/g, "");
|
|
30
|
+
if (templates.includes(name)) {
|
|
17
31
|
results.push({
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
message: `Path "${path}" must not use parameter "{${name}}" more than once`,
|
|
33
|
+
path: [...context.path, path]
|
|
20
34
|
});
|
|
35
|
+
} else {
|
|
36
|
+
templates.push(name);
|
|
37
|
+
}
|
|
21
38
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
const topParams = {};
|
|
40
|
+
if (Array.isArray(item["parameters"])) {
|
|
41
|
+
item["parameters"].forEach((param, index) => {
|
|
42
|
+
if (!isObject(param))
|
|
43
|
+
return;
|
|
44
|
+
const definitionPath = [...context.path, path, "parameters", index];
|
|
45
|
+
const name = recordPathParam(param, definitionPath, topParams, results);
|
|
46
|
+
if (name !== void 0)
|
|
47
|
+
topParams[name] = definitionPath;
|
|
48
|
+
});
|
|
25
49
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (!
|
|
44
|
-
|
|
45
|
-
// (d) Templates declared in the path key, deduplicated (a repeat is an error).
|
|
46
|
-
const templates = [];
|
|
47
|
-
for (const match of path.matchAll(PATH_TEMPLATE)) {
|
|
48
|
-
const name = match[0].replace(/[{}?*;]/g, '');
|
|
49
|
-
if (templates.includes(name)) {
|
|
50
|
-
results.push({
|
|
51
|
-
message: `Path "${path}" must not use parameter "{${name}}" more than once`,
|
|
52
|
-
path: [...context.path, path],
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
templates.push(name);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// Path Item level parameters apply to every operation on the path.
|
|
60
|
-
const topParams = {};
|
|
61
|
-
if (Array.isArray(item['parameters'])) {
|
|
62
|
-
item['parameters'].forEach((param, index) => {
|
|
63
|
-
if (!isObject(param))
|
|
64
|
-
return;
|
|
65
|
-
const definitionPath = [...context.path, path, 'parameters', index];
|
|
66
|
-
const name = recordPathParam(param, definitionPath, topParams, results);
|
|
67
|
-
if (name !== undefined)
|
|
68
|
-
topParams[name] = definitionPath;
|
|
69
|
-
});
|
|
50
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
51
|
+
if (method === "parameters" || !OPERATION_METHODS.has(method) || !isObject(operation))
|
|
52
|
+
continue;
|
|
53
|
+
const operationPath = [...context.path, path, method];
|
|
54
|
+
const operationParams = {};
|
|
55
|
+
if (Array.isArray(operation["parameters"])) {
|
|
56
|
+
operation["parameters"].forEach((param, index) => {
|
|
57
|
+
if (!isObject(param))
|
|
58
|
+
return;
|
|
59
|
+
const definitionPath = [...operationPath, "parameters", index];
|
|
60
|
+
const name = recordPathParam(param, definitionPath, operationParams, results);
|
|
61
|
+
if (name !== void 0)
|
|
62
|
+
operationParams[name] = definitionPath;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const defined = { ...topParams, ...operationParams };
|
|
66
|
+
for (const [name, definitionPath] of Object.entries(defined)) {
|
|
67
|
+
if (definitionPath && !templates.includes(name)) {
|
|
68
|
+
results.push({ message: `Parameter "${name}" must be used in path "${path}"`, path: definitionPath });
|
|
70
69
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
operation['parameters'].forEach((param, index) => {
|
|
79
|
-
if (!isObject(param))
|
|
80
|
-
return;
|
|
81
|
-
const definitionPath = [...operationPath, 'parameters', index];
|
|
82
|
-
const name = recordPathParam(param, definitionPath, operationParams, results);
|
|
83
|
-
if (name !== undefined)
|
|
84
|
-
operationParams[name] = definitionPath;
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
const defined = { ...topParams, ...operationParams };
|
|
88
|
-
// (b) Every defined `in: path` parameter must appear in the path template.
|
|
89
|
-
for (const [name, definitionPath] of Object.entries(defined)) {
|
|
90
|
-
if (definitionPath && !templates.includes(name)) {
|
|
91
|
-
results.push({ message: `Parameter "${name}" must be used in path "${path}"`, path: definitionPath });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// (a) Every `{template}` must have a matching definition on the operation.
|
|
95
|
-
for (const name of templates) {
|
|
96
|
-
if (!(name in defined)) {
|
|
97
|
-
results.push({
|
|
98
|
-
message: `Operation must define path parameter "{${name}}" as expected by path "${path}"`,
|
|
99
|
-
path: operationPath,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
70
|
+
}
|
|
71
|
+
for (const name of templates) {
|
|
72
|
+
if (!(name in defined)) {
|
|
73
|
+
results.push({
|
|
74
|
+
message: `Operation must define path parameter "{${name}}" as expected by path "${path}"`,
|
|
75
|
+
path: operationPath
|
|
76
|
+
});
|
|
103
77
|
}
|
|
78
|
+
}
|
|
104
79
|
}
|
|
105
|
-
|
|
80
|
+
}
|
|
81
|
+
return results;
|
|
82
|
+
};
|
|
83
|
+
export {
|
|
84
|
+
oasPathParam
|
|
106
85
|
};
|