@githolon/dsl 0.32.0 → 0.33.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/dart/lib/src/errors.dart +105 -1
- package/dart/lib/src/webview_bridge.dart +6 -4
- package/dart/test/webview_bridge_test.dart +65 -0
- package/package.json +1 -1
- package/src/build_package.ts +25 -0
- package/src/codegen_dart.ts +347 -3
- package/src/codegen_ts.ts +74 -1
- package/src/compile_package_main.ts +26 -2
- package/src/fields.ts +84 -0
- package/src/index.ts +19 -0
- package/src/usd.ts +21 -0
- package/src/usd_layers.ts +11 -0
- package/src/value_object.ts +413 -0
package/src/codegen_dart.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { z } from "zod";
|
|
|
32
32
|
import type { AggregateHandle } from "./aggregate.js";
|
|
33
33
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
34
34
|
import type { RelationTypeDecl } from "./relation_schema.js";
|
|
35
|
-
import type { Field, FieldKind } from "./fields.js";
|
|
35
|
+
import type { Field, FieldKind, ValueObjectDescriptor, ValueObjectFieldDescriptor } from "./fields.js";
|
|
36
36
|
import type { Directive } from "./directive.js";
|
|
37
37
|
import { executeDirectiveToIntent } from "./wire_encode.js";
|
|
38
38
|
import { deterministicPorts } from "./ctx.js";
|
|
@@ -347,6 +347,13 @@ interface AggFieldSpec {
|
|
|
347
347
|
* `undefined` for a non-map field.
|
|
348
348
|
*/
|
|
349
349
|
mapValueKind?: FieldKind;
|
|
350
|
+
/**
|
|
351
|
+
* For a `json`-kind field lowered from a NAMED VALUE OBJECT (`ValueObject.field()`):
|
|
352
|
+
* the generated Dart class name. The field decodes via `readJsonObject…` +
|
|
353
|
+
* `<VoClass>.fromProjected(m)` (the EvidenceRef pattern) and types as the class.
|
|
354
|
+
* `undefined` for a plain `t.json()`/`t.jsonObject()` field.
|
|
355
|
+
*/
|
|
356
|
+
voClass?: string;
|
|
350
357
|
/**
|
|
351
358
|
* For a `json`-kind field: the asserted decoded SHAPE (`field.jsonShape`). `"object"`
|
|
352
359
|
* (a `t.jsonObject()` leaf, e.g. `location`) means the value is ALWAYS a JSON object —
|
|
@@ -581,8 +588,21 @@ function aggFieldSpec(
|
|
|
581
588
|
...(valueClass !== undefined ? { valueClass } : {}),
|
|
582
589
|
};
|
|
583
590
|
case "json":
|
|
591
|
+
// A NAMED VALUE OBJECT (`ValueObject.field()`) types as its generated Dart class
|
|
592
|
+
// (typed decode via `.fromProjected`, the EvidenceRef pattern). Otherwise
|
|
584
593
|
// `jsonShape` ("object" via `t.jsonObject()`) chooses strict-object vs
|
|
585
594
|
// permissive read-model type.
|
|
595
|
+
if (field.valueObject !== undefined) {
|
|
596
|
+
return {
|
|
597
|
+
name,
|
|
598
|
+
wireName,
|
|
599
|
+
kind: field.kind,
|
|
600
|
+
dartType: field.valueObject.name,
|
|
601
|
+
optional,
|
|
602
|
+
voClass: field.valueObject.name,
|
|
603
|
+
...(field.jsonShape !== undefined ? { jsonShape: field.jsonShape } : {}),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
586
606
|
return {
|
|
587
607
|
name,
|
|
588
608
|
wireName,
|
|
@@ -1166,6 +1186,275 @@ function emitEvidenceRefClass(): string {
|
|
|
1166
1186
|
].join("\n");
|
|
1167
1187
|
}
|
|
1168
1188
|
|
|
1189
|
+
// ── value objects → Dart classes (`value_object.ts` descriptors) ─────────────────────
|
|
1190
|
+
|
|
1191
|
+
/** The Dart coordinates type per RFC 7946 geometry kind. */
|
|
1192
|
+
const geometryCoordsDart: Record<string, string> = {
|
|
1193
|
+
Point: "List<double>",
|
|
1194
|
+
LineString: "List<List<double>>",
|
|
1195
|
+
Polygon: "List<List<List<double>>>",
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* The generated Dart enum NAME for one VO enum field: `<VoName><FieldName>Enum`
|
|
1200
|
+
* (e.g. `PostalAddressCountryCodeEnum`). The VO is the field's semantic home — the
|
|
1201
|
+
* exact analogue of the aggregate-enum convention's domain prefix (`collectEnums`'s
|
|
1202
|
+
* `<Domain><Field>Enum`), so two barrel-exported domains never collide on a bare name.
|
|
1203
|
+
* IDENTITY = THE VO FIELD, not the value set: two identical value-sets in DIFFERENT
|
|
1204
|
+
* VOs stay separate types (VO names are package-unique, so so are these).
|
|
1205
|
+
*/
|
|
1206
|
+
function voEnumName(voName: string, fieldName: string): string {
|
|
1207
|
+
return `${voName}${dartTypeNameFromField(fieldName)}Enum`;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/** The tolerant-decode SENTINEL case name — dodge a literal enum value that would
|
|
1211
|
+
* collide with it (`dartIdent("unknownWire") === "unknownWire"`). */
|
|
1212
|
+
function voEnumSentinel(values: readonly string[]): string {
|
|
1213
|
+
let s = "unknownWire";
|
|
1214
|
+
while (values.some((v) => dartIdent(v) === s)) s = `${s}_`;
|
|
1215
|
+
return s;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/** Emit ONE generated Dart enum for a VO enum field. TOLERANT DECODE (law evolves —
|
|
1219
|
+
* a new value added by a later law must NOT crash an old client): `fromWire` maps an
|
|
1220
|
+
* unrecognized wire string to the [sentinel] case instead of throwing (the deliberate
|
|
1221
|
+
* divergence from the strict aggregate-enum `fromWire`; VO data crosses law versions). */
|
|
1222
|
+
function emitVoEnum(voName: string, fieldName: string, values: readonly string[]): string {
|
|
1223
|
+
const name = voEnumName(voName, fieldName);
|
|
1224
|
+
const sentinel = voEnumSentinel(values);
|
|
1225
|
+
const variants = values.map((v) => ` ${dartIdent(v)}('${v}')`).join(",\n");
|
|
1226
|
+
return [
|
|
1227
|
+
`/// Generated enum for value object field '${voName}.${fieldName}': ${values.join(", ")}.`,
|
|
1228
|
+
`/// TOLERANT DECODE: an unrecognized wire value (law can add enum values; old`,
|
|
1229
|
+
`/// clients must not crash on new data) decodes to [${name}.${sentinel}] — never a throw.`,
|
|
1230
|
+
`/// [wire] is the exact wire string ([${sentinel}].wire is the '__unknown__' marker;`,
|
|
1231
|
+
`/// authoring code constructs KNOWN cases, so toJson never writes it in practice).`,
|
|
1232
|
+
`enum ${name} {`,
|
|
1233
|
+
`${variants},`,
|
|
1234
|
+
` /// The tolerant-decode sentinel — an enum value this client's law snapshot does not know.`,
|
|
1235
|
+
` ${sentinel}('__unknown__');`,
|
|
1236
|
+
``,
|
|
1237
|
+
` final String wire;`,
|
|
1238
|
+
` const ${name}(this.wire);`,
|
|
1239
|
+
``,
|
|
1240
|
+
` static ${name} fromWire(String wire) =>`,
|
|
1241
|
+
` ${name}.values.firstWhere((e) => e.wire == wire,`,
|
|
1242
|
+
` orElse: () => ${name}.${sentinel});`,
|
|
1243
|
+
`}`,
|
|
1244
|
+
].join("\n");
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/** Every (fieldName, values) enum pair a VO descriptor carries (arrays recursed —
|
|
1248
|
+
* an array-of-enum field's element enum is named by the FIELD). */
|
|
1249
|
+
function voEnumFields(d: ValueObjectDescriptor): { fieldName: string; values: readonly string[] }[] {
|
|
1250
|
+
const out: { fieldName: string; values: readonly string[] }[] = [];
|
|
1251
|
+
const walk = (fieldName: string, f: ValueObjectFieldDescriptor): void => {
|
|
1252
|
+
if (f.type === "enum") out.push({ fieldName, values: f.values });
|
|
1253
|
+
else if (f.type === "array") walk(fieldName, f.items);
|
|
1254
|
+
};
|
|
1255
|
+
for (const [fieldName, f] of Object.entries(d.fields ?? {})) walk(fieldName, f);
|
|
1256
|
+
return out;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** The Dart type of one VO descriptor field. `voName`/`fieldName` name the field's
|
|
1260
|
+
* generated enum type (`<VoName><FieldName>Enum`) — threaded through arrays. */
|
|
1261
|
+
function dartTypeOfVoField(f: ValueObjectFieldDescriptor, voName: string, fieldName: string): string {
|
|
1262
|
+
switch (f.type) {
|
|
1263
|
+
case "string":
|
|
1264
|
+
return "String";
|
|
1265
|
+
case "number":
|
|
1266
|
+
return "double";
|
|
1267
|
+
case "boolean":
|
|
1268
|
+
return "bool";
|
|
1269
|
+
case "enum":
|
|
1270
|
+
// A REAL generated Dart enum (`emitVoEnum`), named by the owning VO field.
|
|
1271
|
+
return voEnumName(voName, fieldName);
|
|
1272
|
+
case "array":
|
|
1273
|
+
return `List<${dartTypeOfVoField(f.items, voName, fieldName)}>`;
|
|
1274
|
+
case "object":
|
|
1275
|
+
case "geometry":
|
|
1276
|
+
return f.ref; // the named VO class (emitted alongside / imported)
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/** A Dart expression converting the RAW decoded json value `expr` to the field's typed
|
|
1281
|
+
* value. `depth` names nested lambda vars (`e0`, `e1`, …) for arrays-of-arrays. */
|
|
1282
|
+
function voFromExpr(
|
|
1283
|
+
f: ValueObjectFieldDescriptor,
|
|
1284
|
+
expr: string,
|
|
1285
|
+
voName: string,
|
|
1286
|
+
fieldName: string,
|
|
1287
|
+
depth = 0,
|
|
1288
|
+
): string {
|
|
1289
|
+
switch (f.type) {
|
|
1290
|
+
case "string":
|
|
1291
|
+
return `${expr} as String`;
|
|
1292
|
+
case "enum":
|
|
1293
|
+
// TOLERANT typed decode — an unknown wire value becomes the sentinel case.
|
|
1294
|
+
return `${voEnumName(voName, fieldName)}.fromWire(${expr} as String)`;
|
|
1295
|
+
case "number":
|
|
1296
|
+
return `(${expr} as num).toDouble()`;
|
|
1297
|
+
case "boolean":
|
|
1298
|
+
return `${expr} as bool`;
|
|
1299
|
+
case "array": {
|
|
1300
|
+
const v = `e${depth}`;
|
|
1301
|
+
return `(${expr} as List).map((${v}) => ${voFromExpr(f.items, v, voName, fieldName, depth + 1)}).toList()`;
|
|
1302
|
+
}
|
|
1303
|
+
case "object":
|
|
1304
|
+
case "geometry":
|
|
1305
|
+
return `${f.ref}.fromProjected((${expr} as Map).cast<String, Object?>())`;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/** A Dart expression serializing the typed member `expr` back to plain json. */
|
|
1310
|
+
function voToJsonExpr(f: ValueObjectFieldDescriptor, expr: string, depth = 0): string {
|
|
1311
|
+
switch (f.type) {
|
|
1312
|
+
case "string":
|
|
1313
|
+
case "number":
|
|
1314
|
+
case "boolean":
|
|
1315
|
+
return expr;
|
|
1316
|
+
case "enum":
|
|
1317
|
+
return `${expr}.wire`; // the exact wire string the law's zod enum validates
|
|
1318
|
+
case "array": {
|
|
1319
|
+
const inner = voToJsonExpr(f.items, `e${depth}`, depth + 1);
|
|
1320
|
+
return inner === `e${depth}` ? expr : `${expr}.map((e${depth}) => ${inner}).toList()`;
|
|
1321
|
+
}
|
|
1322
|
+
case "object":
|
|
1323
|
+
case "geometry":
|
|
1324
|
+
return `${expr}.toJson()`;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/** Deep json equality/hash helpers shared by every generated VO class — emitted ONCE
|
|
1329
|
+
* per file when the file emits ≥1 VO class (`toJson()`-based value equality). */
|
|
1330
|
+
function emitVoHelpers(): string {
|
|
1331
|
+
return [
|
|
1332
|
+
`/// Deep json equality for generated value-object classes (toJson-based).`,
|
|
1333
|
+
`bool _voJsonEq(Object? a, Object? b) {`,
|
|
1334
|
+
` if (a is Map && b is Map) {`,
|
|
1335
|
+
` if (a.length != b.length) return false;`,
|
|
1336
|
+
` for (final k in a.keys) {`,
|
|
1337
|
+
` if (!b.containsKey(k) || !_voJsonEq(a[k], b[k])) return false;`,
|
|
1338
|
+
` }`,
|
|
1339
|
+
` return true;`,
|
|
1340
|
+
` }`,
|
|
1341
|
+
` if (a is List && b is List) {`,
|
|
1342
|
+
` if (a.length != b.length) return false;`,
|
|
1343
|
+
` for (var i = 0; i < a.length; i++) {`,
|
|
1344
|
+
` if (!_voJsonEq(a[i], b[i])) return false;`,
|
|
1345
|
+
` }`,
|
|
1346
|
+
` return true;`,
|
|
1347
|
+
` }`,
|
|
1348
|
+
` return a == b;`,
|
|
1349
|
+
`}`,
|
|
1350
|
+
``,
|
|
1351
|
+
`/// Deep json hash for generated value-object classes (toJson-based).`,
|
|
1352
|
+
`int _voJsonHash(Object? v) {`,
|
|
1353
|
+
` if (v is Map) {`,
|
|
1354
|
+
` return Object.hashAllUnordered(`,
|
|
1355
|
+
` v.entries.map((e) => Object.hash(e.key, _voJsonHash(e.value))));`,
|
|
1356
|
+
` }`,
|
|
1357
|
+
` if (v is List) return Object.hashAll(v.map(_voJsonHash));`,
|
|
1358
|
+
` return v.hashCode;`,
|
|
1359
|
+
`}`,
|
|
1360
|
+
].join("\n");
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/** Emit ONE generated Dart value-object class from its canonical descriptor: typed
|
|
1364
|
+
* final fields, a const ctor, `fromProjected` (the projection's decoded json object),
|
|
1365
|
+
* `toJson` (the authored payload shape — optionals OMITTED when null, matching the zod
|
|
1366
|
+
* `.optional()` contract), and toJson-based value equality. A `geo.*` VO emits the
|
|
1367
|
+
* fixed RFC 7946 shape (typed `coordinates`; `type` is the constant geometry kind). */
|
|
1368
|
+
function emitValueObjectClass(d: ValueObjectDescriptor): string {
|
|
1369
|
+
if (d.geometry !== undefined) {
|
|
1370
|
+
const coordsType = geometryCoordsDart[d.geometry]!;
|
|
1371
|
+
const coordsFrom: Record<string, string> = {
|
|
1372
|
+
Point: `(m['coordinates']! as List).map((e0) => (e0 as num).toDouble()).toList()`,
|
|
1373
|
+
LineString: `(m['coordinates']! as List).map((e0) => (e0 as List).map((e1) => (e1 as num).toDouble()).toList()).toList()`,
|
|
1374
|
+
Polygon: `(m['coordinates']! as List).map((e0) => (e0 as List).map((e1) => (e1 as List).map((e2) => (e2 as num).toDouble()).toList()).toList()).toList()`,
|
|
1375
|
+
};
|
|
1376
|
+
return [
|
|
1377
|
+
`/// RFC 7946 ${d.geometry} — a \`geo.*\` value object ('${d.name}'). Positions are`,
|
|
1378
|
+
`/// \`[lng, lat]\` or \`[lng, lat, altitude]\` (RFC 7946 order).`,
|
|
1379
|
+
`class ${d.name} {`,
|
|
1380
|
+
` final ${coordsType} coordinates;`,
|
|
1381
|
+
``,
|
|
1382
|
+
` const ${d.name}({required this.coordinates});`,
|
|
1383
|
+
``,
|
|
1384
|
+
` /// The RFC 7946 geometry kind (constant).`,
|
|
1385
|
+
` String get type => '${d.geometry}';`,
|
|
1386
|
+
``,
|
|
1387
|
+
` factory ${d.name}.fromProjected(Map<String, Object?> m) =>`,
|
|
1388
|
+
` ${d.name}(coordinates: ${coordsFrom[d.geometry]!});`,
|
|
1389
|
+
``,
|
|
1390
|
+
` Map<String, Object?> toJson() => <String, Object?>{`,
|
|
1391
|
+
` 'type': '${d.geometry}',`,
|
|
1392
|
+
` 'coordinates': coordinates,`,
|
|
1393
|
+
` };`,
|
|
1394
|
+
``,
|
|
1395
|
+
` @override`,
|
|
1396
|
+
` bool operator ==(Object other) =>`,
|
|
1397
|
+
` other is ${d.name} && _voJsonEq(other.toJson(), toJson());`,
|
|
1398
|
+
``,
|
|
1399
|
+
` @override`,
|
|
1400
|
+
` int get hashCode => _voJsonHash(toJson());`,
|
|
1401
|
+
`}`,
|
|
1402
|
+
].join("\n");
|
|
1403
|
+
}
|
|
1404
|
+
const fields = Object.entries(d.fields ?? {}).map(([wireName, f]) => ({
|
|
1405
|
+
wireName,
|
|
1406
|
+
member: dartMemberName(wireName),
|
|
1407
|
+
f,
|
|
1408
|
+
optional: f.optional === true,
|
|
1409
|
+
}));
|
|
1410
|
+
const decls = fields.map(
|
|
1411
|
+
(x) => ` final ${dartTypeOfVoField(x.f, d.name, x.wireName)}${x.optional ? "?" : ""} ${x.member};`,
|
|
1412
|
+
);
|
|
1413
|
+
const ctor = fields.map((x) => (x.optional ? ` this.${x.member},` : ` required this.${x.member},`));
|
|
1414
|
+
const fromLines = fields.map((x) =>
|
|
1415
|
+
x.optional
|
|
1416
|
+
? ` ${x.member}: m['${x.wireName}'] == null ? null : ${voFromExpr(x.f, `m['${x.wireName}']!`, d.name, x.wireName)},`
|
|
1417
|
+
: ` ${x.member}: ${voFromExpr(x.f, `m['${x.wireName}']!`, d.name, x.wireName)},`,
|
|
1418
|
+
);
|
|
1419
|
+
const toLines = fields.map((x) =>
|
|
1420
|
+
x.optional
|
|
1421
|
+
? ` if (${x.member} != null) '${x.wireName}': ${voToJsonExpr(x.f, `${x.member}!`)},`
|
|
1422
|
+
: ` '${x.wireName}': ${voToJsonExpr(x.f, x.member)},`,
|
|
1423
|
+
);
|
|
1424
|
+
return [
|
|
1425
|
+
`/// Generated value object '${d.name}' (value_object.ts). Stored as ONE json-object`,
|
|
1426
|
+
`/// value leaf on its aggregate field — replaced WHOLESALE on write (Lww-class; no`,
|
|
1427
|
+
`/// per-subfield merge). Optional members are OMITTED from [toJson] when null (the`,
|
|
1428
|
+
`/// zod \`.optional()\` contract — absent, never JSON null).`,
|
|
1429
|
+
`class ${d.name} {`,
|
|
1430
|
+
...decls,
|
|
1431
|
+
``,
|
|
1432
|
+
` const ${d.name}({`,
|
|
1433
|
+
...ctor,
|
|
1434
|
+
` });`,
|
|
1435
|
+
``,
|
|
1436
|
+
` /// Read from the projection's decoded json object.`,
|
|
1437
|
+
` factory ${d.name}.fromProjected(Map<String, Object?> m) {`,
|
|
1438
|
+
` return ${d.name}(`,
|
|
1439
|
+
...fromLines,
|
|
1440
|
+
` );`,
|
|
1441
|
+
` }`,
|
|
1442
|
+
``,
|
|
1443
|
+
` /// The authored payload shape (embed it in a directive payload / \`set()\`).`,
|
|
1444
|
+
` Map<String, Object?> toJson() => <String, Object?>{`,
|
|
1445
|
+
...toLines,
|
|
1446
|
+
` };`,
|
|
1447
|
+
``,
|
|
1448
|
+
` @override`,
|
|
1449
|
+
` bool operator ==(Object other) =>`,
|
|
1450
|
+
` other is ${d.name} && _voJsonEq(other.toJson(), toJson());`,
|
|
1451
|
+
``,
|
|
1452
|
+
` @override`,
|
|
1453
|
+
` int get hashCode => _voJsonHash(toJson());`,
|
|
1454
|
+
`}`,
|
|
1455
|
+
].join("\n");
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1169
1458
|
/** True when ANY aggregate field in the module is `t.evidence()` — gates emitting the
|
|
1170
1459
|
* shared [emitEvidenceRefClass] once. */
|
|
1171
1460
|
function moduleUsesEvidence(aggregates: readonly AggregateHandle[]): boolean {
|
|
@@ -1235,8 +1524,15 @@ function parseField(s: AggFieldSpec, aggregate: string): string {
|
|
|
1235
1524
|
? `readStrOrNull(data, '${s.wireName}')`
|
|
1236
1525
|
: `readStr(data, '${aggregate}', '${s.wireName}')`;
|
|
1237
1526
|
case "json":
|
|
1238
|
-
// A
|
|
1239
|
-
//
|
|
1527
|
+
// A NAMED VALUE OBJECT reads the projection's decoded object and wraps it in its
|
|
1528
|
+
// generated class (the EvidenceRef pattern). A plain `json`-kind field is typed
|
|
1529
|
+
// `String` on the aggregate; the projection may have parsed its stored JSON into a
|
|
1530
|
+
// container — re-stringify to keep `String`.
|
|
1531
|
+
if (s.voClass !== undefined) {
|
|
1532
|
+
return opt
|
|
1533
|
+
? `() { final m = readJsonObjectOrNull(data, '${aggregate}', '${s.wireName}'); return m == null ? null : ${s.voClass}.fromProjected(m); }()`
|
|
1534
|
+
: `${s.voClass}.fromProjected(readJsonObject(data, '${aggregate}', '${s.wireName}'))`;
|
|
1535
|
+
}
|
|
1240
1536
|
return opt
|
|
1241
1537
|
? `readJsonStrOrNull(data, '${s.wireName}')`
|
|
1242
1538
|
: `readJsonStr(data, '${aggregate}', '${s.wireName}')`;
|
|
@@ -1304,6 +1600,8 @@ function readModelDartType(s: AggFieldSpec): string {
|
|
|
1304
1600
|
// has an UNKNOWN/non-object decoded shape (a double/bool/list authored as a JSON
|
|
1305
1601
|
// leaf — the kernel `Value` is Str|Int only), so it is surfaced as the permissive
|
|
1306
1602
|
// `Object?` (the read engine's decoded value as-is — never coerced/thrown).
|
|
1603
|
+
// A NAMED VALUE OBJECT reads as its generated class (typed decode).
|
|
1604
|
+
if (s.voClass !== undefined) return s.voClass;
|
|
1307
1605
|
return isJsonObjectLeaf(s) ? "Map<String, Object?>" : "Object?";
|
|
1308
1606
|
// `position`/`counter` are normalized to `string`/`int` in `aggFieldSpec` (read-collapsed scalars),
|
|
1309
1607
|
// so these labels are unreachable at runtime — present for FieldKind exhaustiveness.
|
|
@@ -1380,6 +1678,12 @@ function parseReadModelField(s: AggFieldSpec, aggregate: string): string {
|
|
|
1380
1678
|
// shape (a double/bool/list authored as a JSON leaf), so surface the read engine's
|
|
1381
1679
|
// decoded value AS-IS via the NON-THROWING `readJsonValueOrNull` (`Object?`, null
|
|
1382
1680
|
// when absent) — NEVER a strict-object reader that would throw on a legit non-object.
|
|
1681
|
+
if (s.voClass !== undefined) {
|
|
1682
|
+
// A NAMED VALUE OBJECT: the decoded object wrapped in its generated class.
|
|
1683
|
+
return opt
|
|
1684
|
+
? `() { final m = readJsonObjectOrNull(data, '${aggregate}', '${s.wireName}'); return m == null ? null : ${s.voClass}.fromProjected(m); }()`
|
|
1685
|
+
: `${s.voClass}.fromProjected(readJsonObject(data, '${aggregate}', '${s.wireName}'))`;
|
|
1686
|
+
}
|
|
1383
1687
|
if (isPermissiveJsonLeaf(s)) {
|
|
1384
1688
|
return `readJsonValueOrNull(data, '${s.wireName}')`;
|
|
1385
1689
|
}
|
|
@@ -2489,6 +2793,16 @@ export interface DomainModule {
|
|
|
2489
2793
|
* AST is hashed; the recursive Check/Expand EVALUATOR is the Rust engine substrate, never authored.
|
|
2490
2794
|
*/
|
|
2491
2795
|
relationTypes?: RelationTypeDecl[];
|
|
2796
|
+
/**
|
|
2797
|
+
* The domain's FIRST-CLASS VALUE OBJECTS (`value_object.ts`) — the deduped, sorted
|
|
2798
|
+
* canonical descriptors of every named VO the aggregates carry (transitive nested VOs
|
|
2799
|
+
* included; collected by `composeDomainModule`). ADDITIVE + OPTIONAL: a VO-free domain
|
|
2800
|
+
* omits this entirely (the `nomosValueObjects` era rule — package bytes unchanged).
|
|
2801
|
+
* NEVER in the canonical manifest/domainHash: a VO field's manifest bytes equal a plain
|
|
2802
|
+
* `t.jsonObject()` field's. Drives the generated TS interfaces + Dart classes and the
|
|
2803
|
+
* additive `nomosValueObjects` USD key (`usd.ts`).
|
|
2804
|
+
*/
|
|
2805
|
+
valueObjects?: readonly ValueObjectDescriptor[];
|
|
2492
2806
|
/**
|
|
2493
2807
|
* The domain's FIRST-CLASS WORKSPACE TYPES (`workspace_type.ts`; sharding §10
|
|
2494
2808
|
* RATIFIED, slice 1) — the law-declared workspace taxonomy
|
|
@@ -2598,6 +2912,13 @@ export interface GenerateDartDomainOptions {
|
|
|
2598
2912
|
* `import` so skipped framework symbols resolve. Omit when nothing is skipped.
|
|
2599
2913
|
*/
|
|
2600
2914
|
readonly frameworkImportUri?: string;
|
|
2915
|
+
/**
|
|
2916
|
+
* VALUE OBJECT names whose generated Dart class is emitted ONCE per package in the shared
|
|
2917
|
+
* `nomos_framework.dart` (a VO used by ≥2 domains) and MUST NOT be re-emitted here — this
|
|
2918
|
+
* domain references them via {@link frameworkImportUri}. Empty/absent ⇒ emit every VO the
|
|
2919
|
+
* module carries locally.
|
|
2920
|
+
*/
|
|
2921
|
+
readonly skipValueObjectNames?: ReadonlySet<string>;
|
|
2601
2922
|
}
|
|
2602
2923
|
|
|
2603
2924
|
/** Generate the full Dart source for one domain module. */
|
|
@@ -2606,6 +2927,22 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
|
|
|
2606
2927
|
const skipAggregateIds = opts.skipAggregateIds ?? new Set<string>();
|
|
2607
2928
|
const skipDirectiveIds = opts.skipDirectiveIds ?? new Set<string>();
|
|
2608
2929
|
const skipQueryIds = opts.skipQueryIds ?? new Set<string>();
|
|
2930
|
+
const skipValueObjectNames = opts.skipValueObjectNames ?? new Set<string>();
|
|
2931
|
+
// VALUE OBJECTS: one generated class per named VO the module carries (composeDomainModule
|
|
2932
|
+
// collected + sorted them), minus the package-shared ones (emitted once in the framework file).
|
|
2933
|
+
const localValueObjects = (mod.valueObjects ?? []).filter((d) => !skipValueObjectNames.has(d.name));
|
|
2934
|
+
// Each VO's block = its FIELD ENUMS (real Dart enums, tolerant fromWire) then its class —
|
|
2935
|
+
// so skipping a shared VO (the framework-file dedup) skips its enums with it.
|
|
2936
|
+
const voBlock =
|
|
2937
|
+
localValueObjects.length > 0
|
|
2938
|
+
? [
|
|
2939
|
+
emitVoHelpers(),
|
|
2940
|
+
...localValueObjects.flatMap((d) => [
|
|
2941
|
+
...voEnumFields(d).map((e) => emitVoEnum(d.name, e.fieldName, e.values)),
|
|
2942
|
+
emitValueObjectClass(d),
|
|
2943
|
+
]),
|
|
2944
|
+
].join("\n\n")
|
|
2945
|
+
: "";
|
|
2609
2946
|
// The symbols whose Dart surface this file EMITS (framework-shared ones live in the shared file).
|
|
2610
2947
|
const localAggregates = mod.aggregates.filter((a) => !skipAggregateIds.has(a.id));
|
|
2611
2948
|
const localDirectives = mod.directives.filter((d) => !skipDirectiveIds.has(d.id));
|
|
@@ -2841,6 +3178,13 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
|
|
|
2841
3178
|
``,
|
|
2842
3179
|
]
|
|
2843
3180
|
: []),
|
|
3181
|
+
...(voBlock !== ""
|
|
3182
|
+
? [
|
|
3183
|
+
`// ---- value objects (named composable json-object leaves; value_object.ts) ----`,
|
|
3184
|
+
voBlock,
|
|
3185
|
+
``,
|
|
3186
|
+
]
|
|
3187
|
+
: []),
|
|
2844
3188
|
`// ---- aggregates (projection types) ----`,
|
|
2845
3189
|
aggBlock,
|
|
2846
3190
|
``,
|
package/src/codegen_ts.ts
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
import type { z } from "zod";
|
|
38
38
|
|
|
39
39
|
import type { AggregateHandle } from "./aggregate.js";
|
|
40
|
-
import type { Field } from "./fields.js";
|
|
40
|
+
import type { Field, ValueObjectDescriptor, ValueObjectFieldDescriptor } from "./fields.js";
|
|
41
41
|
import type { Directive } from "./directive.js";
|
|
42
42
|
import type { DomainModule } from "./codegen_dart.js";
|
|
43
43
|
import type { QueryDecl } from "./query.js";
|
|
@@ -249,6 +249,9 @@ function tsTypeOfField(field: Field): string {
|
|
|
249
249
|
case "ref":
|
|
250
250
|
return "string"; // the related aggregate's id
|
|
251
251
|
case "json":
|
|
252
|
+
// A NAMED VALUE OBJECT field reads as its generated interface (`value_object.ts`);
|
|
253
|
+
// a bare `t.jsonObject()` stays the anonymous object; a plain `t.json()` is unknown.
|
|
254
|
+
if (field.valueObject !== undefined) return field.valueObject.name;
|
|
252
255
|
return field.jsonShape === "object" ? "Record<string, unknown>" : "unknown";
|
|
253
256
|
case "set":
|
|
254
257
|
return "string[]"; // `t.set(t.string())` is the one set shape (fields.ts)
|
|
@@ -264,6 +267,56 @@ function tsTypeOfField(field: Field): string {
|
|
|
264
267
|
}
|
|
265
268
|
}
|
|
266
269
|
|
|
270
|
+
// ── value objects → TS interfaces (`value_object.ts` descriptors) ─────────────────
|
|
271
|
+
|
|
272
|
+
/** The coordinates TS type per RFC 7946 geometry kind. */
|
|
273
|
+
const geometryCoordsTs: Record<string, string> = {
|
|
274
|
+
Point: "number[]",
|
|
275
|
+
LineString: "number[][]",
|
|
276
|
+
Polygon: "number[][][]",
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/** One descriptor field → its TS type text. NO FALLBACK (the descriptor set is closed). */
|
|
280
|
+
function tsTypeOfVoField(f: ValueObjectFieldDescriptor): string {
|
|
281
|
+
switch (f.type) {
|
|
282
|
+
case "string":
|
|
283
|
+
return "string";
|
|
284
|
+
case "number":
|
|
285
|
+
return "number";
|
|
286
|
+
case "boolean":
|
|
287
|
+
return "boolean";
|
|
288
|
+
case "enum":
|
|
289
|
+
return union(f.values.map((v) => JSON.stringify(v)));
|
|
290
|
+
case "array":
|
|
291
|
+
return `${paren(tsTypeOfVoField(f.items))}[]`;
|
|
292
|
+
case "object":
|
|
293
|
+
case "geometry":
|
|
294
|
+
return f.ref; // the named VO interface (emitted alongside)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Emit one `export interface <VO> { ... }` from its canonical descriptor. A geo VO
|
|
299
|
+
* emits the fixed RFC 7946 shape (`{ type: "<kind>"; coordinates: ... }`). */
|
|
300
|
+
function emitValueObjectTs(d: ValueObjectDescriptor): string[] {
|
|
301
|
+
if (d.geometry !== undefined) {
|
|
302
|
+
return [
|
|
303
|
+
`/** RFC 7946 ${d.geometry} — a \`geo.${d.geometry === "Point" ? "point" : d.geometry === "LineString" ? "lineString" : "polygon"}(...)\` value object. */`,
|
|
304
|
+
`export interface ${d.name} { type: ${JSON.stringify(d.geometry)}; coordinates: ${geometryCoordsTs[d.geometry]}; }`,
|
|
305
|
+
``,
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
const lines = Object.entries(d.fields ?? {}).map(
|
|
309
|
+
([name, f]) => ` ${name}${f.optional === true ? "?" : ""}: ${tsTypeOfVoField(f)};`,
|
|
310
|
+
);
|
|
311
|
+
return [
|
|
312
|
+
`/** Value object \`${d.name}\` — ONE json-object value leaf, replaced WHOLESALE on write (no per-subfield merge). */`,
|
|
313
|
+
`export interface ${d.name} {`,
|
|
314
|
+
...lines,
|
|
315
|
+
`}`,
|
|
316
|
+
``,
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
|
|
267
320
|
/** A loose TS type for a derived/combined field's declared zod `returns`. */
|
|
268
321
|
function tsTypeOfReturns(returns: unknown): string {
|
|
269
322
|
try {
|
|
@@ -420,6 +473,26 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
420
473
|
);
|
|
421
474
|
}
|
|
422
475
|
}
|
|
476
|
+
// ── value objects: one interface per named VO, deduped across the package's domains ──
|
|
477
|
+
const voByName = new Map<string, ValueObjectDescriptor>();
|
|
478
|
+
for (const mod of modules) {
|
|
479
|
+
for (const d of (mod as { valueObjects?: readonly ValueObjectDescriptor[] }).valueObjects ?? []) {
|
|
480
|
+
const existing = voByName.get(d.name);
|
|
481
|
+
if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(d)) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`codegen-ts: value object '${d.name}' has DIVERGENT shapes across domains — one name, one shape.`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
voByName.set(d.name, d);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (voByName.size > 0) {
|
|
490
|
+
out.push(`// ── value objects (named, composable json-object leaves — value_object.ts) ──`, ``);
|
|
491
|
+
for (const [, d] of [...voByName].sort(([a], [b]) => a.localeCompare(b))) {
|
|
492
|
+
out.push(...emitValueObjectTs(d));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
423
496
|
out.push(`// ── read models (projection \`data\` shapes; every field is partial-fold optional) ──`, ``);
|
|
424
497
|
for (const [typeId, agg] of [...aggByType].sort(([a], [b]) => a.localeCompare(b))) {
|
|
425
498
|
out.push(`export interface ${typeId}Data {`);
|
|
@@ -422,8 +422,25 @@ function emitDart(
|
|
|
422
422
|
const frameworkAggIds: ReadonlySet<string> = new Set(frameworkAggById.keys());
|
|
423
423
|
const frameworkDirIds: ReadonlySet<string> = new Set(frameworkDirById.keys());
|
|
424
424
|
const frameworkQueryIds: ReadonlySet<string> = new Set(frameworkQueryById.keys());
|
|
425
|
+
// VALUE OBJECTS shared by ≥2 domains: their generated Dart class emits ONCE in the shared
|
|
426
|
+
// `nomos_framework.dart` (the same collision rule as framework symbols — a barrel re-exporting
|
|
427
|
+
// two domains must not define one VO class twice). A single-domain VO stays in its domain file.
|
|
428
|
+
const voDomainsByName = new Map<string, number>();
|
|
429
|
+
const voDescriptorByName = new Map<string, NonNullable<DomainModule["valueObjects"]>[number]>();
|
|
430
|
+
for (const mod of modules) {
|
|
431
|
+
for (const d of mod.valueObjects ?? []) {
|
|
432
|
+
voDomainsByName.set(d.name, (voDomainsByName.get(d.name) ?? 0) + 1);
|
|
433
|
+
voDescriptorByName.set(d.name, d);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const sharedVoNames: ReadonlySet<string> = new Set(
|
|
437
|
+
[...voDomainsByName.entries()].filter(([, n]) => n > 1).map(([name]) => name),
|
|
438
|
+
);
|
|
439
|
+
const sharedVos = [...sharedVoNames]
|
|
440
|
+
.sort()
|
|
441
|
+
.map((name) => voDescriptorByName.get(name)!);
|
|
425
442
|
const hasFrameworkSymbols =
|
|
426
|
-
frameworkAggs.length > 0 || frameworkDirs.length > 0 || frameworkQueries.length > 0;
|
|
443
|
+
frameworkAggs.length > 0 || frameworkDirs.length > 0 || frameworkQueries.length > 0 || sharedVos.length > 0;
|
|
427
444
|
const FRAMEWORK_FILE = "nomos_framework";
|
|
428
445
|
const frameworkImportUri = hasFrameworkSymbols ? `${FRAMEWORK_FILE}.dart` : undefined;
|
|
429
446
|
|
|
@@ -455,6 +472,8 @@ function emitDart(
|
|
|
455
472
|
directives: frameworkDirs,
|
|
456
473
|
readModelAggregates: frameworkAggs.map((a) => a.id),
|
|
457
474
|
...(frameworkQueries.length > 0 ? { queries: frameworkQueries } : {}),
|
|
475
|
+
// Package-SHARED value objects (used by ≥2 domains) — their class emits here, once.
|
|
476
|
+
...(sharedVos.length > 0 ? { valueObjects: sharedVos } : {}),
|
|
458
477
|
};
|
|
459
478
|
writeGenerated(FRAMEWORK_FILE, generateDartDomain(frameworkMod));
|
|
460
479
|
}
|
|
@@ -480,12 +499,17 @@ function emitDart(
|
|
|
480
499
|
const domainFwQueryIds = new Set(
|
|
481
500
|
(modForDart.queries ?? []).filter((q) => frameworkQueryIds.has(q.id)).map((q) => q.id),
|
|
482
501
|
);
|
|
483
|
-
const
|
|
502
|
+
const domainSharedVoNames = new Set(
|
|
503
|
+
(modForDart.valueObjects ?? []).filter((d) => sharedVoNames.has(d.name)).map((d) => d.name),
|
|
504
|
+
);
|
|
505
|
+
const foldsFramework =
|
|
506
|
+
domainFwAggIds.size > 0 || domainFwDirIds.size > 0 || domainFwQueryIds.size > 0 || domainSharedVoNames.size > 0;
|
|
484
507
|
const domainFrameworkImport = foldsFramework ? frameworkImportUri : undefined;
|
|
485
508
|
const src = generateDartDomain(modForDart, {
|
|
486
509
|
skipAggregateIds: domainFwAggIds,
|
|
487
510
|
skipDirectiveIds: domainFwDirIds,
|
|
488
511
|
skipQueryIds: domainFwQueryIds,
|
|
512
|
+
skipValueObjectNames: domainSharedVoNames,
|
|
489
513
|
...(domainFrameworkImport !== undefined ? { frameworkImportUri: domainFrameworkImport } : {}),
|
|
490
514
|
});
|
|
491
515
|
writeGenerated(mod.name, src);
|