@aigne/afs-world-mapping 1.11.0-beta.10
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.md +26 -0
- package/dist/index.d.mts +246 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +992 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
//#region src/capability.ts
|
|
2
|
+
const REQUIRED_METHODS = [
|
|
3
|
+
"loadWorld",
|
|
4
|
+
"reloadWorld",
|
|
5
|
+
"getWorldStatus",
|
|
6
|
+
"resolve",
|
|
7
|
+
"project",
|
|
8
|
+
"mutate"
|
|
9
|
+
];
|
|
10
|
+
function isWorldMappingCapable(module) {
|
|
11
|
+
if (!module || typeof module !== "object") return false;
|
|
12
|
+
const obj = module;
|
|
13
|
+
return REQUIRED_METHODS.every((method) => typeof obj[method] === "function");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/compiler.ts
|
|
18
|
+
var WorldCompiler = class {
|
|
19
|
+
compile(schema, binding) {
|
|
20
|
+
if (!schema.world) throw new Error("WorldCompiler: schema missing world name");
|
|
21
|
+
if (!schema.version) throw new Error("WorldCompiler: schema missing version");
|
|
22
|
+
for (const [kindName, kindDef] of Object.entries(schema.kinds)) {
|
|
23
|
+
this.validateIdentifier(kindName, `kind name "${kindName}"`);
|
|
24
|
+
if (kindDef.fields) for (const fieldName of Object.keys(kindDef.fields)) this.validateIdentifier(fieldName, `field name "${fieldName}" in kind "${kindName}"`);
|
|
25
|
+
}
|
|
26
|
+
for (const [kindName, kindDef] of Object.entries(schema.kinds)) {
|
|
27
|
+
if (kindDef.fields) for (const [fieldName, fieldType] of Object.entries(kindDef.fields)) this.validateFieldType(fieldType, kindName, fieldName);
|
|
28
|
+
if (kindDef.relations) for (const [relationName, relationType] of Object.entries(kindDef.relations)) {
|
|
29
|
+
const match = relationType.match(/^list:(.+)$/);
|
|
30
|
+
if (match?.[1] && !schema.kinds[match[1]]) throw new Error(`WorldCompiler: relation "${relationName}" in kind "${kindName}" references unknown kind "${match[1]}"`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const trie = this.buildPathTrie(schema);
|
|
34
|
+
const kinds = {};
|
|
35
|
+
for (const [kindName, kindDef] of Object.entries(schema.kinds)) {
|
|
36
|
+
const kindMapping = binding.mappings?.[kindName];
|
|
37
|
+
kinds[kindName] = {
|
|
38
|
+
key: kindDef.key,
|
|
39
|
+
source: kindMapping?.source ?? binding.source,
|
|
40
|
+
fieldMap: kindMapping?.fieldMap ?? {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
world: schema.world,
|
|
45
|
+
version: schema.version,
|
|
46
|
+
resolve: (path) => this.resolvePath(trie, path),
|
|
47
|
+
kinds
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
validateIdentifier(value, context) {
|
|
51
|
+
if (!/^[A-Za-z0-9_]+$/.test(value)) throw new Error(`WorldCompiler: invalid identifier for ${context}. Must only contain [A-Za-z0-9_]`);
|
|
52
|
+
}
|
|
53
|
+
validateFieldType(fieldType, kindName, fieldName) {
|
|
54
|
+
if ([
|
|
55
|
+
"int",
|
|
56
|
+
"string",
|
|
57
|
+
"text",
|
|
58
|
+
"bool",
|
|
59
|
+
"datetime"
|
|
60
|
+
].includes(fieldType)) return;
|
|
61
|
+
if (fieldType.startsWith("enum(") && fieldType.endsWith(")")) return;
|
|
62
|
+
if (fieldType.startsWith("list:")) return;
|
|
63
|
+
if (fieldType.startsWith("ref:")) return;
|
|
64
|
+
throw new Error(`WorldCompiler: field "${fieldName}" in kind "${kindName}" has invalid type "${fieldType}"`);
|
|
65
|
+
}
|
|
66
|
+
buildPathTrie(schema) {
|
|
67
|
+
const root = { children: /* @__PURE__ */ new Map() };
|
|
68
|
+
for (const [kindName, kindDef] of Object.entries(schema.kinds)) {
|
|
69
|
+
const segments = kindDef.path.split("/").filter((s) => s.length > 0);
|
|
70
|
+
const keyParamName = `{${kindDef.key}}`;
|
|
71
|
+
let node = root;
|
|
72
|
+
for (let i = 0; i < segments.length; i++) {
|
|
73
|
+
const segment = segments[i];
|
|
74
|
+
const isLastSegment = i === segments.length - 1;
|
|
75
|
+
if (segment.startsWith("{") && segment.endsWith("}")) {
|
|
76
|
+
const paramName = segment.slice(1, -1);
|
|
77
|
+
if (!node.param) node.param = {
|
|
78
|
+
paramName,
|
|
79
|
+
children: /* @__PURE__ */ new Map()
|
|
80
|
+
};
|
|
81
|
+
node = node.param;
|
|
82
|
+
if (isLastSegment) {
|
|
83
|
+
node.kind = kindName;
|
|
84
|
+
node.isCollection = false;
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
if (!node.children.has(segment)) node.children.set(segment, { children: /* @__PURE__ */ new Map() });
|
|
88
|
+
node = node.children.get(segment);
|
|
89
|
+
const nextSegment = segments[i + 1];
|
|
90
|
+
if (i + 1 < segments.length && nextSegment === keyParamName) {
|
|
91
|
+
node.kind = kindName;
|
|
92
|
+
node.isCollection = true;
|
|
93
|
+
}
|
|
94
|
+
if (isLastSegment) {
|
|
95
|
+
node.kind = kindName;
|
|
96
|
+
node.isCollection = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return root;
|
|
102
|
+
}
|
|
103
|
+
resolvePath(trie, path) {
|
|
104
|
+
const segments = path.split("/").filter((s) => s.length > 0);
|
|
105
|
+
const params = {};
|
|
106
|
+
let node = trie;
|
|
107
|
+
let lastMatch = null;
|
|
108
|
+
for (const segment of segments) {
|
|
109
|
+
if (node.children.has(segment)) node = node.children.get(segment);
|
|
110
|
+
else if (node.param) {
|
|
111
|
+
params[node.param.paramName] = segment;
|
|
112
|
+
node = node.param;
|
|
113
|
+
} else return lastMatch;
|
|
114
|
+
if (node.kind) lastMatch = {
|
|
115
|
+
kind: node.kind,
|
|
116
|
+
params: { ...params },
|
|
117
|
+
isCollection: node.isCollection ?? false
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return lastMatch;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/compute/types.ts
|
|
126
|
+
var ParseError = class extends Error {
|
|
127
|
+
constructor(message) {
|
|
128
|
+
super(message);
|
|
129
|
+
this.name = "ParseError";
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var ExprEvalError = class extends Error {
|
|
133
|
+
constructor(message) {
|
|
134
|
+
super(message);
|
|
135
|
+
this.name = "ExprEvalError";
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/compute/evaluator.ts
|
|
141
|
+
const MAX_DEPTH = 64;
|
|
142
|
+
const BUILTINS = {
|
|
143
|
+
concat: (args) => args.map((a) => a == null ? "" : String(a)).join(""),
|
|
144
|
+
len: ([val]) => {
|
|
145
|
+
if (typeof val === "string") return val.length;
|
|
146
|
+
if (Array.isArray(val)) return val.length;
|
|
147
|
+
throw new ExprEvalError("len() requires a string or array");
|
|
148
|
+
},
|
|
149
|
+
upper: ([val]) => {
|
|
150
|
+
if (typeof val !== "string") throw new ExprEvalError("upper() requires a string");
|
|
151
|
+
return val.toUpperCase();
|
|
152
|
+
},
|
|
153
|
+
lower: ([val]) => {
|
|
154
|
+
if (typeof val !== "string") throw new ExprEvalError("lower() requires a string");
|
|
155
|
+
return val.toLowerCase();
|
|
156
|
+
},
|
|
157
|
+
trim: ([val]) => {
|
|
158
|
+
if (typeof val !== "string") throw new ExprEvalError("trim() requires a string");
|
|
159
|
+
return val.trim();
|
|
160
|
+
},
|
|
161
|
+
round: ([val]) => {
|
|
162
|
+
if (typeof val !== "number") throw new ExprEvalError("round() requires a number");
|
|
163
|
+
return Math.round(val);
|
|
164
|
+
},
|
|
165
|
+
floor: ([val]) => {
|
|
166
|
+
if (typeof val !== "number") throw new ExprEvalError("floor() requires a number");
|
|
167
|
+
return Math.floor(val);
|
|
168
|
+
},
|
|
169
|
+
ceil: ([val]) => {
|
|
170
|
+
if (typeof val !== "number") throw new ExprEvalError("ceil() requires a number");
|
|
171
|
+
return Math.ceil(val);
|
|
172
|
+
},
|
|
173
|
+
abs: ([val]) => {
|
|
174
|
+
if (typeof val !== "number") throw new ExprEvalError("abs() requires a number");
|
|
175
|
+
return Math.abs(val);
|
|
176
|
+
},
|
|
177
|
+
min: (args) => {
|
|
178
|
+
const nums = args.filter((a) => typeof a === "number");
|
|
179
|
+
if (nums.length === 0) throw new ExprEvalError("min() requires at least one number");
|
|
180
|
+
return Math.min(...nums);
|
|
181
|
+
},
|
|
182
|
+
max: (args) => {
|
|
183
|
+
const nums = args.filter((a) => typeof a === "number");
|
|
184
|
+
if (nums.length === 0) throw new ExprEvalError("max() requires at least one number");
|
|
185
|
+
return Math.max(...nums);
|
|
186
|
+
},
|
|
187
|
+
matches: ([val, pattern]) => {
|
|
188
|
+
if (typeof val !== "string" || typeof pattern !== "string") throw new ExprEvalError("matches() requires two strings");
|
|
189
|
+
return new RegExp(pattern).test(val);
|
|
190
|
+
},
|
|
191
|
+
hours_between: ([start, end]) => {
|
|
192
|
+
const s = start instanceof Date ? start.getTime() : typeof start === "string" ? new Date(start).getTime() : NaN;
|
|
193
|
+
const e = end instanceof Date ? end.getTime() : typeof end === "string" ? new Date(end).getTime() : NaN;
|
|
194
|
+
if (Number.isNaN(s) || Number.isNaN(e)) throw new ExprEvalError("hours_between() requires valid dates");
|
|
195
|
+
return Math.floor((e - s) / (1e3 * 60 * 60));
|
|
196
|
+
},
|
|
197
|
+
merge: (args) => {
|
|
198
|
+
const result = {};
|
|
199
|
+
for (const arg of args) {
|
|
200
|
+
if (arg == null || typeof arg !== "object" || Array.isArray(arg)) throw new ExprEvalError("merge() requires plain objects");
|
|
201
|
+
Object.assign(result, arg);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
},
|
|
205
|
+
pick: (args) => {
|
|
206
|
+
const [obj, ...fields] = args;
|
|
207
|
+
if (obj == null || typeof obj !== "object" || Array.isArray(obj)) throw new ExprEvalError("pick() requires a plain object as first argument");
|
|
208
|
+
const result = {};
|
|
209
|
+
for (const f of fields) {
|
|
210
|
+
const key = String(f);
|
|
211
|
+
if (key in obj) result[key] = obj[key];
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
},
|
|
215
|
+
omit: (args) => {
|
|
216
|
+
const [obj, ...fields] = args;
|
|
217
|
+
if (obj == null || typeof obj !== "object" || Array.isArray(obj)) throw new ExprEvalError("omit() requires a plain object as first argument");
|
|
218
|
+
const omitSet = new Set(fields.map(String));
|
|
219
|
+
const result = {};
|
|
220
|
+
for (const [key, value] of Object.entries(obj)) if (!omitSet.has(key)) result[key] = value;
|
|
221
|
+
return result;
|
|
222
|
+
},
|
|
223
|
+
flatten: ([obj]) => {
|
|
224
|
+
if (obj == null || typeof obj !== "object" || Array.isArray(obj)) throw new ExprEvalError("flatten() requires a plain object");
|
|
225
|
+
const result = {};
|
|
226
|
+
for (const value of Object.values(obj)) if (value != null && typeof value === "object" && !Array.isArray(value)) Object.assign(result, value);
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
function resolveField$1(data, path) {
|
|
231
|
+
if (path.length === 1 && path[0] === "*") return data;
|
|
232
|
+
let current = data;
|
|
233
|
+
for (const key of path) {
|
|
234
|
+
if (current == null || typeof current !== "object") return null;
|
|
235
|
+
current = current[key];
|
|
236
|
+
}
|
|
237
|
+
return current ?? null;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Evaluate an AST node against a context.
|
|
241
|
+
* Pure synchronous — no async, no IO.
|
|
242
|
+
*/
|
|
243
|
+
function evaluateExpression(node, ctx, depth = 0) {
|
|
244
|
+
if (depth > MAX_DEPTH) throw new ExprEvalError(`Maximum expression depth (${MAX_DEPTH}) exceeded`);
|
|
245
|
+
switch (node.type) {
|
|
246
|
+
case "NumberLiteral": return node.value;
|
|
247
|
+
case "StringLiteral": return node.value;
|
|
248
|
+
case "BooleanLiteral": return node.value;
|
|
249
|
+
case "NullLiteral": return null;
|
|
250
|
+
case "FieldRef": return resolveField$1(ctx.data, node.path);
|
|
251
|
+
case "ParamRef": return ctx.params?.[node.name] ?? null;
|
|
252
|
+
case "UnaryOp": {
|
|
253
|
+
const operand = evaluateExpression(node.operand, ctx, depth + 1);
|
|
254
|
+
if (node.operator === "-") {
|
|
255
|
+
if (typeof operand !== "number") throw new ExprEvalError("Unary minus requires a number");
|
|
256
|
+
return -operand;
|
|
257
|
+
}
|
|
258
|
+
if (node.operator === "!") return !operand;
|
|
259
|
+
throw new ExprEvalError(`Unknown unary operator: ${node.operator}`);
|
|
260
|
+
}
|
|
261
|
+
case "BinaryOp": return evaluateBinaryOp(node.operator, node.left, node.right, ctx, depth);
|
|
262
|
+
case "FunctionCall": return evaluateFunctionCall(node.name, node.args, ctx, depth);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function evaluateBinaryOp(op, leftNode, rightNode, ctx, depth) {
|
|
266
|
+
if (op === "&&") {
|
|
267
|
+
const left$1 = evaluateExpression(leftNode, ctx, depth + 1);
|
|
268
|
+
if (!left$1) return left$1;
|
|
269
|
+
return evaluateExpression(rightNode, ctx, depth + 1);
|
|
270
|
+
}
|
|
271
|
+
if (op === "||") {
|
|
272
|
+
const left$1 = evaluateExpression(leftNode, ctx, depth + 1);
|
|
273
|
+
if (left$1) return left$1;
|
|
274
|
+
return evaluateExpression(rightNode, ctx, depth + 1);
|
|
275
|
+
}
|
|
276
|
+
const left = evaluateExpression(leftNode, ctx, depth + 1);
|
|
277
|
+
const right = evaluateExpression(rightNode, ctx, depth + 1);
|
|
278
|
+
if (op === "+") {
|
|
279
|
+
if (typeof left === "string" || typeof right === "string") return String(left ?? "") + String(right ?? "");
|
|
280
|
+
if (typeof left === "number" && typeof right === "number") return left + right;
|
|
281
|
+
throw new ExprEvalError(`Cannot apply '+' to ${typeof left} and ${typeof right}`);
|
|
282
|
+
}
|
|
283
|
+
if (op === "-") {
|
|
284
|
+
if (typeof left !== "number" || typeof right !== "number") throw new ExprEvalError(`Cannot apply '-' to ${typeof left} and ${typeof right}`);
|
|
285
|
+
return left - right;
|
|
286
|
+
}
|
|
287
|
+
if (op === "*") {
|
|
288
|
+
if (typeof left !== "number" || typeof right !== "number") throw new ExprEvalError(`Cannot apply '*' to ${typeof left} and ${typeof right}`);
|
|
289
|
+
return left * right;
|
|
290
|
+
}
|
|
291
|
+
if (op === "/") {
|
|
292
|
+
if (typeof left !== "number" || typeof right !== "number") throw new ExprEvalError(`Cannot apply '/' to ${typeof left} and ${typeof right}`);
|
|
293
|
+
if (right === 0) throw new ExprEvalError("Division by zero");
|
|
294
|
+
return left / right;
|
|
295
|
+
}
|
|
296
|
+
if (op === "==") return left === right;
|
|
297
|
+
if (op === "!=") return left !== right;
|
|
298
|
+
if (op === "<") return left < right;
|
|
299
|
+
if (op === ">") return left > right;
|
|
300
|
+
if (op === "<=") return left <= right;
|
|
301
|
+
if (op === ">=") return left >= right;
|
|
302
|
+
throw new ExprEvalError(`Unknown operator: ${op}`);
|
|
303
|
+
}
|
|
304
|
+
function evaluateFunctionCall(name, argNodes, ctx, depth) {
|
|
305
|
+
if (name === "if") {
|
|
306
|
+
if (argNodes.length !== 3) throw new ExprEvalError("if() requires exactly 3 arguments");
|
|
307
|
+
return evaluateExpression(argNodes[0], ctx, depth + 1) ? evaluateExpression(argNodes[1], ctx, depth + 1) : evaluateExpression(argNodes[2], ctx, depth + 1);
|
|
308
|
+
}
|
|
309
|
+
const fn = BUILTINS[name];
|
|
310
|
+
if (!fn) throw new ExprEvalError(`Unknown function: ${name}`);
|
|
311
|
+
return fn(argNodes.map((a) => evaluateExpression(a, ctx, depth + 1)));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/compute/parser.ts
|
|
316
|
+
const MAX_EXPRESSION_LENGTH = 4096;
|
|
317
|
+
const FORBIDDEN_FIELDS = new Set([
|
|
318
|
+
"__proto__",
|
|
319
|
+
"constructor",
|
|
320
|
+
"prototype"
|
|
321
|
+
]);
|
|
322
|
+
function charAt(s, i) {
|
|
323
|
+
return s.charAt(i);
|
|
324
|
+
}
|
|
325
|
+
function tokenize(expr) {
|
|
326
|
+
const tokens = [];
|
|
327
|
+
let i = 0;
|
|
328
|
+
while (i < expr.length) {
|
|
329
|
+
const ch = charAt(expr, i);
|
|
330
|
+
if (/\s/.test(ch)) {
|
|
331
|
+
i++;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const pos = i;
|
|
335
|
+
if (ch === "\"") {
|
|
336
|
+
i++;
|
|
337
|
+
let s = "";
|
|
338
|
+
while (i < expr.length && charAt(expr, i) !== "\"") {
|
|
339
|
+
if (charAt(expr, i) === "\\" && i + 1 < expr.length) {
|
|
340
|
+
const next = charAt(expr, i + 1);
|
|
341
|
+
if (next === "\"") {
|
|
342
|
+
s += "\"";
|
|
343
|
+
i += 2;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (next === "\\") {
|
|
347
|
+
s += "\\";
|
|
348
|
+
i += 2;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (next === "n") {
|
|
352
|
+
s += "\n";
|
|
353
|
+
i += 2;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (next === "t") {
|
|
357
|
+
s += " ";
|
|
358
|
+
i += 2;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
s += charAt(expr, i);
|
|
363
|
+
i++;
|
|
364
|
+
}
|
|
365
|
+
if (i >= expr.length) throw new ParseError(`Unterminated string at position ${pos}`);
|
|
366
|
+
i++;
|
|
367
|
+
tokens.push({
|
|
368
|
+
type: "string",
|
|
369
|
+
value: s,
|
|
370
|
+
pos
|
|
371
|
+
});
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (ch === "$") {
|
|
375
|
+
if (i + 1 < expr.length && charAt(expr, i + 1) === ".") {
|
|
376
|
+
i += 2;
|
|
377
|
+
const parts = [];
|
|
378
|
+
if (i < expr.length && charAt(expr, i) === "*") {
|
|
379
|
+
parts.push("*");
|
|
380
|
+
i++;
|
|
381
|
+
} else {
|
|
382
|
+
let part = "";
|
|
383
|
+
while (i < expr.length && /[a-zA-Z0-9_]/.test(charAt(expr, i))) {
|
|
384
|
+
part += charAt(expr, i);
|
|
385
|
+
i++;
|
|
386
|
+
}
|
|
387
|
+
if (part.length === 0) throw new ParseError(`Expected field name after $. at position ${pos}`);
|
|
388
|
+
if (FORBIDDEN_FIELDS.has(part)) throw new ParseError(`Forbidden field name: ${part}`);
|
|
389
|
+
parts.push(part);
|
|
390
|
+
}
|
|
391
|
+
while (i < expr.length && charAt(expr, i) === "." && i + 1 < expr.length && /[a-zA-Z0-9_*]/.test(charAt(expr, i + 1))) {
|
|
392
|
+
i++;
|
|
393
|
+
if (charAt(expr, i) === "*") {
|
|
394
|
+
parts.push("*");
|
|
395
|
+
i++;
|
|
396
|
+
} else {
|
|
397
|
+
let part = "";
|
|
398
|
+
while (i < expr.length && /[a-zA-Z0-9_]/.test(charAt(expr, i))) {
|
|
399
|
+
part += charAt(expr, i);
|
|
400
|
+
i++;
|
|
401
|
+
}
|
|
402
|
+
if (FORBIDDEN_FIELDS.has(part)) throw new ParseError(`Forbidden field name: ${part}`);
|
|
403
|
+
parts.push(part);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
tokens.push({
|
|
407
|
+
type: "field_ref",
|
|
408
|
+
value: parts.join("."),
|
|
409
|
+
pos
|
|
410
|
+
});
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
throw new ParseError(`Expected '.' after '$' at position ${pos}`);
|
|
414
|
+
}
|
|
415
|
+
if (ch === "@") {
|
|
416
|
+
i++;
|
|
417
|
+
let name = "";
|
|
418
|
+
while (i < expr.length && /[a-zA-Z0-9_]/.test(charAt(expr, i))) {
|
|
419
|
+
name += charAt(expr, i);
|
|
420
|
+
i++;
|
|
421
|
+
}
|
|
422
|
+
if (name.length === 0) throw new ParseError(`Expected parameter name after '@' at position ${pos}`);
|
|
423
|
+
tokens.push({
|
|
424
|
+
type: "param_ref",
|
|
425
|
+
value: name,
|
|
426
|
+
pos
|
|
427
|
+
});
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (/[0-9]/.test(ch)) {
|
|
431
|
+
let num = "";
|
|
432
|
+
while (i < expr.length && /[0-9.]/.test(charAt(expr, i))) {
|
|
433
|
+
num += charAt(expr, i);
|
|
434
|
+
i++;
|
|
435
|
+
}
|
|
436
|
+
tokens.push({
|
|
437
|
+
type: "number",
|
|
438
|
+
value: num,
|
|
439
|
+
pos
|
|
440
|
+
});
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (/[a-zA-Z_]/.test(ch)) {
|
|
444
|
+
let id = "";
|
|
445
|
+
while (i < expr.length && /[a-zA-Z0-9_]/.test(charAt(expr, i))) {
|
|
446
|
+
id += charAt(expr, i);
|
|
447
|
+
i++;
|
|
448
|
+
}
|
|
449
|
+
tokens.push({
|
|
450
|
+
type: "ident",
|
|
451
|
+
value: id,
|
|
452
|
+
pos
|
|
453
|
+
});
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (i + 1 < expr.length) {
|
|
457
|
+
const two = charAt(expr, i) + charAt(expr, i + 1);
|
|
458
|
+
if ([
|
|
459
|
+
"==",
|
|
460
|
+
"!=",
|
|
461
|
+
"<=",
|
|
462
|
+
">=",
|
|
463
|
+
"&&",
|
|
464
|
+
"||"
|
|
465
|
+
].includes(two)) {
|
|
466
|
+
tokens.push({
|
|
467
|
+
type: "op",
|
|
468
|
+
value: two,
|
|
469
|
+
pos
|
|
470
|
+
});
|
|
471
|
+
i += 2;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if ("+-*/<>".includes(ch)) {
|
|
476
|
+
tokens.push({
|
|
477
|
+
type: "op",
|
|
478
|
+
value: ch,
|
|
479
|
+
pos
|
|
480
|
+
});
|
|
481
|
+
i++;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (ch === "(") {
|
|
485
|
+
tokens.push({
|
|
486
|
+
type: "lparen",
|
|
487
|
+
value: "(",
|
|
488
|
+
pos
|
|
489
|
+
});
|
|
490
|
+
i++;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (ch === ")") {
|
|
494
|
+
tokens.push({
|
|
495
|
+
type: "rparen",
|
|
496
|
+
value: ")",
|
|
497
|
+
pos
|
|
498
|
+
});
|
|
499
|
+
i++;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (ch === ",") {
|
|
503
|
+
tokens.push({
|
|
504
|
+
type: "comma",
|
|
505
|
+
value: ",",
|
|
506
|
+
pos
|
|
507
|
+
});
|
|
508
|
+
i++;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (ch === "!") {
|
|
512
|
+
tokens.push({
|
|
513
|
+
type: "bang",
|
|
514
|
+
value: "!",
|
|
515
|
+
pos
|
|
516
|
+
});
|
|
517
|
+
i++;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
throw new ParseError(`Unexpected character '${ch}' at position ${i}`);
|
|
521
|
+
}
|
|
522
|
+
tokens.push({
|
|
523
|
+
type: "eof",
|
|
524
|
+
value: "",
|
|
525
|
+
pos: i
|
|
526
|
+
});
|
|
527
|
+
return tokens;
|
|
528
|
+
}
|
|
529
|
+
const EOF_TOKEN = {
|
|
530
|
+
type: "eof",
|
|
531
|
+
value: "",
|
|
532
|
+
pos: -1
|
|
533
|
+
};
|
|
534
|
+
var Parser = class {
|
|
535
|
+
tokens;
|
|
536
|
+
pos;
|
|
537
|
+
constructor(tokens) {
|
|
538
|
+
this.tokens = tokens;
|
|
539
|
+
this.pos = 0;
|
|
540
|
+
}
|
|
541
|
+
peek() {
|
|
542
|
+
return this.tokens[this.pos] ?? EOF_TOKEN;
|
|
543
|
+
}
|
|
544
|
+
advance() {
|
|
545
|
+
const t = this.tokens[this.pos] ?? EOF_TOKEN;
|
|
546
|
+
this.pos++;
|
|
547
|
+
return t;
|
|
548
|
+
}
|
|
549
|
+
expect(type, value) {
|
|
550
|
+
const t = this.peek();
|
|
551
|
+
if (t.type !== type || value !== void 0 && t.value !== value) throw new ParseError(`Expected ${type}${value ? ` '${value}'` : ""} but got ${t.type} '${t.value}' at position ${t.pos}`);
|
|
552
|
+
return this.advance();
|
|
553
|
+
}
|
|
554
|
+
parse() {
|
|
555
|
+
const node = this.parseOr();
|
|
556
|
+
if (this.peek().type !== "eof") throw new ParseError(`Unexpected token '${this.peek().value}' at position ${this.peek().pos}`);
|
|
557
|
+
return node;
|
|
558
|
+
}
|
|
559
|
+
parseOr() {
|
|
560
|
+
let left = this.parseAnd();
|
|
561
|
+
while (this.peek().type === "op" && this.peek().value === "||") {
|
|
562
|
+
const op = this.advance().value;
|
|
563
|
+
const right = this.parseAnd();
|
|
564
|
+
left = {
|
|
565
|
+
type: "BinaryOp",
|
|
566
|
+
operator: op,
|
|
567
|
+
left,
|
|
568
|
+
right
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
return left;
|
|
572
|
+
}
|
|
573
|
+
parseAnd() {
|
|
574
|
+
let left = this.parseComparison();
|
|
575
|
+
while (this.peek().type === "op" && this.peek().value === "&&") {
|
|
576
|
+
const op = this.advance().value;
|
|
577
|
+
const right = this.parseComparison();
|
|
578
|
+
left = {
|
|
579
|
+
type: "BinaryOp",
|
|
580
|
+
operator: op,
|
|
581
|
+
left,
|
|
582
|
+
right
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return left;
|
|
586
|
+
}
|
|
587
|
+
parseComparison() {
|
|
588
|
+
let left = this.parseAddSub();
|
|
589
|
+
while (this.peek().type === "op" && [
|
|
590
|
+
"==",
|
|
591
|
+
"!=",
|
|
592
|
+
"<",
|
|
593
|
+
">",
|
|
594
|
+
"<=",
|
|
595
|
+
">="
|
|
596
|
+
].includes(this.peek().value)) {
|
|
597
|
+
const op = this.advance().value;
|
|
598
|
+
const right = this.parseAddSub();
|
|
599
|
+
left = {
|
|
600
|
+
type: "BinaryOp",
|
|
601
|
+
operator: op,
|
|
602
|
+
left,
|
|
603
|
+
right
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
return left;
|
|
607
|
+
}
|
|
608
|
+
parseAddSub() {
|
|
609
|
+
let left = this.parseMulDiv();
|
|
610
|
+
while (this.peek().type === "op" && ["+", "-"].includes(this.peek().value)) {
|
|
611
|
+
const op = this.advance().value;
|
|
612
|
+
const right = this.parseMulDiv();
|
|
613
|
+
left = {
|
|
614
|
+
type: "BinaryOp",
|
|
615
|
+
operator: op,
|
|
616
|
+
left,
|
|
617
|
+
right
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
return left;
|
|
621
|
+
}
|
|
622
|
+
parseMulDiv() {
|
|
623
|
+
let left = this.parseUnary();
|
|
624
|
+
while (this.peek().type === "op" && ["*", "/"].includes(this.peek().value)) {
|
|
625
|
+
const op = this.advance().value;
|
|
626
|
+
const right = this.parseUnary();
|
|
627
|
+
left = {
|
|
628
|
+
type: "BinaryOp",
|
|
629
|
+
operator: op,
|
|
630
|
+
left,
|
|
631
|
+
right
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
return left;
|
|
635
|
+
}
|
|
636
|
+
parseUnary() {
|
|
637
|
+
if (this.peek().type === "op" && this.peek().value === "-") {
|
|
638
|
+
this.advance();
|
|
639
|
+
return {
|
|
640
|
+
type: "UnaryOp",
|
|
641
|
+
operator: "-",
|
|
642
|
+
operand: this.parseUnary()
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
if (this.peek().type === "bang") {
|
|
646
|
+
this.advance();
|
|
647
|
+
return {
|
|
648
|
+
type: "UnaryOp",
|
|
649
|
+
operator: "!",
|
|
650
|
+
operand: this.parseUnary()
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
return this.parsePrimary();
|
|
654
|
+
}
|
|
655
|
+
parsePrimary() {
|
|
656
|
+
const t = this.peek();
|
|
657
|
+
if (t.type === "number") {
|
|
658
|
+
this.advance();
|
|
659
|
+
return {
|
|
660
|
+
type: "NumberLiteral",
|
|
661
|
+
value: Number(t.value)
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
if (t.type === "string") {
|
|
665
|
+
this.advance();
|
|
666
|
+
return {
|
|
667
|
+
type: "StringLiteral",
|
|
668
|
+
value: t.value
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
if (t.type === "field_ref") {
|
|
672
|
+
this.advance();
|
|
673
|
+
return {
|
|
674
|
+
type: "FieldRef",
|
|
675
|
+
path: t.value.split(".")
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
if (t.type === "param_ref") {
|
|
679
|
+
this.advance();
|
|
680
|
+
return {
|
|
681
|
+
type: "ParamRef",
|
|
682
|
+
name: t.value
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (t.type === "ident") {
|
|
686
|
+
this.advance();
|
|
687
|
+
if (t.value === "true") return {
|
|
688
|
+
type: "BooleanLiteral",
|
|
689
|
+
value: true
|
|
690
|
+
};
|
|
691
|
+
if (t.value === "false") return {
|
|
692
|
+
type: "BooleanLiteral",
|
|
693
|
+
value: false
|
|
694
|
+
};
|
|
695
|
+
if (t.value === "null") return { type: "NullLiteral" };
|
|
696
|
+
if (this.peek().type === "lparen") {
|
|
697
|
+
this.advance();
|
|
698
|
+
const args = [];
|
|
699
|
+
if (this.peek().type !== "rparen") {
|
|
700
|
+
args.push(this.parseOr());
|
|
701
|
+
while (this.peek().type === "comma") {
|
|
702
|
+
this.advance();
|
|
703
|
+
args.push(this.parseOr());
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
this.expect("rparen");
|
|
707
|
+
return {
|
|
708
|
+
type: "FunctionCall",
|
|
709
|
+
name: t.value,
|
|
710
|
+
args
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
throw new ParseError(`Unexpected identifier '${t.value}' at position ${t.pos}`);
|
|
714
|
+
}
|
|
715
|
+
if (t.type === "lparen") {
|
|
716
|
+
this.advance();
|
|
717
|
+
const node = this.parseOr();
|
|
718
|
+
this.expect("rparen");
|
|
719
|
+
return node;
|
|
720
|
+
}
|
|
721
|
+
throw new ParseError(`Unexpected token '${t.value}' at position ${t.pos}`);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
/**
|
|
725
|
+
* Parse an expression string into an AST.
|
|
726
|
+
*/
|
|
727
|
+
function parseExpression(expr) {
|
|
728
|
+
if (!expr || expr.trim().length === 0) throw new ParseError("Empty expression");
|
|
729
|
+
if (expr.length > MAX_EXPRESSION_LENGTH) throw new ParseError(`Expression length ${expr.length} exceeds maximum ${MAX_EXPRESSION_LENGTH}`);
|
|
730
|
+
return new Parser(tokenize(expr.trim())).parse();
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
//#endregion
|
|
734
|
+
//#region src/compute/template.ts
|
|
735
|
+
/**
|
|
736
|
+
* World Compute — Template Engine
|
|
737
|
+
*
|
|
738
|
+
* Simple {{ $.field }} interpolation for representation transforms.
|
|
739
|
+
* Pure synchronous, no code execution.
|
|
740
|
+
*/
|
|
741
|
+
var TemplateError = class extends Error {
|
|
742
|
+
constructor(message) {
|
|
743
|
+
super(message);
|
|
744
|
+
this.name = "TemplateError";
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
function resolveField(data, path) {
|
|
748
|
+
let current = data;
|
|
749
|
+
for (const key of path) {
|
|
750
|
+
if (current == null || typeof current !== "object") return null;
|
|
751
|
+
current = current[key];
|
|
752
|
+
}
|
|
753
|
+
return current ?? null;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Render a template string with {{ $.field }} interpolation.
|
|
757
|
+
*
|
|
758
|
+
* @param template - Template string with {{ $.field }} placeholders
|
|
759
|
+
* @param data - Data object for field resolution
|
|
760
|
+
* @returns Rendered string
|
|
761
|
+
*/
|
|
762
|
+
function renderTemplate(template, data) {
|
|
763
|
+
let result = "";
|
|
764
|
+
let i = 0;
|
|
765
|
+
while (i < template.length) if (template.charAt(i) === "{" && i + 1 < template.length && template.charAt(i + 1) === "{") {
|
|
766
|
+
const closeIdx = template.indexOf("}}", i + 2);
|
|
767
|
+
if (closeIdx === -1) throw new TemplateError("Template error: unclosed {{ interpolation");
|
|
768
|
+
const expr = template.substring(i + 2, closeIdx).trim();
|
|
769
|
+
if (expr.startsWith("$.")) {
|
|
770
|
+
const value = resolveField(data, expr.substring(2).split("."));
|
|
771
|
+
result += value == null ? "" : String(value);
|
|
772
|
+
} else result += expr;
|
|
773
|
+
i = closeIdx + 2;
|
|
774
|
+
} else {
|
|
775
|
+
result += template.charAt(i);
|
|
776
|
+
i++;
|
|
777
|
+
}
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
//#endregion
|
|
782
|
+
//#region src/core.ts
|
|
783
|
+
var WorldMappingCore = class {
|
|
784
|
+
schema;
|
|
785
|
+
binding;
|
|
786
|
+
compiled;
|
|
787
|
+
constructor(schema, binding) {
|
|
788
|
+
this.schema = schema;
|
|
789
|
+
this.binding = binding;
|
|
790
|
+
this.compiled = new WorldCompiler().compile(schema, binding);
|
|
791
|
+
}
|
|
792
|
+
/** Resolve a target world path to kind + params + isCollection */
|
|
793
|
+
resolve(path) {
|
|
794
|
+
return this.compiled.resolve(path);
|
|
795
|
+
}
|
|
796
|
+
/** Translate a TrieMatch to a source AFS path */
|
|
797
|
+
translatePath(match) {
|
|
798
|
+
const source = this.compiled.kinds[match.kind]?.source ?? this.binding.source;
|
|
799
|
+
const kindMapping = this.binding.mappings?.[match.kind];
|
|
800
|
+
let pathTemplate;
|
|
801
|
+
if (kindMapping?.path) pathTemplate = kindMapping.path;
|
|
802
|
+
else pathTemplate = this.schema.kinds[match.kind]?.path ?? "";
|
|
803
|
+
let resolved = pathTemplate;
|
|
804
|
+
for (const [key, value] of Object.entries(match.params)) resolved = resolved.replace(`{${key}}`, value);
|
|
805
|
+
if (match.isCollection) resolved = resolved.replace(/\/\{[^}]+\}$/, "");
|
|
806
|
+
return (source + resolved).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
|
|
807
|
+
}
|
|
808
|
+
/** Project source fields → world fields (read direction) */
|
|
809
|
+
projectFields(data, kindName, params) {
|
|
810
|
+
const fieldMap = this.compiled.kinds[kindName]?.fieldMap;
|
|
811
|
+
let result;
|
|
812
|
+
if (!fieldMap || Object.keys(fieldMap).length === 0) result = { ...data };
|
|
813
|
+
else {
|
|
814
|
+
const reverseMap = {};
|
|
815
|
+
for (const [worldField, sourceField] of Object.entries(fieldMap)) reverseMap[sourceField] = worldField;
|
|
816
|
+
result = {};
|
|
817
|
+
for (const [key, value] of Object.entries(data)) if (reverseMap[key]) result[reverseMap[key]] = value;
|
|
818
|
+
else result[key] = value;
|
|
819
|
+
}
|
|
820
|
+
const kindDef = this.schema.kinds[kindName];
|
|
821
|
+
if (kindDef?.computed) for (const [fieldName, def] of Object.entries(kindDef.computed)) try {
|
|
822
|
+
const ast = this.getCompiledExpr(kindName, fieldName, def.expr);
|
|
823
|
+
result[fieldName] = evaluateExpression(ast, {
|
|
824
|
+
data: result,
|
|
825
|
+
params
|
|
826
|
+
});
|
|
827
|
+
} catch {
|
|
828
|
+
result[fieldName] = null;
|
|
829
|
+
}
|
|
830
|
+
return result;
|
|
831
|
+
}
|
|
832
|
+
/** Cache for parsed expressions */
|
|
833
|
+
exprCache = /* @__PURE__ */ new Map();
|
|
834
|
+
getCompiledExpr(kindName, fieldName, expr) {
|
|
835
|
+
const key = `${kindName}.${fieldName}`;
|
|
836
|
+
let ast = this.exprCache.get(key);
|
|
837
|
+
if (!ast) {
|
|
838
|
+
ast = parseExpression(expr);
|
|
839
|
+
this.exprCache.set(key, ast);
|
|
840
|
+
}
|
|
841
|
+
return ast;
|
|
842
|
+
}
|
|
843
|
+
/** Reverse project world fields → source fields (write direction) */
|
|
844
|
+
reverseProjectFields(data, kindName) {
|
|
845
|
+
const fieldMap = this.compiled.kinds[kindName]?.fieldMap;
|
|
846
|
+
const computedFields = this.schema.kinds[kindName]?.computed;
|
|
847
|
+
const result = {};
|
|
848
|
+
for (const [key, value] of Object.entries(data)) {
|
|
849
|
+
if (computedFields && key in computedFields) continue;
|
|
850
|
+
if (fieldMap?.[key]) result[fieldMap[key]] = value;
|
|
851
|
+
else result[key] = value;
|
|
852
|
+
}
|
|
853
|
+
return result;
|
|
854
|
+
}
|
|
855
|
+
/** Get kind metadata */
|
|
856
|
+
getKind(name) {
|
|
857
|
+
return this.compiled.kinds[name];
|
|
858
|
+
}
|
|
859
|
+
/** List all kind names */
|
|
860
|
+
getKindNames() {
|
|
861
|
+
return Object.keys(this.compiled.kinds);
|
|
862
|
+
}
|
|
863
|
+
/** Validate data against kind's validation rules */
|
|
864
|
+
validate(data, kindName) {
|
|
865
|
+
const kindDef = this.schema.kinds[kindName];
|
|
866
|
+
if (!kindDef?.validate || kindDef.validate.length === 0) return {
|
|
867
|
+
valid: true,
|
|
868
|
+
errors: []
|
|
869
|
+
};
|
|
870
|
+
const errors = [];
|
|
871
|
+
for (const rule of kindDef.validate) try {
|
|
872
|
+
if (!evaluateExpression(this.getCompiledExpr(kindName, `__validate_${rule.expr}`, rule.expr), { data })) errors.push({
|
|
873
|
+
expr: rule.expr,
|
|
874
|
+
error: rule.error
|
|
875
|
+
});
|
|
876
|
+
} catch {
|
|
877
|
+
errors.push({
|
|
878
|
+
expr: rule.expr,
|
|
879
|
+
error: rule.error
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
valid: errors.length === 0,
|
|
884
|
+
errors
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
/** Render a representation of data for a kind in the given format */
|
|
888
|
+
getRepresentation(data, kindName, format) {
|
|
889
|
+
const kindDef = this.schema.kinds[kindName];
|
|
890
|
+
if (!kindDef?.represent) return null;
|
|
891
|
+
const repr = kindDef.represent[format];
|
|
892
|
+
if (!repr) return null;
|
|
893
|
+
return renderTemplate(repr.template, data);
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
//#endregion
|
|
898
|
+
//#region src/module.ts
|
|
899
|
+
var WorldMappingModule = class {
|
|
900
|
+
name;
|
|
901
|
+
description;
|
|
902
|
+
core;
|
|
903
|
+
source;
|
|
904
|
+
constructor(options) {
|
|
905
|
+
this.core = new WorldMappingCore(options.schema, options.binding);
|
|
906
|
+
this.source = options.source;
|
|
907
|
+
this.name = `world-${options.schema.world}`;
|
|
908
|
+
this.description = `World mapping for ${options.schema.world}`;
|
|
909
|
+
}
|
|
910
|
+
/** List entries at a world path */
|
|
911
|
+
async list(path) {
|
|
912
|
+
const match = this.core.resolve(path);
|
|
913
|
+
if (!match) return { list: [] };
|
|
914
|
+
const sourcePath = this.core.translatePath(match);
|
|
915
|
+
const result = await this.source.list(sourcePath);
|
|
916
|
+
if (result.list && match.kind) result.list = result.list.map((entry) => {
|
|
917
|
+
if (entry && typeof entry === "object" && "content" in entry) return {
|
|
918
|
+
...entry,
|
|
919
|
+
content: this.core.projectFields(entry.content, match.kind)
|
|
920
|
+
};
|
|
921
|
+
return entry;
|
|
922
|
+
});
|
|
923
|
+
return result;
|
|
924
|
+
}
|
|
925
|
+
/** Read a single entry at a world path */
|
|
926
|
+
async read(path) {
|
|
927
|
+
const match = this.core.resolve(path);
|
|
928
|
+
if (!match) return;
|
|
929
|
+
const sourcePath = this.core.translatePath(match);
|
|
930
|
+
const result = await this.source.read(sourcePath);
|
|
931
|
+
if (result && typeof result === "object" && "content" in result) {
|
|
932
|
+
const entry = result;
|
|
933
|
+
return {
|
|
934
|
+
...entry,
|
|
935
|
+
content: this.core.projectFields(entry.content, match.kind)
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
return result;
|
|
939
|
+
}
|
|
940
|
+
/** Write an entry at a world path */
|
|
941
|
+
async write(path, content) {
|
|
942
|
+
if (!this.source.write) throw new Error("Source AFS does not support write operations");
|
|
943
|
+
const match = this.core.resolve(path);
|
|
944
|
+
if (!match) throw new Error(`Cannot resolve world path: ${path}`);
|
|
945
|
+
const sourcePath = this.core.translatePath(match);
|
|
946
|
+
const sourceContent = this.core.reverseProjectFields(content, match.kind);
|
|
947
|
+
return this.source.write(sourcePath, sourceContent);
|
|
948
|
+
}
|
|
949
|
+
/** Delete an entry at a world path */
|
|
950
|
+
async delete(path) {
|
|
951
|
+
if (!this.source.delete) throw new Error("Source AFS does not support delete operations");
|
|
952
|
+
const match = this.core.resolve(path);
|
|
953
|
+
if (!match) throw new Error(`Cannot resolve world path: ${path}`);
|
|
954
|
+
const sourcePath = this.core.translatePath(match);
|
|
955
|
+
return this.source.delete(sourcePath);
|
|
956
|
+
}
|
|
957
|
+
/** Get kind names */
|
|
958
|
+
getKindNames() {
|
|
959
|
+
return this.core.getKindNames();
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
//#endregion
|
|
964
|
+
//#region src/with-world-mapping.ts
|
|
965
|
+
/** Type guard: check if an object supports world mapping injection */
|
|
966
|
+
function isWithWorldMapping(obj) {
|
|
967
|
+
if (!obj || typeof obj !== "object") return false;
|
|
968
|
+
const o = obj;
|
|
969
|
+
return typeof o.applyWorldMapping === "function" && typeof o.getWorldMapping === "function";
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Helper for providers implementing WithWorldMapping.
|
|
973
|
+
* Validates config, creates WorldMappingCore, returns the mapping state.
|
|
974
|
+
* Throws if already applied or config is invalid.
|
|
975
|
+
*/
|
|
976
|
+
function applyWorldMappingHelper(config, existing) {
|
|
977
|
+
if (existing) throw new Error("World mapping already applied. Each provider supports single injection only.");
|
|
978
|
+
const core = new WorldMappingCore(config.schema, config.binding);
|
|
979
|
+
return {
|
|
980
|
+
core,
|
|
981
|
+
status: {
|
|
982
|
+
schema: config.schema,
|
|
983
|
+
binding: config.binding,
|
|
984
|
+
kinds: core.getKindNames(),
|
|
985
|
+
appliedAt: /* @__PURE__ */ new Date()
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
//#endregion
|
|
991
|
+
export { WorldCompiler, WorldMappingCore, WorldMappingModule, applyWorldMappingHelper, isWithWorldMapping, isWorldMappingCapable };
|
|
992
|
+
//# sourceMappingURL=index.mjs.map
|