@capaxle/generator-sdk-ts 0.1.0-alpha.1
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/LICENSE +202 -0
- package/README.md +5 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1645 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1645 @@
|
|
|
1
|
+
export const INTERNAL_FACADE_PRODUCER_ID = "capaxle.internal-facade-ts";
|
|
2
|
+
export const INTERNAL_FACADE_PRODUCER_VERSION = "0.1.0-alpha.1";
|
|
3
|
+
export const INTERNAL_FACADE_ARTIFACT_ID = "capaxle.internal-facade-ts";
|
|
4
|
+
export const INTERNAL_FACADE_ARTIFACT_PATH = "internal-facade.d.ts";
|
|
5
|
+
export const INTERNAL_FACADE_MEDIA_TYPE = "text/typescript";
|
|
6
|
+
export const INTERNAL_FACADE_TARGET = "capaxle:internal-facade-ts@0.1";
|
|
7
|
+
export const INTERNAL_FACADE_DIAGNOSTIC_CODES = Object.freeze([
|
|
8
|
+
Object.freeze({
|
|
9
|
+
code: "CAP_INTERNAL_FACADE_NAME_COLLISION",
|
|
10
|
+
severities: Object.freeze(["error"]),
|
|
11
|
+
}),
|
|
12
|
+
Object.freeze({
|
|
13
|
+
code: "CAP_INTERNAL_FACADE_NAME_INVALID",
|
|
14
|
+
severities: Object.freeze(["error"]),
|
|
15
|
+
}),
|
|
16
|
+
Object.freeze({
|
|
17
|
+
code: "CAP_INTERNAL_FACADE_SCHEMA_UNREPRESENTABLE",
|
|
18
|
+
severities: Object.freeze(["error"]),
|
|
19
|
+
}),
|
|
20
|
+
]);
|
|
21
|
+
const reservedSegments = new Set([
|
|
22
|
+
"__proto__",
|
|
23
|
+
"prototype",
|
|
24
|
+
"constructor",
|
|
25
|
+
"then",
|
|
26
|
+
"toJSON",
|
|
27
|
+
"toString",
|
|
28
|
+
"valueOf",
|
|
29
|
+
"inspect",
|
|
30
|
+
]);
|
|
31
|
+
const capabilityIdPattern = /^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*$/u;
|
|
32
|
+
const identifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/u;
|
|
33
|
+
const hashPattern = /^sha256:[a-f0-9]{64}$/u;
|
|
34
|
+
const semverPattern = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
|
|
35
|
+
const schemaTypes = new Set([
|
|
36
|
+
"null",
|
|
37
|
+
"boolean",
|
|
38
|
+
"object",
|
|
39
|
+
"array",
|
|
40
|
+
"number",
|
|
41
|
+
"string",
|
|
42
|
+
"integer",
|
|
43
|
+
]);
|
|
44
|
+
const schemaKeys = new Set([
|
|
45
|
+
"$schema",
|
|
46
|
+
"$ref",
|
|
47
|
+
"$defs",
|
|
48
|
+
"type",
|
|
49
|
+
"enum",
|
|
50
|
+
"const",
|
|
51
|
+
"properties",
|
|
52
|
+
"required",
|
|
53
|
+
"additionalProperties",
|
|
54
|
+
"items",
|
|
55
|
+
"minimum",
|
|
56
|
+
"maximum",
|
|
57
|
+
"exclusiveMinimum",
|
|
58
|
+
"exclusiveMaximum",
|
|
59
|
+
"multipleOf",
|
|
60
|
+
"minLength",
|
|
61
|
+
"maxLength",
|
|
62
|
+
"pattern",
|
|
63
|
+
"format",
|
|
64
|
+
"minItems",
|
|
65
|
+
"maxItems",
|
|
66
|
+
"uniqueItems",
|
|
67
|
+
"description",
|
|
68
|
+
"default",
|
|
69
|
+
"examples",
|
|
70
|
+
"oneOf",
|
|
71
|
+
]);
|
|
72
|
+
class SchemaEmissionError extends Error {
|
|
73
|
+
path;
|
|
74
|
+
capabilityId;
|
|
75
|
+
constructor(path, capabilityId) {
|
|
76
|
+
super("Schema cannot be represented by the internal facade generator.");
|
|
77
|
+
this.path = path;
|
|
78
|
+
this.capabilityId = capabilityId;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function compareCodePoints(left, right) {
|
|
82
|
+
const leftPoints = Array.from(left, (value) => value.codePointAt(0));
|
|
83
|
+
const rightPoints = Array.from(right, (value) => value.codePointAt(0));
|
|
84
|
+
for (let index = 0; index < Math.min(leftPoints.length, rightPoints.length); index++) {
|
|
85
|
+
const difference = leftPoints[index] - rightPoints[index];
|
|
86
|
+
if (difference !== 0)
|
|
87
|
+
return difference;
|
|
88
|
+
}
|
|
89
|
+
return leftPoints.length - rightPoints.length;
|
|
90
|
+
}
|
|
91
|
+
function compareSemVer(left, right) {
|
|
92
|
+
const leftMatch = semverPattern.exec(left);
|
|
93
|
+
const rightMatch = semverPattern.exec(right);
|
|
94
|
+
if (!leftMatch || !rightMatch)
|
|
95
|
+
return compareCodePoints(left, right);
|
|
96
|
+
for (const index of [1, 2, 3]) {
|
|
97
|
+
const difference = BigInt(leftMatch[index]) - BigInt(rightMatch[index]);
|
|
98
|
+
if (difference !== 0n)
|
|
99
|
+
return difference < 0n ? -1 : 1;
|
|
100
|
+
}
|
|
101
|
+
const leftPre = leftMatch[4]?.split(".");
|
|
102
|
+
const rightPre = rightMatch[4]?.split(".");
|
|
103
|
+
if (!leftPre || !rightPre)
|
|
104
|
+
return leftPre ? -1 : rightPre ? 1 : 0;
|
|
105
|
+
for (let index = 0; index < Math.min(leftPre.length, rightPre.length); index++) {
|
|
106
|
+
const leftPart = leftPre[index];
|
|
107
|
+
const rightPart = rightPre[index];
|
|
108
|
+
const leftNumeric = /^[0-9]+$/u.test(leftPart);
|
|
109
|
+
const rightNumeric = /^[0-9]+$/u.test(rightPart);
|
|
110
|
+
if (leftNumeric && rightNumeric) {
|
|
111
|
+
const difference = BigInt(leftPart) - BigInt(rightPart);
|
|
112
|
+
if (difference !== 0n)
|
|
113
|
+
return difference < 0n ? -1 : 1;
|
|
114
|
+
}
|
|
115
|
+
else if (leftNumeric !== rightNumeric)
|
|
116
|
+
return leftNumeric ? -1 : 1;
|
|
117
|
+
else {
|
|
118
|
+
const difference = compareCodePoints(leftPart, rightPart);
|
|
119
|
+
if (difference !== 0)
|
|
120
|
+
return difference;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return leftPre.length - rightPre.length;
|
|
124
|
+
}
|
|
125
|
+
function pointerToken(value) {
|
|
126
|
+
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
127
|
+
}
|
|
128
|
+
function decodePointerToken(value) {
|
|
129
|
+
return value.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
130
|
+
}
|
|
131
|
+
function isRecord(value) {
|
|
132
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
133
|
+
}
|
|
134
|
+
function propertyName(value) {
|
|
135
|
+
return identifierPattern.test(value) ? value : JSON.stringify(value);
|
|
136
|
+
}
|
|
137
|
+
function literalType(value) {
|
|
138
|
+
if (value === null)
|
|
139
|
+
return "null";
|
|
140
|
+
if (typeof value === "string")
|
|
141
|
+
return JSON.stringify(value);
|
|
142
|
+
if (typeof value === "boolean")
|
|
143
|
+
return String(value);
|
|
144
|
+
if (typeof value === "number") {
|
|
145
|
+
if (!Number.isFinite(value))
|
|
146
|
+
throw new SchemaEmissionError("");
|
|
147
|
+
return Object.is(value, -0) ? "0" : String(value);
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(value))
|
|
150
|
+
return `readonly [${value.map(literalType).join(", ")}]`;
|
|
151
|
+
const record = value;
|
|
152
|
+
return `{ ${Object.keys(record)
|
|
153
|
+
.sort(compareCodePoints)
|
|
154
|
+
.map((key) => `readonly ${JSON.stringify(key)}: ${literalType(record[key])}`)
|
|
155
|
+
.join("; ")} }`;
|
|
156
|
+
}
|
|
157
|
+
function asJsonValue(value, path) {
|
|
158
|
+
if (value === null ||
|
|
159
|
+
typeof value === "string" ||
|
|
160
|
+
typeof value === "boolean" ||
|
|
161
|
+
(typeof value === "number" && Number.isFinite(value)))
|
|
162
|
+
return value;
|
|
163
|
+
if (Array.isArray(value))
|
|
164
|
+
return value.map((item, index) => asJsonValue(item, `${path}/${index}`));
|
|
165
|
+
if (isRecord(value))
|
|
166
|
+
return Object.fromEntries(Object.keys(value).map((key) => [
|
|
167
|
+
key,
|
|
168
|
+
asJsonValue(value[key], `${path}/${pointerToken(key)}`),
|
|
169
|
+
]));
|
|
170
|
+
throw new SchemaEmissionError(path);
|
|
171
|
+
}
|
|
172
|
+
function diagnostic(code, message, options = {}) {
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
code,
|
|
175
|
+
severity: "error",
|
|
176
|
+
message,
|
|
177
|
+
target: INTERNAL_FACADE_TARGET,
|
|
178
|
+
...options,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function aliasName(index) {
|
|
182
|
+
return `__CapaxleSchema${String(index).padStart(4, "0")}`;
|
|
183
|
+
}
|
|
184
|
+
function aliasIdentityKey(identity) {
|
|
185
|
+
const { root } = identity;
|
|
186
|
+
return JSON.stringify(root.kind === "schema"
|
|
187
|
+
? ["schema", root.name, identity.definition ?? null]
|
|
188
|
+
: [
|
|
189
|
+
"capability",
|
|
190
|
+
root.id,
|
|
191
|
+
root.version,
|
|
192
|
+
root.binding,
|
|
193
|
+
root.errorCode ?? null,
|
|
194
|
+
identity.definition ?? null,
|
|
195
|
+
]);
|
|
196
|
+
}
|
|
197
|
+
function rootIdentityKey(root) {
|
|
198
|
+
return aliasIdentityKey({ root });
|
|
199
|
+
}
|
|
200
|
+
function referenceAliasIdentity(reference, root) {
|
|
201
|
+
if (typeof reference === "string" &&
|
|
202
|
+
/^#\/schemas\/(?:[^~/]|~0|~1)+$/u.test(reference))
|
|
203
|
+
return {
|
|
204
|
+
root: {
|
|
205
|
+
kind: "schema",
|
|
206
|
+
name: decodePointerToken(reference.slice(10)),
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
if (typeof reference === "string" &&
|
|
210
|
+
/^#\/\$defs\/(?:[^~/]|~0|~1)+$/u.test(reference))
|
|
211
|
+
return {
|
|
212
|
+
root,
|
|
213
|
+
definition: decodePointerToken(reference.slice(8)),
|
|
214
|
+
};
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
function collectAliases(document, selected) {
|
|
218
|
+
const roots = [];
|
|
219
|
+
for (const name of Object.keys(document.schemas).sort(compareCodePoints)) {
|
|
220
|
+
const root = { kind: "schema", name };
|
|
221
|
+
roots.push({
|
|
222
|
+
identity: { root },
|
|
223
|
+
schema: document.schemas[name],
|
|
224
|
+
root,
|
|
225
|
+
path: `/schemas/${pointerToken(name)}`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const addBinding = (binding, root, path, capabilityId) => {
|
|
229
|
+
if (binding.schema !== undefined)
|
|
230
|
+
roots.push({
|
|
231
|
+
identity: { root },
|
|
232
|
+
schema: binding.schema,
|
|
233
|
+
root,
|
|
234
|
+
path: `${path}/schema`,
|
|
235
|
+
capabilityId,
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
for (const { capability, index } of selected) {
|
|
239
|
+
const base = `/capabilities/${index}`;
|
|
240
|
+
addBinding(capability.input, {
|
|
241
|
+
kind: "capability",
|
|
242
|
+
id: capability.id,
|
|
243
|
+
version: capability.version,
|
|
244
|
+
binding: "input",
|
|
245
|
+
}, `${base}/input`, capability.id);
|
|
246
|
+
addBinding(capability.output, {
|
|
247
|
+
kind: "capability",
|
|
248
|
+
id: capability.id,
|
|
249
|
+
version: capability.version,
|
|
250
|
+
binding: "output",
|
|
251
|
+
}, `${base}/output`, capability.id);
|
|
252
|
+
for (const code of Object.keys(capability.errors).sort(compareCodePoints)) {
|
|
253
|
+
const details = capability.errors[code].details;
|
|
254
|
+
if (details)
|
|
255
|
+
addBinding(details, {
|
|
256
|
+
kind: "capability",
|
|
257
|
+
id: capability.id,
|
|
258
|
+
version: capability.version,
|
|
259
|
+
binding: "error-details",
|
|
260
|
+
errorCode: code,
|
|
261
|
+
}, `${base}/errors/${pointerToken(code)}/details`, capability.id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const specs = [...roots];
|
|
265
|
+
for (const root of roots) {
|
|
266
|
+
const definitions = root.schema.$defs;
|
|
267
|
+
if (definitions === undefined)
|
|
268
|
+
continue;
|
|
269
|
+
if (!isRecord(definitions))
|
|
270
|
+
throw new SchemaEmissionError(`${root.path}/$defs`, root.capabilityId);
|
|
271
|
+
for (const name of Object.keys(definitions).sort(compareCodePoints)) {
|
|
272
|
+
const value = definitions[name];
|
|
273
|
+
if (!isRecord(value))
|
|
274
|
+
throw new SchemaEmissionError(`${root.path}/$defs/${pointerToken(name)}`, root.capabilityId);
|
|
275
|
+
specs.push({
|
|
276
|
+
identity: { root: root.root, definition: name },
|
|
277
|
+
schema: value,
|
|
278
|
+
root: root.root,
|
|
279
|
+
path: `${root.path}/$defs/${pointerToken(name)}`,
|
|
280
|
+
...(root.capabilityId === undefined
|
|
281
|
+
? {}
|
|
282
|
+
: { capabilityId: root.capabilityId }),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
specs.sort((left, right) => compareCodePoints(aliasIdentityKey(left.identity), aliasIdentityKey(right.identity)));
|
|
287
|
+
return {
|
|
288
|
+
specs,
|
|
289
|
+
aliases: new Map(specs.map((spec, index) => [
|
|
290
|
+
aliasIdentityKey(spec.identity),
|
|
291
|
+
aliasName(index),
|
|
292
|
+
])),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function assertRepresentableAliasCycles(specs, aliases) {
|
|
296
|
+
const edges = new Map();
|
|
297
|
+
const specByKey = new Map(specs.map((spec) => [aliasIdentityKey(spec.identity), spec]));
|
|
298
|
+
const collectReferences = (schema, root, path, capabilityId, guarded, dependencies) => {
|
|
299
|
+
if (!isRecord(schema))
|
|
300
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
301
|
+
if (schema.$ref !== undefined) {
|
|
302
|
+
const identity = referenceAliasIdentity(schema.$ref, root);
|
|
303
|
+
const key = identity === undefined ? undefined : aliasIdentityKey(identity);
|
|
304
|
+
if (key === undefined || !aliases.has(key))
|
|
305
|
+
throw new SchemaEmissionError(`${path}/$ref`, capabilityId);
|
|
306
|
+
if (!guarded)
|
|
307
|
+
dependencies.add(key);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (schema.properties !== undefined) {
|
|
311
|
+
if (!isRecord(schema.properties))
|
|
312
|
+
throw new SchemaEmissionError(`${path}/properties`, capabilityId);
|
|
313
|
+
for (const name of Object.keys(schema.properties).sort(compareCodePoints)) {
|
|
314
|
+
const child = schema.properties[name];
|
|
315
|
+
if (!isRecord(child))
|
|
316
|
+
throw new SchemaEmissionError(`${path}/properties/${pointerToken(name)}`, capabilityId);
|
|
317
|
+
collectReferences(child, root, `${path}/properties/${pointerToken(name)}`, capabilityId, true, dependencies);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (schema.items !== undefined) {
|
|
321
|
+
if (!isRecord(schema.items))
|
|
322
|
+
throw new SchemaEmissionError(`${path}/items`, capabilityId);
|
|
323
|
+
collectReferences(schema.items, root, `${path}/items`, capabilityId, true, dependencies);
|
|
324
|
+
}
|
|
325
|
+
if (schema.oneOf !== undefined) {
|
|
326
|
+
if (!Array.isArray(schema.oneOf))
|
|
327
|
+
throw new SchemaEmissionError(`${path}/oneOf`, capabilityId);
|
|
328
|
+
for (const [index, branch] of schema.oneOf.entries()) {
|
|
329
|
+
if (!isRecord(branch))
|
|
330
|
+
throw new SchemaEmissionError(`${path}/oneOf/${index}`, capabilityId);
|
|
331
|
+
collectReferences(branch, root, `${path}/oneOf/${index}`, capabilityId, guarded, dependencies);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
for (const spec of specs) {
|
|
336
|
+
const dependencies = new Set();
|
|
337
|
+
collectReferences(spec.schema, spec.root, spec.path, spec.capabilityId, false, dependencies);
|
|
338
|
+
edges.set(aliasIdentityKey(spec.identity), dependencies);
|
|
339
|
+
}
|
|
340
|
+
const state = new Map();
|
|
341
|
+
const visit = (key) => {
|
|
342
|
+
const current = state.get(key);
|
|
343
|
+
if (current === "visited")
|
|
344
|
+
return;
|
|
345
|
+
if (current === "visiting") {
|
|
346
|
+
const spec = specByKey.get(key);
|
|
347
|
+
throw new SchemaEmissionError(spec?.path ?? "/", spec?.capabilityId);
|
|
348
|
+
}
|
|
349
|
+
state.set(key, "visiting");
|
|
350
|
+
for (const dependency of edges.get(key) ?? [])
|
|
351
|
+
visit(dependency);
|
|
352
|
+
state.set(key, "visited");
|
|
353
|
+
};
|
|
354
|
+
for (const key of [...specByKey.keys()].sort(compareCodePoints))
|
|
355
|
+
visit(key);
|
|
356
|
+
}
|
|
357
|
+
function schemaTypeEmitter(aliases, specs) {
|
|
358
|
+
const specByKey = new Map(specs.map((spec) => [aliasIdentityKey(spec.identity), spec]));
|
|
359
|
+
const allRuntimeTypes = new Set([
|
|
360
|
+
"null",
|
|
361
|
+
"boolean",
|
|
362
|
+
"number",
|
|
363
|
+
"string",
|
|
364
|
+
"array",
|
|
365
|
+
"object",
|
|
366
|
+
]);
|
|
367
|
+
const runtimeType = (value) => {
|
|
368
|
+
if (value === null)
|
|
369
|
+
return "null";
|
|
370
|
+
if (Array.isArray(value))
|
|
371
|
+
return "array";
|
|
372
|
+
if (typeof value === "object")
|
|
373
|
+
return "object";
|
|
374
|
+
return typeof value === "number" ? "number" : typeof value;
|
|
375
|
+
};
|
|
376
|
+
const intersect = (left, right) => new Set([...left].filter((value) => right.has(value)));
|
|
377
|
+
const referencedSpec = (reference, root, path, capabilityId) => {
|
|
378
|
+
const identity = referenceAliasIdentity(reference, root);
|
|
379
|
+
const spec = identity === undefined
|
|
380
|
+
? undefined
|
|
381
|
+
: specByKey.get(aliasIdentityKey(identity));
|
|
382
|
+
if (!spec)
|
|
383
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
384
|
+
return spec;
|
|
385
|
+
};
|
|
386
|
+
const admittedTypes = (schema, root, path, capabilityId, seen = new Set()) => {
|
|
387
|
+
if (!isRecord(schema))
|
|
388
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
389
|
+
if (schema.$ref !== undefined) {
|
|
390
|
+
const spec = referencedSpec(schema.$ref, root, `${path}/$ref`, capabilityId);
|
|
391
|
+
const key = aliasIdentityKey(spec.identity);
|
|
392
|
+
if (seen.has(key))
|
|
393
|
+
return new Set(allRuntimeTypes);
|
|
394
|
+
return admittedTypes(spec.schema, spec.root, spec.path, spec.capabilityId, new Set([...seen, key]));
|
|
395
|
+
}
|
|
396
|
+
let admitted = new Set(allRuntimeTypes);
|
|
397
|
+
if (schema.type !== undefined) {
|
|
398
|
+
const raw = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
399
|
+
admitted = intersect(admitted, new Set(raw
|
|
400
|
+
.filter((value) => typeof value === "string")
|
|
401
|
+
.map((value) => (value === "integer" ? "number" : value))));
|
|
402
|
+
}
|
|
403
|
+
if (Object.hasOwn(schema, "const"))
|
|
404
|
+
admitted = intersect(admitted, new Set([runtimeType(asJsonValue(schema.const, `${path}/const`))]));
|
|
405
|
+
if (Array.isArray(schema.enum))
|
|
406
|
+
admitted = intersect(admitted, new Set(schema.enum.map((value, index) => runtimeType(asJsonValue(value, `${path}/enum/${index}`)))));
|
|
407
|
+
if (Array.isArray(schema.oneOf)) {
|
|
408
|
+
const branchTypes = new Set();
|
|
409
|
+
for (const [index, branch] of schema.oneOf.entries()) {
|
|
410
|
+
if (!isRecord(branch))
|
|
411
|
+
throw new SchemaEmissionError(`${path}/oneOf/${index}`, capabilityId);
|
|
412
|
+
for (const type of admittedTypes(branch, root, `${path}/oneOf/${index}`, capabilityId, seen))
|
|
413
|
+
branchTypes.add(type);
|
|
414
|
+
}
|
|
415
|
+
admitted = intersect(admitted, branchTypes);
|
|
416
|
+
}
|
|
417
|
+
return admitted;
|
|
418
|
+
};
|
|
419
|
+
const maxConstraintAlternatives = 256;
|
|
420
|
+
const maxExpansionWork = 8192;
|
|
421
|
+
const maxExpansionDepth = 128;
|
|
422
|
+
const maxExclusivityProofWork = 65_536;
|
|
423
|
+
const canonicalSchemaCache = new WeakMap();
|
|
424
|
+
const expansionMemo = new Map();
|
|
425
|
+
const referenceExpansionMemo = new Map();
|
|
426
|
+
const spendExpansionWork = (state, amount, path, capabilityId) => {
|
|
427
|
+
if (amount > state.remainingWork)
|
|
428
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
429
|
+
state.remainingWork -= amount;
|
|
430
|
+
};
|
|
431
|
+
const canonicalJson = (value, state, path, capabilityId, depth = 0) => {
|
|
432
|
+
if (depth > maxExpansionDepth)
|
|
433
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
434
|
+
spendExpansionWork(state, 1, path, capabilityId);
|
|
435
|
+
if (value === null || typeof value !== "object")
|
|
436
|
+
return JSON.stringify(value);
|
|
437
|
+
const cached = canonicalSchemaCache.get(value);
|
|
438
|
+
if (cached !== undefined)
|
|
439
|
+
return cached;
|
|
440
|
+
let result;
|
|
441
|
+
if (Array.isArray(value)) {
|
|
442
|
+
if (value.length > state.remainingWork)
|
|
443
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
444
|
+
const items = [];
|
|
445
|
+
for (const item of value)
|
|
446
|
+
items.push(canonicalJson(item, state, path, capabilityId, depth + 1));
|
|
447
|
+
result = `[${items.join(",")}]`;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
const keys = Object.keys(value).sort(compareCodePoints);
|
|
451
|
+
if (keys.length > state.remainingWork)
|
|
452
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
453
|
+
const members = [];
|
|
454
|
+
for (const key of keys)
|
|
455
|
+
members.push(`${JSON.stringify(key)}:${canonicalJson(value[key], state, path, capabilityId, depth + 1)}`);
|
|
456
|
+
result = `{${members.join(",")}}`;
|
|
457
|
+
}
|
|
458
|
+
canonicalSchemaCache.set(value, result);
|
|
459
|
+
return result;
|
|
460
|
+
};
|
|
461
|
+
const constraintKey = (constraint, state) => JSON.stringify([
|
|
462
|
+
rootIdentityKey(constraint.root),
|
|
463
|
+
constraint.capabilityId ?? null,
|
|
464
|
+
canonicalJson(constraint.schema, state, constraint.path, constraint.capabilityId),
|
|
465
|
+
]);
|
|
466
|
+
const normalizedAlternative = (constraints, state) => {
|
|
467
|
+
const unique = new Map();
|
|
468
|
+
for (const constraint of constraints) {
|
|
469
|
+
const key = constraintKey(constraint, state);
|
|
470
|
+
if (!unique.has(key))
|
|
471
|
+
unique.set(key, constraint);
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
constraints: [...unique.values()],
|
|
475
|
+
key: [...unique.keys()].sort(compareCodePoints).join("\u0000"),
|
|
476
|
+
};
|
|
477
|
+
};
|
|
478
|
+
const finiteLiterals = (schema, root, path, capabilityId, seen = new Set()) => {
|
|
479
|
+
if (!isRecord(schema))
|
|
480
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
481
|
+
if (schema.$ref !== undefined) {
|
|
482
|
+
const spec = referencedSpec(schema.$ref, root, `${path}/$ref`, capabilityId);
|
|
483
|
+
const key = aliasIdentityKey(spec.identity);
|
|
484
|
+
if (seen.has(key))
|
|
485
|
+
return undefined;
|
|
486
|
+
return finiteLiterals(spec.schema, spec.root, spec.path, spec.capabilityId, new Set([...seen, key]));
|
|
487
|
+
}
|
|
488
|
+
let finite;
|
|
489
|
+
if (Object.hasOwn(schema, "const")) {
|
|
490
|
+
const value = asJsonValue(schema.const, `${path}/const`);
|
|
491
|
+
finite = new Map([[literalType(value), value]]);
|
|
492
|
+
}
|
|
493
|
+
if (Array.isArray(schema.enum)) {
|
|
494
|
+
const enumeration = new Map(schema.enum.map((value, index) => {
|
|
495
|
+
const literal = asJsonValue(value, `${path}/enum/${index}`);
|
|
496
|
+
return [literalType(literal), literal];
|
|
497
|
+
}));
|
|
498
|
+
finite =
|
|
499
|
+
finite === undefined
|
|
500
|
+
? enumeration
|
|
501
|
+
: new Map([...finite].filter(([key]) => enumeration.has(key)));
|
|
502
|
+
}
|
|
503
|
+
if (Array.isArray(schema.oneOf)) {
|
|
504
|
+
const union = new Map();
|
|
505
|
+
for (const [index, branch] of schema.oneOf.entries()) {
|
|
506
|
+
if (!isRecord(branch))
|
|
507
|
+
throw new SchemaEmissionError(`${path}/oneOf/${index}`, capabilityId);
|
|
508
|
+
const branchFinite = finiteLiterals(branch, root, `${path}/oneOf/${index}`, capabilityId, seen);
|
|
509
|
+
if (branchFinite === undefined)
|
|
510
|
+
return finite;
|
|
511
|
+
for (const [key, value] of branchFinite)
|
|
512
|
+
union.set(key, value);
|
|
513
|
+
}
|
|
514
|
+
finite =
|
|
515
|
+
finite === undefined
|
|
516
|
+
? union
|
|
517
|
+
: new Map([...finite].filter(([key]) => union.has(key)));
|
|
518
|
+
}
|
|
519
|
+
return finite;
|
|
520
|
+
};
|
|
521
|
+
const dereference = (schema, root, path, capabilityId, seen = new Set()) => {
|
|
522
|
+
if (schema.$ref === undefined)
|
|
523
|
+
return { schema, root, path, capabilityId };
|
|
524
|
+
const spec = referencedSpec(schema.$ref, root, `${path}/$ref`, capabilityId);
|
|
525
|
+
const key = aliasIdentityKey(spec.identity);
|
|
526
|
+
if (seen.has(key))
|
|
527
|
+
return undefined;
|
|
528
|
+
return dereference(spec.schema, spec.root, spec.path, spec.capabilityId, new Set([...seen, key]));
|
|
529
|
+
};
|
|
530
|
+
const effectiveTypes = (constraints) => constraints.reduce((types, constraint) => intersect(types, admittedTypes(constraint.schema, constraint.root, constraint.path, constraint.capabilityId)), new Set(allRuntimeTypes));
|
|
531
|
+
const effectiveFiniteLiterals = (constraints) => {
|
|
532
|
+
let effective;
|
|
533
|
+
for (const constraint of constraints) {
|
|
534
|
+
const finite = finiteLiterals(constraint.schema, constraint.root, constraint.path, constraint.capabilityId);
|
|
535
|
+
if (finite === undefined)
|
|
536
|
+
continue;
|
|
537
|
+
effective =
|
|
538
|
+
effective === undefined
|
|
539
|
+
? new Map(finite)
|
|
540
|
+
: new Map([...effective].filter(([key]) => finite.has(key)));
|
|
541
|
+
}
|
|
542
|
+
if (effective === undefined)
|
|
543
|
+
return undefined;
|
|
544
|
+
const types = effectiveTypes(constraints);
|
|
545
|
+
return new Map([...effective].filter(([, value]) => types.has(runtimeType(value))));
|
|
546
|
+
};
|
|
547
|
+
const requiredDiscriminators = (constraints) => {
|
|
548
|
+
const resolved = constraints.flatMap((constraint) => {
|
|
549
|
+
const value = dereference(constraint.schema, constraint.root, constraint.path, constraint.capabilityId);
|
|
550
|
+
return value === undefined ? [] : [value];
|
|
551
|
+
});
|
|
552
|
+
const required = new Set();
|
|
553
|
+
for (const constraint of resolved) {
|
|
554
|
+
if (!Array.isArray(constraint.schema.required))
|
|
555
|
+
continue;
|
|
556
|
+
for (const name of constraint.schema.required)
|
|
557
|
+
if (typeof name === "string")
|
|
558
|
+
required.add(name);
|
|
559
|
+
}
|
|
560
|
+
const result = new Map();
|
|
561
|
+
for (const name of required) {
|
|
562
|
+
const propertyConstraints = [];
|
|
563
|
+
for (const constraint of resolved) {
|
|
564
|
+
if (!isRecord(constraint.schema.properties))
|
|
565
|
+
continue;
|
|
566
|
+
const property = constraint.schema.properties[name];
|
|
567
|
+
if (!isRecord(property))
|
|
568
|
+
continue;
|
|
569
|
+
propertyConstraints.push({
|
|
570
|
+
schema: property,
|
|
571
|
+
root: constraint.root,
|
|
572
|
+
path: `${constraint.path}/properties/${pointerToken(name)}`,
|
|
573
|
+
capabilityId: constraint.capabilityId,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const finite = effectiveFiniteLiterals(propertyConstraints);
|
|
577
|
+
if (finite !== undefined)
|
|
578
|
+
result.set(name, new Set(finite.keys()));
|
|
579
|
+
}
|
|
580
|
+
return result;
|
|
581
|
+
};
|
|
582
|
+
const schemaConstraint = (schema, root, path, capabilityId) => ({ schema, root, path, capabilityId });
|
|
583
|
+
const withoutOneOf = (schema) => {
|
|
584
|
+
const result = { ...schema };
|
|
585
|
+
delete result.oneOf;
|
|
586
|
+
return result;
|
|
587
|
+
};
|
|
588
|
+
const impossible = (constraints) => {
|
|
589
|
+
const types = effectiveTypes(constraints);
|
|
590
|
+
if (types.size === 0)
|
|
591
|
+
return true;
|
|
592
|
+
const finite = effectiveFiniteLiterals(constraints);
|
|
593
|
+
if (finite !== undefined && finite.size === 0)
|
|
594
|
+
return true;
|
|
595
|
+
if (types.size !== 1 || !types.has("object"))
|
|
596
|
+
return false;
|
|
597
|
+
return [...requiredDiscriminators(constraints).values()].some((values) => values.size === 0);
|
|
598
|
+
};
|
|
599
|
+
const expandConstraint = (constraint, state, seen = new Set(), depth = 0) => {
|
|
600
|
+
if (depth > maxExpansionDepth)
|
|
601
|
+
throw new SchemaEmissionError(constraint.path, constraint.capabilityId);
|
|
602
|
+
const memoKey = seen.size === 0 ? constraintKey(constraint, state) : undefined;
|
|
603
|
+
if (memoKey !== undefined) {
|
|
604
|
+
const cached = expansionMemo.get(memoKey);
|
|
605
|
+
if (cached !== undefined) {
|
|
606
|
+
spendExpansionWork(state, 1, constraint.path, constraint.capabilityId);
|
|
607
|
+
return cached;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
spendExpansionWork(state, 1, constraint.path, constraint.capabilityId);
|
|
611
|
+
let result;
|
|
612
|
+
if (constraint.schema.$ref !== undefined) {
|
|
613
|
+
const spec = referencedSpec(constraint.schema.$ref, constraint.root, `${constraint.path}/$ref`, constraint.capabilityId);
|
|
614
|
+
const key = aliasIdentityKey(spec.identity);
|
|
615
|
+
if (seen.has(key))
|
|
616
|
+
result = [[constraint]];
|
|
617
|
+
else {
|
|
618
|
+
const cached = referenceExpansionMemo.get(key);
|
|
619
|
+
if (cached !== undefined) {
|
|
620
|
+
spendExpansionWork(state, 1, constraint.path, constraint.capabilityId);
|
|
621
|
+
result = cached;
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
result = expandConstraint(schemaConstraint(spec.schema, spec.root, spec.path, spec.capabilityId), state, new Set([...seen, key]), depth + 1);
|
|
625
|
+
referenceExpansionMemo.set(key, result);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else if (!Array.isArray(constraint.schema.oneOf)) {
|
|
630
|
+
result = [[constraint]];
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
const base = schemaConstraint(withoutOneOf(constraint.schema), constraint.root, constraint.path, constraint.capabilityId);
|
|
634
|
+
const expanded = [];
|
|
635
|
+
const keys = new Set();
|
|
636
|
+
for (const [index, branch] of constraint.schema.oneOf.entries()) {
|
|
637
|
+
if (!isRecord(branch))
|
|
638
|
+
throw new SchemaEmissionError(`${constraint.path}/oneOf/${index}`, constraint.capabilityId);
|
|
639
|
+
const choices = expandConstraint(schemaConstraint(branch, constraint.root, `${constraint.path}/oneOf/${index}`, constraint.capabilityId), state, seen, depth + 1);
|
|
640
|
+
for (const choice of choices) {
|
|
641
|
+
spendExpansionWork(state, 1, `${constraint.path}/oneOf/${index}`, constraint.capabilityId);
|
|
642
|
+
const normalized = normalizedAlternative([base, ...choice], state);
|
|
643
|
+
if (keys.has(normalized.key))
|
|
644
|
+
continue;
|
|
645
|
+
if (expanded.length >= maxConstraintAlternatives)
|
|
646
|
+
throw new SchemaEmissionError(constraint.path, constraint.capabilityId);
|
|
647
|
+
keys.add(normalized.key);
|
|
648
|
+
expanded.push([...normalized.constraints]);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
result = expanded;
|
|
652
|
+
}
|
|
653
|
+
if (memoKey !== undefined)
|
|
654
|
+
expansionMemo.set(memoKey, result);
|
|
655
|
+
return result;
|
|
656
|
+
};
|
|
657
|
+
const expandAlternatives = (alternatives, state) => {
|
|
658
|
+
const result = [];
|
|
659
|
+
const resultKeys = new Set();
|
|
660
|
+
for (const alternative of alternatives) {
|
|
661
|
+
let expanded = [{ constraints: [], key: "" }];
|
|
662
|
+
for (const constraint of alternative) {
|
|
663
|
+
const choices = expandConstraint(constraint, state);
|
|
664
|
+
const next = [];
|
|
665
|
+
const nextKeys = new Set();
|
|
666
|
+
for (const existing of expanded) {
|
|
667
|
+
for (const choice of choices) {
|
|
668
|
+
spendExpansionWork(state, 1, constraint.path, constraint.capabilityId);
|
|
669
|
+
const normalized = normalizedAlternative([...existing.constraints, ...choice], state);
|
|
670
|
+
if (nextKeys.has(normalized.key))
|
|
671
|
+
continue;
|
|
672
|
+
if (next.length >= maxConstraintAlternatives)
|
|
673
|
+
throw new SchemaEmissionError(constraint.path, constraint.capabilityId);
|
|
674
|
+
nextKeys.add(normalized.key);
|
|
675
|
+
next.push({
|
|
676
|
+
constraints: [...normalized.constraints],
|
|
677
|
+
key: normalized.key,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
expanded = next;
|
|
682
|
+
}
|
|
683
|
+
for (const entry of expanded) {
|
|
684
|
+
if (resultKeys.has(entry.key))
|
|
685
|
+
continue;
|
|
686
|
+
if (result.length >= maxConstraintAlternatives) {
|
|
687
|
+
const first = alternative[0];
|
|
688
|
+
throw new SchemaEmissionError(first?.path ?? "/", first?.capabilityId);
|
|
689
|
+
}
|
|
690
|
+
resultKeys.add(entry.key);
|
|
691
|
+
result.push(entry.constraints);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return result;
|
|
695
|
+
};
|
|
696
|
+
const compatibleAlternatives = (alternatives, current) => {
|
|
697
|
+
const state = { remainingWork: maxExpansionWork };
|
|
698
|
+
return expandAlternatives(alternatives, state).filter((alternative) => {
|
|
699
|
+
spendExpansionWork(state, 1, current.path, current.capabilityId);
|
|
700
|
+
return !impossible([...alternative, current]);
|
|
701
|
+
});
|
|
702
|
+
};
|
|
703
|
+
const deduplicateAlternatives = (alternatives, path, capabilityId) => {
|
|
704
|
+
const state = { remainingWork: maxExpansionWork };
|
|
705
|
+
const result = [];
|
|
706
|
+
const keys = new Set();
|
|
707
|
+
for (const alternative of alternatives) {
|
|
708
|
+
const normalized = normalizedAlternative(alternative, state);
|
|
709
|
+
if (keys.has(normalized.key))
|
|
710
|
+
continue;
|
|
711
|
+
if (result.length >= maxConstraintAlternatives)
|
|
712
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
713
|
+
keys.add(normalized.key);
|
|
714
|
+
result.push([...normalized.constraints]);
|
|
715
|
+
}
|
|
716
|
+
return result;
|
|
717
|
+
};
|
|
718
|
+
const disjoint = (left, right) => [...left].every((value) => !right.has(value));
|
|
719
|
+
const branchesProvablyDisjoint = (left, right, enclosing) => {
|
|
720
|
+
const leftConstraints = [...enclosing, left];
|
|
721
|
+
const rightConstraints = [...enclosing, right];
|
|
722
|
+
if (impossible(leftConstraints) || impossible(rightConstraints))
|
|
723
|
+
return true;
|
|
724
|
+
const leftTypes = effectiveTypes(leftConstraints);
|
|
725
|
+
const rightTypes = effectiveTypes(rightConstraints);
|
|
726
|
+
const commonTypes = intersect(leftTypes, rightTypes);
|
|
727
|
+
if (commonTypes.size === 0)
|
|
728
|
+
return true;
|
|
729
|
+
const leftFinite = effectiveFiniteLiterals(leftConstraints);
|
|
730
|
+
const rightFinite = effectiveFiniteLiterals(rightConstraints);
|
|
731
|
+
if (leftFinite !== undefined &&
|
|
732
|
+
rightFinite !== undefined &&
|
|
733
|
+
disjoint(new Set(leftFinite.keys()), new Set(rightFinite.keys())))
|
|
734
|
+
return true;
|
|
735
|
+
if (commonTypes.size !== 1 || !commonTypes.has("object"))
|
|
736
|
+
return false;
|
|
737
|
+
const leftDiscriminators = requiredDiscriminators(leftConstraints);
|
|
738
|
+
const rightDiscriminators = requiredDiscriminators(rightConstraints);
|
|
739
|
+
for (const [name, leftValues] of leftDiscriminators) {
|
|
740
|
+
const rightValues = rightDiscriminators.get(name);
|
|
741
|
+
if (rightValues && disjoint(leftValues, rightValues))
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
return false;
|
|
745
|
+
};
|
|
746
|
+
const assertExclusiveOneOf = (branches, enclosing, root, path, capabilityId) => {
|
|
747
|
+
const pairCount = (branches.length * (branches.length - 1)) / 2;
|
|
748
|
+
if (branches.length > maxConstraintAlternatives ||
|
|
749
|
+
enclosing.length > maxConstraintAlternatives ||
|
|
750
|
+
pairCount * Math.max(1, enclosing.length) > maxExclusivityProofWork)
|
|
751
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
752
|
+
for (let leftIndex = 0; leftIndex < branches.length; leftIndex++) {
|
|
753
|
+
const left = branches[leftIndex];
|
|
754
|
+
if (!isRecord(left))
|
|
755
|
+
throw new SchemaEmissionError(`${path}/${leftIndex}`, capabilityId);
|
|
756
|
+
for (let rightIndex = leftIndex + 1; rightIndex < branches.length; rightIndex++) {
|
|
757
|
+
const right = branches[rightIndex];
|
|
758
|
+
if (!isRecord(right))
|
|
759
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
760
|
+
const leftConstraint = schemaConstraint(left, root, `${path}/${leftIndex}`, capabilityId);
|
|
761
|
+
const rightConstraint = schemaConstraint(right, root, `${path}/${rightIndex}`, capabilityId);
|
|
762
|
+
if (enclosing.some((alternative) => !branchesProvablyDisjoint(leftConstraint, rightConstraint, alternative)))
|
|
763
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
const renderReference = (reference, root, path, capabilityId) => {
|
|
768
|
+
if (typeof reference !== "string")
|
|
769
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
770
|
+
const identity = referenceAliasIdentity(reference, root);
|
|
771
|
+
const alias = identity === undefined
|
|
772
|
+
? undefined
|
|
773
|
+
: aliases.get(aliasIdentityKey(identity));
|
|
774
|
+
if (alias === undefined)
|
|
775
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
776
|
+
return alias;
|
|
777
|
+
};
|
|
778
|
+
const projectedPropertyConstraints = (enclosing, name) => deduplicateAlternatives(enclosing.map((alternative) => alternative.flatMap((constraint) => {
|
|
779
|
+
const resolved = dereference(constraint.schema, constraint.root, constraint.path, constraint.capabilityId);
|
|
780
|
+
if (!resolved || !isRecord(resolved.schema.properties))
|
|
781
|
+
return [];
|
|
782
|
+
const property = resolved.schema.properties[name];
|
|
783
|
+
if (!isRecord(property))
|
|
784
|
+
return [];
|
|
785
|
+
return [
|
|
786
|
+
schemaConstraint(property, resolved.root, `${resolved.path}/properties/${pointerToken(name)}`, resolved.capabilityId),
|
|
787
|
+
];
|
|
788
|
+
})), enclosing[0]?.[0]?.path ?? "/", enclosing[0]?.[0]?.capabilityId);
|
|
789
|
+
const projectedItemConstraints = (enclosing) => deduplicateAlternatives(enclosing.map((alternative) => alternative.flatMap((constraint) => {
|
|
790
|
+
const resolved = dereference(constraint.schema, constraint.root, constraint.path, constraint.capabilityId);
|
|
791
|
+
if (!resolved || !isRecord(resolved.schema.items))
|
|
792
|
+
return [];
|
|
793
|
+
return [
|
|
794
|
+
schemaConstraint(resolved.schema.items, resolved.root, `${resolved.path}/items`, resolved.capabilityId),
|
|
795
|
+
];
|
|
796
|
+
})), enclosing[0]?.[0]?.path ?? "/", enclosing[0]?.[0]?.capabilityId);
|
|
797
|
+
const renderObject = (schema, root, path, capabilityId, enclosing = [[]], renderDepth = 0) => {
|
|
798
|
+
const rawProperties = schema.properties;
|
|
799
|
+
if (rawProperties !== undefined && !isRecord(rawProperties))
|
|
800
|
+
throw new SchemaEmissionError(`${path}/properties`, capabilityId);
|
|
801
|
+
const properties = (rawProperties ?? {});
|
|
802
|
+
const requiredValue = schema.required;
|
|
803
|
+
if (requiredValue !== undefined &&
|
|
804
|
+
(!Array.isArray(requiredValue) ||
|
|
805
|
+
requiredValue.some((value) => typeof value !== "string")))
|
|
806
|
+
throw new SchemaEmissionError(`${path}/required`, capabilityId);
|
|
807
|
+
const required = new Set((requiredValue ?? []));
|
|
808
|
+
if ([...required].some((name) => !Object.hasOwn(properties, name)))
|
|
809
|
+
throw new SchemaEmissionError(`${path}/required`, capabilityId);
|
|
810
|
+
if (schema.additionalProperties !== undefined &&
|
|
811
|
+
typeof schema.additionalProperties !== "boolean")
|
|
812
|
+
throw new SchemaEmissionError(`${path}/additionalProperties`, capabilityId);
|
|
813
|
+
const members = Object.keys(properties)
|
|
814
|
+
.sort(compareCodePoints)
|
|
815
|
+
.map((name) => {
|
|
816
|
+
const child = properties[name];
|
|
817
|
+
if (!isRecord(child))
|
|
818
|
+
throw new SchemaEmissionError(`${path}/properties/${pointerToken(name)}`, capabilityId);
|
|
819
|
+
return `readonly ${JSON.stringify(name)}${required.has(name) ? "" : "?"}: ${renderSchema(child, root, `${path}/properties/${pointerToken(name)}`, capabilityId, projectedPropertyConstraints(enclosing, name), renderDepth + 1)};`;
|
|
820
|
+
});
|
|
821
|
+
const shape = members.length === 0
|
|
822
|
+
? "Readonly<Record<string, never>>"
|
|
823
|
+
: `{ ${members.join(" ")} }`;
|
|
824
|
+
if (schema.additionalProperties === false)
|
|
825
|
+
return shape;
|
|
826
|
+
if (members.length === 0)
|
|
827
|
+
return "Readonly<Record<string, __CapaxleJsonValue>>";
|
|
828
|
+
return `Readonly<Record<string, __CapaxleJsonValue>> & ${shape}`;
|
|
829
|
+
};
|
|
830
|
+
const renderBase = (schema, root, path, capabilityId, enclosing = [[]], renderDepth = 0) => {
|
|
831
|
+
const rawType = schema.type;
|
|
832
|
+
let types;
|
|
833
|
+
if (rawType === undefined)
|
|
834
|
+
types = ["null", "boolean", "number", "string", "array", "object"];
|
|
835
|
+
else if (typeof rawType === "string")
|
|
836
|
+
types = [rawType];
|
|
837
|
+
else if (Array.isArray(rawType) &&
|
|
838
|
+
rawType.length === 2 &&
|
|
839
|
+
rawType.every((value) => typeof value === "string"))
|
|
840
|
+
types = rawType;
|
|
841
|
+
else
|
|
842
|
+
throw new SchemaEmissionError(`${path}/type`, capabilityId);
|
|
843
|
+
if (types.some((value) => !schemaTypes.has(value)))
|
|
844
|
+
throw new SchemaEmissionError(`${path}/type`, capabilityId);
|
|
845
|
+
const unique = [
|
|
846
|
+
...new Set(types.map((value) => (value === "integer" ? "number" : value))),
|
|
847
|
+
];
|
|
848
|
+
const rendered = unique.map((type) => {
|
|
849
|
+
if (type === "null" ||
|
|
850
|
+
type === "boolean" ||
|
|
851
|
+
type === "number" ||
|
|
852
|
+
type === "string")
|
|
853
|
+
return type;
|
|
854
|
+
if (type === "array") {
|
|
855
|
+
if (schema.items === undefined)
|
|
856
|
+
return "readonly __CapaxleJsonValue[]";
|
|
857
|
+
if (!isRecord(schema.items))
|
|
858
|
+
throw new SchemaEmissionError(`${path}/items`, capabilityId);
|
|
859
|
+
return `readonly (${renderSchema(schema.items, root, `${path}/items`, capabilityId, projectedItemConstraints(enclosing), renderDepth + 1)})[]`;
|
|
860
|
+
}
|
|
861
|
+
return renderObject(schema, root, path, capabilityId, enclosing, renderDepth);
|
|
862
|
+
});
|
|
863
|
+
if (rawType === undefined &&
|
|
864
|
+
schema.properties === undefined &&
|
|
865
|
+
schema.required === undefined &&
|
|
866
|
+
schema.additionalProperties === undefined &&
|
|
867
|
+
schema.items === undefined)
|
|
868
|
+
return "__CapaxleJsonValue";
|
|
869
|
+
return rendered.length === 1 ? rendered[0] : `(${rendered.join(" | ")})`;
|
|
870
|
+
};
|
|
871
|
+
const renderSchema = (schema, root, path, capabilityId, enclosing = [[]], renderDepth = 0) => {
|
|
872
|
+
if (renderDepth > maxExpansionDepth)
|
|
873
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
874
|
+
if (!isRecord(schema))
|
|
875
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
876
|
+
if (Object.keys(schema).some((key) => !schemaKeys.has(key)))
|
|
877
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
878
|
+
if (schema.$schema !== undefined &&
|
|
879
|
+
schema.$schema !== "https://json-schema.org/draft/2020-12/schema")
|
|
880
|
+
throw new SchemaEmissionError(`${path}/$schema`, capabilityId);
|
|
881
|
+
if (schema.$ref !== undefined) {
|
|
882
|
+
if (Object.keys(schema).length !== 1)
|
|
883
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
884
|
+
return renderReference(schema.$ref, root, `${path}/$ref`, capabilityId);
|
|
885
|
+
}
|
|
886
|
+
const localConstraint = schemaConstraint(withoutOneOf(schema), root, path, capabilityId);
|
|
887
|
+
const compatible = compatibleAlternatives(enclosing, localConstraint);
|
|
888
|
+
const components = [
|
|
889
|
+
renderBase(schema, root, path, capabilityId, compatible, renderDepth),
|
|
890
|
+
];
|
|
891
|
+
if (Object.hasOwn(schema, "const"))
|
|
892
|
+
components.push(literalType(asJsonValue(schema.const, `${path}/const`)));
|
|
893
|
+
if (schema.enum !== undefined) {
|
|
894
|
+
if (!Array.isArray(schema.enum) || schema.enum.length === 0)
|
|
895
|
+
throw new SchemaEmissionError(`${path}/enum`, capabilityId);
|
|
896
|
+
components.push(`(${schema.enum
|
|
897
|
+
.map((value, index) => literalType(asJsonValue(value, `${path}/enum/${index}`)))
|
|
898
|
+
.join(" | ")})`);
|
|
899
|
+
}
|
|
900
|
+
if (schema.oneOf !== undefined) {
|
|
901
|
+
if (!Array.isArray(schema.oneOf) || schema.oneOf.length < 2)
|
|
902
|
+
throw new SchemaEmissionError(`${path}/oneOf`, capabilityId);
|
|
903
|
+
const effectiveEnclosing = compatible.map((alternative) => [
|
|
904
|
+
...alternative,
|
|
905
|
+
localConstraint,
|
|
906
|
+
]);
|
|
907
|
+
assertExclusiveOneOf(schema.oneOf, effectiveEnclosing, root, `${path}/oneOf`, capabilityId);
|
|
908
|
+
components.push(`(${schema.oneOf
|
|
909
|
+
.map((branch, index) => {
|
|
910
|
+
if (!isRecord(branch))
|
|
911
|
+
throw new SchemaEmissionError(`${path}/oneOf/${index}`, capabilityId);
|
|
912
|
+
return renderSchema(branch, root, `${path}/oneOf/${index}`, capabilityId, effectiveEnclosing, renderDepth + 1);
|
|
913
|
+
})
|
|
914
|
+
.join(" | ")})`);
|
|
915
|
+
}
|
|
916
|
+
return components.length === 1
|
|
917
|
+
? components[0]
|
|
918
|
+
: components.map((component) => `(${component})`).join(" & ");
|
|
919
|
+
};
|
|
920
|
+
const renderBinding = (binding, root, path, capabilityId) => {
|
|
921
|
+
if (binding.schema !== undefined && binding.$ref === undefined) {
|
|
922
|
+
const alias = aliases.get(rootIdentityKey(root));
|
|
923
|
+
if (alias !== undefined)
|
|
924
|
+
return alias;
|
|
925
|
+
}
|
|
926
|
+
if (binding.$ref !== undefined && binding.schema === undefined)
|
|
927
|
+
return renderReference(binding.$ref, root, `${path}/$ref`, capabilityId);
|
|
928
|
+
throw new SchemaEmissionError(path, capabilityId);
|
|
929
|
+
};
|
|
930
|
+
return { renderSchema, renderBinding };
|
|
931
|
+
}
|
|
932
|
+
function renderDeclaredErrors(capability, capabilityPath, renderBinding) {
|
|
933
|
+
const errors = Object.keys(capability.errors).sort(compareCodePoints);
|
|
934
|
+
if (errors.length === 0)
|
|
935
|
+
return "never";
|
|
936
|
+
return errors
|
|
937
|
+
.map((code) => {
|
|
938
|
+
const error = capability.errors[code];
|
|
939
|
+
if (typeof error.message !== "string" ||
|
|
940
|
+
typeof error.retryable !== "boolean" ||
|
|
941
|
+
typeof error.status !== "string")
|
|
942
|
+
throw new SchemaEmissionError(`${capabilityPath}/errors/${pointerToken(code)}`, capability.id);
|
|
943
|
+
const details = error.details
|
|
944
|
+
? ` readonly details: ${renderBinding(error.details, {
|
|
945
|
+
kind: "capability",
|
|
946
|
+
id: capability.id,
|
|
947
|
+
version: capability.version,
|
|
948
|
+
binding: "error-details",
|
|
949
|
+
errorCode: code,
|
|
950
|
+
}, `${capabilityPath}/errors/${pointerToken(code)}/details`, capability.id)};`
|
|
951
|
+
: "";
|
|
952
|
+
return `{ readonly code: ${JSON.stringify(code)}; readonly status: ${JSON.stringify(error.status)}; readonly message: ${JSON.stringify(error.message)}; readonly retryable: ${String(error.retryable)}; readonly correlationId: string;${details} }`;
|
|
953
|
+
})
|
|
954
|
+
.join(" | ");
|
|
955
|
+
}
|
|
956
|
+
function renderFacadeNode(node, indent) {
|
|
957
|
+
const entries = [...node.children.entries()].sort(([left], [right]) => compareCodePoints(left, right));
|
|
958
|
+
if (entries.length === 0)
|
|
959
|
+
return "Readonly<Record<string, never>>";
|
|
960
|
+
const nextIndent = `${indent} `;
|
|
961
|
+
return `Readonly<{\n${entries
|
|
962
|
+
.map(([name, child]) => {
|
|
963
|
+
const value = child.leaf
|
|
964
|
+
? `(input: ${child.leaf.input}, options?: import("@capaxle/core").InternalInvocationOptions) => Promise<import("@capaxle/core").CapabilityInvocationResult<${child.leaf.output}, ${child.leaf.error}>>`
|
|
965
|
+
: renderFacadeNode(child, nextIndent);
|
|
966
|
+
return `${nextIndent}readonly ${propertyName(name)}: ${value};`;
|
|
967
|
+
})
|
|
968
|
+
.join("\n")}\n${indent}}>`;
|
|
969
|
+
}
|
|
970
|
+
function selectedCapabilities(document) {
|
|
971
|
+
return document.capabilities
|
|
972
|
+
.map((capability, index) => ({ capability, index }))
|
|
973
|
+
.filter(({ capability }) => capability.access.exposure.internal !== "disabled")
|
|
974
|
+
.sort((left, right) => compareCodePoints(left.capability.id, right.capability.id) ||
|
|
975
|
+
compareSemVer(left.capability.version, right.capability.version));
|
|
976
|
+
}
|
|
977
|
+
function validateSelection(selected) {
|
|
978
|
+
const diagnostics = [];
|
|
979
|
+
const seen = new Map();
|
|
980
|
+
for (const item of selected) {
|
|
981
|
+
const { id, version } = item.capability;
|
|
982
|
+
const path = `/capabilities/${item.index}/id`;
|
|
983
|
+
const segments = typeof id === "string" ? id.split(".") : [];
|
|
984
|
+
if (typeof id !== "string" ||
|
|
985
|
+
!capabilityIdPattern.test(id) ||
|
|
986
|
+
segments.some((segment) => reservedSegments.has(segment))) {
|
|
987
|
+
diagnostics.push(diagnostic("CAP_INTERNAL_FACADE_NAME_INVALID", "Internal facade capability paths must be canonical and contain no reserved segment.", {
|
|
988
|
+
...(typeof id === "string" ? { capabilityId: id } : {}),
|
|
989
|
+
path,
|
|
990
|
+
}));
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const previous = seen.get(id);
|
|
994
|
+
if (previous) {
|
|
995
|
+
diagnostics.push(diagnostic("CAP_INTERNAL_FACADE_NAME_COLLISION", "Internal facade selection contains a duplicate path or multiple selected versions.", {
|
|
996
|
+
capabilityId: id,
|
|
997
|
+
path,
|
|
998
|
+
details: {
|
|
999
|
+
conflictingVersion: previous.capability.version,
|
|
1000
|
+
selectedVersion: version,
|
|
1001
|
+
},
|
|
1002
|
+
}));
|
|
1003
|
+
}
|
|
1004
|
+
else
|
|
1005
|
+
seen.set(id, item);
|
|
1006
|
+
}
|
|
1007
|
+
const ids = [...seen.keys()].sort(compareCodePoints);
|
|
1008
|
+
for (const id of ids) {
|
|
1009
|
+
const prefix = ids.find((candidate) => id.startsWith(`${candidate}.`));
|
|
1010
|
+
if (prefix)
|
|
1011
|
+
diagnostics.push(diagnostic("CAP_INTERNAL_FACADE_NAME_COLLISION", "Internal facade selection contains a leaf and namespace collision.", {
|
|
1012
|
+
capabilityId: id,
|
|
1013
|
+
path: `/capabilities/${seen.get(id).index}/id`,
|
|
1014
|
+
details: { prefix },
|
|
1015
|
+
}));
|
|
1016
|
+
}
|
|
1017
|
+
return diagnostics.sort((left, right) => compareCodePoints(left.capabilityId ?? "", right.capabilityId ?? "") ||
|
|
1018
|
+
compareCodePoints(left.path ?? "", right.path ?? "") ||
|
|
1019
|
+
compareCodePoints(left.code, right.code));
|
|
1020
|
+
}
|
|
1021
|
+
export function generateInternalFacade(options) {
|
|
1022
|
+
const { document, irHash } = options;
|
|
1023
|
+
if (!isRecord(document) ||
|
|
1024
|
+
document.irVersion !== "0.1" ||
|
|
1025
|
+
!isRecord(document.service) ||
|
|
1026
|
+
typeof document.service.name !== "string" ||
|
|
1027
|
+
typeof document.service.version !== "string" ||
|
|
1028
|
+
!isRecord(document.schemas) ||
|
|
1029
|
+
!Array.isArray(document.capabilities) ||
|
|
1030
|
+
!hashPattern.test(irHash))
|
|
1031
|
+
return Object.freeze({
|
|
1032
|
+
ok: false,
|
|
1033
|
+
diagnostics: Object.freeze([
|
|
1034
|
+
diagnostic("CAP_INTERNAL_FACADE_SCHEMA_UNREPRESENTABLE", "The normalized Capability IR dependency is invalid.", { path: "/" }),
|
|
1035
|
+
]),
|
|
1036
|
+
});
|
|
1037
|
+
const selected = selectedCapabilities(document);
|
|
1038
|
+
const selectionDiagnostics = validateSelection(selected);
|
|
1039
|
+
if (selectionDiagnostics.length > 0)
|
|
1040
|
+
return Object.freeze({
|
|
1041
|
+
ok: false,
|
|
1042
|
+
diagnostics: Object.freeze(selectionDiagnostics),
|
|
1043
|
+
});
|
|
1044
|
+
try {
|
|
1045
|
+
const { specs, aliases } = collectAliases(document, selected);
|
|
1046
|
+
assertRepresentableAliasCycles(specs, aliases);
|
|
1047
|
+
const { renderSchema, renderBinding } = schemaTypeEmitter(aliases, specs);
|
|
1048
|
+
const root = { children: new Map() };
|
|
1049
|
+
for (const { capability, index } of selected) {
|
|
1050
|
+
const capabilityPath = `/capabilities/${index}`;
|
|
1051
|
+
const leaf = {
|
|
1052
|
+
input: renderBinding(capability.input, {
|
|
1053
|
+
kind: "capability",
|
|
1054
|
+
id: capability.id,
|
|
1055
|
+
version: capability.version,
|
|
1056
|
+
binding: "input",
|
|
1057
|
+
}, `${capabilityPath}/input`, capability.id),
|
|
1058
|
+
output: renderBinding(capability.output, {
|
|
1059
|
+
kind: "capability",
|
|
1060
|
+
id: capability.id,
|
|
1061
|
+
version: capability.version,
|
|
1062
|
+
binding: "output",
|
|
1063
|
+
}, `${capabilityPath}/output`, capability.id),
|
|
1064
|
+
error: renderDeclaredErrors(capability, capabilityPath, renderBinding),
|
|
1065
|
+
};
|
|
1066
|
+
let node = root;
|
|
1067
|
+
for (const segment of capability.id.split(".")) {
|
|
1068
|
+
let child = node.children.get(segment);
|
|
1069
|
+
if (!child) {
|
|
1070
|
+
child = { children: new Map() };
|
|
1071
|
+
node.children.set(segment, child);
|
|
1072
|
+
}
|
|
1073
|
+
node = child;
|
|
1074
|
+
}
|
|
1075
|
+
node.leaf = leaf;
|
|
1076
|
+
}
|
|
1077
|
+
const aliasesSource = specs
|
|
1078
|
+
.map((spec) => {
|
|
1079
|
+
const alias = aliases.get(aliasIdentityKey(spec.identity));
|
|
1080
|
+
return `type ${alias} = ${renderSchema(spec.schema, spec.root, spec.path, spec.capabilityId)};`;
|
|
1081
|
+
})
|
|
1082
|
+
.join("\n");
|
|
1083
|
+
const facade = renderFacadeNode(root, "");
|
|
1084
|
+
const topLevel = [...root.children.keys()].sort(compareCodePoints);
|
|
1085
|
+
const serviceMarker = `${document.service.name}@${document.service.version}`;
|
|
1086
|
+
const augmentationMembers = [
|
|
1087
|
+
...topLevel.map((name) => ` readonly ${propertyName(name)}: InternalCapabilities[${JSON.stringify(name)}];`),
|
|
1088
|
+
` readonly __capaxleGeneratedFacadeService__?: ${JSON.stringify(serviceMarker)};`,
|
|
1089
|
+
].join("\n");
|
|
1090
|
+
const source = [
|
|
1091
|
+
`// @generated producer=${INTERNAL_FACADE_PRODUCER_ID} version=${INTERNAL_FACADE_PRODUCER_VERSION} target=${INTERNAL_FACADE_TARGET} irVersion=${document.irVersion} irHash=${irHash} service=${JSON.stringify(serviceMarker)}`,
|
|
1092
|
+
"type __CapaxleJsonValue = null | boolean | number | string | readonly __CapaxleJsonValue[] | { readonly [key: string]: __CapaxleJsonValue };",
|
|
1093
|
+
...(aliasesSource === "" ? [] : [aliasesSource]),
|
|
1094
|
+
`export type InternalCapabilities = ${facade};`,
|
|
1095
|
+
'declare module "@capaxle/core" {',
|
|
1096
|
+
" interface GeneratedCapabilityFacade {",
|
|
1097
|
+
augmentationMembers,
|
|
1098
|
+
" }",
|
|
1099
|
+
"}",
|
|
1100
|
+
"",
|
|
1101
|
+
].join("\n");
|
|
1102
|
+
return Object.freeze({
|
|
1103
|
+
ok: true,
|
|
1104
|
+
bytes: new TextEncoder().encode(source),
|
|
1105
|
+
diagnostics: Object.freeze([]),
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
catch (error) {
|
|
1109
|
+
const failure = error instanceof SchemaEmissionError
|
|
1110
|
+
? error
|
|
1111
|
+
: new SchemaEmissionError("/");
|
|
1112
|
+
return Object.freeze({
|
|
1113
|
+
ok: false,
|
|
1114
|
+
diagnostics: Object.freeze([
|
|
1115
|
+
diagnostic("CAP_INTERNAL_FACADE_SCHEMA_UNREPRESENTABLE", "An accepted Capability IR schema cannot be represented truthfully as TypeScript.", {
|
|
1116
|
+
...(failure.capabilityId === undefined
|
|
1117
|
+
? {}
|
|
1118
|
+
: { capabilityId: failure.capabilityId }),
|
|
1119
|
+
path: failure.path || "/",
|
|
1120
|
+
}),
|
|
1121
|
+
]),
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
// The HTTP SDK is a separate artifact from the internal facade. It shares only
|
|
1126
|
+
// the pure schema-to-TypeScript emitter above; no internal invocation code enters
|
|
1127
|
+
// the generated client.
|
|
1128
|
+
export const SDK_HTTP_PRODUCER_ID = "capaxle.sdk-http-ts";
|
|
1129
|
+
export const SDK_HTTP_PRODUCER_VERSION = "0.1.0-alpha.1";
|
|
1130
|
+
export const SDK_HTTP_ARTIFACT_ID = "capaxle.sdk-http-ts";
|
|
1131
|
+
export const SDK_HTTP_ARTIFACT_PATH = "capaxle-sdk.ts";
|
|
1132
|
+
export const SDK_HTTP_MEDIA_TYPE = "text/typescript";
|
|
1133
|
+
export const SDK_HTTP_TARGET = "capaxle:sdk-http-ts@0.1";
|
|
1134
|
+
export const SDK_HTTP_DIAGNOSTIC_CODES = Object.freeze([
|
|
1135
|
+
Object.freeze({
|
|
1136
|
+
code: "CAP_SDK_HTTP_BINDING_INVALID",
|
|
1137
|
+
severities: Object.freeze(["error"]),
|
|
1138
|
+
}),
|
|
1139
|
+
Object.freeze({
|
|
1140
|
+
code: "CAP_SDK_NAME_COLLISION",
|
|
1141
|
+
severities: Object.freeze(["error"]),
|
|
1142
|
+
}),
|
|
1143
|
+
Object.freeze({
|
|
1144
|
+
code: "CAP_SDK_NAME_INVALID",
|
|
1145
|
+
severities: Object.freeze(["error"]),
|
|
1146
|
+
}),
|
|
1147
|
+
Object.freeze({
|
|
1148
|
+
code: "CAP_SDK_SCHEMA_UNREPRESENTABLE",
|
|
1149
|
+
severities: Object.freeze(["error"]),
|
|
1150
|
+
}),
|
|
1151
|
+
]);
|
|
1152
|
+
function sdkDiagnostic(code, message, path, capabilityId) {
|
|
1153
|
+
return {
|
|
1154
|
+
code,
|
|
1155
|
+
severity: "error",
|
|
1156
|
+
message,
|
|
1157
|
+
target: SDK_HTTP_TARGET,
|
|
1158
|
+
path,
|
|
1159
|
+
...(capabilityId ? { capabilityId } : {}),
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
function sdkTypeName(path, suffix) {
|
|
1163
|
+
return `Capability_${path
|
|
1164
|
+
.map((segment) => {
|
|
1165
|
+
const bytes = new TextEncoder().encode(segment);
|
|
1166
|
+
return `${bytes.length}_${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
1167
|
+
})
|
|
1168
|
+
.join("__")}_${suffix}`;
|
|
1169
|
+
}
|
|
1170
|
+
function sdkReadableTypeName(path, suffix) {
|
|
1171
|
+
return `Capability${path
|
|
1172
|
+
.map((segment) => Array.from(segment)
|
|
1173
|
+
.map((letter, index) => (index === 0 ? letter.toUpperCase() : letter))
|
|
1174
|
+
.join("")
|
|
1175
|
+
.replace(/[^A-Za-z0-9_$]/gu, (character) => `_u${character.codePointAt(0).toString(16)}_`))
|
|
1176
|
+
.join("")}${suffix}`;
|
|
1177
|
+
}
|
|
1178
|
+
function sdkValidSegment(segment) {
|
|
1179
|
+
return (typeof segment === "string" &&
|
|
1180
|
+
segment.length > 0 &&
|
|
1181
|
+
!reservedSegments.has(segment));
|
|
1182
|
+
}
|
|
1183
|
+
function sdkNodeType(node, indent) {
|
|
1184
|
+
const entries = [...node.children.entries()].sort(([a], [b]) => compareCodePoints(a, b));
|
|
1185
|
+
return `Readonly<{\n${entries
|
|
1186
|
+
.map(([name, child]) => {
|
|
1187
|
+
const type = child.leaf
|
|
1188
|
+
? `CapaxleSdkMethod<${child.leaf.input}, ${child.leaf.output}, ${child.leaf.error}>`
|
|
1189
|
+
: sdkNodeType(child, `${indent} `);
|
|
1190
|
+
const annotation = child.leaf?.deprecated
|
|
1191
|
+
? `${indent} /** @deprecated ${child.leaf.deprecated} */\n`
|
|
1192
|
+
: "";
|
|
1193
|
+
return `${annotation}${indent} readonly ${propertyName(name)}: ${type};`;
|
|
1194
|
+
})
|
|
1195
|
+
.join("\n")}\n${indent}}>`;
|
|
1196
|
+
}
|
|
1197
|
+
function sdkNodeValue(node, indent) {
|
|
1198
|
+
const entries = [...node.children.entries()].sort(([a], [b]) => compareCodePoints(a, b));
|
|
1199
|
+
return `{\n${entries.map(([name, child]) => `${indent} [${JSON.stringify(name)}]: ${child.leaf ? `makeMethod<${child.leaf.input}, ${child.leaf.output}, ${child.leaf.error}>(routes[${child.leaf.index}]!)` : sdkNodeValue(child, `${indent} `)},`).join("\n")}\n${indent}}`;
|
|
1200
|
+
}
|
|
1201
|
+
function sdkInputOpaque(document, binding) {
|
|
1202
|
+
let resource = binding.schema;
|
|
1203
|
+
let schema = resource;
|
|
1204
|
+
if (binding.$ref?.startsWith("#/schemas/")) {
|
|
1205
|
+
resource = document.schemas[decodePointerToken(binding.$ref.slice(10))];
|
|
1206
|
+
schema = resource;
|
|
1207
|
+
}
|
|
1208
|
+
const seen = new Set();
|
|
1209
|
+
while (schema && !seen.has(schema)) {
|
|
1210
|
+
seen.add(schema);
|
|
1211
|
+
const ref = schema.$ref;
|
|
1212
|
+
if (typeof ref !== "string")
|
|
1213
|
+
break;
|
|
1214
|
+
if (ref.startsWith("#/schemas/")) {
|
|
1215
|
+
resource = document.schemas[decodePointerToken(ref.slice(10))];
|
|
1216
|
+
schema = resource;
|
|
1217
|
+
}
|
|
1218
|
+
else if (ref.startsWith("#/$defs/") && isRecord(resource?.$defs)) {
|
|
1219
|
+
const candidate = resource.$defs[decodePointerToken(ref.slice(8))];
|
|
1220
|
+
schema = isRecord(candidate) ? candidate : undefined;
|
|
1221
|
+
}
|
|
1222
|
+
else
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
return !(schema?.type === "object" &&
|
|
1226
|
+
schema.additionalProperties === false &&
|
|
1227
|
+
isRecord(schema.properties) &&
|
|
1228
|
+
schema.const === undefined &&
|
|
1229
|
+
schema.enum === undefined &&
|
|
1230
|
+
schema.oneOf === undefined);
|
|
1231
|
+
}
|
|
1232
|
+
function sdkValidHttpPath(path) {
|
|
1233
|
+
if (typeof path !== "string" ||
|
|
1234
|
+
!/^\/(?:[A-Za-z0-9._~-]+|\{[A-Za-z_][A-Za-z0-9_.-]*\})(?:\/(?:[A-Za-z0-9._~-]+|\{[A-Za-z_][A-Za-z0-9_.-]*\}))*$/u.test(path) ||
|
|
1235
|
+
path.includes("//") ||
|
|
1236
|
+
path.split("/").some((segment) => segment === "." || segment === ".."))
|
|
1237
|
+
return false;
|
|
1238
|
+
const variables = [...path.matchAll(/\{([A-Za-z_][A-Za-z0-9_.-]*)\}/gu)].map((match) => match[1]);
|
|
1239
|
+
return new Set(variables).size === variables.length;
|
|
1240
|
+
}
|
|
1241
|
+
function renderSdkDeclaredErrors(capability, capabilityPath, renderBinding) {
|
|
1242
|
+
const codes = Object.keys(capability.errors).sort(compareCodePoints);
|
|
1243
|
+
if (codes.length === 0)
|
|
1244
|
+
return "never";
|
|
1245
|
+
return codes
|
|
1246
|
+
.map((code) => {
|
|
1247
|
+
const definition = capability.errors[code];
|
|
1248
|
+
const details = definition.details
|
|
1249
|
+
? ` readonly details: ${renderBinding(definition.details, { kind: "capability", id: capability.id, version: capability.version, binding: "error-details", errorCode: code }, `${capabilityPath}/errors/${pointerToken(code)}/details`, capability.id)};`
|
|
1250
|
+
: "";
|
|
1251
|
+
return `{ readonly code: ${JSON.stringify(code)}; readonly status: ${JSON.stringify(definition.status)}; readonly message: string; readonly retryable: ${String(definition.retryable)}; readonly correlationId: string;${details} }`;
|
|
1252
|
+
})
|
|
1253
|
+
.join(" | ");
|
|
1254
|
+
}
|
|
1255
|
+
const SDK_HTTP_RUNTIME_SOURCE = String.raw `
|
|
1256
|
+
export type CapaxleErrorStatus = "invalid_argument" | "unauthenticated" | "permission_denied" | "not_found" | "already_exists" | "failed_precondition" | "conflict" | "resource_exhausted" | "cancelled" | "deadline_exceeded" | "unavailable" | "internal";
|
|
1257
|
+
export interface CapaxleCanonicalError {
|
|
1258
|
+
readonly code: string;
|
|
1259
|
+
readonly status: CapaxleErrorStatus;
|
|
1260
|
+
readonly message: string;
|
|
1261
|
+
readonly retryable: boolean;
|
|
1262
|
+
readonly correlationId: string;
|
|
1263
|
+
readonly details?: __CapaxleJsonValue;
|
|
1264
|
+
}
|
|
1265
|
+
export class CapaxleClientError<Declared = never> extends Error {
|
|
1266
|
+
readonly name = "CapaxleClientError";
|
|
1267
|
+
readonly code: string;
|
|
1268
|
+
readonly status: CapaxleErrorStatus;
|
|
1269
|
+
readonly retryable: boolean;
|
|
1270
|
+
readonly correlationId: string;
|
|
1271
|
+
readonly details?: __CapaxleJsonValue;
|
|
1272
|
+
readonly httpStatus?: number;
|
|
1273
|
+
readonly origin: "server" | "client";
|
|
1274
|
+
readonly capabilityId?: string;
|
|
1275
|
+
readonly capabilityVersion?: string;
|
|
1276
|
+
readonly declared: Declared | null;
|
|
1277
|
+
constructor(error: CapaxleCanonicalError, origin: "server" | "client", httpStatus?: number, declaredErrors: readonly { readonly code: string; readonly status: CapaxleErrorStatus; readonly retryable: boolean }[] = [], capabilityId?: string, capabilityVersion?: string) {
|
|
1278
|
+
super(error.message);
|
|
1279
|
+
this.code = error.code;
|
|
1280
|
+
this.status = error.status;
|
|
1281
|
+
this.retryable = error.retryable;
|
|
1282
|
+
this.correlationId = error.correlationId;
|
|
1283
|
+
if (error.details !== undefined) this.details = error.details;
|
|
1284
|
+
if (httpStatus !== undefined) this.httpStatus = httpStatus;
|
|
1285
|
+
this.origin = origin;
|
|
1286
|
+
if (capabilityId !== undefined) this.capabilityId = capabilityId;
|
|
1287
|
+
if (capabilityVersion !== undefined) this.capabilityVersion = capabilityVersion;
|
|
1288
|
+
this.declared = origin === "server" && declaredErrors.some((definition) => definition.code === error.code && definition.status === error.status && definition.retryable === error.retryable) ? error as unknown as Declared : null;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
export interface CapaxleCallOptions {
|
|
1292
|
+
readonly idempotencyKey?: string;
|
|
1293
|
+
readonly confirmationToken?: string;
|
|
1294
|
+
readonly correlationId?: string;
|
|
1295
|
+
readonly deadlineMs?: number;
|
|
1296
|
+
readonly signal?: AbortSignal;
|
|
1297
|
+
}
|
|
1298
|
+
export interface CapaxleClientConfig {
|
|
1299
|
+
readonly baseUrl: string;
|
|
1300
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
1301
|
+
readonly headers?: HeadersInit;
|
|
1302
|
+
readonly auth?: () => HeadersInit | Promise<HeadersInit>;
|
|
1303
|
+
readonly credentials?: RequestCredentials;
|
|
1304
|
+
}
|
|
1305
|
+
export interface CapaxleSdkMethod<Input, Output, DeclaredError> {
|
|
1306
|
+
(input: Input, options?: CapaxleCallOptions): Promise<Output>;
|
|
1307
|
+
readonly raw: (input: Input, options?: CapaxleCallOptions) => Promise<{ readonly value: Output; readonly response: Response }>;
|
|
1308
|
+
readonly isDeclaredError: (error: unknown) => error is CapaxleClientError<DeclaredError> & { readonly declared: DeclaredError };
|
|
1309
|
+
}
|
|
1310
|
+
interface Route {
|
|
1311
|
+
readonly id: string;
|
|
1312
|
+
readonly version: string;
|
|
1313
|
+
readonly method: string;
|
|
1314
|
+
readonly path: string;
|
|
1315
|
+
readonly bindings: Readonly<Record<string, "path" | "query" | "header" | "body">>;
|
|
1316
|
+
readonly opaque: boolean;
|
|
1317
|
+
readonly declaredErrors: readonly { readonly code: string; readonly status: CapaxleErrorStatus; readonly retryable: boolean }[];
|
|
1318
|
+
}
|
|
1319
|
+
const canonicalStatuses = new Set<string>(["invalid_argument", "unauthenticated", "permission_denied", "not_found", "already_exists", "failed_precondition", "conflict", "resource_exhausted", "cancelled", "deadline_exceeded", "unavailable", "internal"]);
|
|
1320
|
+
function clientFailure(code: string, status: CapaxleErrorStatus, message: string): CapaxleClientError {
|
|
1321
|
+
return new CapaxleClientError({ code, status, message, retryable: false, correlationId: "" }, "client");
|
|
1322
|
+
}
|
|
1323
|
+
function canonicalError(value: unknown): CapaxleCanonicalError | null {
|
|
1324
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
1325
|
+
const envelope = value as Record<string, unknown>;
|
|
1326
|
+
if (envelope.ok !== false || typeof envelope.error !== "object" || envelope.error === null || Array.isArray(envelope.error)) return null;
|
|
1327
|
+
const error = envelope.error as Record<string, unknown>;
|
|
1328
|
+
if (typeof error.code !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(error.code) ||
|
|
1329
|
+
typeof error.status !== "string" || !canonicalStatuses.has(error.status) ||
|
|
1330
|
+
typeof error.message !== "string" || typeof error.retryable !== "boolean" ||
|
|
1331
|
+
typeof error.correlationId !== "string") return null;
|
|
1332
|
+
return error as unknown as CapaxleCanonicalError;
|
|
1333
|
+
}
|
|
1334
|
+
function wireValue(value: unknown): string {
|
|
1335
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1336
|
+
throw clientFailure("CAP_SDK_INPUT_INVALID", "invalid_argument", "HTTP-bound input must be scalar.");
|
|
1337
|
+
}
|
|
1338
|
+
function interruptible<T>(start: () => T | Promise<T>, signal: AbortSignal): Promise<T> {
|
|
1339
|
+
return new Promise<T>((resolve, reject) => {
|
|
1340
|
+
if (signal.aborted) { reject(new Error("aborted")); return; }
|
|
1341
|
+
const abort = () => reject(new Error("aborted"));
|
|
1342
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1343
|
+
Promise.resolve().then(() => {
|
|
1344
|
+
if (signal.aborted) throw new Error("aborted");
|
|
1345
|
+
return start();
|
|
1346
|
+
}).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
function makeMethodFactory(config: CapaxleClientConfig) {
|
|
1350
|
+
if (typeof config.baseUrl !== "string" || config.baseUrl.length === 0) throw new TypeError("baseUrl is required");
|
|
1351
|
+
const transport = config.fetch ?? globalThis.fetch;
|
|
1352
|
+
if (typeof transport !== "function") throw new TypeError("fetch is required");
|
|
1353
|
+
const base = new URL(config.baseUrl);
|
|
1354
|
+
return function makeMethod<Input, Output, DeclaredError>(route: Route): CapaxleSdkMethod<Input, Output, DeclaredError> {
|
|
1355
|
+
async function request(input: Input, options: CapaxleCallOptions = {}, preserveResponse = false): Promise<{ value: Output; response: Response }> {
|
|
1356
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) throw clientFailure("CAP_SDK_INPUT_INVALID", "invalid_argument", "Capability input must be an object.");
|
|
1357
|
+
const record = input as Record<string, unknown>;
|
|
1358
|
+
let path = route.path;
|
|
1359
|
+
const headers = new Headers(config.headers);
|
|
1360
|
+
const body: Record<string, unknown> = Object.create(null);
|
|
1361
|
+
let bodyFields = 0;
|
|
1362
|
+
for (const [name, binding] of Object.entries(route.bindings)) {
|
|
1363
|
+
if (!Object.hasOwn(record, name) || record[name] === undefined) continue;
|
|
1364
|
+
const value = record[name];
|
|
1365
|
+
if (binding === "body") { body[name] = value; bodyFields++; }
|
|
1366
|
+
else if (binding === "path") {
|
|
1367
|
+
const marker = "{" + name + "}";
|
|
1368
|
+
if (!path.includes(marker)) throw clientFailure("CAP_SDK_ROUTE_INVALID", "internal", "Generated route binding is invalid.");
|
|
1369
|
+
const wire = wireValue(value);
|
|
1370
|
+
if (wire === "." || wire === "..") throw clientFailure("CAP_SDK_INPUT_INVALID", "invalid_argument", "Path input cannot be a dot segment.");
|
|
1371
|
+
path = path.replace(marker, encodeURIComponent(wire));
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (path.includes("{")) throw clientFailure("CAP_SDK_INPUT_INVALID", "invalid_argument", "Required path input is missing.");
|
|
1375
|
+
const url = new URL(path, base);
|
|
1376
|
+
if (url.origin !== base.origin) throw clientFailure("CAP_SDK_ROUTE_INVALID", "internal", "Generated route changes the configured origin.");
|
|
1377
|
+
for (const [name, binding] of Object.entries(route.bindings)) {
|
|
1378
|
+
if (binding !== "query" || !Object.hasOwn(record, name) || record[name] === undefined) continue;
|
|
1379
|
+
const value = record[name];
|
|
1380
|
+
if (Array.isArray(value)) for (const element of value) url.searchParams.append(name, wireValue(element));
|
|
1381
|
+
else url.searchParams.append(name, wireValue(value));
|
|
1382
|
+
}
|
|
1383
|
+
for (const [name, value] of Object.entries(record)) if (!Object.hasOwn(route.bindings, name)) { body[name] = value; bodyFields++; }
|
|
1384
|
+
const opaque = route.opaque;
|
|
1385
|
+
const hasBody = opaque || bodyFields > 0;
|
|
1386
|
+
if (hasBody && (route.method === "GET" || route.method === "DELETE")) throw clientFailure("CAP_SDK_INPUT_INVALID", "invalid_argument", "This HTTP route cannot carry a body.");
|
|
1387
|
+
if (options.deadlineMs !== undefined && (!Number.isFinite(options.deadlineMs) || options.deadlineMs < 0))
|
|
1388
|
+
throw clientFailure("CAP_SDK_INPUT_INVALID", "invalid_argument", "deadlineMs must be a nonnegative finite number.");
|
|
1389
|
+
const controller = new AbortController();
|
|
1390
|
+
let timedOut = false;
|
|
1391
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1392
|
+
const abort = () => controller.abort();
|
|
1393
|
+
if (options.signal?.aborted) controller.abort();
|
|
1394
|
+
else options.signal?.addEventListener("abort", abort, { once: true });
|
|
1395
|
+
if (options.deadlineMs !== undefined) {
|
|
1396
|
+
timer = setTimeout(() => { timedOut = true; controller.abort(); }, options.deadlineMs);
|
|
1397
|
+
}
|
|
1398
|
+
let response: Response;
|
|
1399
|
+
let rawResponse: Response | undefined;
|
|
1400
|
+
let decoded: unknown;
|
|
1401
|
+
try {
|
|
1402
|
+
if (config.auth) {
|
|
1403
|
+
const authHeaders = new Headers(await interruptible(() => config.auth!(), controller.signal));
|
|
1404
|
+
authHeaders.forEach((value, key) => headers.set(key, value));
|
|
1405
|
+
}
|
|
1406
|
+
for (const [name, binding] of Object.entries(route.bindings))
|
|
1407
|
+
if (binding === "header" && Object.hasOwn(record, name) && record[name] !== undefined)
|
|
1408
|
+
headers.set("X-Cap-Input-" + name, wireValue(record[name]));
|
|
1409
|
+
if (hasBody) headers.set("Content-Type", "application/json");
|
|
1410
|
+
if (options.idempotencyKey !== undefined) headers.set("Idempotency-Key", options.idempotencyKey);
|
|
1411
|
+
if (options.confirmationToken !== undefined) headers.set("X-Cap-Confirmation", options.confirmationToken);
|
|
1412
|
+
if (options.correlationId !== undefined) headers.set("X-Correlation-Id", options.correlationId);
|
|
1413
|
+
response = await interruptible(() => transport(url, { method: route.method, headers, ...(hasBody ? { body: JSON.stringify(body) } : {}), ...(config.credentials ? { credentials: config.credentials } : {}), signal: controller.signal }), controller.signal);
|
|
1414
|
+
if (preserveResponse) rawResponse = response.clone();
|
|
1415
|
+
try { decoded = await interruptible(() => response.json(), controller.signal); }
|
|
1416
|
+
catch {
|
|
1417
|
+
if (timedOut) throw clientFailure("CAP_DEADLINE_EXCEEDED", "deadline_exceeded", "Client deadline exceeded.");
|
|
1418
|
+
if (controller.signal.aborted) throw clientFailure("CAP_CANCELLED", "cancelled", "Client request cancelled.");
|
|
1419
|
+
throw clientFailure("CAP_SDK_PROTOCOL_ERROR", "unavailable", "HTTP response is not valid JSON.");
|
|
1420
|
+
}
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
if (error instanceof CapaxleClientError) throw error;
|
|
1423
|
+
if (timedOut) throw clientFailure("CAP_DEADLINE_EXCEEDED", "deadline_exceeded", "Client deadline exceeded.");
|
|
1424
|
+
if (controller.signal.aborted) throw clientFailure("CAP_CANCELLED", "cancelled", "Client request cancelled.");
|
|
1425
|
+
throw clientFailure("CAP_SDK_TRANSPORT_ERROR", "unavailable", "HTTP transport failed.");
|
|
1426
|
+
} finally {
|
|
1427
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
1428
|
+
options.signal?.removeEventListener("abort", abort);
|
|
1429
|
+
}
|
|
1430
|
+
if (!response.ok) {
|
|
1431
|
+
const canonical = canonicalError(decoded);
|
|
1432
|
+
if (canonical === null) throw clientFailure("CAP_SDK_PROTOCOL_ERROR", "unavailable", "HTTP error lacks a canonical envelope.");
|
|
1433
|
+
throw new CapaxleClientError<DeclaredError>(canonical, "server", response.status, route.declaredErrors, route.id, route.version);
|
|
1434
|
+
}
|
|
1435
|
+
return { value: decoded as Output, response: rawResponse ?? response };
|
|
1436
|
+
}
|
|
1437
|
+
const method = ((input: Input, options?: CapaxleCallOptions) => request(input, options).then((result) => result.value)) as CapaxleSdkMethod<Input, Output, DeclaredError>;
|
|
1438
|
+
Object.defineProperty(method, "raw", { value: (input: Input, options?: CapaxleCallOptions) => request(input, options, true), enumerable: true });
|
|
1439
|
+
Object.defineProperty(method, "isDeclaredError", { value: (error: unknown) => error instanceof CapaxleClientError && error.declared !== null && error.capabilityId === route.id && error.capabilityVersion === route.version && route.declaredErrors.some((definition) => definition.code === error.code && definition.status === error.status && definition.retryable === error.retryable), enumerable: true });
|
|
1440
|
+
return method;
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
`;
|
|
1444
|
+
export function generateSdkHttp(options) {
|
|
1445
|
+
const { document, irHash } = options;
|
|
1446
|
+
if (!isRecord(document) ||
|
|
1447
|
+
document.irVersion !== "0.1" ||
|
|
1448
|
+
!isRecord(document.service) ||
|
|
1449
|
+
typeof document.service.name !== "string" ||
|
|
1450
|
+
typeof document.service.version !== "string" ||
|
|
1451
|
+
!isRecord(document.schemas) ||
|
|
1452
|
+
!Array.isArray(document.capabilities) ||
|
|
1453
|
+
!hashPattern.test(irHash))
|
|
1454
|
+
return {
|
|
1455
|
+
ok: false,
|
|
1456
|
+
diagnostics: [
|
|
1457
|
+
sdkDiagnostic("CAP_SDK_SCHEMA_UNREPRESENTABLE", "The normalized Capability IR dependency is invalid.", "/"),
|
|
1458
|
+
],
|
|
1459
|
+
};
|
|
1460
|
+
const selected = document.capabilities
|
|
1461
|
+
.map((capability, index) => ({ capability, index }))
|
|
1462
|
+
.filter(({ capability }) => capability?.access?.exposure?.http !== "disabled" &&
|
|
1463
|
+
capability?.interfaces?.http?.enabled === true &&
|
|
1464
|
+
capability?.interfaces?.sdk?.enabled === true)
|
|
1465
|
+
.sort((a, b) => compareCodePoints(a.capability.id, b.capability.id) ||
|
|
1466
|
+
compareSemVer(a.capability.version, b.capability.version));
|
|
1467
|
+
const diagnostics = [];
|
|
1468
|
+
const root = { children: new Map() };
|
|
1469
|
+
const typeNames = new Set();
|
|
1470
|
+
for (const { capability, index } of selected) {
|
|
1471
|
+
const path = capability.interfaces.sdk.enabled
|
|
1472
|
+
? capability.interfaces.sdk.path
|
|
1473
|
+
: [];
|
|
1474
|
+
if (!Array.isArray(path) ||
|
|
1475
|
+
path.length === 0 ||
|
|
1476
|
+
path.some((segment) => !sdkValidSegment(segment))) {
|
|
1477
|
+
diagnostics.push(sdkDiagnostic("CAP_SDK_NAME_INVALID", "SDK path must contain nonempty, safe property names.", `/capabilities/${index}/interfaces/sdk/path`, capability.id));
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
const inputName = sdkTypeName(path, "Input");
|
|
1481
|
+
if (typeNames.has(inputName))
|
|
1482
|
+
diagnostics.push(sdkDiagnostic("CAP_SDK_NAME_COLLISION", "SDK type names collide.", `/capabilities/${index}/interfaces/sdk/path`, capability.id));
|
|
1483
|
+
typeNames.add(inputName);
|
|
1484
|
+
let node = root;
|
|
1485
|
+
for (const segment of path) {
|
|
1486
|
+
let child = node.children.get(segment);
|
|
1487
|
+
if (!child) {
|
|
1488
|
+
child = { children: new Map() };
|
|
1489
|
+
node.children.set(segment, child);
|
|
1490
|
+
}
|
|
1491
|
+
node = child;
|
|
1492
|
+
}
|
|
1493
|
+
if (node.leaf || node.children.size > 0)
|
|
1494
|
+
diagnostics.push(sdkDiagnostic("CAP_SDK_NAME_COLLISION", "SDK paths collide.", `/capabilities/${index}/interfaces/sdk/path`, capability.id));
|
|
1495
|
+
node.leaf = {
|
|
1496
|
+
input: inputName,
|
|
1497
|
+
output: sdkTypeName(path, "Output"),
|
|
1498
|
+
error: sdkTypeName(path, "DeclaredError"),
|
|
1499
|
+
index: selected.findIndex((item) => item.index === index),
|
|
1500
|
+
...(capability.lifecycle?.status === "deprecated"
|
|
1501
|
+
? {
|
|
1502
|
+
deprecated: capability.lifecycle.replacement
|
|
1503
|
+
? `Use ${capability.lifecycle.replacement}.`
|
|
1504
|
+
: "This capability is deprecated.",
|
|
1505
|
+
}
|
|
1506
|
+
: {}),
|
|
1507
|
+
};
|
|
1508
|
+
const http = capability.interfaces.http;
|
|
1509
|
+
if (!http.enabled ||
|
|
1510
|
+
!/^(GET|POST|PUT|PATCH|DELETE)$/u.test(http.method) ||
|
|
1511
|
+
!sdkValidHttpPath(http.path) ||
|
|
1512
|
+
!isRecord(http.bindings))
|
|
1513
|
+
diagnostics.push(sdkDiagnostic("CAP_SDK_HTTP_BINDING_INVALID", "Resolved HTTP route is invalid.", `/capabilities/${index}/interfaces/http`, capability.id));
|
|
1514
|
+
else if (Object.values(http.bindings).some((binding) => !["path", "query", "header", "body"].includes(binding)) ||
|
|
1515
|
+
(() => {
|
|
1516
|
+
const variables = [
|
|
1517
|
+
...http.path.matchAll(/\{([A-Za-z_][A-Za-z0-9_.-]*)\}/gu),
|
|
1518
|
+
].map((match) => match[1]);
|
|
1519
|
+
const bound = Object.entries(http.bindings)
|
|
1520
|
+
.filter(([, binding]) => binding === "path")
|
|
1521
|
+
.map(([name]) => name);
|
|
1522
|
+
return (variables.length !== bound.length ||
|
|
1523
|
+
variables.some((name) => !bound.includes(name)));
|
|
1524
|
+
})())
|
|
1525
|
+
diagnostics.push(sdkDiagnostic("CAP_SDK_HTTP_BINDING_INVALID", "Resolved HTTP bindings are invalid.", `/capabilities/${index}/interfaces/http/bindings`, capability.id));
|
|
1526
|
+
}
|
|
1527
|
+
for (const { capability, index } of selected) {
|
|
1528
|
+
const path = capability.interfaces.sdk.enabled
|
|
1529
|
+
? capability.interfaces.sdk.path
|
|
1530
|
+
: [];
|
|
1531
|
+
if (!Array.isArray(path) ||
|
|
1532
|
+
path.length === 0 ||
|
|
1533
|
+
path.some((segment) => !sdkValidSegment(segment)))
|
|
1534
|
+
continue;
|
|
1535
|
+
for (let length = 1; length < path.length; length++) {
|
|
1536
|
+
let node = root;
|
|
1537
|
+
for (const segment of path.slice(0, length))
|
|
1538
|
+
node = node.children.get(segment);
|
|
1539
|
+
if (node.leaf)
|
|
1540
|
+
diagnostics.push(sdkDiagnostic("CAP_SDK_NAME_COLLISION", "SDK leaf collides with namespace.", `/capabilities/${index}/interfaces/sdk/path`, capability.id));
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
if (diagnostics.length)
|
|
1544
|
+
return {
|
|
1545
|
+
ok: false,
|
|
1546
|
+
diagnostics: diagnostics.sort((a, b) => compareCodePoints(a.path ?? "", b.path ?? "") ||
|
|
1547
|
+
compareCodePoints(a.code, b.code)),
|
|
1548
|
+
};
|
|
1549
|
+
try {
|
|
1550
|
+
const facadeSelected = selected;
|
|
1551
|
+
const { specs, aliases } = collectAliases(document, facadeSelected);
|
|
1552
|
+
assertRepresentableAliasCycles(specs, aliases);
|
|
1553
|
+
const { renderSchema, renderBinding } = schemaTypeEmitter(aliases, specs);
|
|
1554
|
+
const aliasesSource = specs
|
|
1555
|
+
.map((spec) => `type ${aliases.get(aliasIdentityKey(spec.identity))} = ${renderSchema(spec.schema, spec.root, spec.path, spec.capabilityId)};`)
|
|
1556
|
+
.join("\n");
|
|
1557
|
+
const types = [];
|
|
1558
|
+
const routes = [];
|
|
1559
|
+
const canonicalNames = new Set();
|
|
1560
|
+
const readableCounts = new Map();
|
|
1561
|
+
for (const { capability } of selected) {
|
|
1562
|
+
const path = capability.interfaces.sdk.enabled
|
|
1563
|
+
? capability.interfaces.sdk.path
|
|
1564
|
+
: [];
|
|
1565
|
+
for (const suffix of ["Input", "Output", "DeclaredError"]) {
|
|
1566
|
+
canonicalNames.add(sdkTypeName(path, suffix));
|
|
1567
|
+
if (path.every((segment) => identifierPattern.test(segment))) {
|
|
1568
|
+
const readable = sdkReadableTypeName(path, suffix);
|
|
1569
|
+
readableCounts.set(readable, (readableCounts.get(readable) ?? 0) + 1);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
const readableAlias = (path, suffix) => {
|
|
1574
|
+
if (!path.every((segment) => identifierPattern.test(segment)))
|
|
1575
|
+
return undefined;
|
|
1576
|
+
const name = sdkReadableTypeName(path, suffix);
|
|
1577
|
+
return readableCounts.get(name) === 1 && !canonicalNames.has(name)
|
|
1578
|
+
? `export type ${name} = ${sdkTypeName(path, suffix)};`
|
|
1579
|
+
: undefined;
|
|
1580
|
+
};
|
|
1581
|
+
for (const { capability, index } of selected) {
|
|
1582
|
+
const path = capability.interfaces.sdk.enabled
|
|
1583
|
+
? capability.interfaces.sdk.path
|
|
1584
|
+
: [];
|
|
1585
|
+
const base = `/capabilities/${index}`;
|
|
1586
|
+
const input = sdkTypeName(path, "Input");
|
|
1587
|
+
const output = sdkTypeName(path, "Output");
|
|
1588
|
+
const error = sdkTypeName(path, "DeclaredError");
|
|
1589
|
+
types.push(`export type ${input} = ${renderBinding(capability.input, { kind: "capability", id: capability.id, version: capability.version, binding: "input" }, `${base}/input`, capability.id)};`);
|
|
1590
|
+
types.push(`export type ${output} = ${renderBinding(capability.output, { kind: "capability", id: capability.id, version: capability.version, binding: "output" }, `${base}/output`, capability.id)};`);
|
|
1591
|
+
types.push(`export type ${error} = ${renderSdkDeclaredErrors(capability, base, renderBinding)};`);
|
|
1592
|
+
for (const suffix of ["Input", "Output", "DeclaredError"]) {
|
|
1593
|
+
const alias = readableAlias(path, suffix);
|
|
1594
|
+
if (alias)
|
|
1595
|
+
types.push(alias);
|
|
1596
|
+
}
|
|
1597
|
+
const http = capability.interfaces.http;
|
|
1598
|
+
if (!http.enabled)
|
|
1599
|
+
throw new SchemaEmissionError(`${base}/interfaces/http`, capability.id);
|
|
1600
|
+
routes.push(JSON.stringify({
|
|
1601
|
+
id: capability.id,
|
|
1602
|
+
version: capability.version,
|
|
1603
|
+
method: http.method,
|
|
1604
|
+
path: http.path,
|
|
1605
|
+
bindings: http.bindings,
|
|
1606
|
+
opaque: sdkInputOpaque(document, capability.input),
|
|
1607
|
+
declaredErrors: Object.keys(capability.errors)
|
|
1608
|
+
.sort(compareCodePoints)
|
|
1609
|
+
.map((code) => ({
|
|
1610
|
+
code,
|
|
1611
|
+
status: capability.errors[code].status,
|
|
1612
|
+
retryable: capability.errors[code].retryable,
|
|
1613
|
+
})),
|
|
1614
|
+
}));
|
|
1615
|
+
}
|
|
1616
|
+
const source = [
|
|
1617
|
+
`// @generated producer=${SDK_HTTP_PRODUCER_ID} version=${SDK_HTTP_PRODUCER_VERSION} target=${SDK_HTTP_TARGET} irVersion=${document.irVersion} irHash=${irHash} service=${JSON.stringify(`${document.service.name}@${document.service.version}`)}`,
|
|
1618
|
+
"type __CapaxleJsonValue = null | boolean | number | string | readonly __CapaxleJsonValue[] | { readonly [key: string]: __CapaxleJsonValue };",
|
|
1619
|
+
aliasesSource,
|
|
1620
|
+
...types,
|
|
1621
|
+
SDK_HTTP_RUNTIME_SOURCE,
|
|
1622
|
+
`export type CapaxleClient = ${sdkNodeType(root, "")};`,
|
|
1623
|
+
`const routes: readonly Route[] = [${routes.join(",")}] as const;`,
|
|
1624
|
+
`export function createCapaxleClient(config: CapaxleClientConfig): CapaxleClient {\n const makeMethod = makeMethodFactory(config);\n return ${sdkNodeValue(root, " ")} as CapaxleClient;\n}`,
|
|
1625
|
+
"",
|
|
1626
|
+
].join("\n");
|
|
1627
|
+
return {
|
|
1628
|
+
ok: true,
|
|
1629
|
+
bytes: new TextEncoder().encode(source),
|
|
1630
|
+
diagnostics: [],
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
catch (error) {
|
|
1634
|
+
const failure = error instanceof SchemaEmissionError
|
|
1635
|
+
? error
|
|
1636
|
+
: new SchemaEmissionError("/");
|
|
1637
|
+
return {
|
|
1638
|
+
ok: false,
|
|
1639
|
+
diagnostics: [
|
|
1640
|
+
sdkDiagnostic("CAP_SDK_SCHEMA_UNREPRESENTABLE", "An accepted Capability IR schema cannot be represented truthfully as TypeScript.", failure.path || "/", failure.capabilityId),
|
|
1641
|
+
],
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
//# sourceMappingURL=index.js.map
|