@metaobjectsdev/codegen-ts 0.8.1 → 0.9.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +123 -46
- package/dist/column-mapper.js.map +1 -1
- package/dist/generators/callable-file.d.ts +8 -0
- package/dist/generators/callable-file.d.ts.map +1 -0
- package/dist/generators/callable-file.js +32 -0
- package/dist/generators/callable-file.js.map +1 -0
- package/dist/generators/docs-data-builder.d.ts.map +1 -1
- package/dist/generators/docs-data-builder.js +5 -2
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/extractor-file.d.ts +9 -0
- package/dist/generators/extractor-file.d.ts.map +1 -0
- package/dist/generators/extractor-file.js +45 -0
- package/dist/generators/extractor-file.js.map +1 -0
- package/dist/generators/index.d.ts +3 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +3 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/render-helper-file.d.ts +9 -0
- package/dist/generators/render-helper-file.d.ts.map +1 -0
- package/dist/generators/render-helper-file.js +58 -0
- package/dist/generators/render-helper-file.js.map +1 -0
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +42 -8
- package/dist/payload-codegen.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +11 -3
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/render-engine/framework-provider.d.ts +6 -5
- package/dist/render-engine/framework-provider.d.ts.map +1 -1
- package/dist/render-engine/framework-provider.js +53 -11
- package/dist/render-engine/framework-provider.js.map +1 -1
- package/dist/templates/callable-file.d.ts +8 -0
- package/dist/templates/callable-file.d.ts.map +1 -0
- package/dist/templates/callable-file.js +98 -0
- package/dist/templates/callable-file.js.map +1 -0
- package/dist/templates/extract-delegate-emitter.d.ts +42 -0
- package/dist/templates/extract-delegate-emitter.d.ts.map +1 -0
- package/dist/templates/extract-delegate-emitter.js +339 -0
- package/dist/templates/extract-delegate-emitter.js.map +1 -0
- package/dist/templates/{recover-schema-emitter.d.ts → extract-schema-emitter.d.ts} +2 -2
- package/dist/templates/extract-schema-emitter.d.ts.map +1 -0
- package/dist/templates/{recover-schema-emitter.js → extract-schema-emitter.js} +37 -20
- package/dist/templates/extract-schema-emitter.js.map +1 -0
- package/dist/templates/extractor.d.ts +9 -0
- package/dist/templates/extractor.d.ts.map +1 -0
- package/dist/templates/extractor.js +296 -0
- package/dist/templates/extractor.js.map +1 -0
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +2 -1
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/filter-type.d.ts.map +1 -1
- package/dist/templates/filter-type.js +8 -5
- package/dist/templates/filter-type.js.map +1 -1
- package/dist/templates/fr010-field-mapping.d.ts +22 -6
- package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
- package/dist/templates/fr010-field-mapping.js +66 -21
- package/dist/templates/fr010-field-mapping.js.map +1 -1
- package/dist/templates/inferred-types.d.ts +15 -1
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +30 -17
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +98 -34
- package/dist/templates/output-parser.js.map +1 -1
- package/dist/templates/output-prompt.js +2 -2
- package/dist/templates/render-helper.d.ts +14 -0
- package/dist/templates/render-helper.d.ts.map +1 -0
- package/dist/templates/render-helper.js +180 -0
- package/dist/templates/render-helper.js.map +1 -0
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +59 -3
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +10 -4
- package/src/column-mapper.ts +128 -45
- package/src/generators/callable-file.ts +44 -0
- package/src/generators/docs-data-builder.ts +5 -1
- package/src/generators/extractor-file.ts +57 -0
- package/src/generators/index.ts +3 -0
- package/src/generators/render-helper-file.ts +74 -0
- package/src/payload-codegen.ts +52 -7
- package/src/projection/extract-view-spec.ts +11 -3
- package/src/render-engine/framework-provider.ts +53 -16
- package/src/templates/callable-file.ts +122 -0
- package/src/templates/extract-delegate-emitter.ts +370 -0
- package/src/templates/{recover-schema-emitter.ts → extract-schema-emitter.ts} +39 -19
- package/src/templates/extractor.ts +333 -0
- package/src/templates/field-meta.ts +2 -0
- package/src/templates/filter-type.ts +7 -5
- package/src/templates/fr010-field-mapping.ts +71 -18
- package/src/templates/inferred-types.ts +32 -18
- package/src/templates/output-parser.ts +108 -35
- package/src/templates/output-prompt.ts +2 -2
- package/src/templates/render-helper.ts +244 -0
- package/src/templates/zod-validators.ts +51 -4
- package/dist/templates/recover-schema-emitter.d.ts.map +0 -1
- package/dist/templates/recover-schema-emitter.js.map +0 -1
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// server/typescript/packages/codegen-ts/src/templates/extract-delegate-emitter.ts
|
|
2
|
+
//
|
|
3
|
+
// FR-010 Plan 2.1 (nested codegen gap) — the runtime-DELEGATING extract emitter.
|
|
4
|
+
//
|
|
5
|
+
// The self-contained extract<Name>(text) path (extract-schema-emitter + the baked
|
|
6
|
+
// ExtractSchema) covers scalars / enums / scalar-arrays but leaves nested-object and
|
|
7
|
+
// array-of-object components NULL — the historical FR-010 codegen gap. This module emits
|
|
8
|
+
// the additive delegating overload that CLOSES that gap by wrapping the runtime extract:
|
|
9
|
+
//
|
|
10
|
+
// extract<Name>(root: MetaRoot, text, opts?) -> ExtractionResult<<Name>Extracted>
|
|
11
|
+
//
|
|
12
|
+
// It resolves this payload's MetaObject by its baked simple name from the supplied MetaRoot,
|
|
13
|
+
// delegates to extractObject() in @metaobjectsdev/runtime-ts (which assembles the FULL nested
|
|
14
|
+
// object graph reflection-free via the Phase A object model — MetaObject.newInstance() + the
|
|
15
|
+
// MetaField SPI), then maps the assembled ValueObject graph into the typed nullable mirror
|
|
16
|
+
// graph via generated from<VO>(...) mapper functions (payload + every nested VO, deduped).
|
|
17
|
+
//
|
|
18
|
+
// This is the codegen-wrapping-runtime pattern (a generated DAO calling the dynamic-metadata
|
|
19
|
+
// runtime), mirroring the Java SpringOutputParserGenerator + Kotlin pilots. The generated
|
|
20
|
+
// mappers read the assembled graph through a tiny readProp() helper that mirrors the MetaField
|
|
21
|
+
// getValue SPI (ValueObject.get(name) else plain-property access), so the emitted code stays
|
|
22
|
+
// self-sufficient and reflection-free.
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
type MetaData,
|
|
26
|
+
TYPE_OBJECT,
|
|
27
|
+
TYPE_FIELD,
|
|
28
|
+
FIELD_SUBTYPE_OBJECT,
|
|
29
|
+
FIELD_SUBTYPE_ENUM,
|
|
30
|
+
FIELD_ATTR_OBJECT_REF,
|
|
31
|
+
PACKAGE_SEPARATOR,
|
|
32
|
+
} from "@metaobjectsdev/metadata";
|
|
33
|
+
import { fields, isArray, scalarKind, jsonStringLiteral } from "./fr010-field-mapping.js";
|
|
34
|
+
|
|
35
|
+
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
36
|
+
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The @objectRef target VO for a nested-object field, or undefined when unresolvable. */
|
|
40
|
+
function refVo(field: MetaData, root: MetaData): MetaData | undefined {
|
|
41
|
+
const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
42
|
+
if (typeof ref !== "string") return undefined;
|
|
43
|
+
const direct = findObject(root, ref);
|
|
44
|
+
if (direct !== undefined) return direct;
|
|
45
|
+
const sep = ref.lastIndexOf(PACKAGE_SEPARATOR);
|
|
46
|
+
if (sep >= 0) return findObject(root, ref.slice(sep + PACKAGE_SEPARATOR.length));
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True iff the field is a nested object reference (field.object — distinct from the
|
|
51
|
+
* string-backed field.enum, which is treated as a scalar). */
|
|
52
|
+
function isObjectField(field: MetaData): boolean {
|
|
53
|
+
return field.subType === FIELD_SUBTYPE_OBJECT;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The extracted-mirror interface name for a value-object (`<Name>Extracted`). */
|
|
57
|
+
export function mirrorName(vo: MetaData): string {
|
|
58
|
+
return `${vo.name}Extracted`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The mapper function name for a value-object (`from<Name>Extracted`). */
|
|
62
|
+
function mapperName(vo: MetaData): string {
|
|
63
|
+
return `from${vo.name}Extracted`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// Nested-aware mirror interfaces (payload + every reachable nested VO)
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
/** The nullable mirror TS type for one field — nested-aware (recurses into nested mirror names). */
|
|
71
|
+
function nestedMirrorType(field: MetaData, root: MetaData): string {
|
|
72
|
+
if (isObjectField(field)) {
|
|
73
|
+
const target = refVo(field, root);
|
|
74
|
+
const base = target !== undefined ? mirrorName(target) : "unknown";
|
|
75
|
+
const elem = `${base} | null`;
|
|
76
|
+
return isArray(field) ? `(${elem})[] | null` : elem;
|
|
77
|
+
}
|
|
78
|
+
if (isArray(field)) return "(string | null)[] | null";
|
|
79
|
+
if (field.subType === FIELD_SUBTYPE_ENUM) return "string | null";
|
|
80
|
+
switch (scalarKind(field.subType)) {
|
|
81
|
+
case "INT":
|
|
82
|
+
case "LONG":
|
|
83
|
+
case "DOUBLE":
|
|
84
|
+
return "number | null";
|
|
85
|
+
case "BOOLEAN":
|
|
86
|
+
return "boolean | null";
|
|
87
|
+
default:
|
|
88
|
+
return "string | null";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Emit the nested-aware mirror interface for `vo` and every value-object reachable from it
|
|
94
|
+
* (deduped by simple name; cycle-safe). The payload mirror keeps the canonical `<Payload>Extracted`
|
|
95
|
+
* name (passed in) so the existing self-contained extract<Name>() and the delegating overload
|
|
96
|
+
* share one mirror type. Returns the joined interface declarations in stable (BFS) order.
|
|
97
|
+
*/
|
|
98
|
+
export function nestedMirrorInterfaces(vo: MetaData, root: MetaData, payloadMirror: string): string {
|
|
99
|
+
const out: string[] = [];
|
|
100
|
+
const seen = new Set<string>();
|
|
101
|
+
emitMirror(vo, root, payloadMirror, seen, out);
|
|
102
|
+
return out.join("\n\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function emitMirror(
|
|
106
|
+
vo: MetaData,
|
|
107
|
+
root: MetaData,
|
|
108
|
+
interfaceName: string,
|
|
109
|
+
seen: Set<string>,
|
|
110
|
+
out: string[],
|
|
111
|
+
): void {
|
|
112
|
+
if (seen.has(vo.name)) return;
|
|
113
|
+
seen.add(vo.name);
|
|
114
|
+
|
|
115
|
+
const base = interfaceName.endsWith("Extracted")
|
|
116
|
+
? interfaceName.slice(0, -"Extracted".length)
|
|
117
|
+
: interfaceName;
|
|
118
|
+
const lines: string[] = [];
|
|
119
|
+
lines.push(
|
|
120
|
+
`/** Best-effort extracted twin of \`${base}\` — every field nullable (null where lost/malformed). */`,
|
|
121
|
+
);
|
|
122
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
123
|
+
for (const f of fields(vo)) {
|
|
124
|
+
lines.push(` ${f.name}: ${nestedMirrorType(f, root)};`);
|
|
125
|
+
}
|
|
126
|
+
lines.push("}");
|
|
127
|
+
out.push(lines.join("\n"));
|
|
128
|
+
|
|
129
|
+
// Recurse into nested VOs (post-order via the shared seen set → dedupe + cycle guard).
|
|
130
|
+
for (const f of fields(vo)) {
|
|
131
|
+
if (isObjectField(f)) {
|
|
132
|
+
const target = refVo(f, root);
|
|
133
|
+
if (target !== undefined) emitMirror(target, root, mirrorName(target), seen, out);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// Mapper functions (assembled ValueObject graph -> typed nullable mirror graph)
|
|
140
|
+
// =============================================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Emit one `from<VO>Extracted(o)` mapper per value-object reachable from `vo` (payload + nested,
|
|
144
|
+
* deduped). Each mapper reads the assembled object via readProp() and recurses into nested
|
|
145
|
+
* mappers for object/array-of-object components. Nested mappers use `from<NestedName>Extracted`
|
|
146
|
+
* returning `<NestedName>Extracted`. The ROOT mapper is overridden to the template-derived names
|
|
147
|
+
* (`rootMapperFn` / `rootMirror`) so it matches the canonically-named root mirror interface — the
|
|
148
|
+
* payload VO's own name may differ from the template name.
|
|
149
|
+
*/
|
|
150
|
+
export function nestedMappers(
|
|
151
|
+
vo: MetaData,
|
|
152
|
+
root: MetaData,
|
|
153
|
+
rootMapperFn: string,
|
|
154
|
+
rootMirror: string,
|
|
155
|
+
): string {
|
|
156
|
+
const out: string[] = [];
|
|
157
|
+
const seen = new Set<string>();
|
|
158
|
+
emitMapper(vo, root, seen, out, { fn: rootMapperFn, mirror: rootMirror });
|
|
159
|
+
return out.join("\n\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The root mapper's name + mirror — derived from the template, not the payload VO. */
|
|
163
|
+
export function rootMapperName(template: string): string {
|
|
164
|
+
return `from${template}Extracted`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function emitMapper(
|
|
168
|
+
vo: MetaData,
|
|
169
|
+
root: MetaData,
|
|
170
|
+
seen: Set<string>,
|
|
171
|
+
out: string[],
|
|
172
|
+
override?: { fn: string; mirror: string },
|
|
173
|
+
): void {
|
|
174
|
+
if (seen.has(vo.name)) return;
|
|
175
|
+
seen.add(vo.name);
|
|
176
|
+
|
|
177
|
+
const fn = override?.fn ?? mapperName(vo);
|
|
178
|
+
const mir = override?.mirror ?? mirrorName(vo);
|
|
179
|
+
const assigns = fields(vo).map((f) => ` ${f.name}: ${mapperArg(f, root)},`);
|
|
180
|
+
const body = [
|
|
181
|
+
`/** Map an assembled ValueObject graph into a typed \`${mir}\` mirror. Generated; null-tolerant. */`,
|
|
182
|
+
`function ${fn}(o: unknown): ${mir} | null {`,
|
|
183
|
+
` if (o == null) return null;`,
|
|
184
|
+
` return {`,
|
|
185
|
+
...assigns,
|
|
186
|
+
` };`,
|
|
187
|
+
`}`,
|
|
188
|
+
].join("\n");
|
|
189
|
+
out.push(body);
|
|
190
|
+
|
|
191
|
+
for (const f of fields(vo)) {
|
|
192
|
+
if (isObjectField(f)) {
|
|
193
|
+
const target = refVo(f, root);
|
|
194
|
+
if (target !== undefined) emitMapper(target, root, seen, out);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The mirror-field initializer expression that reads `field` from the assembled object `o`. */
|
|
200
|
+
function mapperArg(field: MetaData, root: MetaData): string {
|
|
201
|
+
const key = jsonStringLiteral(field.name);
|
|
202
|
+
|
|
203
|
+
if (isObjectField(field)) {
|
|
204
|
+
const target = refVo(field, root);
|
|
205
|
+
if (target === undefined) return "null /* unresolved @objectRef */";
|
|
206
|
+
const fn = mapperName(target);
|
|
207
|
+
if (isArray(field)) {
|
|
208
|
+
return `mapObjectList(readProp(o, ${key}), ${fn})`;
|
|
209
|
+
}
|
|
210
|
+
return `${fn}(readProp(o, ${key}))`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Enum / scalar / scalar-array: the runtime already coerced; read + light-coerce to the
|
|
214
|
+
// mirror's nullable shape via the locally-defined dlg* readers. (These are distinct from the
|
|
215
|
+
// render ExtractMap helpers `asString(d, key)` etc., which the self-contained path imports — a
|
|
216
|
+
// local helper must NOT shadow those, so the delegate readers carry the `dlg` prefix.)
|
|
217
|
+
// An enum ARRAY is string-backed PER ELEMENT — it must use the list reader, NOT dlgString
|
|
218
|
+
// (which would String()-collapse the whole array into "A,B"). Check isArray before the enum
|
|
219
|
+
// scalar case. The mirror TYPE for an enum array is already `(string | null)[] | null` (see
|
|
220
|
+
// mirrorFieldType), so the list reader matches.
|
|
221
|
+
if (field.subType === FIELD_SUBTYPE_ENUM && isArray(field)) return `dlgStringList(readProp(o, ${key}))`;
|
|
222
|
+
if (field.subType === FIELD_SUBTYPE_ENUM) return `dlgString(readProp(o, ${key}))`;
|
|
223
|
+
if (isArray(field)) return `dlgStringList(readProp(o, ${key}))`;
|
|
224
|
+
switch (scalarKind(field.subType)) {
|
|
225
|
+
case "INT":
|
|
226
|
+
case "LONG":
|
|
227
|
+
return `dlgInt(readProp(o, ${key}))`;
|
|
228
|
+
case "DOUBLE":
|
|
229
|
+
return `dlgNumber(readProp(o, ${key}))`;
|
|
230
|
+
case "BOOLEAN":
|
|
231
|
+
return `dlgBool(readProp(o, ${key}))`;
|
|
232
|
+
default:
|
|
233
|
+
return `dlgString(readProp(o, ${key}))`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The set of generated-helper names the mappers for `vo` (+ reachable nested VOs) actually
|
|
239
|
+
* reference. Used to emit only the needed helpers (consumer projects may run noUnusedLocals).
|
|
240
|
+
* `readProp` + at least one dlg* reader are always present once any mapper is emitted.
|
|
241
|
+
*/
|
|
242
|
+
export function usedHelpers(vo: MetaData, root: MetaData): Set<string> {
|
|
243
|
+
const used = new Set<string>(["readProp"]);
|
|
244
|
+
const seen = new Set<string>();
|
|
245
|
+
const stack = [vo];
|
|
246
|
+
while (stack.length > 0) {
|
|
247
|
+
const cur = stack.pop()!;
|
|
248
|
+
if (seen.has(cur.name)) continue;
|
|
249
|
+
seen.add(cur.name);
|
|
250
|
+
for (const f of fields(cur)) {
|
|
251
|
+
if (isObjectField(f)) {
|
|
252
|
+
const target = refVo(f, root);
|
|
253
|
+
if (target === undefined) continue;
|
|
254
|
+
stack.push(target);
|
|
255
|
+
if (isArray(f)) used.add("mapObjectList");
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (f.subType === FIELD_SUBTYPE_ENUM && isArray(f)) {
|
|
259
|
+
used.add("dlgStringList");
|
|
260
|
+
} else if (f.subType === FIELD_SUBTYPE_ENUM) {
|
|
261
|
+
used.add("dlgString");
|
|
262
|
+
} else if (isArray(f)) {
|
|
263
|
+
used.add("dlgStringList");
|
|
264
|
+
} else {
|
|
265
|
+
switch (scalarKind(f.subType)) {
|
|
266
|
+
case "INT":
|
|
267
|
+
case "LONG":
|
|
268
|
+
used.add("dlgInt");
|
|
269
|
+
break;
|
|
270
|
+
case "DOUBLE":
|
|
271
|
+
used.add("dlgNumber");
|
|
272
|
+
break;
|
|
273
|
+
case "BOOLEAN":
|
|
274
|
+
used.add("dlgBool");
|
|
275
|
+
break;
|
|
276
|
+
default:
|
|
277
|
+
used.add("dlgString");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return used;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** True iff the payload (or any reachable nested VO) has a nested-object / array-of-object field. */
|
|
286
|
+
export function hasNested(vo: MetaData, root: MetaData): boolean {
|
|
287
|
+
const seen = new Set<string>();
|
|
288
|
+
const stack = [vo];
|
|
289
|
+
while (stack.length > 0) {
|
|
290
|
+
const cur = stack.pop()!;
|
|
291
|
+
if (seen.has(cur.name)) continue;
|
|
292
|
+
seen.add(cur.name);
|
|
293
|
+
for (const f of cur.children().filter((c) => c.type === TYPE_FIELD)) {
|
|
294
|
+
if (isObjectField(f)) {
|
|
295
|
+
const target = refVo(f, root);
|
|
296
|
+
if (target !== undefined) {
|
|
297
|
+
stack.push(target);
|
|
298
|
+
}
|
|
299
|
+
// a nested object field exists regardless of resolvability → still "has nested"
|
|
300
|
+
if (f.subType === FIELD_SUBTYPE_OBJECT) return true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The shared runtime-side helper block the generated mappers rely on:
|
|
309
|
+
* • readProp — null-tolerant read mirroring the MetaField getValue SPI (ValueObject.get(name)
|
|
310
|
+
* else plain-property access); keeps the mappers reflection-free + backing-agnostic.
|
|
311
|
+
* • mapObjectList — map each element of an assembled array via a per-element mapper.
|
|
312
|
+
* • dlg* readers — light null-tolerant coercion to the mirror's nullable scalar shapes. The
|
|
313
|
+
* `dlg` prefix avoids shadowing the render ExtractMap helpers (`asString(d, key)` etc.) that
|
|
314
|
+
* the self-contained extract path imports into the SAME file — a collision would silently
|
|
315
|
+
* rebind those two-arg map readers to these one-arg readers.
|
|
316
|
+
* Emitted once per parser file.
|
|
317
|
+
*/
|
|
318
|
+
export function delegateHelpers(used: Set<string>): string {
|
|
319
|
+
const blocks: string[] = ["// ---- runtime-delegating extract helpers (generated) ----"];
|
|
320
|
+
|
|
321
|
+
// readProp is always needed once a mapper exists.
|
|
322
|
+
blocks.push(`/** Read a property from an assembled backing object, mirroring the MetaField getValue SPI. */
|
|
323
|
+
function readProp(o: unknown, name: string): unknown {
|
|
324
|
+
if (o == null) return undefined;
|
|
325
|
+
const vo = o as { get?: (n: string) => unknown };
|
|
326
|
+
if (typeof vo.get === "function") return vo.get(name);
|
|
327
|
+
return (o as Record<string, unknown>)[name];
|
|
328
|
+
}`);
|
|
329
|
+
|
|
330
|
+
if (used.has("mapObjectList")) {
|
|
331
|
+
blocks.push(`/** Map each element of an assembled array via \`fn\`; null/absent -> null; non-mappable -> filtered. */
|
|
332
|
+
function mapObjectList<T>(v: unknown, fn: (o: unknown) => T | null): (T | null)[] | null {
|
|
333
|
+
if (!Array.isArray(v)) return null;
|
|
334
|
+
return v.map((e) => fn(e));
|
|
335
|
+
}`);
|
|
336
|
+
}
|
|
337
|
+
if (used.has("dlgString")) {
|
|
338
|
+
blocks.push(`function dlgString(v: unknown): string | null {
|
|
339
|
+
return v == null ? null : String(v);
|
|
340
|
+
}`);
|
|
341
|
+
}
|
|
342
|
+
if (used.has("dlgInt")) {
|
|
343
|
+
blocks.push(`function dlgInt(v: unknown): number | null {
|
|
344
|
+
if (v == null) return null;
|
|
345
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
346
|
+
return Number.isFinite(n) ? Math.trunc(n) : null;
|
|
347
|
+
}`);
|
|
348
|
+
}
|
|
349
|
+
if (used.has("dlgNumber")) {
|
|
350
|
+
blocks.push(`function dlgNumber(v: unknown): number | null {
|
|
351
|
+
if (v == null) return null;
|
|
352
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
353
|
+
return Number.isFinite(n) ? n : null;
|
|
354
|
+
}`);
|
|
355
|
+
}
|
|
356
|
+
if (used.has("dlgBool")) {
|
|
357
|
+
blocks.push(`function dlgBool(v: unknown): boolean | null {
|
|
358
|
+
if (v == null) return null;
|
|
359
|
+
if (typeof v === "boolean") return v;
|
|
360
|
+
return String(v).toLowerCase() === "true";
|
|
361
|
+
}`);
|
|
362
|
+
}
|
|
363
|
+
if (used.has("dlgStringList")) {
|
|
364
|
+
blocks.push(`function dlgStringList(v: unknown): (string | null)[] | null {
|
|
365
|
+
if (!Array.isArray(v)) return null;
|
|
366
|
+
return v.map((e) => (e == null ? null : String(e)));
|
|
367
|
+
}`);
|
|
368
|
+
}
|
|
369
|
+
return blocks.join("\n\n");
|
|
370
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
// server/typescript/packages/codegen-ts/src/templates/
|
|
1
|
+
// server/typescript/packages/codegen-ts/src/templates/extract-schema-emitter.ts
|
|
2
2
|
//
|
|
3
|
-
// Turns a payload value-object into TS source fragments for the FR-010
|
|
4
|
-
// • schemaLiteral — a `
|
|
3
|
+
// Turns a payload value-object into TS source fragments for the FR-010 extract codegen:
|
|
4
|
+
// • schemaLiteral — a `extractSchema(Format.JSON, "root", [ … ])` baked descriptor
|
|
5
5
|
// built from FieldSpec factories (scalar / enumField).
|
|
6
|
-
// • mirrorInterface — an all-nullable mirror interface `<Payload>
|
|
7
|
-
// component `T | null`);
|
|
6
|
+
// • mirrorInterface — an all-nullable mirror interface `<Payload>Extracted` (each
|
|
7
|
+
// component `T | null`); extract returns this nullable twin rather
|
|
8
8
|
// than the strict payload (same reasoning as the Java/C#/Kotlin ports).
|
|
9
9
|
// • mirrorInitializer — `{ prop: asString(d, "prop"), … }` building the mirror from the
|
|
10
10
|
// forgiving outcome map `d`.
|
|
11
11
|
//
|
|
12
|
-
// Mirrors the C#
|
|
12
|
+
// Mirrors the C# ExtractSchemaEmitter (adapted to TS syntax + the `| null` nullable mirror).
|
|
13
13
|
// Bounded scope: scalar / enum / scalar-array. Nested object + array-of-enum deferred.
|
|
14
14
|
|
|
15
15
|
import {
|
|
@@ -24,40 +24,60 @@ import {
|
|
|
24
24
|
isArray,
|
|
25
25
|
scalarKind,
|
|
26
26
|
mirrorType,
|
|
27
|
-
|
|
27
|
+
extractMapCall,
|
|
28
28
|
enumValues,
|
|
29
|
+
coerceDefault,
|
|
30
|
+
defaultValue,
|
|
31
|
+
resolveNormalize,
|
|
29
32
|
jsonStringLiteral,
|
|
30
33
|
stringArrayLiteral,
|
|
31
34
|
propertiesMapLiteral,
|
|
32
35
|
} from "./fr010-field-mapping.js";
|
|
36
|
+
import { NORMALIZE_DEFAULT } from "@metaobjectsdev/metadata";
|
|
33
37
|
|
|
34
|
-
/** Emit `
|
|
38
|
+
/** Emit `extractSchema(Format.X, "rootName", [ … ])`. */
|
|
35
39
|
export function schemaLiteral(vo: MetaData, format: string, rootName: string): string {
|
|
36
40
|
const formatEnum = format.toLowerCase() === "xml" ? "Format.XML" : "Format.JSON";
|
|
37
|
-
const specs = fields(vo).map(fieldSpecLiteral);
|
|
38
|
-
return `
|
|
41
|
+
const specs = fields(vo).map((f) => fieldSpecLiteral(f, vo));
|
|
42
|
+
return `extractSchema(${formatEnum}, ${jsonStringLiteral(rootName)}, [${specs.join(", ")}])`;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
function fieldSpecLiteral(field: MetaData): string {
|
|
45
|
+
function fieldSpecLiteral(field: MetaData, owner: MetaData): string {
|
|
42
46
|
const name = jsonStringLiteral(field.name);
|
|
43
47
|
const required = isRequired(field);
|
|
44
48
|
|
|
45
49
|
if (field.subType === FIELD_SUBTYPE_ENUM) {
|
|
46
50
|
const valuesLit = stringArrayLiteral(enumValues(field));
|
|
47
51
|
const aliasLit = propertiesMapLiteral(field.ownAttr(FIELD_ATTR_ENUM_ALIAS));
|
|
52
|
+
// FR-011: extended enumField signature is (name, required, values, aliases,
|
|
53
|
+
// coerceDefault?, normalize="strip", defaultValue?). Resolve the three new args
|
|
54
|
+
// (field → object → "strip" for normalize) and emit only what's needed: keep the
|
|
55
|
+
// back-compat 4-arg form when nothing is set, else emit the positional tail up to
|
|
56
|
+
// the last meaningful arg.
|
|
57
|
+
const cd = coerceDefault(field);
|
|
58
|
+
const dv = defaultValue(field);
|
|
59
|
+
const normalize = resolveNormalize(field, owner);
|
|
48
60
|
// enumField() sets array:false; enum-array is a bounded deferral (parity with Java/C#).
|
|
49
|
-
|
|
61
|
+
if (cd == null && dv == null && normalize === NORMALIZE_DEFAULT) {
|
|
62
|
+
return `enumField(${name}, ${required}, ${valuesLit}, ${aliasLit})`;
|
|
63
|
+
}
|
|
64
|
+
const cdLit = cd == null ? "null" : jsonStringLiteral(cd);
|
|
65
|
+
const normLit = jsonStringLiteral(normalize);
|
|
66
|
+
if (dv == null) {
|
|
67
|
+
return `enumField(${name}, ${required}, ${valuesLit}, ${aliasLit}, ${cdLit}, ${normLit})`;
|
|
68
|
+
}
|
|
69
|
+
return `enumField(${name}, ${required}, ${valuesLit}, ${aliasLit}, ${cdLit}, ${normLit}, ${jsonStringLiteral(dv)})`;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
53
|
-
// FR-010: nested
|
|
54
|
-
return `scalar(${name}, FieldKind.STRING, ${required}) /* FR-010: nested
|
|
73
|
+
// FR-010: nested extract deferred — treat as an opaque required/optional string slot.
|
|
74
|
+
return `scalar(${name}, FieldKind.STRING, ${required}) /* FR-010: nested extract deferred */`;
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
const kind = scalarKind(field.subType) ?? "STRING";
|
|
58
78
|
// Scalar-array: the scalar() factory only builds singular specs (array:false), so emit a
|
|
59
79
|
// FieldSpec object literal with array:true. Tier-2 win over the Roslyn proof: the emitted
|
|
60
|
-
//
|
|
80
|
+
// extract() actually populates the array at runtime (ExtractMap.asStringList).
|
|
61
81
|
if (isArray(field)) {
|
|
62
82
|
return (
|
|
63
83
|
`{ name: ${name}, kind: FieldKind.${kind}, required: ${required}, array: true, ` +
|
|
@@ -69,12 +89,12 @@ function fieldSpecLiteral(field: MetaData): string {
|
|
|
69
89
|
|
|
70
90
|
/** Emit the all-nullable mirror interface declaration. */
|
|
71
91
|
export function mirrorInterface(vo: MetaData, interfaceName: string): string {
|
|
72
|
-
const base = interfaceName.endsWith("
|
|
73
|
-
? interfaceName.slice(0, -"
|
|
92
|
+
const base = interfaceName.endsWith("Extracted")
|
|
93
|
+
? interfaceName.slice(0, -"Extracted".length)
|
|
74
94
|
: interfaceName;
|
|
75
95
|
const lines: string[] = [];
|
|
76
96
|
lines.push(
|
|
77
|
-
`/** Best-effort
|
|
97
|
+
`/** Best-effort extracted twin of \`${base}\` — every field nullable (null where lost/malformed). */`,
|
|
78
98
|
);
|
|
79
99
|
lines.push(`export interface ${interfaceName} {`);
|
|
80
100
|
for (const f of fields(vo)) {
|
|
@@ -86,6 +106,6 @@ export function mirrorInterface(vo: MetaData, interfaceName: string): string {
|
|
|
86
106
|
|
|
87
107
|
/** Emit `{ prop: asString(d, "prop"), … }` building the mirror from the forgiving map `d`. */
|
|
88
108
|
export function mirrorInitializer(vo: MetaData): string {
|
|
89
|
-
const assigns = fields(vo).map((f) => `${f.name}: ${
|
|
109
|
+
const assigns = fields(vo).map((f) => `${f.name}: ${extractMapCall(f)}`);
|
|
90
110
|
return `{ ${assigns.join(", ")} }`;
|
|
91
111
|
}
|