@amritk/asyncapi 0.1.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 +35 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/detect-version.d.ts +14 -0
- package/dist/detect-version.js +16 -0
- package/dist/extract-async-api.d.ts +13 -0
- package/dist/extract-async-api.js +25 -0
- package/dist/extract-channels-v2.d.ts +16 -0
- package/dist/extract-channels-v2.js +71 -0
- package/dist/extract-channels-v3.d.ts +10 -0
- package/dist/extract-channels-v3.js +208 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +12 -0
- package/dist/merge-traits.d.ts +25 -0
- package/dist/merge-traits.js +51 -0
- package/dist/message-schemas.d.ts +19 -0
- package/dist/message-schemas.js +62 -0
- package/dist/normalize-message.d.ts +29 -0
- package/dist/normalize-message.js +36 -0
- package/dist/normalize-schema.d.ts +24 -0
- package/dist/normalize-schema.js +18 -0
- package/dist/rebase-component-refs.d.ts +40 -0
- package/dist/rebase-component-refs.js +349 -0
- package/dist/resolve-pointer.d.ts +21 -0
- package/dist/resolve-pointer.js +61 -0
- package/dist/schema-format.d.ts +16 -0
- package/dist/schema-format.js +20 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.js +0 -0
- package/dist/unwrap-multi-format.d.ts +14 -0
- package/dist/unwrap-multi-format.js +14 -0
- package/package.json +58 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { toKebabCase } from "@amritk/helpers/ref-to-filename";
|
|
2
|
+
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
3
|
+
const sanitizeToken = (value, fallback) => {
|
|
4
|
+
const token = toKebabCase(value).replace(/[^\p{ID_Continue}.]+/gu, "-").replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
|
5
|
+
return token === "" ? fallback : token;
|
|
6
|
+
};
|
|
7
|
+
const claimToken = (base, taken, issues, path) => {
|
|
8
|
+
let token = base;
|
|
9
|
+
for (let n = 2; taken.has(token); n++) {
|
|
10
|
+
token = `${base}-${n}`;
|
|
11
|
+
}
|
|
12
|
+
if (token !== base) {
|
|
13
|
+
issues.push({ path, message: `output name "${base}" already claimed; using "${token}"` });
|
|
14
|
+
}
|
|
15
|
+
taken.add(token);
|
|
16
|
+
return token;
|
|
17
|
+
};
|
|
18
|
+
const claimMessageToken = (base, taken, issues, path) => {
|
|
19
|
+
let token = base;
|
|
20
|
+
for (let n = 2; taken.has(token) || taken.has(`${token}-headers`); n++) {
|
|
21
|
+
token = `${base}-${n}`;
|
|
22
|
+
}
|
|
23
|
+
if (token !== base) {
|
|
24
|
+
issues.push({ path, message: `output name "${base}" already claimed; using "${token}"` });
|
|
25
|
+
}
|
|
26
|
+
taken.add(token);
|
|
27
|
+
taken.add(`${token}-headers`);
|
|
28
|
+
return token;
|
|
29
|
+
};
|
|
30
|
+
const listMessageSchemas = (model, issues) => {
|
|
31
|
+
const collected = issues ?? model.issues;
|
|
32
|
+
const schemas = [];
|
|
33
|
+
const channelTokens = /* @__PURE__ */ new Set();
|
|
34
|
+
for (const channel of model.channels) {
|
|
35
|
+
const channelToken = claimToken(sanitizeToken(channel.key, "channel"), channelTokens, collected, `#/channels/${channel.key}`);
|
|
36
|
+
const messageTokens = /* @__PURE__ */ new Set();
|
|
37
|
+
for (const message of channel.messages) {
|
|
38
|
+
if (message.payload === void 0 && message.headers === void 0)
|
|
39
|
+
continue;
|
|
40
|
+
const messageToken = claimMessageToken(sanitizeToken(message.name, "message"), messageTokens, collected, `#/channels/${channel.key}/messages/${message.name}`);
|
|
41
|
+
const rootTypeName = refToName(messageToken);
|
|
42
|
+
if (message.payload !== void 0) {
|
|
43
|
+
schemas.push({
|
|
44
|
+
subDir: `channels/${channelToken}/${messageToken}`,
|
|
45
|
+
rootTypeName,
|
|
46
|
+
schema: message.payload
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (message.headers !== void 0) {
|
|
50
|
+
schemas.push({
|
|
51
|
+
subDir: `channels/${channelToken}/${messageToken}-headers`,
|
|
52
|
+
rootTypeName: `${rootTypeName}Headers`,
|
|
53
|
+
schema: message.headers
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return schemas;
|
|
59
|
+
};
|
|
60
|
+
export {
|
|
61
|
+
listMessageSchemas
|
|
62
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ExtractionIssue, MessageDirection, NormalizedMessage } from './types.js';
|
|
2
|
+
export type RawMessage = {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly channelKey: string;
|
|
5
|
+
readonly direction?: MessageDirection;
|
|
6
|
+
readonly contentType?: string;
|
|
7
|
+
/** The message's effective payload `schemaFormat` (post-trait-merge / unwrapped). */
|
|
8
|
+
readonly payloadSchemaFormat?: unknown;
|
|
9
|
+
readonly payload?: unknown;
|
|
10
|
+
/**
|
|
11
|
+
* The headers' own format: in 2.x headers are always an AsyncAPI Schema
|
|
12
|
+
* Object whatever the payload declares, while a 3.0 headers value may carry
|
|
13
|
+
* its own Multi Format wrapper — so the two cannot share one field.
|
|
14
|
+
*/
|
|
15
|
+
readonly headersSchemaFormat?: unknown;
|
|
16
|
+
readonly headers?: unknown;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Turns one raw message (already dereferenced and trait-merged by the version
|
|
20
|
+
* walkers) into a {@link NormalizedMessage}, normalizing its payload and
|
|
21
|
+
* headers into self-contained 2020-12 schemas.
|
|
22
|
+
*
|
|
23
|
+
* A schema whose format is not a JSON Schema dialect — Avro, Protobuf, a
|
|
24
|
+
* malformed value — is skipped with an issue naming the format, keeping the
|
|
25
|
+
* message itself in the model so a consumer can still see it exists. A
|
|
26
|
+
* non-object schema (AsyncAPI allows boolean schemas; the generators need an
|
|
27
|
+
* object root) is skipped the same way.
|
|
28
|
+
*/
|
|
29
|
+
export declare const normalizeMessage: (raw: RawMessage, document: unknown, issues: ExtractionIssue[], path: string) => NormalizedMessage;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { normalizeSchema } from "./normalize-schema.js";
|
|
2
|
+
import { rebaseComponentRefs } from "./rebase-component-refs.js";
|
|
3
|
+
import { classifySchemaFormat } from "./schema-format.js";
|
|
4
|
+
const normalizeMessage = (raw, document, issues, path) => {
|
|
5
|
+
const normalizeOne = (value, schemaFormat, label) => {
|
|
6
|
+
if (value === void 0)
|
|
7
|
+
return void 0;
|
|
8
|
+
const family = classifySchemaFormat(schemaFormat);
|
|
9
|
+
if (family === "unsupported") {
|
|
10
|
+
issues.push({
|
|
11
|
+
path: `${path}/${label}`,
|
|
12
|
+
message: `skipped: unsupported schemaFormat ${JSON.stringify(schemaFormat)} (not a JSON Schema dialect)`
|
|
13
|
+
});
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
17
|
+
issues.push({ path: `${path}/${label}`, message: `skipped: ${label} is not an object schema` });
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
return rebaseComponentRefs(normalizeSchema(value, family), document, family, issues, `${path}/${label}`);
|
|
21
|
+
};
|
|
22
|
+
const payload = normalizeOne(raw.payload, raw.payloadSchemaFormat, "payload");
|
|
23
|
+
const headers = normalizeOne(raw.headers, raw.headersSchemaFormat, "headers");
|
|
24
|
+
return {
|
|
25
|
+
name: raw.name,
|
|
26
|
+
channelKey: raw.channelKey,
|
|
27
|
+
...raw.direction !== void 0 ? { direction: raw.direction } : {},
|
|
28
|
+
...raw.contentType !== void 0 ? { contentType: raw.contentType } : {},
|
|
29
|
+
...typeof raw.payloadSchemaFormat === "string" ? { schemaFormat: raw.payloadSchemaFormat } : {},
|
|
30
|
+
...payload !== void 0 ? { payload } : {},
|
|
31
|
+
...headers !== void 0 ? { headers } : {}
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export {
|
|
35
|
+
normalizeMessage
|
|
36
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SchemaFormatFamily } from './schema-format.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes one extracted schema into the 2020-12 conventions the generators
|
|
4
|
+
* expect, according to the dialect its `schemaFormat` named:
|
|
5
|
+
*
|
|
6
|
+
* - `'asyncapi'` / `'draft-07'` — run the draft-07 upgrade (`definitions` →
|
|
7
|
+
* `$defs`, refs rewritten, nested defs hoisted). The upgrade only fires on a
|
|
8
|
+
* schema that *declares* draft-07, and an AsyncAPI payload almost never
|
|
9
|
+
* carries `$schema` at all, so the declaration is stamped on first: the
|
|
10
|
+
* dialect was declared at the message level, out of the schema's sight. The
|
|
11
|
+
* upgrade strips the stamp again on output.
|
|
12
|
+
* - `'openapi'` — fold `nullable: true` into the `type` list, the one 3.0-ism
|
|
13
|
+
* the generators would otherwise read as "never null".
|
|
14
|
+
* - `'2020-12'` — pass through.
|
|
15
|
+
*
|
|
16
|
+
* Keywords the AsyncAPI dialect adds beyond draft-07 (and draft-07 spellings
|
|
17
|
+
* the upgrade does not rewrite, like array-form `items`) pass through
|
|
18
|
+
* unchanged: `@amritk/runtime-validators` implements them directly, and an
|
|
19
|
+
* unknown keyword is an annotation everywhere else in the pipeline.
|
|
20
|
+
*
|
|
21
|
+
* An empty `$defs` left behind by the upgrade is dropped so a schema with no
|
|
22
|
+
* definitions round-trips without growing keys.
|
|
23
|
+
*/
|
|
24
|
+
export declare const normalizeSchema: (schema: Record<string, unknown>, family: Exclude<SchemaFormatFamily, 'unsupported'>) => Record<string, unknown>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { foldNullable } from "@amritk/helpers/fold-nullable";
|
|
2
|
+
import { upgradeDraft07Schema } from "@amritk/helpers/upgrade-draft07-schema";
|
|
3
|
+
const normalizeSchema = (schema, family) => {
|
|
4
|
+
if (family === "openapi")
|
|
5
|
+
return foldNullable(schema);
|
|
6
|
+
if (family === "2020-12")
|
|
7
|
+
return schema;
|
|
8
|
+
const upgraded = upgradeDraft07Schema({ ...schema, $schema: "http://json-schema.org/draft-07/schema" });
|
|
9
|
+
const defs = upgraded["$defs"];
|
|
10
|
+
if (typeof defs === "object" && defs !== null && Object.keys(defs).length === 0) {
|
|
11
|
+
const { $defs: _, ...rest } = upgraded;
|
|
12
|
+
return rest;
|
|
13
|
+
}
|
|
14
|
+
return upgraded;
|
|
15
|
+
};
|
|
16
|
+
export {
|
|
17
|
+
normalizeSchema
|
|
18
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SchemaFormatFamily } from './schema-format.js';
|
|
2
|
+
import type { ExtractionIssue } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Makes an extracted schema self-contained: every `$ref` into the document's
|
|
5
|
+
* `#/components/schemas/...` is rewritten to a local `#/$defs/...` entry, and
|
|
6
|
+
* the referenced components — transitively, since components reference each
|
|
7
|
+
* other — are copied in beside it, normalized with the same dialect rules as
|
|
8
|
+
* the root.
|
|
9
|
+
*
|
|
10
|
+
* Copying rather than leaving document-relative pointers is what lets each
|
|
11
|
+
* message's schema stand alone as a generator input; the cost is that a
|
|
12
|
+
* component used by N messages appears in N output trees, the same trade
|
|
13
|
+
* `--schema-dir` makes across schema files. Component names keep their exact
|
|
14
|
+
* spelling as `$defs` keys so the rewritten pointers resolve; a name the root
|
|
15
|
+
* schema already claims in its own `$defs` (or one needing pointer escapes) is
|
|
16
|
+
* moved to a `component-` prefixed key instead, consistently across every ref
|
|
17
|
+
* to it.
|
|
18
|
+
*
|
|
19
|
+
* A copied component's own definitions cannot stay nested: normalization
|
|
20
|
+
* renames a draft-07 `definitions` block to a component-root `$defs` and
|
|
21
|
+
* rewrites the component's internal refs to `#/$defs/...` — pointers that,
|
|
22
|
+
* embedded under the message root, would resolve against the *root's* `$defs`
|
|
23
|
+
* and land on nothing (or on the wrong schema). So each copied component's
|
|
24
|
+
* definitions — from `$defs` *and* from a `definitions` block a
|
|
25
|
+
* 2020-12/OpenAPI component keeps verbatim, each block under its own key —
|
|
26
|
+
* are hoisted to the root under `<component>-<name>` keys, with the
|
|
27
|
+
* component's internal refs and any external ref whose pointer tail dives
|
|
28
|
+
* through a block re-aimed at the hoisted entries.
|
|
29
|
+
*
|
|
30
|
+
* Two more pointer shapes are handled the same way: a tail through a Multi
|
|
31
|
+
* Format component's `schema` wrapper key (the copy is unwrapped, so the hop
|
|
32
|
+
* is stripped), and a document-root `#/$defs/...` reference — which the
|
|
33
|
+
* cross-file resolver manufactures when it hoists a reference cycle onto the
|
|
34
|
+
* document root — whose target is copied in beside the components.
|
|
35
|
+
*
|
|
36
|
+
* A reference to anything the document does not declare becomes an empty
|
|
37
|
+
* (`{}`, match-anything) definition plus an issue — one dangling pointer
|
|
38
|
+
* should cost precision on one branch, not the whole message.
|
|
39
|
+
*/
|
|
40
|
+
export declare const rebaseComponentRefs: (root: Record<string, unknown>, document: unknown, family: Exclude<SchemaFormatFamily, 'unsupported'>, issues: ExtractionIssue[], path: string) => Record<string, unknown>;
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { assignKey } from "@amritk/helpers/assign-key";
|
|
2
|
+
import { entersSchemaMap, isDataPosition } from "@amritk/helpers/build-resource-registry";
|
|
3
|
+
import { assertSchemaDepth } from "@amritk/helpers/max-schema-depth";
|
|
4
|
+
import { readKey } from "@amritk/helpers/read-key";
|
|
5
|
+
import { normalizeSchema } from "./normalize-schema.js";
|
|
6
|
+
import { classifySchemaFormat } from "./schema-format.js";
|
|
7
|
+
import { unwrapMultiFormat } from "./unwrap-multi-format.js";
|
|
8
|
+
const COMPONENT_SCHEMA_REF = /^#\/components\/schemas\/([^/]+)(\/.*)?$/;
|
|
9
|
+
const TAIL_THROUGH_DEFS = /^\/(definitions|\$defs)\/([^/]+)(\/.*)?$/;
|
|
10
|
+
const LOCAL_DEFS_REF = /^#\/(\$defs|definitions)\/([^/]+)(\/.*)?$/;
|
|
11
|
+
const NAME_MAP_KEYWORDS = /* @__PURE__ */ new Set(["properties", "patternProperties", "dependentSchemas", "dependencies"]);
|
|
12
|
+
const decodeSegment = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
13
|
+
const KEYWORD_SEGMENTS = /* @__PURE__ */ new Set(["definitions", "$defs", "schema", "schemaFormat"]);
|
|
14
|
+
const decodeKeywordSegments = (pointer) => pointer.split("/").map((segment) => {
|
|
15
|
+
if (!segment.includes("%"))
|
|
16
|
+
return segment;
|
|
17
|
+
try {
|
|
18
|
+
const decoded = decodeURIComponent(segment);
|
|
19
|
+
return KEYWORD_SEGMENTS.has(decoded) ? decoded : segment;
|
|
20
|
+
} catch {
|
|
21
|
+
return segment;
|
|
22
|
+
}
|
|
23
|
+
}).join("/");
|
|
24
|
+
const divesThroughDefs = (tail) => {
|
|
25
|
+
let expectName = false;
|
|
26
|
+
for (const rawSegment of tail.split("/").slice(1)) {
|
|
27
|
+
const segment = decodeSegment(rawSegment);
|
|
28
|
+
if (expectName) {
|
|
29
|
+
expectName = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (segment === "definitions" || segment === "$defs")
|
|
33
|
+
return true;
|
|
34
|
+
if (NAME_MAP_KEYWORDS.has(segment))
|
|
35
|
+
expectName = true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
};
|
|
39
|
+
const sanitizeKey = (name) => name.replace(/[~/%]+/g, "-");
|
|
40
|
+
const ROOT_SCOPE = { kind: "root" };
|
|
41
|
+
const DOC_DEF_SCOPE = { kind: "docDef" };
|
|
42
|
+
const rebaseComponentRefs = (root, document, family, issues, path) => {
|
|
43
|
+
const documentRecord = typeof document === "object" && document !== null && !Array.isArray(document) ? document : void 0;
|
|
44
|
+
const componentSchemas = documentRecord ? readKey(readKey(documentRecord, "components") ?? {}, "schemas") : void 0;
|
|
45
|
+
const documentDefs = (() => {
|
|
46
|
+
const block = documentRecord === void 0 ? void 0 : readKey(documentRecord, "$defs");
|
|
47
|
+
return typeof block === "object" && block !== null && !Array.isArray(block) ? block : void 0;
|
|
48
|
+
})();
|
|
49
|
+
const rootDefs = readKey(root, "$defs");
|
|
50
|
+
const rootOwnDefNames = new Set(typeof rootDefs === "object" && rootDefs !== null ? Object.keys(rootDefs) : []);
|
|
51
|
+
const taken = new Set(rootOwnDefNames);
|
|
52
|
+
const keyByName = /* @__PURE__ */ new Map();
|
|
53
|
+
const defKeyByName = /* @__PURE__ */ new Map();
|
|
54
|
+
const defKeyEntries = [];
|
|
55
|
+
const docDefKeyByName = /* @__PURE__ */ new Map();
|
|
56
|
+
const unresolvable = [];
|
|
57
|
+
const componentQueue = [];
|
|
58
|
+
const docDefQueue = [];
|
|
59
|
+
const claimKey = (preferred) => {
|
|
60
|
+
let key = preferred;
|
|
61
|
+
for (let n = 2; taken.has(key); n++)
|
|
62
|
+
key = `${preferred}-${n}`;
|
|
63
|
+
taken.add(key);
|
|
64
|
+
return key;
|
|
65
|
+
};
|
|
66
|
+
const allocateKey = (name) => {
|
|
67
|
+
const existing = keyByName.get(name);
|
|
68
|
+
if (existing !== void 0)
|
|
69
|
+
return existing;
|
|
70
|
+
const key = taken.has(name) || /[~/%]/.test(name) ? claimKey(`component-${sanitizeKey(name)}`) : claimKey(name);
|
|
71
|
+
keyByName.set(name, key);
|
|
72
|
+
componentQueue.push(name);
|
|
73
|
+
return key;
|
|
74
|
+
};
|
|
75
|
+
const canonicalName = (segment) => {
|
|
76
|
+
const raw = decodeSegment(segment);
|
|
77
|
+
if (componentSchemas !== void 0 && readKey(componentSchemas, raw) !== void 0)
|
|
78
|
+
return raw;
|
|
79
|
+
if (raw.includes("%")) {
|
|
80
|
+
try {
|
|
81
|
+
const decoded = decodeURIComponent(raw);
|
|
82
|
+
if (componentSchemas !== void 0 && readKey(componentSchemas, decoded) !== void 0)
|
|
83
|
+
return decoded;
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return raw;
|
|
88
|
+
};
|
|
89
|
+
const allocateDefKey = (componentName, block, defName) => {
|
|
90
|
+
const mapKey = JSON.stringify([componentName, block, defName]);
|
|
91
|
+
const existing = defKeyByName.get(mapKey);
|
|
92
|
+
if (existing !== void 0)
|
|
93
|
+
return existing;
|
|
94
|
+
const componentKey = allocateKey(componentName);
|
|
95
|
+
const key = claimKey(`${componentKey}-${sanitizeKey(defName)}`);
|
|
96
|
+
defKeyByName.set(mapKey, key);
|
|
97
|
+
defKeyEntries.push({ component: componentName, block, defName, key });
|
|
98
|
+
return key;
|
|
99
|
+
};
|
|
100
|
+
const allocateDocDefKey = (name) => {
|
|
101
|
+
const existing = docDefKeyByName.get(name);
|
|
102
|
+
if (existing !== void 0)
|
|
103
|
+
return existing;
|
|
104
|
+
const key = /[~/%]/.test(name) ? claimKey(sanitizeKey(name)) : claimKey(name);
|
|
105
|
+
docDefKeyByName.set(name, key);
|
|
106
|
+
docDefQueue.push(name);
|
|
107
|
+
return key;
|
|
108
|
+
};
|
|
109
|
+
const isWrappedComponent = (name) => {
|
|
110
|
+
const raw = componentSchemas === void 0 ? void 0 : readKey(componentSchemas, name);
|
|
111
|
+
return raw !== void 0 && unwrapMultiFormat(raw).schemaFormat !== void 0;
|
|
112
|
+
};
|
|
113
|
+
const componentBlockNames = (name) => {
|
|
114
|
+
const raw = componentSchemas === void 0 ? void 0 : readKey(componentSchemas, name);
|
|
115
|
+
if (raw === void 0)
|
|
116
|
+
return void 0;
|
|
117
|
+
const { schemaFormat, schema } = unwrapMultiFormat(raw);
|
|
118
|
+
const componentFamily = schemaFormat === void 0 ? family : classifySchemaFormat(schemaFormat);
|
|
119
|
+
if (componentFamily === "unsupported" || typeof schema !== "object" || schema === null || Array.isArray(schema)) {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
const names = { definitions: /* @__PURE__ */ new Set(), $defs: /* @__PURE__ */ new Set() };
|
|
123
|
+
for (const blockName of ["definitions", "$defs"]) {
|
|
124
|
+
const block = readKey(schema, blockName);
|
|
125
|
+
if (typeof block !== "object" || block === null || Array.isArray(block))
|
|
126
|
+
continue;
|
|
127
|
+
for (const key of Object.keys(block))
|
|
128
|
+
names[blockName].add(key);
|
|
129
|
+
}
|
|
130
|
+
return names;
|
|
131
|
+
};
|
|
132
|
+
const canonicalDefName = (segment, declared) => {
|
|
133
|
+
const raw = decodeSegment(segment);
|
|
134
|
+
if (declared(raw))
|
|
135
|
+
return raw;
|
|
136
|
+
if (raw.includes("%")) {
|
|
137
|
+
try {
|
|
138
|
+
const decoded = decodeURIComponent(raw);
|
|
139
|
+
if (declared(decoded))
|
|
140
|
+
return decoded;
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return raw;
|
|
145
|
+
};
|
|
146
|
+
const degrade = (ref, reason) => {
|
|
147
|
+
const key = claimKey("unsupported-pointer");
|
|
148
|
+
unresolvable.push({ key, ref, reason });
|
|
149
|
+
return `#/$defs/${key}`;
|
|
150
|
+
};
|
|
151
|
+
const rewriteRef = (ref, scope) => {
|
|
152
|
+
const component = COMPONENT_SCHEMA_REF.exec(ref);
|
|
153
|
+
if (component) {
|
|
154
|
+
const name = canonicalName(component[1]);
|
|
155
|
+
let tail = decodeKeywordSegments(component[2] ?? "");
|
|
156
|
+
if (tail !== "" && isWrappedComponent(name)) {
|
|
157
|
+
const wrapperHop = /^\/schema(\/.*)?$/.exec(tail);
|
|
158
|
+
if (wrapperHop)
|
|
159
|
+
tail = wrapperHop[1] ?? "";
|
|
160
|
+
else if (/^\/schemaFormat(\/|$)/.test(tail))
|
|
161
|
+
return degrade(ref, "points into a Multi Format wrapper key the unwrapped copy does not keep");
|
|
162
|
+
}
|
|
163
|
+
const throughDefs = TAIL_THROUGH_DEFS.exec(tail);
|
|
164
|
+
if (throughDefs) {
|
|
165
|
+
const rest = throughDefs[3] ?? "";
|
|
166
|
+
if (divesThroughDefs(rest))
|
|
167
|
+
return degrade(ref, "dives through nested definitions, which rebasing does not support");
|
|
168
|
+
const block = throughDefs[1];
|
|
169
|
+
const blocks = componentBlockNames(name);
|
|
170
|
+
const defName = canonicalDefName(throughDefs[2], (candidate) => blocks !== void 0 && (blocks[block].has(candidate) || blocks.$defs.has(candidate)));
|
|
171
|
+
if (rest !== "") {
|
|
172
|
+
const declared = blocks !== void 0 && (blocks[block].has(defName) || blocks.$defs.has(defName) || blocks.definitions.has(defName));
|
|
173
|
+
if (!declared)
|
|
174
|
+
return degrade(ref, "points below a definition its component does not declare");
|
|
175
|
+
}
|
|
176
|
+
return `#/$defs/${allocateDefKey(name, block, defName)}${rest}`;
|
|
177
|
+
}
|
|
178
|
+
if (tail !== "" && componentBlockNames(name) === void 0)
|
|
179
|
+
return degrade(ref, "points into a component that cannot be copied");
|
|
180
|
+
if (tail !== "" && divesThroughDefs(tail))
|
|
181
|
+
return degrade(ref, "points through a definitions block in a form rebasing does not support");
|
|
182
|
+
return `#/$defs/${allocateKey(name)}${tail}`;
|
|
183
|
+
}
|
|
184
|
+
const local = LOCAL_DEFS_REF.exec(decodeKeywordSegments(ref));
|
|
185
|
+
if (local) {
|
|
186
|
+
const block = local[1];
|
|
187
|
+
const tail = local[3] ?? "";
|
|
188
|
+
const nestedTail = tail !== "" && divesThroughDefs(tail);
|
|
189
|
+
const degradeNestedTail = () => degrade(ref, "dives through nested definitions, which rebasing does not support");
|
|
190
|
+
if (scope.kind === "component") {
|
|
191
|
+
const defName = canonicalDefName(local[2], (candidate) => scope.blocks[block].has(candidate) || scope.blocks.$defs.has(candidate));
|
|
192
|
+
if (scope.blocks[block].has(defName)) {
|
|
193
|
+
if (nestedTail)
|
|
194
|
+
return degradeNestedTail();
|
|
195
|
+
return `#/$defs/${allocateDefKey(scope.component, block, defName)}${tail}`;
|
|
196
|
+
}
|
|
197
|
+
if (block === "definitions" && scope.blocks.$defs.has(defName)) {
|
|
198
|
+
if (nestedTail)
|
|
199
|
+
return degradeNestedTail();
|
|
200
|
+
return `#/$defs/${allocateDefKey(scope.component, "$defs", defName)}${tail}`;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (block === "$defs") {
|
|
204
|
+
const defName = canonicalDefName(local[2], (candidate) => scope.kind === "root" && rootOwnDefNames.has(candidate) || documentDefs !== void 0 && readKey(documentDefs, candidate) !== void 0);
|
|
205
|
+
if (scope.kind === "root" && rootOwnDefNames.has(defName))
|
|
206
|
+
return ref;
|
|
207
|
+
if (documentDefs !== void 0 && readKey(documentDefs, defName) !== void 0) {
|
|
208
|
+
if (nestedTail)
|
|
209
|
+
return degradeNestedTail();
|
|
210
|
+
return `#/$defs/${allocateDocDefKey(defName)}${tail}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (scope.kind === "component" && block === "$defs") {
|
|
214
|
+
const defName = canonicalDefName(local[2], (candidate) => scope.blocks.definitions.has(candidate));
|
|
215
|
+
if (scope.blocks.definitions.has(defName)) {
|
|
216
|
+
if (nestedTail)
|
|
217
|
+
return degradeNestedTail();
|
|
218
|
+
return `#/$defs/${allocateDefKey(scope.component, "definitions", defName)}${tail}`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (scope.kind === "component" && /^#\/(?:definitions|\$defs)$/.test(decodeKeywordSegments(ref)))
|
|
223
|
+
return degrade(ref, "points at a definitions block the copy does not keep");
|
|
224
|
+
return ref;
|
|
225
|
+
};
|
|
226
|
+
const rewrite = (node, depth, inSchemaMap, scope) => {
|
|
227
|
+
assertSchemaDepth(depth, "rebaseComponentRefs");
|
|
228
|
+
if (Array.isArray(node))
|
|
229
|
+
return node.map((item) => rewrite(item, depth + 1, inSchemaMap, scope));
|
|
230
|
+
if (typeof node !== "object" || node === null)
|
|
231
|
+
return node;
|
|
232
|
+
const record = node;
|
|
233
|
+
const result = {};
|
|
234
|
+
for (const [key, value] of Object.entries(record)) {
|
|
235
|
+
if (isDataPosition(key, inSchemaMap)) {
|
|
236
|
+
assignKey(result, key, value);
|
|
237
|
+
} else if (!inSchemaMap && key === "$ref" && typeof value === "string") {
|
|
238
|
+
assignKey(result, key, rewriteRef(value, scope));
|
|
239
|
+
} else {
|
|
240
|
+
assignKey(result, key, rewrite(value, depth + 1, entersSchemaMap(key, inSchemaMap), scope));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
};
|
|
245
|
+
const rewrittenRoot = rewrite(root, 0, false, ROOT_SCOPE);
|
|
246
|
+
const copiedDefs = {};
|
|
247
|
+
const processedBlocks = /* @__PURE__ */ new Map();
|
|
248
|
+
const processComponent = (name) => {
|
|
249
|
+
const defsKey = keyByName.get(name);
|
|
250
|
+
const raw = componentSchemas === void 0 ? void 0 : readKey(componentSchemas, name);
|
|
251
|
+
if (raw === void 0) {
|
|
252
|
+
issues.push({
|
|
253
|
+
path,
|
|
254
|
+
message: `$ref to undeclared component "#/components/schemas/${name}"; treated as an unconstrained schema`
|
|
255
|
+
});
|
|
256
|
+
assignKey(copiedDefs, defsKey, {});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const { schemaFormat, schema } = unwrapMultiFormat(raw);
|
|
260
|
+
const componentFamily = schemaFormat === void 0 ? family : classifySchemaFormat(schemaFormat);
|
|
261
|
+
if (componentFamily === "unsupported" || typeof schema !== "object" || schema === null || Array.isArray(schema)) {
|
|
262
|
+
issues.push({
|
|
263
|
+
path,
|
|
264
|
+
message: componentFamily === "unsupported" ? `component "${name}" uses unsupported schemaFormat ${JSON.stringify(schemaFormat)}; treated as an unconstrained schema` : `component "${name}" is not an object schema; treated as an unconstrained schema`
|
|
265
|
+
});
|
|
266
|
+
assignKey(copiedDefs, defsKey, {});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const normalized = normalizeSchema(schema, componentFamily);
|
|
270
|
+
const blocks = { definitions: /* @__PURE__ */ new Set(), $defs: /* @__PURE__ */ new Set() };
|
|
271
|
+
const blockValues = [];
|
|
272
|
+
for (const blockName of ["definitions", "$defs"]) {
|
|
273
|
+
const block = readKey(schema, blockName);
|
|
274
|
+
if (typeof block !== "object" || block === null || Array.isArray(block))
|
|
275
|
+
continue;
|
|
276
|
+
for (const [defName, value] of Object.entries(block)) {
|
|
277
|
+
blocks[blockName].add(defName);
|
|
278
|
+
blockValues.push({ block: blockName, defName, value });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
processedBlocks.set(name, blocks);
|
|
282
|
+
const scope = { kind: "component", component: name, blocks };
|
|
283
|
+
for (const { block, defName, value } of blockValues) {
|
|
284
|
+
const prepared = typeof value === "object" && value !== null && !Array.isArray(value) ? normalizeSchema(value, componentFamily) : value;
|
|
285
|
+
assignKey(copiedDefs, allocateDefKey(name, block, defName), rewrite(prepared, 0, false, scope));
|
|
286
|
+
}
|
|
287
|
+
const { $defs: _, definitions: __, ...body } = normalized;
|
|
288
|
+
assignKey(copiedDefs, defsKey, rewrite(body, 0, false, scope));
|
|
289
|
+
};
|
|
290
|
+
const processDocDef = (name) => {
|
|
291
|
+
const key = docDefKeyByName.get(name);
|
|
292
|
+
const raw = documentDefs === void 0 ? void 0 : readKey(documentDefs, name);
|
|
293
|
+
if (raw === void 0 || typeof raw !== "object" || Array.isArray(raw)) {
|
|
294
|
+
issues.push({
|
|
295
|
+
path,
|
|
296
|
+
message: `$ref to "#/$defs/${name}" has no target on the document root; treated as an unconstrained schema`
|
|
297
|
+
});
|
|
298
|
+
assignKey(copiedDefs, key, {});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
assignKey(copiedDefs, key, rewrite(normalizeSchema(raw, family), 0, false, DOC_DEF_SCOPE));
|
|
302
|
+
};
|
|
303
|
+
let componentIndex = 0;
|
|
304
|
+
let docDefIndex = 0;
|
|
305
|
+
while (componentIndex < componentQueue.length || docDefIndex < docDefQueue.length) {
|
|
306
|
+
if (componentIndex < componentQueue.length) {
|
|
307
|
+
processComponent(componentQueue[componentIndex++]);
|
|
308
|
+
} else {
|
|
309
|
+
processDocDef(docDefQueue[docDefIndex++]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const { component, block, defName, key } of defKeyEntries) {
|
|
313
|
+
if (readKey(copiedDefs, key) !== void 0)
|
|
314
|
+
continue;
|
|
315
|
+
const declared = processedBlocks.get(component);
|
|
316
|
+
const aliasBlock = block === "definitions" && declared?.$defs.has(defName) ? "$defs" : block === "$defs" && declared?.definitions.has(defName) ? "definitions" : void 0;
|
|
317
|
+
if (aliasBlock !== void 0) {
|
|
318
|
+
const aliasKey = defKeyByName.get(JSON.stringify([component, aliasBlock, defName]));
|
|
319
|
+
if (aliasKey !== void 0) {
|
|
320
|
+
assignKey(copiedDefs, key, readKey(copiedDefs, aliasKey));
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
issues.push({
|
|
325
|
+
path,
|
|
326
|
+
message: `$ref into "#/components/schemas/${component}" names a definition "${defName}" it does not declare; treated as an unconstrained schema`
|
|
327
|
+
});
|
|
328
|
+
assignKey(copiedDefs, key, {});
|
|
329
|
+
}
|
|
330
|
+
for (const { key, ref, reason } of unresolvable) {
|
|
331
|
+
issues.push({
|
|
332
|
+
path,
|
|
333
|
+
message: `$ref "${ref}" ${reason}; treated as an unconstrained schema`
|
|
334
|
+
});
|
|
335
|
+
assignKey(copiedDefs, key, {});
|
|
336
|
+
}
|
|
337
|
+
if (Object.keys(copiedDefs).length === 0)
|
|
338
|
+
return rewrittenRoot;
|
|
339
|
+
const existingDefs = typeof rewrittenRoot["$defs"] === "object" && rewrittenRoot["$defs"] !== null ? rewrittenRoot["$defs"] : {};
|
|
340
|
+
const mergedDefs = {};
|
|
341
|
+
for (const [key, value] of Object.entries(existingDefs))
|
|
342
|
+
assignKey(mergedDefs, key, value);
|
|
343
|
+
for (const [key, value] of Object.entries(copiedDefs))
|
|
344
|
+
assignKey(mergedDefs, key, value);
|
|
345
|
+
return { ...rewrittenRoot, $defs: mergedDefs };
|
|
346
|
+
};
|
|
347
|
+
export {
|
|
348
|
+
rebaseComponentRefs
|
|
349
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExtractionIssue } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Follows a same-document JSON Pointer (`#/a/b`) to its target value, or
|
|
4
|
+
* `undefined` when any segment is missing. Segments are unescaped per RFC 6901
|
|
5
|
+
* (`~1` → `/`, `~0` → `~`), and each map step reads own properties only — a
|
|
6
|
+
* pointer segment of `constructor` must find the document's key or nothing,
|
|
7
|
+
* never `Object.prototype`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const getByPointer: (document: unknown, pointer: string) => unknown;
|
|
10
|
+
/**
|
|
11
|
+
* Dereferences a document node that may be a Reference Object, following
|
|
12
|
+
* chained `$ref`s until an object without one (or a failure) is reached.
|
|
13
|
+
*
|
|
14
|
+
* Only same-document `#/...` pointers are followed: by the time extraction
|
|
15
|
+
* runs, cross-file and remote references are the loader's job
|
|
16
|
+
* (`@amritk/resolve-refs` inlines them), so one still standing here is
|
|
17
|
+
* reported rather than fetched. Failures — an external ref, a dangling
|
|
18
|
+
* pointer, a `$ref` cycle, a non-object target — land on `issues` and resolve
|
|
19
|
+
* to `undefined` so a single broken reference skips one node, not the run.
|
|
20
|
+
*/
|
|
21
|
+
export declare const resolveNode: (document: unknown, node: unknown, issues: ExtractionIssue[], path: string) => Record<string, unknown> | undefined;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readKey } from "@amritk/helpers/read-key";
|
|
2
|
+
const getByPointer = (document, pointer) => {
|
|
3
|
+
if (pointer === "#" || pointer === "#/")
|
|
4
|
+
return document;
|
|
5
|
+
if (!pointer.startsWith("#/"))
|
|
6
|
+
return void 0;
|
|
7
|
+
let current = document;
|
|
8
|
+
for (const rawSegment of pointer.slice(2).split("/")) {
|
|
9
|
+
const segment = rawSegment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
10
|
+
if (Array.isArray(current)) {
|
|
11
|
+
const index = Number(segment);
|
|
12
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length)
|
|
13
|
+
return void 0;
|
|
14
|
+
current = current[index];
|
|
15
|
+
} else if (typeof current === "object" && current !== null) {
|
|
16
|
+
const record = current;
|
|
17
|
+
let next = readKey(record, segment);
|
|
18
|
+
if (next === void 0 && segment.includes("%")) {
|
|
19
|
+
try {
|
|
20
|
+
next = readKey(record, decodeURIComponent(segment));
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (next === void 0)
|
|
25
|
+
return void 0;
|
|
26
|
+
current = next;
|
|
27
|
+
} else {
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return current;
|
|
32
|
+
};
|
|
33
|
+
const resolveNode = (document, node, issues, path) => {
|
|
34
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35
|
+
let current = node;
|
|
36
|
+
while (typeof current === "object" && current !== null && !Array.isArray(current)) {
|
|
37
|
+
const ref = readKey(current, "$ref");
|
|
38
|
+
if (typeof ref !== "string")
|
|
39
|
+
return current;
|
|
40
|
+
if (!ref.startsWith("#")) {
|
|
41
|
+
issues.push({ path, message: `external $ref "${ref}" was not resolved before extraction; node skipped` });
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
if (seen.has(ref)) {
|
|
45
|
+
issues.push({ path, message: `$ref cycle through "${ref}"; node skipped` });
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
seen.add(ref);
|
|
49
|
+
current = getByPointer(document, ref);
|
|
50
|
+
if (current === void 0) {
|
|
51
|
+
issues.push({ path, message: `$ref "${ref}" does not resolve; node skipped` });
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
issues.push({ path, message: "expected an object node; node skipped" });
|
|
56
|
+
return void 0;
|
|
57
|
+
};
|
|
58
|
+
export {
|
|
59
|
+
getByPointer,
|
|
60
|
+
resolveNode
|
|
61
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON Schema dialect a `schemaFormat` names, or `'unsupported'` for
|
|
3
|
+
* everything the pipeline cannot turn into JSON Schema (Avro, Protobuf, RAML,
|
|
4
|
+
* unrecognized media types). The four supported families each get their own
|
|
5
|
+
* normalization: the AsyncAPI default dialect and declared draft-07 go through
|
|
6
|
+
* the draft-07 upgrade, OpenAPI schema objects get `nullable` folded, and
|
|
7
|
+
* 2020-12 passes through.
|
|
8
|
+
*/
|
|
9
|
+
export type SchemaFormatFamily = 'asyncapi' | 'draft-07' | '2020-12' | 'openapi' | 'unsupported';
|
|
10
|
+
/**
|
|
11
|
+
* Classifies a message's effective `schemaFormat`. An absent format means the
|
|
12
|
+
* AsyncAPI default dialect — the spec's own rule — so `undefined` is
|
|
13
|
+
* `'asyncapi'`, while a present-but-non-string value is malformed and lands in
|
|
14
|
+
* `'unsupported'` with everything else the pipeline cannot generate from.
|
|
15
|
+
*/
|
|
16
|
+
export declare const classifySchemaFormat: (schemaFormat: unknown) => SchemaFormatFamily;
|