@coldsmirk/abacus-core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -6
- package/dist/index.cjs +331 -16
- package/dist/index.d.cts +166 -6
- package/dist/index.d.ts +166 -6
- package/dist/index.js +325 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Part of [abacus](https://github.com/coldsmirk/abacus). To edit expressions in an
|
|
|
9
9
|
- **Evaluate** ZEN expressions against a data context, sync or async.
|
|
10
10
|
- **Compile** a structured, UI-authored condition tree into a single ZEN boolean expression — and **lift** the canonical form back into a tree for round-trip editing — plus **select** a matching branch by priority.
|
|
11
11
|
- **Type-analyze** an expression against a variable-context type tree — the inferred type of every span, autocomplete metadata, and positioned diagnostics that power an editor.
|
|
12
|
+
- **Project and infer JSON Schema** through a lossless field-tree subset, then turn that tree into an expression variable type.
|
|
12
13
|
- **Localize** all editor-produced text through a typed, extensible message catalog.
|
|
13
14
|
- **Strong typing** end to end, dual ESM / CJS, and a single lazily-loaded WebAssembly engine instance.
|
|
14
15
|
|
|
@@ -213,6 +214,49 @@ interface ExpressionCompletion {
|
|
|
213
214
|
|
|
214
215
|
> Building an editor? You usually do not call these directly — `@coldsmirk/abacus-codemirror` and `@coldsmirk/abacus-react` wire them into CodeMirror for you.
|
|
215
216
|
|
|
217
|
+
## JSON Schema field trees
|
|
218
|
+
|
|
219
|
+
The schema helpers provide the data model behind a visual JSON Schema builder without depending on a UI framework. The tree hosts an object-oriented draft 2020-12 subset: `type`, `properties`, `items`, `required`, and `description`, plus the optional root `$schema` declaration. Parsing refuses every unsupported construct with a structured `SchemaTreeIssue`; it never accepts a keyword and then silently removes it on serialization.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import {
|
|
223
|
+
inferSchema,
|
|
224
|
+
parseSchemaTree,
|
|
225
|
+
schemaTreeToExpressionType,
|
|
226
|
+
serializeSchemaTree
|
|
227
|
+
} from "@coldsmirk/abacus-core";
|
|
228
|
+
|
|
229
|
+
const inferred = inferSchema({
|
|
230
|
+
amount: 120,
|
|
231
|
+
customer: { name: "Ada" },
|
|
232
|
+
tags: ["priority"]
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const parsed = parseSchemaTree(JSON.stringify(inferred));
|
|
236
|
+
|
|
237
|
+
if (parsed.ok) {
|
|
238
|
+
serializeSchemaTree(parsed.tree); // pretty-printed schema JSON
|
|
239
|
+
schemaTreeToExpressionType(parsed.tree); // { Object: { amount: "Number", ... } }
|
|
240
|
+
} else {
|
|
241
|
+
parsed.issue; // a localizable SchemaTreeIssue
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
function inferSchema(sample: Json): Record<string, Json>;
|
|
247
|
+
function parseSchemaTree(text: string): SchemaTreeParseResult;
|
|
248
|
+
function serializeSchemaTree(tree: SchemaTree): string;
|
|
249
|
+
function schemaTreeToExpressionType(tree: SchemaTree): ExpressionType;
|
|
250
|
+
function newSchemaTreeField(overrides?: Partial<Omit<SchemaTreeField, "id">>): SchemaTreeField;
|
|
251
|
+
const SCHEMA_DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`inferSchema` is intentionally conservative: integer samples infer `number`, no property becomes `required` from one observation, `null` says nothing (`{}`), and heterogeneous arrays merge to the loosest schema the field-tree subset can express.
|
|
255
|
+
|
|
256
|
+
The parser preserves JSON Schema applicability, not merely its visible fields. A schema with `properties` or `items` but no `type` still permits instances of other types, so `SchemaTree.explicitType` and an object/array field's `explicitType` remember that omission and serialization does not add an object/array type. Hand-built trees that omit the optional metadata keep the original builder default and emit an explicit type.
|
|
257
|
+
|
|
258
|
+
Property names are arbitrary JSON strings. Empty, whitespace-only, and prototype-like names such as `__proto__` round-trip and enter the generated `ExpressionType`. A parsed blank name carries `preserveBlankName: true`; a blank row from `newSchemaTreeField()` has no marker and remains an unfinished UI draft that is omitted from serialization and expression types. Hosts editing a parsed tree should update nodes immutably (for example, with object spread) so these optional source-preservation fields survive.
|
|
259
|
+
|
|
216
260
|
## Compiling conditions
|
|
217
261
|
|
|
218
262
|
A common need is to let users author conditions in a UI (field / operator / value rows, AND/OR groups, prioritized branches) and turn that structure into a ZEN expression. This package models that structure and compiles it — without depending on any editor.
|
|
@@ -254,7 +298,7 @@ interface ConditionBranchInput { // a branch guarded by groups co
|
|
|
254
298
|
| `contains` / `not_contains` | `contains(subject, value)` / `not contains(subject, value)` |
|
|
255
299
|
| `starts_with` / `ends_with` | `startsWith(subject, value)` / `endsWith(subject, value)` |
|
|
256
300
|
| `in` / `not_in` | `subject in [..]` / `not (subject in [..])` (a scalar value is wrapped to a one-element array) |
|
|
257
|
-
| `is_empty` / `is_not_empty` | a typed emptiness test (null / blank string / empty array) and its negation
|
|
301
|
+
| `is_empty` / `is_not_empty` | a typed emptiness test (null / blank string / empty array / empty object) and its negation |
|
|
258
302
|
|
|
259
303
|
### Compiling and selecting
|
|
260
304
|
|
|
@@ -354,14 +398,15 @@ All tree consumers share `MAX_CONDITION_TREE_DEPTH`: the root is depth 0 and up
|
|
|
354
398
|
|
|
355
399
|
The compiler is designed not to become an expression-injection sink:
|
|
356
400
|
|
|
357
|
-
- A **subject** is emitted verbatim, so it must be a plain identifier path (`amount`, `user.age`, `items[0]`)
|
|
358
|
-
- A **value** is serialized to a ZEN literal via `toZenLiteral`, never interpolated as code. Strings are quoted _raw_ (ZEN honours no backslash escapes) by picking a delimiter the value does not contain; a
|
|
401
|
+
- A **subject** is emitted verbatim, so it must be a plain identifier path (`amount`, `user.age`, `items[0]`). ZEN keywords cannot start the path; in dotted member positions the parser accepts `and` / `or` / `not` / `in` / `null` as object keys (`metadata.in` is valid), while `true` / `false` remain literal tokens and are rejected there too. Every array index token must also be inside ZEN's unsigned 96-bit decimal domain (`0` through `2^96 - 1`); the next value is a parser error, so the compiler rejects it instead of poisoning a larger expression. Anything else (`len(secret)`, `a or b`, a bare `true`, …) makes the condition compile to `null` and be dropped.
|
|
402
|
+
- A **value** is serialized to a ZEN literal via `toZenLiteral`, never interpolated as code. Strings are quoted _raw_ (ZEN honours no backslash escapes) by picking a single-quote, double-quote, or backtick delimiter the value does not contain; only a string containing all three delimiters is unrepresentable. For numbers, faithful representation means passing `isZenRepresentableNumber`: inside ZEN's 96-bit decimal domain (finite, magnitude below ~7.9e28, no finer than 1e-28) **and** held identically by the engine's own context conversion — the engine converts context JS numbers lossily (roughly: integers whose text fits u64 are exact; other f64s are trimmed to ~16 significant digits), so a value like `0.1 + 0.2` or `1.2345678901234567` would compile into a literal that silently compares **unequal to itself** in a context. Such values are rejected instead.
|
|
359
403
|
|
|
360
404
|
```ts
|
|
361
405
|
function toZenLiteral(value: unknown): string; // throws if the value has no faithful ZEN representation
|
|
362
406
|
|
|
363
407
|
toZenLiteral("active"); // "'active'"
|
|
364
408
|
toZenLiteral("it's"); // "\"it's\"" (switches delimiter)
|
|
409
|
+
toZenLiteral(`He said "it's"`); // "`He said \"it's\"`"
|
|
365
410
|
toZenLiteral(1500); // "1500"
|
|
366
411
|
toZenLiteral(["BJ", "SH"]); // "['BJ', 'SH']"
|
|
367
412
|
toZenLiteral(null); // "null"
|
|
@@ -390,7 +435,19 @@ configureExpressionMessages({ locale: "zh-CN" }); // switc
|
|
|
390
435
|
configureExpressionMessages({ messages: { typeCheckSource: "Validation" } }); // override individual strings
|
|
391
436
|
```
|
|
392
437
|
|
|
393
|
-
The catalog is **process-global and last-write-wins** — there is one active catalog per runtime, so configure it once at startup. Built-in catalogs `enMessages` (the default) and `zhCNMessages` are exported. `getExpressionMessages()` returns the active one.
|
|
438
|
+
The catalog is **process-global and last-write-wins** — there is one active catalog per runtime, so configure it once at startup. Built-in catalogs `enMessages` (the default) and `zhCNMessages` are exported. `getExpressionMessages()` returns the active one. Each `messages` object replaces the previous override set over the current locale base; omitting it removes earlier overrides instead of accumulating them.
|
|
439
|
+
|
|
440
|
+
Framework integrations that keep already-rendered diagnostics can subscribe to catalog changes. The listener runs synchronously after the new catalog becomes active; the returned cleanup is idempotent:
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
import { subscribeExpressionMessages } from "@coldsmirk/abacus-core";
|
|
444
|
+
|
|
445
|
+
const unsubscribe = subscribeExpressionMessages(messages => {
|
|
446
|
+
refreshDiagnostics(messages);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
unsubscribe();
|
|
450
|
+
```
|
|
394
451
|
|
|
395
452
|
To add a language the library does not ship, register a full catalog under a key, then select it like any built-in — built-in and custom locales share one registry, so there is no privileged path:
|
|
396
453
|
|
|
@@ -426,9 +483,10 @@ class ExpressionNotReadyError extends ExpressionError { // a *Sync helper calle
|
|
|
426
483
|
- **Templates** — `analyzeTemplate` / `analyzeTemplateSync`, `getTemplateDiagnostics` / `getTemplateDiagnosticsSync`, `parseTemplateHoles`, `templateHoleAt`
|
|
427
484
|
- **Conditions** — `compileCondition`, `compileGroup`, `compileBranch`, `selectBranch`, `selectBranchWith`, `toZenLiteral`, `isZenRepresentableNumber`, `CONDITION_OPERATORS`, `conditionOperatorArity`
|
|
428
485
|
- **Condition trees** — `compileConditionTree`, `liftConditionTree`, `emptyConditionGroup`, `newConditionNodeId`, `ensureConditionNodeIds`, `MAX_CONDITION_TREE_DEPTH`, `CONDITION_TREE_OPERATORS`
|
|
429
|
-
- **
|
|
486
|
+
- **JSON Schema** — `inferSchema`, `parseSchemaTree`, `serializeSchemaTree`, `schemaTreeToExpressionType`, `newSchemaTreeField`, `SCHEMA_DIALECT_2020_12`
|
|
487
|
+
- **Localization** — `configureExpressionMessages`, `registerExpressionLocale`, `getExpressionMessages`, `subscribeExpressionMessages`, `enMessages`, `zhCNMessages`
|
|
430
488
|
- **Errors** — `ExpressionError`, `ExpressionNotReadyError`
|
|
431
|
-
- **Types** — `ExpressionType`, `ExpressionMode`, `TemplateHole`, `ExpressionContext`, `ExpressionEngine`, `LoadEngineOptions`, `ExpressionAnalysis`, `ExpressionTypeSpan`, `ExpressionCompletion`, `ExpressionDiagnostic`, `ExpressionMessages`, `ExpressionLocale`, `BuiltInExpressionLocale`, `ConfigureMessagesOptions`, `ConditionInput`, `FieldConditionInput`, `ExpressionConditionInput`, `ConditionGroupInput`, `ConditionBranchInput`, `ConditionOperator`, `ConditionOperatorArity`, `BranchSelection`, `ConditionTreeGroup`, `ConditionTreeNode`, `ConditionTreeRule`, `ConditionTreeOperator`, `ConditionTreeValue`, `ConditionScalar`
|
|
489
|
+
- **Types** — `ExpressionType`, `ExpressionMode`, `TemplateHole`, `ExpressionContext`, `ExpressionEngine`, `LoadEngineOptions`, `ExpressionAnalysis`, `ExpressionTypeSpan`, `ExpressionCompletion`, `ExpressionDiagnostic`, `ExpressionMessages`, `ExpressionMessagesListener`, `ExpressionLocale`, `BuiltInExpressionLocale`, `ConfigureMessagesOptions`, `ConditionInput`, `FieldConditionInput`, `ExpressionConditionInput`, `ConditionGroupInput`, `ConditionBranchInput`, `ConditionOperator`, `ConditionOperatorArity`, `BranchSelection`, `ConditionTreeGroup`, `ConditionTreeNode`, `ConditionTreeRule`, `ConditionTreeOperator`, `ConditionTreeValue`, `ConditionScalar`, `Json`, `SchemaTree`, `SchemaTreeField`, `SchemaTreeFieldType`, `SchemaTreeIssue`, `SchemaTreeParseResult`
|
|
432
490
|
|
|
433
491
|
## License
|
|
434
492
|
|
package/dist/index.cjs
CHANGED
|
@@ -359,8 +359,9 @@ function isZenConsistentNumber(value) {
|
|
|
359
359
|
return context !== null && literal.digits === context.digits && literal.exponent === context.exponent;
|
|
360
360
|
}
|
|
361
361
|
const SUBJECT_PATTERN = /^[A-Z_$][\w$]*(?:\.[A-Z_$][\w$]*|\[\d+\])*$/i;
|
|
362
|
+
const SUBJECT_IDENTIFIER_PATTERN = /[A-Z_$][\w$]*/gi;
|
|
362
363
|
const SUBJECT_INDEX_PATTERN = /\[(?<index>\d+)\]/g;
|
|
363
|
-
const
|
|
364
|
+
const ZEN_ROOT_RESERVED_WORDS = new Set([
|
|
364
365
|
"and",
|
|
365
366
|
"or",
|
|
366
367
|
"not",
|
|
@@ -369,8 +370,11 @@ const ZEN_RESERVED_WORDS = new Set([
|
|
|
369
370
|
"false",
|
|
370
371
|
"null"
|
|
371
372
|
]);
|
|
373
|
+
const ZEN_MEMBER_RESERVED_WORDS = new Set(["true", "false"]);
|
|
372
374
|
function isIdentifierPath(subject) {
|
|
373
|
-
if (!SUBJECT_PATTERN.test(subject)
|
|
375
|
+
if (!SUBJECT_PATTERN.test(subject)) return false;
|
|
376
|
+
const [root, ...members] = subject.match(SUBJECT_IDENTIFIER_PATTERN) ?? [];
|
|
377
|
+
if (root === void 0 || ZEN_ROOT_RESERVED_WORDS.has(root) || members.some((member) => ZEN_MEMBER_RESERVED_WORDS.has(member))) return false;
|
|
374
378
|
for (const match of subject.matchAll(SUBJECT_INDEX_PATTERN)) if (match.groups?.index === void 0 || !isZenUnsignedIntegerText(match.groups.index)) return false;
|
|
375
379
|
return true;
|
|
376
380
|
}
|
|
@@ -395,14 +399,17 @@ function toZenLiteral(value) {
|
|
|
395
399
|
function encodeZenString(value) {
|
|
396
400
|
const hasSingle = value.includes("'");
|
|
397
401
|
const hasDouble = value.includes("\"");
|
|
398
|
-
|
|
399
|
-
|
|
402
|
+
const hasBacktick = value.includes("`");
|
|
403
|
+
if (!hasSingle) return `'${value}'`;
|
|
404
|
+
if (!hasDouble) return `"${value}"`;
|
|
405
|
+
if (!hasBacktick) return `\`${value}\``;
|
|
406
|
+
throw new ExpressionError("String contains every ZEN raw-string delimiter and has no literal representation");
|
|
400
407
|
}
|
|
401
408
|
function toArrayLiteral(value) {
|
|
402
409
|
return isArray(value) ? toZenLiteral(value) : `[${toZenLiteral(value)}]`;
|
|
403
410
|
}
|
|
404
411
|
function zenIsEmpty(subject) {
|
|
405
|
-
return `(${subject} == null or (type(${subject}) == 'string' and len(trim(${subject})) == 0) or (type(${subject}) == 'array' and len(${subject}) == 0))`;
|
|
412
|
+
return `(${subject} == null or (type(${subject}) == 'string' and len(trim(${subject})) == 0) or (type(${subject}) == 'array' and len(${subject}) == 0) or (type(${subject}) == 'object' and len(keys(${subject})) == 0))`;
|
|
406
413
|
}
|
|
407
414
|
function compileFieldCondition(subject, operator, value) {
|
|
408
415
|
switch (operator) {
|
|
@@ -970,7 +977,7 @@ function tokenize(input) {
|
|
|
970
977
|
index += 1;
|
|
971
978
|
continue;
|
|
972
979
|
}
|
|
973
|
-
if (char === "'" || char === "\"") {
|
|
980
|
+
if (char === "'" || char === "\"" || char === "`") {
|
|
974
981
|
const end = input.indexOf(char, index + 1);
|
|
975
982
|
if (end === -1) return null;
|
|
976
983
|
tokens.push({
|
|
@@ -1153,33 +1160,45 @@ const zhCNMessages = {
|
|
|
1153
1160
|
expectedType: (expectedType, actualType) => `期望 \`${expectedType}\`,实际为 \`${actualType}\`。`
|
|
1154
1161
|
};
|
|
1155
1162
|
const localeRegistry = new Map([["en-US", enMessages], ["zh-CN", zhCNMessages]]);
|
|
1163
|
+
let activeBaseMessages = enMessages;
|
|
1156
1164
|
let activeMessages = enMessages;
|
|
1165
|
+
const messageListeners = /* @__PURE__ */ new Set();
|
|
1157
1166
|
function registerExpressionLocale(locale, messages) {
|
|
1158
1167
|
localeRegistry.set(locale, messages);
|
|
1159
1168
|
}
|
|
1160
1169
|
function configureExpressionMessages({ locale, messages }) {
|
|
1161
|
-
const base = locale === void 0 ?
|
|
1162
|
-
|
|
1170
|
+
const base = locale === void 0 ? activeBaseMessages : localeRegistry.get(locale) ?? activeBaseMessages;
|
|
1171
|
+
const nextMessages = messages === void 0 ? base : {
|
|
1163
1172
|
...base,
|
|
1164
1173
|
...messages
|
|
1165
1174
|
};
|
|
1175
|
+
activeBaseMessages = base;
|
|
1176
|
+
if (nextMessages === activeMessages) return;
|
|
1177
|
+
activeMessages = nextMessages;
|
|
1178
|
+
for (const listener of messageListeners) listener(activeMessages);
|
|
1166
1179
|
}
|
|
1167
1180
|
function getExpressionMessages() {
|
|
1168
1181
|
return activeMessages;
|
|
1169
1182
|
}
|
|
1183
|
+
function subscribeExpressionMessages(listener) {
|
|
1184
|
+
messageListeners.add(listener);
|
|
1185
|
+
return () => {
|
|
1186
|
+
messageListeners.delete(listener);
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1170
1189
|
function parseOffset(text) {
|
|
1171
1190
|
const trimmed = text?.trim();
|
|
1172
1191
|
return trimmed ? Number(trimmed) : NaN;
|
|
1173
1192
|
}
|
|
1174
1193
|
function extractPosition(message) {
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
return
|
|
1194
|
+
const positionPattern = / at (?:\(\s*(?<rangeFrom>\d+)\s*,\s*(?<rangeTo>\d+)\s*\)|(?<point>\d+))(?=\s*(?:;|$))/g;
|
|
1195
|
+
let position = null;
|
|
1196
|
+
for (const match of message.matchAll(positionPattern)) {
|
|
1197
|
+
const from = parseOffset(match.groups?.rangeFrom ?? match.groups?.point);
|
|
1198
|
+
const to = parseOffset(match.groups?.rangeTo);
|
|
1199
|
+
position = [from, Number.isNaN(to) ? from : to];
|
|
1200
|
+
}
|
|
1201
|
+
return position;
|
|
1183
1202
|
}
|
|
1184
1203
|
function normalizeDiagnostic(raw, source) {
|
|
1185
1204
|
if (raw === null || raw === void 0) return null;
|
|
@@ -1299,11 +1318,301 @@ async function getTemplateDiagnostics(source) {
|
|
|
1299
1318
|
await loadEngine();
|
|
1300
1319
|
return getTemplateDiagnosticsSync(source);
|
|
1301
1320
|
}
|
|
1321
|
+
function fieldType(field) {
|
|
1322
|
+
switch (field.type) {
|
|
1323
|
+
case "string": return "String";
|
|
1324
|
+
case "number": return "Number";
|
|
1325
|
+
case "integer": return "Number";
|
|
1326
|
+
case "boolean": return "Bool";
|
|
1327
|
+
case "object": return { Object: fieldRecord(field.children) };
|
|
1328
|
+
case "array": return { Array: field.items === null ? "Any" : fieldType(field.items) };
|
|
1329
|
+
case "any": return "Any";
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function fieldRecord(fields) {
|
|
1333
|
+
return Object.fromEntries(fields.filter((field) => field.name.trim() !== "" || field.preserveBlankName === true).map((field) => [field.name, fieldType(field)]));
|
|
1334
|
+
}
|
|
1335
|
+
function schemaTreeToExpressionType(tree) {
|
|
1336
|
+
return { Object: fieldRecord(tree.fields) };
|
|
1337
|
+
}
|
|
1338
|
+
function mergeSchemas(a, b) {
|
|
1339
|
+
if (a.type === "object" && b.type === "object") {
|
|
1340
|
+
const left = a.properties ?? {};
|
|
1341
|
+
const right = b.properties ?? {};
|
|
1342
|
+
const entries = [];
|
|
1343
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
1344
|
+
for (const key of keys) {
|
|
1345
|
+
const hasLeft = Object.hasOwn(left, key);
|
|
1346
|
+
const hasRight = Object.hasOwn(right, key);
|
|
1347
|
+
entries.push([key, hasLeft && hasRight ? mergeSchemas(left[key], right[key]) : hasLeft ? left[key] : right[key]]);
|
|
1348
|
+
}
|
|
1349
|
+
const properties = Object.fromEntries(entries);
|
|
1350
|
+
return Object.keys(properties).length > 0 ? {
|
|
1351
|
+
type: "object",
|
|
1352
|
+
properties
|
|
1353
|
+
} : { type: "object" };
|
|
1354
|
+
}
|
|
1355
|
+
if (a.type === "array" && b.type === "array") return a.items !== void 0 && b.items !== void 0 ? {
|
|
1356
|
+
type: "array",
|
|
1357
|
+
items: mergeSchemas(a.items, b.items)
|
|
1358
|
+
} : { type: "array" };
|
|
1359
|
+
return JSON.stringify(a) === JSON.stringify(b) ? a : {};
|
|
1360
|
+
}
|
|
1361
|
+
function inferSchema(sample) {
|
|
1362
|
+
if (sample === null) return {};
|
|
1363
|
+
if (Array.isArray(sample)) {
|
|
1364
|
+
if (sample.length === 0) return { type: "array" };
|
|
1365
|
+
return {
|
|
1366
|
+
type: "array",
|
|
1367
|
+
items: sample.map((element) => inferSchema(element)).reduce((left, right) => mergeSchemas(left, right))
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
if (typeof sample === "string") return { type: "string" };
|
|
1371
|
+
if (typeof sample === "number") return { type: "number" };
|
|
1372
|
+
if (typeof sample === "boolean") return { type: "boolean" };
|
|
1373
|
+
const properties = Object.fromEntries(Object.entries(sample).map(([key, value]) => [key, inferSchema(value)]));
|
|
1374
|
+
return Object.keys(properties).length > 0 ? {
|
|
1375
|
+
type: "object",
|
|
1376
|
+
properties
|
|
1377
|
+
} : { type: "object" };
|
|
1378
|
+
}
|
|
1379
|
+
const SCHEMA_DIALECT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
|
|
1380
|
+
const fieldIdRealm = Math.random().toString(36).slice(2, 7);
|
|
1381
|
+
let fieldIdCounter = 0;
|
|
1382
|
+
function newSchemaTreeField(overrides = {}) {
|
|
1383
|
+
fieldIdCounter += 1;
|
|
1384
|
+
return {
|
|
1385
|
+
id: `sf-${fieldIdRealm}-${fieldIdCounter}`,
|
|
1386
|
+
name: "",
|
|
1387
|
+
type: "string",
|
|
1388
|
+
required: false,
|
|
1389
|
+
description: "",
|
|
1390
|
+
children: [],
|
|
1391
|
+
items: null,
|
|
1392
|
+
...overrides
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
const SCALAR_TYPES = new Set([
|
|
1396
|
+
"string",
|
|
1397
|
+
"number",
|
|
1398
|
+
"integer",
|
|
1399
|
+
"boolean"
|
|
1400
|
+
]);
|
|
1401
|
+
const ROOT_KEYS = new Set([
|
|
1402
|
+
"$schema",
|
|
1403
|
+
"type",
|
|
1404
|
+
"properties",
|
|
1405
|
+
"required",
|
|
1406
|
+
"description"
|
|
1407
|
+
]);
|
|
1408
|
+
const SUBSCHEMA_KEYS = new Set([
|
|
1409
|
+
"type",
|
|
1410
|
+
"properties",
|
|
1411
|
+
"required",
|
|
1412
|
+
"items",
|
|
1413
|
+
"description"
|
|
1414
|
+
]);
|
|
1415
|
+
var Unsupported = class extends Error {
|
|
1416
|
+
issue;
|
|
1417
|
+
constructor(issue) {
|
|
1418
|
+
super(issue.code);
|
|
1419
|
+
this.issue = issue;
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
function asPlainObject(value) {
|
|
1423
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1424
|
+
}
|
|
1425
|
+
function checkKeys(schema, allowed) {
|
|
1426
|
+
for (const key of Object.keys(schema)) if (!allowed.has(key)) throw new Unsupported({
|
|
1427
|
+
code: "unsupported-keyword",
|
|
1428
|
+
keyword: key
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
function parseObjectFields(schema) {
|
|
1432
|
+
const properties = schema.properties === void 0 ? null : asPlainObject(schema.properties);
|
|
1433
|
+
if (schema.properties !== void 0 && properties === null) throw new Unsupported({ code: "invalid-properties" });
|
|
1434
|
+
const fields = [];
|
|
1435
|
+
const entries = Object.entries(properties ?? {});
|
|
1436
|
+
for (const [name, subschema] of entries) fields.push({
|
|
1437
|
+
...parseSubschema(subschema, name),
|
|
1438
|
+
name,
|
|
1439
|
+
...name.trim() === "" && { preserveBlankName: true }
|
|
1440
|
+
});
|
|
1441
|
+
if (schema.required !== void 0) {
|
|
1442
|
+
if (!Array.isArray(schema.required)) throw new Unsupported({ code: "invalid-required" });
|
|
1443
|
+
for (const entry of schema.required) {
|
|
1444
|
+
if (typeof entry !== "string") throw new Unsupported({ code: "invalid-required" });
|
|
1445
|
+
const field = fields.find((candidate) => candidate.name === entry);
|
|
1446
|
+
if (field === void 0) throw new Unsupported({
|
|
1447
|
+
code: "unknown-required-field",
|
|
1448
|
+
field: entry
|
|
1449
|
+
});
|
|
1450
|
+
field.required = true;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return fields;
|
|
1454
|
+
}
|
|
1455
|
+
function parseDescription(schema) {
|
|
1456
|
+
if (schema.description === void 0) return "";
|
|
1457
|
+
if (typeof schema.description !== "string") throw new Unsupported({ code: "invalid-description" });
|
|
1458
|
+
return schema.description;
|
|
1459
|
+
}
|
|
1460
|
+
function ensureAbsent(schema, key, holder) {
|
|
1461
|
+
if (schema[key] !== void 0) throw new Unsupported({
|
|
1462
|
+
code: "misplaced-keyword",
|
|
1463
|
+
keyword: key,
|
|
1464
|
+
holder
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
function parseSubschema(value, name) {
|
|
1468
|
+
const schema = asPlainObject(value);
|
|
1469
|
+
if (schema === null) throw new Unsupported({
|
|
1470
|
+
code: "invalid-field-definition",
|
|
1471
|
+
field: name
|
|
1472
|
+
});
|
|
1473
|
+
checkKeys(schema, SUBSCHEMA_KEYS);
|
|
1474
|
+
const { type } = schema;
|
|
1475
|
+
if (type !== void 0 && typeof type !== "string") throw new Unsupported({
|
|
1476
|
+
code: "invalid-field-type",
|
|
1477
|
+
field: name
|
|
1478
|
+
});
|
|
1479
|
+
const description = parseDescription(schema);
|
|
1480
|
+
if (type === "object" || type === void 0 && (schema.properties !== void 0 || schema.required !== void 0)) {
|
|
1481
|
+
ensureAbsent(schema, "items", "array");
|
|
1482
|
+
return newSchemaTreeField({
|
|
1483
|
+
type: "object",
|
|
1484
|
+
description,
|
|
1485
|
+
children: parseObjectFields(schema),
|
|
1486
|
+
explicitType: type === "object"
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
if (type === "array" || type === void 0 && schema.items !== void 0) {
|
|
1490
|
+
ensureAbsent(schema, "properties", "object");
|
|
1491
|
+
ensureAbsent(schema, "required", "object");
|
|
1492
|
+
return newSchemaTreeField({
|
|
1493
|
+
type: "array",
|
|
1494
|
+
description,
|
|
1495
|
+
items: schema.items === void 0 ? null : parseSubschema(schema.items, name),
|
|
1496
|
+
explicitType: type === "array"
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
if (type === void 0) {
|
|
1500
|
+
ensureAbsent(schema, "items", "array");
|
|
1501
|
+
return newSchemaTreeField({
|
|
1502
|
+
type: "any",
|
|
1503
|
+
description
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
if (!SCALAR_TYPES.has(type)) throw new Unsupported({
|
|
1507
|
+
code: "unsupported-field-type",
|
|
1508
|
+
field: name,
|
|
1509
|
+
type
|
|
1510
|
+
});
|
|
1511
|
+
ensureAbsent(schema, "properties", "object");
|
|
1512
|
+
ensureAbsent(schema, "required", "object");
|
|
1513
|
+
ensureAbsent(schema, "items", "array");
|
|
1514
|
+
return newSchemaTreeField({
|
|
1515
|
+
type,
|
|
1516
|
+
description
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
function parseSchemaTree(text) {
|
|
1520
|
+
if (text.trim() === "") return {
|
|
1521
|
+
ok: true,
|
|
1522
|
+
tree: {
|
|
1523
|
+
fields: [],
|
|
1524
|
+
dialect: false,
|
|
1525
|
+
description: ""
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
let parsed;
|
|
1529
|
+
try {
|
|
1530
|
+
parsed = JSON.parse(text);
|
|
1531
|
+
} catch {
|
|
1532
|
+
return {
|
|
1533
|
+
ok: false,
|
|
1534
|
+
issue: { code: "invalid-json" }
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
try {
|
|
1538
|
+
const root = asPlainObject(parsed);
|
|
1539
|
+
if (root === null) throw new Unsupported({ code: "root-not-object" });
|
|
1540
|
+
checkKeys(root, ROOT_KEYS);
|
|
1541
|
+
const dialect = root.$schema !== void 0;
|
|
1542
|
+
if (dialect && root.$schema !== "https://json-schema.org/draft/2020-12/schema") throw new Unsupported({ code: "unsupported-dialect" });
|
|
1543
|
+
if (root.type !== void 0 && root.type !== "object") throw new Unsupported({ code: "root-not-object" });
|
|
1544
|
+
return {
|
|
1545
|
+
ok: true,
|
|
1546
|
+
tree: {
|
|
1547
|
+
fields: parseObjectFields(root),
|
|
1548
|
+
dialect,
|
|
1549
|
+
description: parseDescription(root),
|
|
1550
|
+
explicitType: root.type === "object"
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
if (error instanceof Unsupported) return {
|
|
1555
|
+
ok: false,
|
|
1556
|
+
issue: error.issue
|
|
1557
|
+
};
|
|
1558
|
+
throw error;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function serializeObjectBody(fields) {
|
|
1562
|
+
const named = fields.filter((field) => field.name.trim() !== "" || field.preserveBlankName === true);
|
|
1563
|
+
const finalByName = /* @__PURE__ */ new Map();
|
|
1564
|
+
const finalFields = [];
|
|
1565
|
+
const body = {};
|
|
1566
|
+
for (const field of named) finalByName.set(field.name, field);
|
|
1567
|
+
finalByName.forEach((field) => {
|
|
1568
|
+
finalFields.push(field);
|
|
1569
|
+
});
|
|
1570
|
+
if (finalFields.length > 0) body.properties = Object.fromEntries(finalFields.map((field) => [field.name, serializeField(field)]));
|
|
1571
|
+
const required = finalFields.filter((field) => field.required).map((field) => field.name);
|
|
1572
|
+
if (required.length > 0) body.required = required;
|
|
1573
|
+
return body;
|
|
1574
|
+
}
|
|
1575
|
+
function serializeField(field) {
|
|
1576
|
+
const description = field.description === "" ? {} : { description: field.description };
|
|
1577
|
+
switch (field.type) {
|
|
1578
|
+
case "any": return { ...description };
|
|
1579
|
+
case "object": return {
|
|
1580
|
+
...field.explicitType !== false && { type: "object" },
|
|
1581
|
+
...description,
|
|
1582
|
+
...serializeObjectBody(field.children)
|
|
1583
|
+
};
|
|
1584
|
+
case "array": {
|
|
1585
|
+
const type = field.explicitType === false ? {} : { type: "array" };
|
|
1586
|
+
return field.items === null || field.items.type === "any" && field.items.description === "" ? {
|
|
1587
|
+
...type,
|
|
1588
|
+
...description
|
|
1589
|
+
} : {
|
|
1590
|
+
...type,
|
|
1591
|
+
...description,
|
|
1592
|
+
items: serializeField(field.items)
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
default: return {
|
|
1596
|
+
type: field.type,
|
|
1597
|
+
...description
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function serializeSchemaTree(tree) {
|
|
1602
|
+
const root = {
|
|
1603
|
+
...tree.dialect && { $schema: "https://json-schema.org/draft/2020-12/schema" },
|
|
1604
|
+
...tree.explicitType !== false && { type: "object" },
|
|
1605
|
+
...tree.description !== "" && { description: tree.description },
|
|
1606
|
+
...serializeObjectBody(tree.fields)
|
|
1607
|
+
};
|
|
1608
|
+
return `${JSON.stringify(root, null, 2)}\n`;
|
|
1609
|
+
}
|
|
1302
1610
|
exports.CONDITION_OPERATORS = CONDITION_OPERATORS;
|
|
1303
1611
|
exports.CONDITION_TREE_OPERATORS = CONDITION_TREE_OPERATORS;
|
|
1304
1612
|
exports.ExpressionError = ExpressionError;
|
|
1305
1613
|
exports.ExpressionNotReadyError = ExpressionNotReadyError;
|
|
1306
1614
|
exports.MAX_CONDITION_TREE_DEPTH = MAX_CONDITION_TREE_DEPTH;
|
|
1615
|
+
exports.SCHEMA_DIALECT_2020_12 = SCHEMA_DIALECT_2020_12;
|
|
1307
1616
|
exports.analyzeTemplate = analyzeTemplate;
|
|
1308
1617
|
exports.analyzeTemplateSync = analyzeTemplateSync;
|
|
1309
1618
|
exports.analyzeTypes = analyzeTypes;
|
|
@@ -1331,18 +1640,24 @@ exports.getEngineSync = getEngineSync;
|
|
|
1331
1640
|
exports.getExpressionMessages = getExpressionMessages;
|
|
1332
1641
|
exports.getTemplateDiagnostics = getTemplateDiagnostics;
|
|
1333
1642
|
exports.getTemplateDiagnosticsSync = getTemplateDiagnosticsSync;
|
|
1643
|
+
exports.inferSchema = inferSchema;
|
|
1334
1644
|
exports.isEngineReady = isEngineReady;
|
|
1335
1645
|
exports.isZenRepresentableNumber = isZenRepresentableNumber;
|
|
1336
1646
|
exports.liftConditionTree = liftConditionTree;
|
|
1337
1647
|
exports.loadEngine = loadEngine;
|
|
1338
1648
|
exports.newConditionNodeId = newConditionNodeId;
|
|
1649
|
+
exports.newSchemaTreeField = newSchemaTreeField;
|
|
1650
|
+
exports.parseSchemaTree = parseSchemaTree;
|
|
1339
1651
|
exports.parseTemplateHoles = parseTemplateHoles;
|
|
1340
1652
|
exports.registerExpressionLocale = registerExpressionLocale;
|
|
1341
1653
|
exports.resetEngine = resetEngine;
|
|
1342
1654
|
exports.satisfiesType = satisfiesType;
|
|
1343
1655
|
exports.satisfiesTypeSync = satisfiesTypeSync;
|
|
1656
|
+
exports.schemaTreeToExpressionType = schemaTreeToExpressionType;
|
|
1344
1657
|
exports.selectBranch = selectBranch;
|
|
1345
1658
|
exports.selectBranchWith = selectBranchWith;
|
|
1659
|
+
exports.serializeSchemaTree = serializeSchemaTree;
|
|
1660
|
+
exports.subscribeExpressionMessages = subscribeExpressionMessages;
|
|
1346
1661
|
exports.templateHoleAt = templateHoleAt;
|
|
1347
1662
|
exports.toZenLiteral = toZenLiteral;
|
|
1348
1663
|
exports.zhCNMessages = zhCNMessages;
|