Skip to content

Commit ff4253a

Browse files
dmealingclaude
andcommitted
feat(codegen-ts): neutral structural template data-dict builder
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky
1 parent 29ce342 commit ff4253a

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// The NEUTRAL, structural codegen template data dict (SP-1 §3.2). Distinct from
2+
// the Markdown-flavored EntityDocData — this carries raw structural facts only,
3+
// so a consumer's Mustache template can emit any language's code from it. The
4+
// field names here are a byte-gated cross-port contract; change them only via the
5+
// spec (and the conformance corpus).
6+
import type { MetaObject, MetaRoot, MetaField } from "@metaobjectsdev/metadata";
7+
import { FIELD_ATTR_VALUES, FIELD_SUBTYPE_ENUM } from "@metaobjectsdev/metadata";
8+
9+
export interface FieldTemplateData {
10+
name: string;
11+
/** Neutral field subtype, e.g. "string" | "int" | "currency" | "enum".
12+
* Arrayness is carried by `isArray`, NOT appended here. */
13+
type: string;
14+
required: boolean;
15+
isArray: boolean;
16+
maxLength?: number;
17+
enumValues?: string[];
18+
}
19+
export interface IdentityTemplateData { kind: string; fields: string[]; }
20+
export interface RelationshipTemplateData { name: string; cardinality: string; targetRef: string; }
21+
export interface EntityTemplateData {
22+
name: string;
23+
package: string;
24+
fields: FieldTemplateData[];
25+
identities: IdentityTemplateData[];
26+
relationships: RelationshipTemplateData[];
27+
}
28+
export interface PackageTemplateData { package: string; entities: EntityTemplateData[]; }
29+
export interface ModelTemplateData { packages: PackageTemplateData[]; }
30+
31+
function fieldData(field: MetaField): FieldTemplateData {
32+
const d: FieldTemplateData = {
33+
name: field.name,
34+
type: field.subType,
35+
required: field.isRequired,
36+
isArray: field.isArray === true,
37+
};
38+
if (typeof field.maxLength === "number") d.maxLength = field.maxLength;
39+
if (field.subType === FIELD_SUBTYPE_ENUM) {
40+
const vals = field.attr(FIELD_ATTR_VALUES);
41+
if (Array.isArray(vals)) d.enumValues = vals.map((v) => String(v));
42+
}
43+
return d;
44+
}
45+
46+
export function buildEntityTemplateData(entity: MetaObject): EntityTemplateData {
47+
return {
48+
name: entity.name,
49+
package: entity.package ?? "",
50+
fields: entity.fields().map(fieldData),
51+
identities: entity.identities().map((i) => ({ kind: i.subType, fields: [...i.fields] })),
52+
relationships: entity.relationships().map((r) => ({
53+
name: r.name,
54+
cardinality: r.cardinality ?? "",
55+
targetRef: r.objectRef ?? "",
56+
})),
57+
};
58+
}
59+
60+
export function buildPackageTemplateData(pkg: string, entities: MetaObject[]): PackageTemplateData {
61+
return { package: pkg, entities: entities.map(buildEntityTemplateData) };
62+
}
63+
64+
/** Groups concrete (non-abstract) objects by package — packages ascending,
65+
* entities in `root.objects()` order. Abstract objects never emit instance
66+
* artifacts, so they are excluded. */
67+
export function buildModelTemplateData(root: MetaRoot): ModelTemplateData {
68+
const concrete = root.objects().filter((o) => o.isAbstract !== true);
69+
const byPkg = new Map<string, MetaObject[]>();
70+
for (const o of concrete) {
71+
const pkg = o.package ?? "";
72+
let bucket = byPkg.get(pkg);
73+
if (bucket === undefined) { bucket = []; byPkg.set(pkg, bucket); }
74+
bucket.push(o);
75+
}
76+
return {
77+
packages: [...byPkg.keys()].sort().map((pkg) => buildPackageTemplateData(pkg, byPkg.get(pkg)!)),
78+
};
79+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, test, expect } from "bun:test";
2+
import { buildEntityTemplateData, buildModelTemplateData } from "../../src/template-codegen/template-data.js";
3+
import { MetaDataLoader } from "@metaobjectsdev/metadata";
4+
import { FileSource } from "@metaobjectsdev/metadata/core";
5+
import { resolve } from "node:path";
6+
7+
async function load(path: string) {
8+
const loader = new MetaDataLoader();
9+
const res = await loader.load([new FileSource(path)]);
10+
expect(res.errors).toEqual([]);
11+
return res.root;
12+
}
13+
14+
describe("buildEntityTemplateData", () => {
15+
test("emits neutral structural fields (subtype, required, isArray)", async () => {
16+
const root = await load(resolve(import.meta.dir, "../fixtures/single-entity.json"));
17+
const entity = root.objects()[0]!;
18+
const data = buildEntityTemplateData(entity);
19+
expect(data.name).toBe(entity.name);
20+
expect(data.package).toBe(entity.package ?? "");
21+
expect(Array.isArray(data.fields)).toBe(true);
22+
for (const f of data.fields) {
23+
expect(typeof f.type).toBe("string");
24+
expect(f.type).not.toContain("[]"); // isArray carries arrayness
25+
expect(typeof f.required).toBe("boolean");
26+
expect(typeof f.isArray).toBe("boolean");
27+
}
28+
// the `title` field carries @required + @maxLength=200 in the fixture
29+
const title = data.fields.find((f) => f.name === "title");
30+
expect(title?.required).toBe(true);
31+
expect(title?.maxLength).toBe(200);
32+
for (const id of data.identities) {
33+
expect(typeof id.kind).toBe("string");
34+
expect(Array.isArray(id.fields)).toBe(true);
35+
}
36+
});
37+
});
38+
39+
describe("buildModelTemplateData", () => {
40+
test("groups by package ascending; abstracts excluded", async () => {
41+
const root = await load(resolve(import.meta.dir, "../fixtures/single-entity.json"));
42+
const model = buildModelTemplateData(root);
43+
const pkgs = model.packages.map((p) => p.package);
44+
expect(pkgs).toEqual([...pkgs].sort());
45+
const names = model.packages.flatMap((p) => p.entities.map((e) => e.name)).sort();
46+
const concrete = root.objects().filter((o) => o.isAbstract !== true).map((o) => o.name).sort();
47+
expect(names).toEqual(concrete);
48+
});
49+
});

0 commit comments

Comments
 (0)