zod-to-x 2.2.1-dev.0 → 2.4.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 +32 -8
- package/dist/core/ast_node.d.ts +1 -1
- package/dist/core/ast_node.js +47 -34
- package/dist/layered-modeling/layer.js +3 -1
- package/dist/layered-modeling/model.d.ts +2 -0
- package/dist/layered-modeling/model.js +1 -1
- package/dist/lib/zod_helpers.js +8 -0
- package/dist/transpilers/cpp/runner.d.ts +15 -2
- package/dist/transpilers/cpp/runner.js +45 -8
- package/dist/transpilers/dart/libs.d.ts +6 -0
- package/dist/transpilers/dart/libs.js +11 -0
- package/dist/transpilers/dart/options.d.ts +21 -0
- package/dist/transpilers/dart/options.js +10 -0
- package/dist/transpilers/dart/runner.d.ts +149 -0
- package/dist/transpilers/dart/runner.js +624 -0
- package/dist/transpilers/go/libs.d.ts +8 -0
- package/dist/transpilers/go/libs.js +13 -0
- package/dist/transpilers/go/options.d.ts +17 -0
- package/dist/transpilers/go/options.js +11 -0
- package/dist/transpilers/go/runner.d.ts +100 -0
- package/dist/transpilers/go/runner.js +399 -0
- package/dist/transpilers/index.d.ts +2 -0
- package/dist/transpilers/index.js +5 -1
- package/dist/transpilers/python/runner.d.ts +7 -0
- package/dist/transpilers/python/runner.js +40 -3
- package/dist/transpilers/typescript/runner.d.ts +6 -0
- package/dist/transpilers/typescript/runner.js +22 -8
- package/package.json +7 -4
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Zod2Dart = void 0;
|
|
7
|
+
const case_1 = __importDefault(require("case"));
|
|
8
|
+
const core_1 = require("../../core");
|
|
9
|
+
const options_1 = require("./options");
|
|
10
|
+
const libs_1 = require("./libs");
|
|
11
|
+
/**
|
|
12
|
+
* Transpiler for Zod schemas to Dart classes using package:json_annotation.
|
|
13
|
+
*
|
|
14
|
+
* The `.g.dart` part file is NOT generated by this transpiler — it must be produced
|
|
15
|
+
* externally via `dart run build_runner build`.
|
|
16
|
+
*/
|
|
17
|
+
class Zod2Dart extends core_1.Zod2X {
|
|
18
|
+
constructor(opt = {}) {
|
|
19
|
+
super(Object.assign(Object.assign({}, options_1.defaultOpts), opt));
|
|
20
|
+
this.commentKey = "//";
|
|
21
|
+
/**
|
|
22
|
+
* Maps a local struct/class name to the discriminated union names it participates in.
|
|
23
|
+
* Populated by a pre-scan of the AST before the normal transpile pass.
|
|
24
|
+
*/
|
|
25
|
+
this._unionMembers = new Map();
|
|
26
|
+
/**
|
|
27
|
+
* Names of locally-defined discriminated union abstract classes.
|
|
28
|
+
* Used to emit @JsonKey(fromJson/toJson) when such a type appears as a field.
|
|
29
|
+
*/
|
|
30
|
+
this._discriminatedUnionTypes = new Set();
|
|
31
|
+
/**
|
|
32
|
+
* Names of typedef-emitted types (alias, union typedef, struct extends).
|
|
33
|
+
* When these appear as field types in @JsonSerializable classes, we emit
|
|
34
|
+
* @JsonKey(fromJson: TypeName.fromJson) to prevent json_serializable from
|
|
35
|
+
* following the typedef chain and referencing transitive imports.
|
|
36
|
+
*/
|
|
37
|
+
this._typedefNames = new Set();
|
|
38
|
+
/**
|
|
39
|
+
* Maps a disc union typedef name (e.g. "NewDiscriminatedUnionItem") to the
|
|
40
|
+
* fromJson function name of the underlying entity-level union
|
|
41
|
+
* (e.g. "discriminatedUnionItemFromJson").
|
|
42
|
+
* Populated during pre-scan when an ASTUnion node has aliasOf set.
|
|
43
|
+
*/
|
|
44
|
+
this._discriminatedUnionDelegateFromJson = new Map();
|
|
45
|
+
/**
|
|
46
|
+
* Unified set of type names needing custom @JsonKey annotations.
|
|
47
|
+
* - Discriminated union types: fromJson = <camelName>FromJson (local function)
|
|
48
|
+
* - Typedef types: fromJson = _<camelName>FromJson (standalone wrapper)
|
|
49
|
+
* - TypedefList: list fromJson/toJson helpers
|
|
50
|
+
* _toJson helpers are emitted in runAfter() for all entries.
|
|
51
|
+
*/
|
|
52
|
+
this._usedCustomFromJsonTypes = new Map();
|
|
53
|
+
/**
|
|
54
|
+
* Maps a type name (e.g. "GenericUserEntity") to its full Dart type with
|
|
55
|
+
* template args (e.g. "GenericUserEntity<SomeDtoResult>"). Used in runAfter()
|
|
56
|
+
* to generate helpers with correct generic return types and factory calls.
|
|
57
|
+
*/
|
|
58
|
+
this._usedCustomFromJsonFullTypes = new Map();
|
|
59
|
+
/**
|
|
60
|
+
* Maps a typedef name to its full extended type (the right-hand side of the
|
|
61
|
+
* typedef). E.g. "AdminUserEntity" → "GenericUserEntity<AdminUserMetadata>".
|
|
62
|
+
* Populated in addExtendedType() when the extended type has generic args.
|
|
63
|
+
* Used in runAfter() to generate 2-arg fromJson calls for generic classes.
|
|
64
|
+
*/
|
|
65
|
+
this._typedefToExtendedType = new Map();
|
|
66
|
+
// ── Primitive type methods ──────────────────────────────────────────────
|
|
67
|
+
this.getStringType = () => "String";
|
|
68
|
+
this.getBooleanType = () => "bool";
|
|
69
|
+
this.getAnyType = () => "dynamic";
|
|
70
|
+
this.getDateType = () => "DateTime";
|
|
71
|
+
/** Ex: Set<T> */
|
|
72
|
+
this.getSetType = (itemType) => `Set<${itemType}>`;
|
|
73
|
+
/** Ex: Map<K, V> */
|
|
74
|
+
this.getMapType = (keyType, valueType) => `Map<${keyType}, ${valueType}>`;
|
|
75
|
+
/** Ex: Map<String, V> (record key is always String) */
|
|
76
|
+
this.getRecordType = (keyType, valueType) => `Map<${keyType}, ${valueType}>`;
|
|
77
|
+
/** Ex: (T1, T2, T3) — Dart 3 positional record type */
|
|
78
|
+
this.getTupleType = (itemsType) => `(${itemsType.join(", ")})`;
|
|
79
|
+
/** Dart has no native union type; use Object */
|
|
80
|
+
this.getUnionType = (_itemsType) => "Object";
|
|
81
|
+
/** Dart has no native intersection type; use Object */
|
|
82
|
+
this.getIntersectionType = (_itemsType) => "Object";
|
|
83
|
+
this.lib = (0, libs_1.getLibs)();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Overrides transpile() to pre-scan discriminated union memberships before the normal pass.
|
|
87
|
+
* This allows transpileStruct() to emit `implements UnionName` on member classes.
|
|
88
|
+
*/
|
|
89
|
+
transpile(transpilerQueue) {
|
|
90
|
+
this._unionMembers.clear();
|
|
91
|
+
this._discriminatedUnionTypes.clear();
|
|
92
|
+
this._typedefNames.clear();
|
|
93
|
+
this._usedCustomFromJsonTypes.clear();
|
|
94
|
+
this._usedCustomFromJsonFullTypes.clear();
|
|
95
|
+
this._discriminatedUnionDelegateFromJson.clear();
|
|
96
|
+
this._typedefToExtendedType.clear();
|
|
97
|
+
transpilerQueue.nodes.forEach((node) => {
|
|
98
|
+
if (node instanceof core_1.ASTUnion && node.discriminantKey) {
|
|
99
|
+
// Track the union name for @JsonKey emission
|
|
100
|
+
this._discriminatedUnionTypes.add(node.name);
|
|
101
|
+
// If this is a typedef alias of another disc union (aliasOf set),
|
|
102
|
+
// record which entity-level fromJson function to delegate to.
|
|
103
|
+
const aliasOf = node.aliasOf;
|
|
104
|
+
if (aliasOf) {
|
|
105
|
+
this._discriminatedUnionDelegateFromJson.set(node.name, `${case_1.default.camel(aliasOf)}FromJson`);
|
|
106
|
+
}
|
|
107
|
+
node.options.forEach((opt) => {
|
|
108
|
+
var _a;
|
|
109
|
+
const name = opt.name;
|
|
110
|
+
// Only track types local to this file (no parentFile = not imported)
|
|
111
|
+
if (name && !opt.parentFile) {
|
|
112
|
+
const existing = (_a = this._unionMembers.get(name)) !== null && _a !== void 0 ? _a : [];
|
|
113
|
+
existing.push(node.name);
|
|
114
|
+
this._unionMembers.set(name, existing);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// Detect cross-file nodes that are generic type instantiations:
|
|
119
|
+
// e.g. AdminUserEntity (parentFile=app, description="GenericUserEntity",
|
|
120
|
+
// templatesTranslation=[{aliasOf:"AdminUserMetadata"}])
|
|
121
|
+
// → store "AdminUserEntity" → "GenericUserEntity<AdminUserMetadata>" in
|
|
122
|
+
// _typedefToExtendedType so runAfter() can generate 2-arg fromJson helpers.
|
|
123
|
+
const n = node;
|
|
124
|
+
if (n.parentFile &&
|
|
125
|
+
typeof n.description === "string" &&
|
|
126
|
+
n.description.length > 0 &&
|
|
127
|
+
Array.isArray(n.templatesTranslation) &&
|
|
128
|
+
n.templatesTranslation.length > 0) {
|
|
129
|
+
const typeArgs = n.templatesTranslation
|
|
130
|
+
.map((t) => t.aliasOf || t.name || "")
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join(", ");
|
|
133
|
+
if (typeArgs && n.name) {
|
|
134
|
+
this._typedefToExtendedType.set(n.name, `${n.description}<${typeArgs}>`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return super.transpile(transpilerQueue);
|
|
139
|
+
}
|
|
140
|
+
runBefore() {
|
|
141
|
+
if (this.opt.useJsonSerialization !== false) {
|
|
142
|
+
this.preImports.add(this.lib.jsonAnnotation);
|
|
143
|
+
if (this.opt.partFile) {
|
|
144
|
+
this.postImports.add(`part '${this.opt.partFile}.g.dart';`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
runAfter() {
|
|
149
|
+
var _a, _b;
|
|
150
|
+
// Emit helper functions for field types that need custom @JsonKey annotations.
|
|
151
|
+
// These helpers prevent json_serializable from following typedef chains and generating
|
|
152
|
+
// code that references transitively-imported types not directly in scope.
|
|
153
|
+
if (this.opt.useJsonSerialization !== false) {
|
|
154
|
+
for (const [typeName, kind] of this._usedCustomFromJsonTypes) {
|
|
155
|
+
const fnBase = case_1.default.camel(typeName);
|
|
156
|
+
// Use full type (including generics) if available, e.g. GenericUserEntity<SomeDtoResult>
|
|
157
|
+
const fullType = (_a = this._usedCustomFromJsonFullTypes.get(typeName)) !== null && _a !== void 0 ? _a : typeName;
|
|
158
|
+
// Extract type argument for generic classes: "Foo<Bar>" → "Bar"
|
|
159
|
+
const genericArgMatch = fullType.match(/^([^<]+)<(.+)>$/);
|
|
160
|
+
const baseTypeName = genericArgMatch ? genericArgMatch[1] : fullType;
|
|
161
|
+
const typeArg = genericArgMatch ? genericArgMatch[2] : undefined;
|
|
162
|
+
const isGeneric = typeArg !== undefined;
|
|
163
|
+
if (kind === "discUnion") {
|
|
164
|
+
// Abstract class: toJson helper only (fromJson is the named union function)
|
|
165
|
+
this.push0(`Map<String, dynamic> _${fnBase}ToJson(${fullType} value) =>`);
|
|
166
|
+
this.push1(`(value as dynamic).toJson() as Map<String, dynamic>;\n`);
|
|
167
|
+
}
|
|
168
|
+
else if (kind === "typedef") {
|
|
169
|
+
// Use full extended type if it has generics (e.g. AdminUserEntity → GenericUserEntity<AdminUserMetadata>)
|
|
170
|
+
const extendedType = (_b = this._usedCustomFromJsonFullTypes.get(typeName)) !== null && _b !== void 0 ? _b : this._typedefToExtendedType.get(typeName);
|
|
171
|
+
const genericArgMatch = extendedType === null || extendedType === void 0 ? void 0 : extendedType.match(/^([^<]+)<(.+)>$/);
|
|
172
|
+
const extBaseType = genericArgMatch ? genericArgMatch[1] : undefined;
|
|
173
|
+
const extTypeArg = genericArgMatch ? genericArgMatch[2] : undefined;
|
|
174
|
+
if (isGeneric) {
|
|
175
|
+
// Direct generic field (e.g. GenericUserEntity<SomeDtoResult>): factory pattern
|
|
176
|
+
this.push0(`${fullType} _${fnBase}FromJson(Object? json) =>`);
|
|
177
|
+
this.push1(`${baseTypeName}.fromJson(json as Map<String, dynamic>, (j) => ${typeArg}.fromJson(j as Map<String, dynamic>));\n`);
|
|
178
|
+
this.push0(`Map<String, dynamic> _${fnBase}ToJson(${fullType} value) =>`);
|
|
179
|
+
this.push1(`value.toJson((v) => v.toJson());\n`);
|
|
180
|
+
}
|
|
181
|
+
else if (extBaseType && extTypeArg) {
|
|
182
|
+
// Typedef of a generic class (e.g. AdminUserEntity = GenericUserEntity<AdminUserMetadata>)
|
|
183
|
+
this.push0(`${typeName} _${fnBase}FromJson(Object? json) =>`);
|
|
184
|
+
this.push1(`${extBaseType}.fromJson(json as Map<String, dynamic>, (j) => ${extTypeArg}.fromJson(j as Map<String, dynamic>));\n`);
|
|
185
|
+
this.push0(`Map<String, dynamic> _${fnBase}ToJson(${typeName} value) =>`);
|
|
186
|
+
this.push1(`value.toJson((v) => v.toJson());\n`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// Non-generic class: simple fromJson + toJson helpers
|
|
190
|
+
this.push0(`${typeName} _${fnBase}FromJson(Object? json) =>`);
|
|
191
|
+
this.push1(`${typeName}.fromJson(json as Map<String, dynamic>);\n`);
|
|
192
|
+
this.push0(`Map<String, dynamic> _${fnBase}ToJson(${typeName} value) =>`);
|
|
193
|
+
this.push1(`(value as dynamic).toJson() as Map<String, dynamic>;\n`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (kind === "typedefList") {
|
|
197
|
+
// List<typedef>: list fromJson + toJson helpers
|
|
198
|
+
this.push0(`List<${typeName}> _${fnBase}ListFromJson(Object? json) =>`);
|
|
199
|
+
this.push1(`(json as List<dynamic>).map((e) => ${typeName}.fromJson(e as Map<String, dynamic>)).toList();\n`);
|
|
200
|
+
this.push0(`List<Map<String, dynamic>> _${fnBase}ListToJson(List<${typeName}> list) =>`);
|
|
201
|
+
this.push1(`list.map((e) => (e as dynamic).toJson() as Map<String, dynamic>).toList();\n`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
this._usedCustomFromJsonTypes.clear();
|
|
206
|
+
this._usedCustomFromJsonFullTypes.clear();
|
|
207
|
+
}
|
|
208
|
+
// ── Import / namespace ──────────────────────────────────────────────────
|
|
209
|
+
addImportFromFile(filename, _namespace) {
|
|
210
|
+
const base = filename.endsWith(".dart") ? filename.slice(0, -5) : filename;
|
|
211
|
+
return `import './${base}.dart';`;
|
|
212
|
+
}
|
|
213
|
+
getTypeFromExternalNamespace(_namespace, typeName) {
|
|
214
|
+
return typeName;
|
|
215
|
+
}
|
|
216
|
+
// ── Generics ────────────────────────────────────────────────────────────
|
|
217
|
+
getGenericTemplatesTranslation(data) {
|
|
218
|
+
if ((data instanceof core_1.ASTObject || data instanceof core_1.ASTDefinition) &&
|
|
219
|
+
data.templatesTranslation.length > 0) {
|
|
220
|
+
return ("<" +
|
|
221
|
+
data.templatesTranslation
|
|
222
|
+
.map((t) => {
|
|
223
|
+
if (this.isExternalTypeImport(t)) {
|
|
224
|
+
this.addExternalTypeImport(t);
|
|
225
|
+
return this.getTypeFromExternalNamespace(t.parentNamespace, t.aliasOf);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
return t.aliasOf;
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
.join(", ") +
|
|
232
|
+
">");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// ── Layered modeling ────────────────────────────────────────────────────
|
|
236
|
+
/**
|
|
237
|
+
* Emits an alias/extension declaration for cross-layer references.
|
|
238
|
+
*/
|
|
239
|
+
checkExtendedTypeInclusion(data, type) {
|
|
240
|
+
const isStruct = data instanceof core_1.ASTObject ||
|
|
241
|
+
(data instanceof core_1.ASTIntersection && data.newObject !== undefined);
|
|
242
|
+
const translatedTemplates = this.getGenericTemplatesTranslation(data);
|
|
243
|
+
const templates = translatedTemplates || undefined;
|
|
244
|
+
const declaredTemplates = !translatedTemplates && data instanceof core_1.ASTObject && data.templates.size > 0
|
|
245
|
+
? `<${[...data.templates].join(", ")}>`
|
|
246
|
+
: undefined;
|
|
247
|
+
if (this.isExternalTypeImport(data)) {
|
|
248
|
+
if (data.aliasOf) {
|
|
249
|
+
this.addExtendedType(data.name, data.parentNamespace, data.aliasOf, {
|
|
250
|
+
type: isStruct ? undefined : type,
|
|
251
|
+
templates,
|
|
252
|
+
declaredTemplates,
|
|
253
|
+
});
|
|
254
|
+
this.addExternalTypeImport(data);
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
else if (data.aliasOf) {
|
|
259
|
+
this.addExtendedType(data.name, data.parentNamespace, data.aliasOf, {
|
|
260
|
+
type: isStruct ? undefined : type,
|
|
261
|
+
isInternal: true,
|
|
262
|
+
templates,
|
|
263
|
+
declaredTemplates,
|
|
264
|
+
});
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
addExtendedType(name, parentNamespace, aliasOf, opt) {
|
|
270
|
+
var _a, _b;
|
|
271
|
+
const extendedType = (opt === null || opt === void 0 ? void 0 : opt.isInternal)
|
|
272
|
+
? aliasOf
|
|
273
|
+
: this.getTypeFromExternalNamespace(parentNamespace, aliasOf);
|
|
274
|
+
const templates = (_a = opt === null || opt === void 0 ? void 0 : opt.templates) !== null && _a !== void 0 ? _a : "";
|
|
275
|
+
const declaredTemplates = (_b = opt === null || opt === void 0 ? void 0 : opt.declaredTemplates) !== null && _b !== void 0 ? _b : "";
|
|
276
|
+
if ((opt === null || opt === void 0 ? void 0 : opt.type) === "alias" || (opt === null || opt === void 0 ? void 0 : opt.type) === "union") {
|
|
277
|
+
this._typedefNames.add(name);
|
|
278
|
+
this.push0(`typedef ${name}${declaredTemplates} = ${extendedType}${templates};\n`);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
// Dart 3: use typedef for empty class extensions to keep json_serializable happy.
|
|
282
|
+
// `class B extends A {}` without @JsonSerializable is rejected by build_runner.
|
|
283
|
+
// A typedef is valid Dart 3 syntax and correctly aliases the type for serialization.
|
|
284
|
+
this._typedefNames.add(name);
|
|
285
|
+
this.push0(`typedef ${name}${declaredTemplates} = ${extendedType}${templates};\n`);
|
|
286
|
+
}
|
|
287
|
+
// Track generic extended types so runAfter() can generate 2-arg fromJson calls.
|
|
288
|
+
const fullExtended = `${extendedType}${templates !== null && templates !== void 0 ? templates : ""}`;
|
|
289
|
+
if (fullExtended.includes("<")) {
|
|
290
|
+
this._typedefToExtendedType.set(name, fullExtended);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
getNumberType(isInt, _range) {
|
|
294
|
+
// Dart has a single `int` type regardless of range
|
|
295
|
+
return isInt ? "int" : "double";
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Returns the Dart type for a literal value.
|
|
299
|
+
* Dart has no structural literal types, so we return the underlying primitive type.
|
|
300
|
+
* When a parent enum is provided, returns the enum type name (not a member expression),
|
|
301
|
+
* since Dart enum members cannot be used as types.
|
|
302
|
+
*/
|
|
303
|
+
getLiteralStringType(value, parentEnumNameKey) {
|
|
304
|
+
if (parentEnumNameKey) {
|
|
305
|
+
// Return the enum type name only — member access is not a valid Dart type
|
|
306
|
+
return parentEnumNameKey[0];
|
|
307
|
+
}
|
|
308
|
+
if (typeof value === "boolean")
|
|
309
|
+
return "bool";
|
|
310
|
+
if (typeof value === "number") {
|
|
311
|
+
return Number.isInteger(value) ? "int" : "double";
|
|
312
|
+
}
|
|
313
|
+
return "String";
|
|
314
|
+
}
|
|
315
|
+
// ── Composite type methods ──────────────────────────────────────────────
|
|
316
|
+
/** Ex: List<List<T>> for 2D array */
|
|
317
|
+
getArrayType(arrayType, arrayDeep) {
|
|
318
|
+
let output = `List<${arrayType}>`;
|
|
319
|
+
for (let i = 0; i < arrayDeep - 1; i++) {
|
|
320
|
+
output = `List<${output}>`;
|
|
321
|
+
}
|
|
322
|
+
return output;
|
|
323
|
+
}
|
|
324
|
+
// ── Transpile methods ───────────────────────────────────────────────────
|
|
325
|
+
transpileAliasedType(data) {
|
|
326
|
+
if (this.checkExtendedTypeInclusion(data, "alias"))
|
|
327
|
+
return;
|
|
328
|
+
this.addComment(data.description);
|
|
329
|
+
let resolvedType;
|
|
330
|
+
if (data instanceof core_1.ASTArray) {
|
|
331
|
+
resolvedType = this.getAttributeType(data.item);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
resolvedType = this.getAttributeType(data);
|
|
335
|
+
}
|
|
336
|
+
this.push0(`typedef ${data.name} = ${resolvedType};\n`);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Emits a Dart enhanced enum with a typed `value` field and `@JsonEnum` annotation.
|
|
340
|
+
*
|
|
341
|
+
* All-string values:
|
|
342
|
+
* @JsonEnum(valueField: 'value')
|
|
343
|
+
* enum EnumItem {
|
|
344
|
+
* enum1('Enum1'),
|
|
345
|
+
* enum2('Enum2');
|
|
346
|
+
*
|
|
347
|
+
* const EnumItem(this.value);
|
|
348
|
+
* final String value;
|
|
349
|
+
* }
|
|
350
|
+
*
|
|
351
|
+
* Mixed (int + string): typedef alias with comment.
|
|
352
|
+
*/
|
|
353
|
+
transpileEnum(data) {
|
|
354
|
+
if (this.checkExtendedTypeInclusion(data, "alias"))
|
|
355
|
+
return;
|
|
356
|
+
this.addComment(data.description);
|
|
357
|
+
const allStrings = data.values.every(([, v]) => typeof v === "string");
|
|
358
|
+
const allInts = data.values.every(([, v]) => typeof v === "number");
|
|
359
|
+
if (allStrings || allInts) {
|
|
360
|
+
const valueType = allStrings ? "String" : "int";
|
|
361
|
+
const q = allStrings ? "'" : "";
|
|
362
|
+
if (this.opt.useJsonSerialization !== false) {
|
|
363
|
+
this.push0(`@JsonEnum(valueField: 'value')`);
|
|
364
|
+
}
|
|
365
|
+
this.push0(`enum ${data.name} {`);
|
|
366
|
+
data.values.forEach(([key, value], index) => {
|
|
367
|
+
const memberName = case_1.default.camel(key);
|
|
368
|
+
const comma = index < data.values.length - 1 ? "," : ";";
|
|
369
|
+
this.push1(`${memberName}(${q}${value}${q})${comma}`);
|
|
370
|
+
});
|
|
371
|
+
this.push0("");
|
|
372
|
+
this.push1(`const ${data.name}(this.value);`);
|
|
373
|
+
this.push1(`final ${valueType} value;`);
|
|
374
|
+
this.push0(`}\n`);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// Mixed-type enum: fall back to typedef alias + comment
|
|
378
|
+
this.push0(`typedef ${data.name} = dynamic;`);
|
|
379
|
+
this.push0(`// ${data.name}: mixed-type enum — no single Dart base type`);
|
|
380
|
+
data.values.forEach(([key, value]) => {
|
|
381
|
+
const constName = case_1.default.camel(key);
|
|
382
|
+
const v = typeof value === "string" ? `'${value}'` : `${value}`;
|
|
383
|
+
this.push0(`// const ${constName} = ${v};`);
|
|
384
|
+
});
|
|
385
|
+
this.push0("");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
transpileStruct(data) {
|
|
389
|
+
if (this.checkExtendedTypeInclusion(data))
|
|
390
|
+
return;
|
|
391
|
+
this.addComment(data.description);
|
|
392
|
+
this._transpileStructBody(data);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Dart intersection:
|
|
396
|
+
* - If the AST produced a merged `newObject`, emit it as a flat class.
|
|
397
|
+
* - Otherwise emit a typedef alias to Object with a comment.
|
|
398
|
+
*/
|
|
399
|
+
transpileIntersection(data) {
|
|
400
|
+
if (this.checkExtendedTypeInclusion(data))
|
|
401
|
+
return;
|
|
402
|
+
this.addComment(data.description);
|
|
403
|
+
if (data.newObject) {
|
|
404
|
+
this._transpileStructBody(data.newObject);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
const leftType = this.getAttributeType(data.left);
|
|
408
|
+
const rightType = this.getAttributeType(data.right);
|
|
409
|
+
this.push0(`typedef ${data.name} = Object; // Intersection of: ${leftType}, ${rightType}\n`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Dart union:
|
|
414
|
+
* - Non-discriminated: typedef alias to Object with a comment listing member types.
|
|
415
|
+
* - Discriminated: abstract base class + factory unmarshal function using Dart 3 switch.
|
|
416
|
+
*/
|
|
417
|
+
transpileUnion(data) {
|
|
418
|
+
if (this.checkExtendedTypeInclusion(data, "union"))
|
|
419
|
+
return;
|
|
420
|
+
this.addComment(data.description);
|
|
421
|
+
const optionNames = data.options.map(this.getAttributeType.bind(this));
|
|
422
|
+
if (data.discriminantKey) {
|
|
423
|
+
this.push0(`// Discriminated union on '${data.discriminantKey}'.`);
|
|
424
|
+
this.push0(`// Possible types: ${optionNames.join(", ")}`);
|
|
425
|
+
this.push0(`abstract class ${data.name} {}\n`);
|
|
426
|
+
// Emit factory unmarshal function when all members have a discriminant value
|
|
427
|
+
const optionsData = data.options.map((opt, i) => {
|
|
428
|
+
var _a;
|
|
429
|
+
return ({
|
|
430
|
+
typeName: optionNames[i],
|
|
431
|
+
discriminantValue: (_a = opt.constraints) === null || _a === void 0 ? void 0 : _a.discriminantValue,
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
const allHaveDiscriminantValue = optionsData.every((o) => o.discriminantValue !== undefined);
|
|
435
|
+
if (allHaveDiscriminantValue && this.opt.useJsonSerialization !== false) {
|
|
436
|
+
const fnName = case_1.default.camel(data.name);
|
|
437
|
+
this.push0(`${data.name} ${fnName}FromJson(Map<String, dynamic> json) {`);
|
|
438
|
+
this.push1(`return switch (json['${data.discriminantKey}']) {`);
|
|
439
|
+
for (const opt of optionsData) {
|
|
440
|
+
const dv = opt.discriminantValue;
|
|
441
|
+
const isNumber = !isNaN(Number(dv));
|
|
442
|
+
const isBool = dv === "true" || dv === "false";
|
|
443
|
+
const caseVal = isNumber || isBool ? dv : `'${dv}'`;
|
|
444
|
+
// For generic types like Foo<Bar>, use 2-arg factory fromJson(json, Bar.fromJson)
|
|
445
|
+
const typeNameStr = opt.typeName;
|
|
446
|
+
const genericMatch = typeNameStr.match(/^([^<]+)<(.+)>$/);
|
|
447
|
+
if (genericMatch) {
|
|
448
|
+
const baseType = genericMatch[1];
|
|
449
|
+
const typeArgStr = genericMatch[2];
|
|
450
|
+
this.push2(`${caseVal} => ${baseType}.fromJson(json, (j) => ${typeArgStr}.fromJson(j as Map<String, dynamic>)),`);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
this.push2(`${caseVal} => ${typeNameStr}.fromJson(json),`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const escapedKey = data.discriminantKey.replace(/"/g, '\\"');
|
|
457
|
+
this.push2(`_ => throw Exception("Unknown ${data.name} discriminant: \${json['${escapedKey}']}")`);
|
|
458
|
+
this.push1(`};`);
|
|
459
|
+
this.push0(`}\n`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
this.push0(`// ${data.name} is a union of: ${optionNames.join(", ")}`);
|
|
464
|
+
this.push0(`typedef ${data.name} = Object;\n`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// ── Private helpers ─────────────────────────────────────────────────────
|
|
468
|
+
/** Renders the full body of a Dart class for an ASTObject. */
|
|
469
|
+
_transpileStructBody(data) {
|
|
470
|
+
var _a, _b;
|
|
471
|
+
const isGeneric = data.templates.size > 0;
|
|
472
|
+
const templateStr = isGeneric ? `<${[...data.templates].join(", ")}>` : "";
|
|
473
|
+
const unionNames = (_b = this._unionMembers.get((_a = data.name) !== null && _a !== void 0 ? _a : "")) !== null && _b !== void 0 ? _b : [];
|
|
474
|
+
const implementsClause = unionNames.length > 0 ? ` implements ${unionNames.join(", ")}` : "";
|
|
475
|
+
const useJson = this.opt.useJsonSerialization !== false;
|
|
476
|
+
if (useJson) {
|
|
477
|
+
if (isGeneric) {
|
|
478
|
+
this.push0(`@JsonSerializable(explicitToJson: true, genericArgumentFactories: true)`);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
this.push0(`@JsonSerializable(explicitToJson: true)`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
this.push0(`class ${data.name}${templateStr}${implementsClause} {`);
|
|
485
|
+
const properties = Object.entries(data.properties);
|
|
486
|
+
if (properties.length === 0) {
|
|
487
|
+
this.push0(`}`);
|
|
488
|
+
if (useJson) {
|
|
489
|
+
this._emitFromToJson(data);
|
|
490
|
+
}
|
|
491
|
+
this.push0("");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// Field declarations
|
|
495
|
+
for (const [key, value] of properties) {
|
|
496
|
+
this._transpileMember(key, value);
|
|
497
|
+
}
|
|
498
|
+
// Constructor
|
|
499
|
+
this.push0("");
|
|
500
|
+
this.push1(`${data.name}({`);
|
|
501
|
+
for (const [key, value] of properties) {
|
|
502
|
+
const fieldName = this.opt.keepKeys === true ? key : case_1.default.camel(key);
|
|
503
|
+
const isOpt = value.isOptional || value.isNullable;
|
|
504
|
+
if (isOpt) {
|
|
505
|
+
this.push2(`this.${fieldName},`);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
this.push2(`required this.${fieldName},`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
this.push1(`});`);
|
|
512
|
+
if (useJson) {
|
|
513
|
+
this._emitFromToJson(data);
|
|
514
|
+
}
|
|
515
|
+
this.push0(`}\n`);
|
|
516
|
+
}
|
|
517
|
+
/** Renders a single class field declaration with optional `@JsonKey` annotation. */
|
|
518
|
+
_transpileMember(key, memberNode) {
|
|
519
|
+
var _a, _b;
|
|
520
|
+
const fieldName = this.opt.keepKeys === true ? key : case_1.default.camel(key);
|
|
521
|
+
const isOpt = memberNode.isOptional || memberNode.isNullable;
|
|
522
|
+
const varType = this.getAttributeType(memberNode);
|
|
523
|
+
if (memberNode.description && !memberNode.name && !this.isTranspilerable(memberNode)) {
|
|
524
|
+
this.addComment(memberNode.description, `\n${this.indent[1]}`);
|
|
525
|
+
}
|
|
526
|
+
const useJson = this.opt.useJsonSerialization !== false;
|
|
527
|
+
let annotationEmitted = false;
|
|
528
|
+
if (useJson) {
|
|
529
|
+
// Only @JsonSerializable classes (ASTObject) need @JsonKey to prevent json_serializable
|
|
530
|
+
// from following the typedef chain to a type not directly imported.
|
|
531
|
+
// Primitives (String, int, double, bool, DateTime), collections (List, Map, Set,
|
|
532
|
+
// Tuple), enums, and dynamic are all handled natively by json_serializable.
|
|
533
|
+
const instanceType = memberNode.instanceType;
|
|
534
|
+
const isClassLike = instanceType === "ASTObject";
|
|
535
|
+
// Cross-file reference that is a class-like type: needs @JsonKey wrapper.
|
|
536
|
+
const memberIsAlias = memberNode.parentFile !== undefined && isClassLike;
|
|
537
|
+
const isList = ((_a = memberNode.arrayDimension) !== null && _a !== void 0 ? _a : 0) > 0;
|
|
538
|
+
// Actual type name (before array/optional wrapping)
|
|
539
|
+
const nodeName = memberNode.name;
|
|
540
|
+
const baseType = varType.replace(/\?$/, "").replace(/^List<(.+)>$/, "$1");
|
|
541
|
+
const localTypeName = baseType.includes(".") ? baseType.split(".").pop() : baseType;
|
|
542
|
+
if (this._discriminatedUnionTypes.has(localTypeName) && !isList) {
|
|
543
|
+
// Abstract discriminated union — use local or delegate fromJson function.
|
|
544
|
+
// If this union is a typedef alias of another (aliasOf set), delegate to
|
|
545
|
+
// the entity-level fromJson so we don't define a duplicate dispatcher.
|
|
546
|
+
this._usedCustomFromJsonTypes.set(localTypeName, "discUnion");
|
|
547
|
+
const fnBase = case_1.default.camel(localTypeName);
|
|
548
|
+
const fromJsonFn = (_b = this._discriminatedUnionDelegateFromJson.get(localTypeName)) !== null && _b !== void 0 ? _b : `${fnBase}FromJson`;
|
|
549
|
+
const namePart = fieldName !== key ? `name: '${key}', ` : "";
|
|
550
|
+
this.push1(`@JsonKey(${namePart}fromJson: ${fromJsonFn}, toJson: _${fnBase}ToJson)`);
|
|
551
|
+
annotationEmitted = true;
|
|
552
|
+
}
|
|
553
|
+
else if (this._typedefNames.has(localTypeName) && !isList && isClassLike) {
|
|
554
|
+
// Locally-defined typedef of a class — use standalone fromJson helper to
|
|
555
|
+
// prevent json_serializable from following the chain to an un-imported type.
|
|
556
|
+
this._usedCustomFromJsonTypes.set(localTypeName, "typedef");
|
|
557
|
+
const fnBase = case_1.default.camel(localTypeName);
|
|
558
|
+
const namePart = fieldName !== key ? `name: '${key}', ` : "";
|
|
559
|
+
this.push1(`@JsonKey(${namePart}fromJson: _${fnBase}FromJson, toJson: _${fnBase}ToJson)`);
|
|
560
|
+
annotationEmitted = true;
|
|
561
|
+
}
|
|
562
|
+
else if (memberIsAlias) {
|
|
563
|
+
// Cross-file class alias not in _typedefNames — same treatment (including lists).
|
|
564
|
+
const typeName = nodeName || localTypeName;
|
|
565
|
+
const fnBase = case_1.default.camel(typeName);
|
|
566
|
+
const kind = isList ? "typedefList" : "typedef";
|
|
567
|
+
this._usedCustomFromJsonTypes.set(typeName, kind);
|
|
568
|
+
// Store full type (including generics) for helper generation in runAfter()
|
|
569
|
+
const fullType = varType.replace(/\?$/, "");
|
|
570
|
+
if (fullType !== typeName) {
|
|
571
|
+
this._usedCustomFromJsonFullTypes.set(typeName, fullType);
|
|
572
|
+
}
|
|
573
|
+
const namePart = fieldName !== key ? `name: '${key}', ` : "";
|
|
574
|
+
if (isList) {
|
|
575
|
+
this.push1(`@JsonKey(${namePart}fromJson: _${fnBase}ListFromJson, toJson: _${fnBase}ListToJson)`);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
this.push1(`@JsonKey(${namePart}fromJson: _${fnBase}FromJson, toJson: _${fnBase}ToJson)`);
|
|
579
|
+
}
|
|
580
|
+
annotationEmitted = true;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (!annotationEmitted && fieldName !== key) {
|
|
584
|
+
// @JsonKey is needed when the Dart field name differs from the original JSON key
|
|
585
|
+
this.push1(`@JsonKey(name: '${key}')`);
|
|
586
|
+
}
|
|
587
|
+
const typeStr = isOpt ? `${varType}?` : varType;
|
|
588
|
+
this.push1(`final ${typeStr} ${fieldName};`);
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Emits `fromJson` factory and `toJson` method delegation stubs.
|
|
592
|
+
* For generic classes, extra `fromJsonT` / `toJsonT` function parameters are added
|
|
593
|
+
* as required by `json_serializable` with `genericArgumentFactories: true`.
|
|
594
|
+
*/
|
|
595
|
+
_emitFromToJson(data) {
|
|
596
|
+
const className = data.name;
|
|
597
|
+
const templates = [...data.templates];
|
|
598
|
+
const isGeneric = templates.length > 0;
|
|
599
|
+
if (isGeneric) {
|
|
600
|
+
const fromParams = templates
|
|
601
|
+
.map((t) => `${t} Function(Object? json) fromJson${t}`)
|
|
602
|
+
.join(", ");
|
|
603
|
+
const toParams = templates
|
|
604
|
+
.map((t) => `Object? Function(${t} value) toJson${t}`)
|
|
605
|
+
.join(", ");
|
|
606
|
+
const fromArgs = templates.map((t) => `fromJson${t}`).join(", ");
|
|
607
|
+
const toArgs = templates.map((t) => `toJson${t}`).join(", ");
|
|
608
|
+
this.push0("");
|
|
609
|
+
this.push1(`factory ${className}.fromJson(Map<String, dynamic> json, ${fromParams}) =>`);
|
|
610
|
+
this.push2(`_\$${className}FromJson(json, ${fromArgs});`);
|
|
611
|
+
this.push0("");
|
|
612
|
+
this.push1(`Map<String, dynamic> toJson(${toParams}) =>`);
|
|
613
|
+
this.push2(`_\$${className}ToJson(this, ${toArgs});`);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
this.push0("");
|
|
617
|
+
this.push1(`factory ${className}.fromJson(Map<String, dynamic> json) =>`);
|
|
618
|
+
this.push2(`_\$${className}FromJson(json);`);
|
|
619
|
+
this.push0("");
|
|
620
|
+
this.push1(`Map<String, dynamic> toJson() => _\$${className}ToJson(this);`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
exports.Zod2Dart = Zod2Dart;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLibs = getLibs;
|
|
4
|
+
/**
|
|
5
|
+
* Go standard library and common package import strings.
|
|
6
|
+
*/
|
|
7
|
+
function getLibs() {
|
|
8
|
+
return {
|
|
9
|
+
timePackage: `"time"`,
|
|
10
|
+
jsonPackage: `"encoding/json"`,
|
|
11
|
+
fmtPackage: `"fmt"`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { IZodToXOpt } from "../../core";
|
|
2
|
+
export interface IZod2GoOpt extends IZodToXOpt {
|
|
3
|
+
/**
|
|
4
|
+
* The Go package name for the generated file. Default: "models"
|
|
5
|
+
*/
|
|
6
|
+
packageName?: string;
|
|
7
|
+
/**
|
|
8
|
+
* By default (false), struct field names are exported (PascalCase) and JSON tags use the
|
|
9
|
+
* original property name. If set to true, original property names are preserved as-is.
|
|
10
|
+
*/
|
|
11
|
+
keepKeys?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether to emit `json:"fieldName"` struct tags. Default: true
|
|
14
|
+
*/
|
|
15
|
+
useJsonTags?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare const defaultOpts: IZod2GoOpt;
|