@amritk/lint 0.4.7 → 0.5.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/AI.md +73 -4
- package/README.md +65 -14
- package/dist/core/filter-expression.js +17 -4
- package/dist/core/filter.js +82 -35
- package/dist/core/glob.js +25 -31
- package/dist/core/jsonpath.d.ts +9 -1
- package/dist/core/jsonpath.js +74 -16
- package/dist/core/lint.d.ts +8 -1
- package/dist/core/lint.js +4 -18
- package/dist/core/order.d.ts +30 -0
- package/dist/core/order.js +36 -0
- package/dist/core/own-key.d.ts +18 -0
- package/dist/core/own-key.js +12 -0
- package/dist/core/pointers.d.ts +3 -2
- package/dist/core/pointers.js +27 -18
- package/dist/core/ruleset.js +74 -27
- package/dist/core/runner.js +16 -23
- package/dist/core/severity.d.ts +11 -0
- package/dist/core/severity.js +13 -0
- package/dist/fix/apply.js +9 -2
- package/dist/fix/plugin.js +2 -1
- package/dist/functions/alphabetical.d.ts +14 -0
- package/dist/functions/alphabetical.js +4 -3
- package/dist/functions/or.js +1 -1
- package/dist/functions/ref-index.d.ts +15 -0
- package/dist/functions/ref-index.js +33 -0
- package/dist/functions/schema.d.ts +15 -0
- package/dist/functions/schema.js +2 -0
- package/dist/functions/typed-enum.js +2 -1
- package/dist/functions/unreferenced-reusable-object.js +3 -20
- package/dist/functions/xor.js +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +25 -94
- package/dist/rules/asyncapi/asyncapi.d.ts +3 -0
- package/dist/rules/asyncapi/asyncapi.js +563 -0
- package/dist/rules/asyncapi/formats.d.ts +23 -0
- package/dist/rules/asyncapi/formats.js +49 -0
- package/dist/rules/asyncapi/functions/aas-server-variables.d.ts +8 -0
- package/dist/rules/asyncapi/functions/aas-server-variables.js +5 -0
- package/dist/rules/asyncapi/functions/aas-tags-unique.d.ts +2 -0
- package/dist/rules/asyncapi/functions/aas-tags-unique.js +5 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.d.ts +7 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.js +46 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-servers.d.ts +27 -0
- package/dist/rules/asyncapi/functions/asyncapi-channel-servers.js +61 -0
- package/dist/rules/asyncapi/functions/asyncapi-document-schema.d.ts +17 -0
- package/dist/rules/asyncapi/functions/asyncapi-document-schema.js +12 -0
- package/dist/rules/asyncapi/functions/asyncapi-headers-object.d.ts +23 -0
- package/dist/rules/asyncapi/functions/asyncapi-headers-object.js +25 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-examples.d.ts +8 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-examples.js +42 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.d.ts +3 -0
- package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.js +30 -0
- package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.d.ts +3 -0
- package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.js +30 -0
- package/dist/rules/asyncapi/functions/asyncapi-payload.d.ts +14 -0
- package/dist/rules/asyncapi/functions/asyncapi-payload.js +32 -0
- package/dist/rules/asyncapi/functions/asyncapi-schema-validation.d.ts +11 -0
- package/dist/rules/asyncapi/functions/asyncapi-schema-validation.js +17 -0
- package/dist/rules/asyncapi/functions/asyncapi-security.d.ts +22 -0
- package/dist/rules/asyncapi/functions/asyncapi-security.js +78 -0
- package/dist/rules/asyncapi/functions/helpers.d.ts +51 -0
- package/dist/rules/asyncapi/functions/helpers.js +91 -0
- package/dist/rules/asyncapi/functions/index.d.ts +15 -0
- package/dist/rules/asyncapi/functions/index.js +53 -0
- package/dist/rules/asyncapi/functions/pointer.d.ts +11 -0
- package/dist/rules/asyncapi/functions/pointer.js +11 -0
- package/dist/rules/asyncapi/functions/schema-format.d.ts +4 -0
- package/dist/rules/asyncapi/functions/schema-format.js +7 -0
- package/dist/rules/asyncapi/index.d.ts +33 -0
- package/dist/rules/asyncapi/index.js +60 -0
- package/dist/rules/asyncapi/schemas/aas20.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas20.js +4 -0
- package/dist/rules/asyncapi/schemas/aas21.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas21.js +4 -0
- package/dist/rules/asyncapi/schemas/aas22.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas22.js +4 -0
- package/dist/rules/asyncapi/schemas/aas23.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas23.js +4 -0
- package/dist/rules/asyncapi/schemas/aas24.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas24.js +4 -0
- package/dist/rules/asyncapi/schemas/aas25.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas25.js +4 -0
- package/dist/rules/asyncapi/schemas/aas26.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas26.js +4 -0
- package/dist/rules/asyncapi/schemas/aas30.d.ts +2 -0
- package/dist/rules/asyncapi/schemas/aas30.js +4 -0
- package/dist/rules/asyncapi/schemas/index.d.ts +20 -0
- package/dist/rules/asyncapi/schemas/index.js +45 -0
- package/dist/rules/openapi/fixers.d.ts +6 -2
- package/dist/rules/openapi/fixers.js +9 -6
- package/dist/rules/openapi/functions/oas-discriminator.js +1 -1
- package/dist/rules/openapi/functions/oas-mutually-exclusive.js +1 -1
- package/dist/rules/openapi/functions/oas-op-security-defined.js +1 -1
- package/dist/rules/openapi/functions/oas-path-param.js +5 -4
- package/dist/rules/openapi/functions/oas-server-variables.d.ts +6 -6
- package/dist/rules/openapi/functions/oas-server-variables.js +2 -45
- package/dist/rules/openapi/functions/oas-tags-unique.d.ts +2 -3
- package/dist/rules/openapi/functions/oas-tags-unique.js +2 -16
- package/dist/rules/openapi/functions/oas-unused-component.js +3 -25
- package/dist/rules/openapi/index.d.ts +9 -2
- package/dist/rules/openapi/index.js +15 -70
- package/dist/rules/openapi/schemas/index.js +5 -1
- package/dist/rules/shared/helpers.d.ts +2 -0
- package/dist/rules/shared/helpers.js +4 -0
- package/dist/rules/shared/server-variables.d.ts +27 -0
- package/dist/rules/shared/server-variables.js +55 -0
- package/dist/rules/shared/tags-unique.d.ts +7 -0
- package/dist/rules/shared/tags-unique.js +19 -0
- package/dist/ruleset-files.d.ts +47 -0
- package/dist/ruleset-files.js +92 -0
- package/package.json +9 -5
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isObject, parseUrlVariables } from "./helpers.js";
|
|
2
|
+
const channelAddress = (channel, path, document) => {
|
|
3
|
+
const declaredAddress = channel["address"];
|
|
4
|
+
if (typeof declaredAddress === "string")
|
|
5
|
+
return declaredAddress;
|
|
6
|
+
const version = isObject(document) ? document["asyncapi"] : void 0;
|
|
7
|
+
if (typeof version !== "string" || !version.startsWith("2."))
|
|
8
|
+
return void 0;
|
|
9
|
+
if (path.length !== 2 || path[0] !== "channels")
|
|
10
|
+
return void 0;
|
|
11
|
+
return typeof path[1] === "string" ? path[1] : void 0;
|
|
12
|
+
};
|
|
13
|
+
const asyncApiChannelParameters = (channel, _options, context) => {
|
|
14
|
+
if (!isObject(channel))
|
|
15
|
+
return [];
|
|
16
|
+
if (typeof channel["$ref"] === "string")
|
|
17
|
+
return [];
|
|
18
|
+
const declaredParameters = channel["parameters"];
|
|
19
|
+
if (declaredParameters !== void 0 && !isObject(declaredParameters))
|
|
20
|
+
return [];
|
|
21
|
+
const parameters = isObject(declaredParameters) ? declaredParameters : {};
|
|
22
|
+
const address = channelAddress(channel, context.path, context.document.data);
|
|
23
|
+
if (address === void 0)
|
|
24
|
+
return [];
|
|
25
|
+
const declared = new Set(parseUrlVariables(address));
|
|
26
|
+
const results = [];
|
|
27
|
+
const missing = [...declared].filter((name) => !Object.hasOwn(parameters, name));
|
|
28
|
+
if (missing.length > 0) {
|
|
29
|
+
results.push({
|
|
30
|
+
message: `Channel parameters must be described: ${missing.join(", ")}`,
|
|
31
|
+
path: [...context.path, "parameters"]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
for (const name of Object.keys(parameters)) {
|
|
35
|
+
if (!declared.has(name)) {
|
|
36
|
+
results.push({
|
|
37
|
+
message: `Channel parameter "${name}" is not used in the channel address`,
|
|
38
|
+
path: [...context.path, "parameters", name]
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return results;
|
|
43
|
+
};
|
|
44
|
+
export {
|
|
45
|
+
asyncApiChannelParameters
|
|
46
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Checks that every server a channel lists is one the document declares.
|
|
4
|
+
*
|
|
5
|
+
* The spec scopes this to where the channel is written, and the two cases differ
|
|
6
|
+
* (AsyncAPI 3.0, Channel Object `servers`): a channel in the **root** Channels
|
|
7
|
+
* Object "MUST point to a subset of server definitions located in the root
|
|
8
|
+
* Servers Object, and MUST NOT point to … the Components Object or anywhere
|
|
9
|
+
* else", while a channel in the Components Object "MAY point to a Server Object
|
|
10
|
+
* in any location".
|
|
11
|
+
*
|
|
12
|
+
* The carve-out excuses only references pointing *outside* `#/servers`, so it
|
|
13
|
+
* suspends one of the two checks and not the other. A reusable 3.0 channel
|
|
14
|
+
* pointing at `#/components/servers/staging` is permitted and must stay silent;
|
|
15
|
+
* one pointing at `#/servers/nope` still names the root Servers Object and must
|
|
16
|
+
* still exist there. Skipping reusable channels altogether lost that second
|
|
17
|
+
* check, and reporting them lost the first.
|
|
18
|
+
*
|
|
19
|
+
* 2.x has no carve-out at all — a channel's `servers` is a list of names from
|
|
20
|
+
* the root Servers Object wherever the channel is written — so both checks apply
|
|
21
|
+
* to both locations there.
|
|
22
|
+
*
|
|
23
|
+
* Given the whole document (`$`), because the check needs the top-level
|
|
24
|
+
* `servers` map as well as each channel. Runs unresolved, so a 3.0 `$ref` is
|
|
25
|
+
* still a reference rather than the server it points at.
|
|
26
|
+
*/
|
|
27
|
+
export declare const asyncApiChannelServers: RulesetFunction;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isObject } from "./helpers.js";
|
|
2
|
+
import { pointerSegment } from "./pointer.js";
|
|
3
|
+
const SERVERS_REF = "#/servers/";
|
|
4
|
+
const readEntry = (entry) => {
|
|
5
|
+
if (typeof entry === "string")
|
|
6
|
+
return { kind: "named", name: entry, path: [] };
|
|
7
|
+
if (!isObject(entry))
|
|
8
|
+
return void 0;
|
|
9
|
+
const ref = entry["$ref"];
|
|
10
|
+
if (typeof ref !== "string")
|
|
11
|
+
return void 0;
|
|
12
|
+
if (!ref.startsWith(SERVERS_REF))
|
|
13
|
+
return { kind: "elsewhere", ref, path: ["$ref"] };
|
|
14
|
+
return { kind: "named", name: pointerSegment(ref.slice(SERVERS_REF.length)), path: ["$ref"] };
|
|
15
|
+
};
|
|
16
|
+
const asyncApiChannelServers = (document, _options, _context) => {
|
|
17
|
+
if (!isObject(document))
|
|
18
|
+
return [];
|
|
19
|
+
const declared = new Set(isObject(document["servers"]) ? Object.keys(document["servers"]) : []);
|
|
20
|
+
const version = document["asyncapi"];
|
|
21
|
+
const isV2 = typeof version === "string" && version.startsWith("2.");
|
|
22
|
+
const components = isObject(document["components"]) ? document["components"] : void 0;
|
|
23
|
+
const roots = [
|
|
24
|
+
{ path: ["channels"], channels: document["channels"], mustPointAtRootServers: true },
|
|
25
|
+
// Reusable channels: in 3.0 they may reference a server anywhere, so only the
|
|
26
|
+
// "does it exist" half applies to the ones that do name `#/servers`.
|
|
27
|
+
{ path: ["components", "channels"], channels: components?.["channels"], mustPointAtRootServers: isV2 }
|
|
28
|
+
];
|
|
29
|
+
const results = [];
|
|
30
|
+
for (const root of roots) {
|
|
31
|
+
if (!isObject(root.channels))
|
|
32
|
+
continue;
|
|
33
|
+
for (const [address, channel] of Object.entries(root.channels)) {
|
|
34
|
+
if (!isObject(channel))
|
|
35
|
+
continue;
|
|
36
|
+
const servers = channel["servers"];
|
|
37
|
+
if (!Array.isArray(servers))
|
|
38
|
+
continue;
|
|
39
|
+
servers.forEach((entry, index) => {
|
|
40
|
+
const read = readEntry(entry);
|
|
41
|
+
if (read === void 0)
|
|
42
|
+
return;
|
|
43
|
+
const at = () => [...root.path, address, "servers", index, ...read.path];
|
|
44
|
+
if (read.kind === "elsewhere") {
|
|
45
|
+
if (!root.mustPointAtRootServers)
|
|
46
|
+
return;
|
|
47
|
+
results.push({
|
|
48
|
+
message: `A root channel's server must reference "${SERVERS_REF}\u2026", not "${read.ref}"`,
|
|
49
|
+
path: at()
|
|
50
|
+
});
|
|
51
|
+
} else if (!declared.has(read.name)) {
|
|
52
|
+
results.push({ message: `Channel server "${read.name}" is not defined in the "servers" object`, path: at() });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
};
|
|
59
|
+
export {
|
|
60
|
+
asyncApiChannelServers
|
|
61
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates a whole AsyncAPI document against the official structural
|
|
4
|
+
* meta-schema for the version it declares, parsing that version's schema on
|
|
5
|
+
* first use. A document declaring a version this package bundles no schema for
|
|
6
|
+
* (a future 2.7, say) reports nothing rather than being judged against a
|
|
7
|
+
* neighbouring version's schema.
|
|
8
|
+
*
|
|
9
|
+
* The rule that calls this runs `resolved: false`, against the document as
|
|
10
|
+
* written, which is what keeps one authored mistake to one finding. Validating
|
|
11
|
+
* the dereferenced tree instead would re-check every `components` entry once per
|
|
12
|
+
* `$ref` that reaches it, so a single bad reusable message reported three times
|
|
13
|
+
* in a document that used it twice. The cost is that content pulled in from
|
|
14
|
+
* another file is not structurally checked — the same trade-off the OpenAPI
|
|
15
|
+
* preset's `oas*-schema` rules make.
|
|
16
|
+
*/
|
|
17
|
+
export declare const asyncApiDocumentSchema: RulesetFunction;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { schema as schemaFunction } from "../../../functions/index.js";
|
|
2
|
+
import { asyncApiSchemaVersion, loadAsyncApiSchema } from "../schemas/index.js";
|
|
3
|
+
import { isObject } from "./helpers.js";
|
|
4
|
+
const asyncApiDocumentSchema = (input, _options, context) => {
|
|
5
|
+
const version = asyncApiSchemaVersion(isObject(input) ? input["asyncapi"] : void 0);
|
|
6
|
+
if (version === void 0)
|
|
7
|
+
return [];
|
|
8
|
+
return schemaFunction(input, { schema: loadAsyncApiSchema(version), allErrors: true }, context) ?? [];
|
|
9
|
+
};
|
|
10
|
+
export {
|
|
11
|
+
asyncApiDocumentSchema
|
|
12
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A message's `headers` must describe an object — headers are name/value pairs,
|
|
4
|
+
* so any other type is a mistake.
|
|
5
|
+
*
|
|
6
|
+
* 3.0 lets `headers` be either a Schema Object or a Multi Format Schema Object
|
|
7
|
+
* (`{ schemaFormat, schema }`), and a plain `type: object` check rejected the
|
|
8
|
+
* second form outright — flagging documents the bundled meta-schema accepts, so
|
|
9
|
+
* the ruleset contradicted its own structural rule. When the headers are wrapped
|
|
10
|
+
* that way the check moves inside `schema`, and headers written in some other
|
|
11
|
+
* schema language are left alone, exactly as a non-AsyncAPI payload is.
|
|
12
|
+
*/
|
|
13
|
+
/** Options for {@link asyncApiHeadersObject}. */
|
|
14
|
+
export type IAsyncApiHeadersOptions = {
|
|
15
|
+
/**
|
|
16
|
+
* Whether `headers` may be a Multi Format Schema Object (`{ schemaFormat,
|
|
17
|
+
* schema }`). That shape is 3.0 only — in 2.x `headers` is a Schema Object and
|
|
18
|
+
* nothing else, so accepting the wrapper there let a `schema` key switch the
|
|
19
|
+
* check off entirely on a document that never had one.
|
|
20
|
+
*/
|
|
21
|
+
multiFormat?: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare const asyncApiHeadersObject: RulesetFunction<unknown, IAsyncApiHeadersOptions | undefined>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isObject } from "./helpers.js";
|
|
2
|
+
import { isAsyncApiSchemaFormat } from "./schema-format.js";
|
|
3
|
+
const MESSAGE = 'Headers schema type must be "object"';
|
|
4
|
+
const asyncApiHeadersObject = (headers, options, context) => {
|
|
5
|
+
if (typeof headers === "boolean")
|
|
6
|
+
return [{ message: MESSAGE, path: [...context.path] }];
|
|
7
|
+
if (!isObject(headers))
|
|
8
|
+
return [];
|
|
9
|
+
if (typeof headers["$ref"] === "string")
|
|
10
|
+
return [];
|
|
11
|
+
if (options?.multiFormat === true && Object.hasOwn(headers, "schema")) {
|
|
12
|
+
if (!isAsyncApiSchemaFormat(headers["schemaFormat"]))
|
|
13
|
+
return [];
|
|
14
|
+
const inner = headers["schema"];
|
|
15
|
+
if (!isObject(inner))
|
|
16
|
+
return [{ message: MESSAGE, path: [...context.path, "schema"] }];
|
|
17
|
+
if (typeof inner["$ref"] === "string" || inner["type"] === "object")
|
|
18
|
+
return [];
|
|
19
|
+
return [{ message: MESSAGE, path: [...context.path, "schema"] }];
|
|
20
|
+
}
|
|
21
|
+
return headers["type"] === "object" ? [] : [{ message: MESSAGE, path: [...context.path] }];
|
|
22
|
+
};
|
|
23
|
+
export {
|
|
24
|
+
asyncApiHeadersObject
|
|
25
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Checks every entry of a Message Object's `examples` against the message's own
|
|
4
|
+
* `payload` and `headers` schemas. Traits are folded in first, so an example is
|
|
5
|
+
* judged against the message a tool would actually assemble rather than against
|
|
6
|
+
* the half of it written inline.
|
|
7
|
+
*/
|
|
8
|
+
export declare const asyncApiMessageExamples: RulesetFunction;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { schema as schemaFunction } from "../../../functions/index.js";
|
|
2
|
+
import { isObject, mergeTraits } from "./helpers.js";
|
|
3
|
+
import { isAsyncApiSchemaFormat } from "./schema-format.js";
|
|
4
|
+
const examplesOrigin = (message) => {
|
|
5
|
+
const traits = message["traits"];
|
|
6
|
+
if (Array.isArray(traits)) {
|
|
7
|
+
for (let index = traits.length - 1; index >= 0; index--) {
|
|
8
|
+
const trait = traits[index];
|
|
9
|
+
if (isObject(trait) && Array.isArray(trait["examples"]))
|
|
10
|
+
return ["traits", index, "examples"];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return ["examples"];
|
|
14
|
+
};
|
|
15
|
+
const asyncApiMessageExamples = (input, _options, context) => {
|
|
16
|
+
if (!isObject(input))
|
|
17
|
+
return [];
|
|
18
|
+
const message = mergeTraits(input);
|
|
19
|
+
const examples = message["examples"];
|
|
20
|
+
if (!Array.isArray(examples))
|
|
21
|
+
return [];
|
|
22
|
+
const origin = examplesOrigin(input);
|
|
23
|
+
const payloadIsSchema = isAsyncApiSchemaFormat(message["schemaFormat"]);
|
|
24
|
+
const results = [];
|
|
25
|
+
examples.forEach((example, index) => {
|
|
26
|
+
if (!isObject(example))
|
|
27
|
+
return;
|
|
28
|
+
for (const part of ["payload", "headers"]) {
|
|
29
|
+
if (example[part] === void 0)
|
|
30
|
+
continue;
|
|
31
|
+
if (part === "payload" && !payloadIsSchema)
|
|
32
|
+
continue;
|
|
33
|
+
const findings = schemaFunction(example[part], { schema: isObject(message[part]) ? message[part] : {}, allErrors: true, skipUnusableSchema: true }, { ...context, path: [...context.path, ...origin, index, part] });
|
|
34
|
+
if (findings)
|
|
35
|
+
results.push(...findings);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return results;
|
|
39
|
+
};
|
|
40
|
+
export {
|
|
41
|
+
asyncApiMessageExamples
|
|
42
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getAllMessages, isObject } from "./helpers.js";
|
|
2
|
+
const effectiveMessageId = (message) => {
|
|
3
|
+
const traits = message["traits"];
|
|
4
|
+
if (Array.isArray(traits)) {
|
|
5
|
+
for (let index = traits.length - 1; index >= 0; index--) {
|
|
6
|
+
const trait = traits[index];
|
|
7
|
+
if (isObject(trait) && typeof trait["messageId"] === "string") {
|
|
8
|
+
return { id: trait["messageId"], path: ["traits", index, "messageId"] };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return typeof message["messageId"] === "string" ? { id: message["messageId"], path: ["messageId"] } : void 0;
|
|
13
|
+
};
|
|
14
|
+
const asyncApiMessageIdUnique = (document, _options, _context) => {
|
|
15
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16
|
+
const results = [];
|
|
17
|
+
for (const { path, message } of getAllMessages(document)) {
|
|
18
|
+
const found = effectiveMessageId(message);
|
|
19
|
+
if (found === void 0)
|
|
20
|
+
continue;
|
|
21
|
+
if (seen.has(found.id)) {
|
|
22
|
+
results.push({ message: `messageId "${found.id}" must be unique`, path: [...path, ...found.path] });
|
|
23
|
+
}
|
|
24
|
+
seen.add(found.id);
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
};
|
|
28
|
+
export {
|
|
29
|
+
asyncApiMessageIdUnique
|
|
30
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getAllOperations, isObject } from "./helpers.js";
|
|
2
|
+
const effectiveOperationId = (operation) => {
|
|
3
|
+
const traits = operation["traits"];
|
|
4
|
+
if (Array.isArray(traits)) {
|
|
5
|
+
for (let index = traits.length - 1; index >= 0; index--) {
|
|
6
|
+
const trait = traits[index];
|
|
7
|
+
if (isObject(trait) && typeof trait["operationId"] === "string") {
|
|
8
|
+
return { id: trait["operationId"], path: ["traits", index, "operationId"] };
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return typeof operation["operationId"] === "string" ? { id: operation["operationId"], path: ["operationId"] } : void 0;
|
|
13
|
+
};
|
|
14
|
+
const asyncApiOperationIdUnique = (document, _options, _context) => {
|
|
15
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16
|
+
const results = [];
|
|
17
|
+
for (const { path, operation } of getAllOperations(document)) {
|
|
18
|
+
const found = effectiveOperationId(operation);
|
|
19
|
+
if (found === void 0)
|
|
20
|
+
continue;
|
|
21
|
+
if (seen.has(found.id)) {
|
|
22
|
+
results.push({ message: `operationId "${found.id}" must be unique`, path: [...path, ...found.path] });
|
|
23
|
+
}
|
|
24
|
+
seen.add(found.id);
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
};
|
|
28
|
+
export {
|
|
29
|
+
asyncApiOperationIdUnique
|
|
30
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates a message's `payload` against the AsyncAPI Schema Object definition
|
|
4
|
+
* of the document's own version — the check that catches a payload using a JSON
|
|
5
|
+
* Schema keyword AsyncAPI does not allow, or a `type` that is not a type.
|
|
6
|
+
*
|
|
7
|
+
* Takes the whole message rather than the payload so that traits are folded in
|
|
8
|
+
* before the `schemaFormat` is read. A payload in Avro or Protobuf is not an
|
|
9
|
+
* AsyncAPI Schema Object at all, and a `schemaFormat` naming one of those can be
|
|
10
|
+
* contributed by a trait — invisible to a `given` filter, which is how an Avro
|
|
11
|
+
* payload came to be judged as JSON Schema and reported at error severity.
|
|
12
|
+
* `asyncapi-payload-unsupported-schemaFormat` reports those separately.
|
|
13
|
+
*/
|
|
14
|
+
export declare const asyncApiPayload: RulesetFunction;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { schema as schemaFunction } from "../../../functions/index.js";
|
|
2
|
+
import { asyncApiSchemaVersion, loadAsyncApiSchema } from "../schemas/index.js";
|
|
3
|
+
import { isObject, mergeTraits } from "./helpers.js";
|
|
4
|
+
import { isAsyncApiSchemaFormat } from "./schema-format.js";
|
|
5
|
+
const payloadSchemaId = (version) => `http://asyncapi.com/definitions/${version}.0/schema.json`;
|
|
6
|
+
const payloadSchemas = /* @__PURE__ */ new Map();
|
|
7
|
+
const payloadSchema = (version) => {
|
|
8
|
+
let wrapper = payloadSchemas.get(version);
|
|
9
|
+
if (!wrapper) {
|
|
10
|
+
const meta = loadAsyncApiSchema(version);
|
|
11
|
+
wrapper = { $ref: payloadSchemaId(version), definitions: meta.definitions };
|
|
12
|
+
payloadSchemas.set(version, wrapper);
|
|
13
|
+
}
|
|
14
|
+
return wrapper;
|
|
15
|
+
};
|
|
16
|
+
const asyncApiPayload = (message, _options, context) => {
|
|
17
|
+
if (!isObject(message))
|
|
18
|
+
return [];
|
|
19
|
+
const merged = mergeTraits(message);
|
|
20
|
+
if (!isAsyncApiSchemaFormat(merged["schemaFormat"]))
|
|
21
|
+
return [];
|
|
22
|
+
const payload = merged["payload"];
|
|
23
|
+
if (payload === void 0)
|
|
24
|
+
return [];
|
|
25
|
+
const version = asyncApiSchemaVersion(isObject(context.document.data) ? context.document.data["asyncapi"] : void 0);
|
|
26
|
+
if (version === void 0)
|
|
27
|
+
return [];
|
|
28
|
+
return schemaFunction(payload, { schema: payloadSchema(version), allErrors: true }, { ...context, path: [...context.path, "payload"] }) ?? [];
|
|
29
|
+
};
|
|
30
|
+
export {
|
|
31
|
+
asyncApiPayload
|
|
32
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/types.js';
|
|
2
|
+
/** Options for {@link asyncApiSchemaValidation}: which sibling of the schema to check. */
|
|
3
|
+
export type IAsyncApiSchemaValidationOptions = {
|
|
4
|
+
type: 'default' | 'examples';
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Validates a Schema Object's own `default` or `examples` against that same
|
|
8
|
+
* schema. The rule targets the schema (via a `^` parent selector), so the input
|
|
9
|
+
* here is the schema and the values under test sit inside it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const asyncApiSchemaValidation: RulesetFunction<unknown, IAsyncApiSchemaValidationOptions>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { schema as schemaFunction } from "../../../functions/index.js";
|
|
2
|
+
import { isObject } from "./helpers.js";
|
|
3
|
+
const asyncApiSchemaValidation = (input, options, context) => {
|
|
4
|
+
if (!isObject(input) || options?.type === void 0)
|
|
5
|
+
return [];
|
|
6
|
+
const targets = options.type === "default" ? [{ path: ["default"], value: input["default"] }] : Array.isArray(input["examples"]) ? input["examples"].map((value, index) => ({ path: ["examples", index], value })) : [];
|
|
7
|
+
const results = [];
|
|
8
|
+
for (const target of targets) {
|
|
9
|
+
const findings = schemaFunction(target.value, { schema: input, allErrors: true, skipUnusableSchema: true }, { ...context, path: [...context.path, ...target.path] });
|
|
10
|
+
if (findings)
|
|
11
|
+
results.push(...findings);
|
|
12
|
+
}
|
|
13
|
+
return results;
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
asyncApiSchemaValidation
|
|
17
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/types.js';
|
|
2
|
+
/** Options for {@link asyncApiSecurity}: which object the requirement hangs off, for the message. */
|
|
3
|
+
export type IAsyncApiSecurityOptions = {
|
|
4
|
+
objectType: 'Server' | 'Operation';
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Checks that a security entry points at a security scheme the document
|
|
8
|
+
* declares. The two spec majors spell that entry differently, so the shape
|
|
9
|
+
* decides which check runs:
|
|
10
|
+
*
|
|
11
|
+
* - **2.x** — a Security Requirement Object: `{ <schemeName>: [<scope>, …] }`.
|
|
12
|
+
* Every key must name a declared scheme, and for an OAuth2 scheme every
|
|
13
|
+
* requested scope must be one its flows declare.
|
|
14
|
+
* - **3.x** — a Security Scheme Object or a Reference to one. A reference must
|
|
15
|
+
* point into `#/components/securitySchemes/` and name a scheme that exists; an
|
|
16
|
+
* inline scheme carries its own definition and has nothing to cross-check
|
|
17
|
+
* (the structural rules validate its shape).
|
|
18
|
+
*
|
|
19
|
+
* The lookup goes through the raw document, since a requirement names its scheme
|
|
20
|
+
* by key rather than by holding it.
|
|
21
|
+
*/
|
|
22
|
+
export declare const asyncApiSecurity: RulesetFunction<unknown, IAsyncApiSecurityOptions>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { isObject } from "./helpers.js";
|
|
2
|
+
import { pointerSegment } from "./pointer.js";
|
|
3
|
+
const OAUTH2_FLOWS = ["implicit", "password", "clientCredentials", "authorizationCode"];
|
|
4
|
+
const SECURITY_SCHEMES_REF = "#/components/securitySchemes/";
|
|
5
|
+
const declaredScopes = (flows) => {
|
|
6
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
7
|
+
if (!isObject(flows))
|
|
8
|
+
return scopes;
|
|
9
|
+
for (const name of OAUTH2_FLOWS) {
|
|
10
|
+
const flow = flows[name];
|
|
11
|
+
if (!isObject(flow) || !isObject(flow["scopes"]))
|
|
12
|
+
continue;
|
|
13
|
+
for (const scope of Object.keys(flow["scopes"]))
|
|
14
|
+
scopes.add(scope);
|
|
15
|
+
}
|
|
16
|
+
return scopes;
|
|
17
|
+
};
|
|
18
|
+
const securitySchemes = (data) => {
|
|
19
|
+
const components = isObject(data) ? data["components"] : void 0;
|
|
20
|
+
const schemes = isObject(components) ? components["securitySchemes"] : void 0;
|
|
21
|
+
return isObject(schemes) ? schemes : {};
|
|
22
|
+
};
|
|
23
|
+
const isDeclared = (schemes, name) => Object.hasOwn(schemes, name);
|
|
24
|
+
const asyncApiSecurity = (entry, options, context) => {
|
|
25
|
+
if (!isObject(entry))
|
|
26
|
+
return [];
|
|
27
|
+
const objectType = options?.objectType ?? "Operation";
|
|
28
|
+
const schemes = securitySchemes(context.document.data);
|
|
29
|
+
const ref = entry["$ref"];
|
|
30
|
+
if (typeof ref === "string") {
|
|
31
|
+
if (!ref.startsWith(SECURITY_SCHEMES_REF)) {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
message: `${objectType} security must reference "${SECURITY_SCHEMES_REF}\u2026", not "${ref}"`,
|
|
35
|
+
path: [...context.path, "$ref"]
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
const name = pointerSegment(ref.slice(SECURITY_SCHEMES_REF.length));
|
|
40
|
+
return isDeclared(schemes, name) ? [] : [
|
|
41
|
+
{
|
|
42
|
+
message: `${objectType} security requirement "${name}" is not a defined security scheme`,
|
|
43
|
+
path: [...context.path, "$ref"]
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
if (typeof entry["type"] === "string")
|
|
48
|
+
return [];
|
|
49
|
+
const results = [];
|
|
50
|
+
for (const key of Object.keys(entry)) {
|
|
51
|
+
const requested = entry[key];
|
|
52
|
+
if (!Array.isArray(requested))
|
|
53
|
+
continue;
|
|
54
|
+
if (!isDeclared(schemes, key)) {
|
|
55
|
+
results.push({
|
|
56
|
+
message: `${objectType} security requirement "${key}" is not a defined security scheme`,
|
|
57
|
+
path: [...context.path, key]
|
|
58
|
+
});
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const scheme = schemes[key];
|
|
62
|
+
if (!isObject(scheme) || scheme["type"] !== "oauth2")
|
|
63
|
+
continue;
|
|
64
|
+
const available = declaredScopes(scheme["flows"]);
|
|
65
|
+
requested.forEach((scope, index) => {
|
|
66
|
+
if (typeof scope === "string" && !available.has(scope)) {
|
|
67
|
+
results.push({
|
|
68
|
+
message: `Security scope "${scope}" is not declared by the "${key}" scheme. Available: [${[...available].join(", ")}]`,
|
|
69
|
+
path: [...context.path, key, index]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return results;
|
|
75
|
+
};
|
|
76
|
+
export {
|
|
77
|
+
asyncApiSecurity
|
|
78
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { JsonPath } from '../../../core/types.js';
|
|
2
|
+
export { isObject } from '../../shared/helpers.js';
|
|
3
|
+
/** The two operation fields an AsyncAPI 2.x Channel Item Object may carry. */
|
|
4
|
+
export declare const V2_OPERATIONS: readonly ['subscribe', 'publish'];
|
|
5
|
+
/** One AsyncAPI 2.x operation, with the path it sits at. */
|
|
6
|
+
export type LocatedOperation = {
|
|
7
|
+
path: JsonPath;
|
|
8
|
+
kind: (typeof V2_OPERATIONS)[number];
|
|
9
|
+
operation: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
/** One AsyncAPI 2.x message, with the path it sits at. */
|
|
12
|
+
export type LocatedMessage = {
|
|
13
|
+
path: JsonPath;
|
|
14
|
+
message: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Every `{variable}` name in a channel address or server URL, in order. A name
|
|
18
|
+
* is a run of non-`}` characters, so `{}` yields nothing (the dedicated
|
|
19
|
+
* `*-no-empty-parameter` rules report that).
|
|
20
|
+
*
|
|
21
|
+
* The character class matters: a lazy `.+?` swallowed the closing brace of `{}`
|
|
22
|
+
* and ran on to the next one, so `a/{}/b/{id}` yielded the single name
|
|
23
|
+
* `}/b/{id` — a parameter nobody wrote, while the one that really was
|
|
24
|
+
* undescribed went unnamed.
|
|
25
|
+
*/
|
|
26
|
+
export declare const parseUrlVariables: (value: unknown) => string[];
|
|
27
|
+
/**
|
|
28
|
+
* Walks the `publish`/`subscribe` operations of every 2.x channel — both the
|
|
29
|
+
* channels the document serves and the reusable ones under
|
|
30
|
+
* `components.channels`. A reusable channel is a declaration like any other, so
|
|
31
|
+
* a rule asking for "every operation" has to see it; leaving it out meant a
|
|
32
|
+
* duplicate `operationId` between a served channel and a reusable one went
|
|
33
|
+
* unreported.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getAllOperations(document: unknown): Generator<LocatedOperation>;
|
|
36
|
+
/**
|
|
37
|
+
* Walks every 2.x message: each operation's `message` (or, when that message is a
|
|
38
|
+
* `oneOf` list, each alternative in it), plus every reusable message under
|
|
39
|
+
* `components.messages`. These are the same locations the ruleset's `V2_MESSAGES`
|
|
40
|
+
* enumerates, so "every message" means the same thing to a rule and to this
|
|
41
|
+
* walker.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getAllMessages(document: unknown): Generator<LocatedMessage>;
|
|
44
|
+
/**
|
|
45
|
+
* Folds a Message (or Operation) Object's `traits` into the object itself, the
|
|
46
|
+
* way a 2.x tool resolving the document would. The spec applies each trait as a
|
|
47
|
+
* JSON Merge Patch in declaration order, so a trait overrides what the object
|
|
48
|
+
* (and any earlier trait) declared. Returns the input unchanged when it declares
|
|
49
|
+
* no `traits`.
|
|
50
|
+
*/
|
|
51
|
+
export declare const mergeTraits: (target: Record<string, unknown>) => Record<string, unknown>;
|