@typra/emitter 0.2.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.
Files changed (82) hide show
  1. package/dist/src/cleanup/generated-file.d.ts +6 -0
  2. package/dist/src/cleanup/generated-file.js +61 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +110 -0
  5. package/dist/src/decorators.d.ts +56 -0
  6. package/dist/src/decorators.js +177 -0
  7. package/dist/src/emitter.d.ts +13 -0
  8. package/dist/src/emitter.js +137 -0
  9. package/dist/src/generate.d.ts +86 -0
  10. package/dist/src/generate.js +104 -0
  11. package/dist/src/index.d.ts +4 -0
  12. package/dist/src/index.js +5 -0
  13. package/dist/src/ir/ast.d.ts +235 -0
  14. package/dist/src/ir/ast.js +589 -0
  15. package/dist/src/ir/declarations.d.ts +364 -0
  16. package/dist/src/ir/declarations.js +23 -0
  17. package/dist/src/ir/expansion.d.ts +140 -0
  18. package/dist/src/ir/expansion.js +407 -0
  19. package/dist/src/ir/lower.d.ts +53 -0
  20. package/dist/src/ir/lower.js +480 -0
  21. package/dist/src/ir/utilities.d.ts +12 -0
  22. package/dist/src/ir/utilities.js +39 -0
  23. package/dist/src/ir/visitor.d.ts +29 -0
  24. package/dist/src/ir/visitor.js +48 -0
  25. package/dist/src/languages/csharp/driver.d.ts +5 -0
  26. package/dist/src/languages/csharp/driver.js +315 -0
  27. package/dist/src/languages/csharp/emitter.d.ts +33 -0
  28. package/dist/src/languages/csharp/emitter.js +1140 -0
  29. package/dist/src/languages/csharp/scaffolding.d.ts +18 -0
  30. package/dist/src/languages/csharp/scaffolding.js +591 -0
  31. package/dist/src/languages/csharp/test-emitter.d.ts +43 -0
  32. package/dist/src/languages/csharp/test-emitter.js +274 -0
  33. package/dist/src/languages/csharp/visitor.d.ts +14 -0
  34. package/dist/src/languages/csharp/visitor.js +79 -0
  35. package/dist/src/languages/go/driver.d.ts +12 -0
  36. package/dist/src/languages/go/driver.js +128 -0
  37. package/dist/src/languages/go/emitter.d.ts +33 -0
  38. package/dist/src/languages/go/emitter.js +879 -0
  39. package/dist/src/languages/go/scaffolding.d.ts +18 -0
  40. package/dist/src/languages/go/scaffolding.js +53 -0
  41. package/dist/src/languages/go/test-emitter.d.ts +20 -0
  42. package/dist/src/languages/go/test-emitter.js +300 -0
  43. package/dist/src/languages/go/visitor.d.ts +14 -0
  44. package/dist/src/languages/go/visitor.js +78 -0
  45. package/dist/src/languages/markdown/driver.d.ts +19 -0
  46. package/dist/src/languages/markdown/driver.js +408 -0
  47. package/dist/src/languages/python/driver.d.ts +14 -0
  48. package/dist/src/languages/python/driver.js +372 -0
  49. package/dist/src/languages/python/emitter.d.ts +31 -0
  50. package/dist/src/languages/python/emitter.js +856 -0
  51. package/dist/src/languages/python/scaffolding.d.ts +33 -0
  52. package/dist/src/languages/python/scaffolding.js +279 -0
  53. package/dist/src/languages/python/test-emitter.d.ts +29 -0
  54. package/dist/src/languages/python/test-emitter.js +388 -0
  55. package/dist/src/languages/python/visitor.d.ts +14 -0
  56. package/dist/src/languages/python/visitor.js +65 -0
  57. package/dist/src/languages/rust/driver.d.ts +13 -0
  58. package/dist/src/languages/rust/driver.js +624 -0
  59. package/dist/src/languages/rust/emitter.d.ts +45 -0
  60. package/dist/src/languages/rust/emitter.js +1596 -0
  61. package/dist/src/languages/rust/visitor.d.ts +25 -0
  62. package/dist/src/languages/rust/visitor.js +153 -0
  63. package/dist/src/languages/typescript/driver.d.ts +8 -0
  64. package/dist/src/languages/typescript/driver.js +209 -0
  65. package/dist/src/languages/typescript/emitter.d.ts +42 -0
  66. package/dist/src/languages/typescript/emitter.js +904 -0
  67. package/dist/src/languages/typescript/scaffolding.d.ts +32 -0
  68. package/dist/src/languages/typescript/scaffolding.js +303 -0
  69. package/dist/src/languages/typescript/test-emitter.d.ts +23 -0
  70. package/dist/src/languages/typescript/test-emitter.js +204 -0
  71. package/dist/src/languages/typescript/visitor.d.ts +14 -0
  72. package/dist/src/languages/typescript/visitor.js +64 -0
  73. package/dist/src/lib.d.ts +33 -0
  74. package/dist/src/lib.js +101 -0
  75. package/dist/src/testing/index.d.ts +2 -0
  76. package/dist/src/testing/index.js +8 -0
  77. package/dist/src/testing/test-context.d.ts +63 -0
  78. package/dist/src/testing/test-context.js +355 -0
  79. package/fixtures/shapes/main.tsp +43 -0
  80. package/fixtures/tspconfig.yaml +13 -0
  81. package/package.json +76 -0
  82. package/src/lib/main.tsp +110 -0
@@ -0,0 +1,624 @@
1
+ import { resolvePath } from "@typespec/compiler";
2
+ import { execFileSync } from "child_process";
3
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
4
+ import { resolve } from "path";
5
+ import { enumerateTypes, } from "../../ir/ast.js";
6
+ import { filterNodes } from "../../emitter.js";
7
+ import { TypeRegistry } from "../../ir/expansion.js";
8
+ import { RustExprVisitor } from "./visitor.js";
9
+ import { buildBaseTestContext, rustTestOptions } from "../../testing/test-context.js";
10
+ import { toSnakeCase } from "../../ir/utilities.js";
11
+ import { lowerFile, collectPolymorphicTypeNames } from "../../ir/lower.js";
12
+ import { emitRustFile as emitRustFileDecl } from "./emitter.js";
13
+ import { emitGeneratedFile } from "../../cleanup/generated-file.js";
14
+ /**
15
+ * Type mapping from TypeSpec scalar types to Rust types.
16
+ * Retained for use by the test template context.
17
+ */
18
+ export const rustTypeMapper = {
19
+ "string": "String",
20
+ "number": "f64",
21
+ "array": "Vec<serde_json::Value>",
22
+ "object": "serde_json::Value",
23
+ "boolean": "bool",
24
+ "int64": "i64",
25
+ "int32": "i32",
26
+ "float64": "f64",
27
+ "float32": "f32",
28
+ "integer": "i64",
29
+ "float": "f64",
30
+ "numeric": "f64",
31
+ "any": "serde_json::Value",
32
+ "dictionary": "serde_json::Value",
33
+ };
34
+ const RUST_KEYWORDS = new Set([
35
+ "as", "break", "const", "continue", "crate", "else", "enum", "extern",
36
+ "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod",
37
+ "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct",
38
+ "super", "trait", "true", "type", "unsafe", "use", "where", "while",
39
+ "async", "await", "dyn",
40
+ ]);
41
+ function rustFieldName(name) {
42
+ const snake = toSnakeCase(name);
43
+ return RUST_KEYWORDS.has(snake) ? `r#${snake}` : snake;
44
+ }
45
+ /**
46
+ * Stale-file deletion is intentionally disabled until manifest cleanup is enabled.
47
+ */
48
+ function cleanupFlatTypeFiles(relDir, isTypeFile) {
49
+ void relDir;
50
+ void isTypeFile;
51
+ return;
52
+ }
53
+ /**
54
+ * Main entry point for Rust code generation.
55
+ */
56
+ export const generateRust = async (context, node, emitTarget, options) => {
57
+ const allTypes = Array.from(enumerateTypes(node));
58
+ const nodes = filterNodes(allTypes, options);
59
+ // Stale flat-file cleanup is disabled in this slice.
60
+ cleanupFlatTypeFiles(emitTarget["output-dir"], name => name.endsWith(".rs") && name !== "context.rs" && name !== "mod.rs" && name !== "lib.rs");
61
+ cleanupFlatTypeFiles(emitTarget["test-dir"], name => name.endsWith(".rs") && name !== "mod.rs" && name !== "main.rs");
62
+ // Build the expression IR infrastructure for this compilation
63
+ const registry = TypeRegistry.fromTypeGraph(allTypes);
64
+ const visitor = new RustExprVisitor(registry);
65
+ // Collect all polymorphic type names across all nodes
66
+ const polymorphicTypeNames = new Set();
67
+ for (const n of nodes) {
68
+ for (const name of collectPolymorphicTypeNames(n, registry)) {
69
+ polymorphicTypeNames.add(name);
70
+ }
71
+ }
72
+ // Build a map from polymorphic child type names to their parent type names.
73
+ // In Rust, child types become enum variants, not standalone structs.
74
+ // When importing a child type, we need to import ParentKind instead.
75
+ const childToParent = new Map();
76
+ for (const n of nodes) {
77
+ if (n.discriminator && n.childTypes.length > 0) {
78
+ for (const child of n.childTypes) {
79
+ childToParent.set(child.typeName.name, n.typeName.name);
80
+ }
81
+ }
82
+ }
83
+ // Render context.rs
84
+ const contextContent = emitRustContext("Prompty Context");
85
+ await emitRustFile(context, 'context.rs', contextContent, emitTarget["output-dir"]);
86
+ // Group root nodes by semantic group folder
87
+ const groupMap = new Map();
88
+ for (const n of nodes) {
89
+ if (!n.base) {
90
+ const g = n.group || "";
91
+ if (!groupMap.has(g))
92
+ groupMap.set(g, []);
93
+ groupMap.get(g).push(n);
94
+ }
95
+ }
96
+ // Render each base type and its children as a single file, into group subfolder
97
+ const groupModuleNames = new Map(); // group → module names
98
+ const testGroupModuleNames = new Map(); // group → test module names
99
+ for (const n of nodes) {
100
+ if (!n.base) {
101
+ const group = n.group || "";
102
+ const fileDecl = lowerFile(n, registry, polymorphicTypeNames);
103
+ const fileContent = emitRustFileDecl(fileDecl, visitor, polymorphicTypeNames, childToParent);
104
+ const fileName = toSnakeCase(n.typeName.name) + '.rs';
105
+ const outDir = group ? `${emitTarget["output-dir"]}/${group}` : emitTarget["output-dir"];
106
+ await emitRustFile(context, fileName, fileContent, outDir);
107
+ if (!groupModuleNames.has(group))
108
+ groupModuleNames.set(group, []);
109
+ groupModuleNames.get(group).push(toSnakeCase(n.typeName.name));
110
+ }
111
+ // Render test file — skip children of polymorphic hierarchies (they're enum variants now) and protocols
112
+ if (emitTarget["test-dir"] && !childToParent.has(n.typeName.name) && !n.isProtocol) {
113
+ const importPath = emitTarget["import-path"] || "crate";
114
+ const testContext = buildTestContext(n);
115
+ const isPolymorphicBase = !!(n.discriminator && n.childTypes.length > 0);
116
+ const testContent = emitRustTest({
117
+ ...testContext,
118
+ importPath,
119
+ isPolymorphicBase,
120
+ });
121
+ const testFileName = toSnakeCase(n.typeName.name) + '_test.rs';
122
+ const testGroup = n.group || "";
123
+ const testDir = testGroup ? `${emitTarget["test-dir"]}/${testGroup}` : emitTarget["test-dir"];
124
+ await emitRustFile(context, testFileName, testContent, testDir);
125
+ if (!testGroupModuleNames.has(testGroup))
126
+ testGroupModuleNames.set(testGroup, []);
127
+ testGroupModuleNames.get(testGroup).push(toSnakeCase(n.typeName.name) + '_test');
128
+ }
129
+ }
130
+ // Render per-group mod.rs files (source)
131
+ for (const [group, modules] of groupModuleNames) {
132
+ if (!group)
133
+ continue; // Root-level types handled in root mod.rs
134
+ const groupModContent = emitRustGroupMod(modules);
135
+ await emitRustFile(context, 'mod.rs', groupModContent, `${emitTarget["output-dir"]}/${group}`);
136
+ }
137
+ // Render test group mod.rs files and test main.rs
138
+ if (emitTarget["test-dir"]) {
139
+ // Emit per-group mod.rs (test)
140
+ const testGroups = [];
141
+ for (const [group, testMods] of testGroupModuleNames) {
142
+ if (group) {
143
+ const groupModContent = '// Code generated by Typra emitter; DO NOT EDIT.\n\n#![allow(unused_imports, dead_code, non_camel_case_types, unused_variables, clippy::all)]\n\n'
144
+ + testMods.map(m => `mod ${m};`).join('\n') + '\n';
145
+ await emitRustFile(context, 'mod.rs', groupModContent, `${emitTarget["test-dir"]}/${group}`);
146
+ testGroups.push(group);
147
+ }
148
+ }
149
+ // Emit root-level test files (no group)
150
+ const rootTestMods = testGroupModuleNames.get("") || [];
151
+ const allTopLevel = [...rootTestMods.map(m => `mod ${m};`), ...testGroups.sort().map(g => `mod ${g};`)];
152
+ const mainContent = '// Code generated by Typra emitter; DO NOT EDIT.\n\n#![allow(unused_imports, dead_code, non_camel_case_types, unused_variables, clippy::all)]\n\n'
153
+ + allTopLevel.join('\n') + '\n';
154
+ await emitRustFile(context, 'main.rs', mainContent, emitTarget["test-dir"]);
155
+ }
156
+ // Render root mod.rs
157
+ const rootModules = groupModuleNames.get("") || [];
158
+ const groups = Array.from(groupModuleNames.keys()).filter(g => g !== "").sort();
159
+ const libContent = emitRustLib(['context', ...rootModules], groups);
160
+ await emitRustFile(context, 'mod.rs', libContent, emitTarget["output-dir"]);
161
+ // Format emitted files
162
+ if (emitTarget.format !== false) {
163
+ const outputDir = emitTarget["output-dir"]
164
+ ? resolve(process.cwd(), emitTarget["output-dir"])
165
+ : context.emitterOutputDir;
166
+ formatRustFiles(outputDir);
167
+ }
168
+ };
169
+ /**
170
+ * Format Rust files using cargo fmt.
171
+ */
172
+ function formatRustFiles(outputDir) {
173
+ // Run cargo fmt if Cargo.toml exists in parent
174
+ const cargoToml = resolve(outputDir, '../Cargo.toml');
175
+ if (existsSync(cargoToml)) {
176
+ try {
177
+ execFileSync("cargo", ["fmt", "--manifest-path", cargoToml], {
178
+ stdio: 'pipe',
179
+ encoding: 'utf-8'
180
+ });
181
+ normalizeRustFileEndings(resolve(outputDir, '..'));
182
+ }
183
+ catch (error) {
184
+ console.warn(`Warning: cargo fmt failed. You may need to install Rust.`);
185
+ }
186
+ }
187
+ }
188
+ function normalizeRustFileEndings(dir) {
189
+ for (const entry of readdirSync(dir)) {
190
+ const fullPath = resolve(dir, entry);
191
+ const stat = statSync(fullPath);
192
+ if (stat.isDirectory()) {
193
+ normalizeRustFileEndings(fullPath);
194
+ continue;
195
+ }
196
+ if (!entry.endsWith(".rs")) {
197
+ continue;
198
+ }
199
+ const content = readFileSync(fullPath, "utf-8");
200
+ writeFileSync(fullPath, `${content.trimEnd()}\n`, "utf-8");
201
+ }
202
+ }
203
+ /**
204
+ * Build context for rendering a test file.
205
+ */
206
+ function buildTestContext(node) {
207
+ return buildBaseTestContext(node, undefined, rustTestOptions);
208
+ }
209
+ /**
210
+ * Write generated Rust content to file.
211
+ */
212
+ async function emitRustFile(context, filename, content, outputDir) {
213
+ outputDir = outputDir || `${context.emitterOutputDir}/rust`;
214
+ const filePath = resolvePath(outputDir, filename);
215
+ await emitGeneratedFile(context, filePath, `${content.trimEnd()}\n`);
216
+ }
217
+ /**
218
+ * Emit the context.rs file content (LoadContext/SaveContext structs).
219
+ */
220
+ function emitRustContext(header) {
221
+ return `// Code generated by Typra emitter; DO NOT EDIT.
222
+ // ${header}
223
+
224
+ #![allow(unused_imports, dead_code, non_camel_case_types, unused_variables, clippy::all)]
225
+
226
+ /// Callback type for pre-processing input data before parsing.
227
+ pub type PreProcessFn = Box<dyn Fn(serde_json::Value) -> serde_json::Value + Send + Sync>;
228
+
229
+ /// Callback type for post-processing the result after instantiation.
230
+ pub type PostProcessFn = Box<dyn Fn(serde_json::Value) -> serde_json::Value + Send + Sync>;
231
+
232
+ /// Callback type for pre-processing an object before serialization.
233
+ pub type PreSaveFn = Box<dyn Fn(serde_json::Value) -> serde_json::Value + Send + Sync>;
234
+
235
+ /// Callback type for post-processing a dictionary after serialization.
236
+ pub type PostSaveFn = Box<dyn Fn(serde_json::Value) -> serde_json::Value + Send + Sync>;
237
+
238
+ /// Context for customizing the loading process of agent definitions.
239
+ ///
240
+ /// Provides hooks for pre-processing input data before parsing and
241
+ /// post-processing output data after instantiation.
242
+ pub struct LoadContext {
243
+ /// Optional callback to transform input data before parsing.
244
+ pub pre_process: Option<PreProcessFn>,
245
+ /// Optional callback to transform the result after instantiation.
246
+ pub post_process: Option<PostProcessFn>,
247
+ }
248
+
249
+ impl std::fmt::Debug for LoadContext {
250
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251
+ f.debug_struct("LoadContext")
252
+ .field("pre_process", &self.pre_process.as_ref().map(|_| "..."))
253
+ .field("post_process", &self.post_process.as_ref().map(|_| "..."))
254
+ .finish()
255
+ }
256
+ }
257
+
258
+ impl Default for LoadContext {
259
+ fn default() -> Self {
260
+ Self {
261
+ pre_process: None,
262
+ post_process: None,
263
+ }
264
+ }
265
+ }
266
+
267
+ impl LoadContext {
268
+ /// Create a new empty LoadContext.
269
+ pub fn new() -> Self {
270
+ Self::default()
271
+ }
272
+
273
+ /// Apply pre-processing to input data if a pre_process callback is set.
274
+ ///
275
+ /// # Arguments
276
+ /// * \`data\` - The raw input value to process.
277
+ ///
278
+ /// # Returns
279
+ /// The processed value, or the original if no callback is set.
280
+ pub fn process_input(&self, data: serde_json::Value) -> serde_json::Value {
281
+ if let Some(ref f) = self.pre_process {
282
+ f(data)
283
+ } else {
284
+ data
285
+ }
286
+ }
287
+
288
+ /// Apply post-processing to the result if a post_process callback is set.
289
+ ///
290
+ /// # Arguments
291
+ /// * \`result\` - The instantiated value to process.
292
+ ///
293
+ /// # Returns
294
+ /// The processed result, or the original if no callback is set.
295
+ pub fn process_output(&self, result: serde_json::Value) -> serde_json::Value {
296
+ if let Some(ref f) = self.post_process {
297
+ f(result)
298
+ } else {
299
+ result
300
+ }
301
+ }
302
+ }
303
+
304
+ /// Context for customizing the serialization process of agent definitions.
305
+ ///
306
+ /// Provides hooks for pre-processing the object before serialization and
307
+ /// post-processing the dictionary after serialization.
308
+ pub struct SaveContext {
309
+ /// Optional callback to transform the object before serialization.
310
+ pub pre_save: Option<PreSaveFn>,
311
+ /// Optional callback to transform the dictionary after serialization.
312
+ pub post_save: Option<PostSaveFn>,
313
+ /// Output format for collections: "object" (name as key) or "array" (list of dicts).
314
+ /// Defaults to "object".
315
+ pub collection_format: String,
316
+ /// Use shorthand scalar representation when possible.
317
+ /// Defaults to true.
318
+ pub use_shorthand: bool,
319
+ }
320
+
321
+ impl std::fmt::Debug for SaveContext {
322
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323
+ f.debug_struct("SaveContext")
324
+ .field("pre_save", &self.pre_save.as_ref().map(|_| "..."))
325
+ .field("post_save", &self.post_save.as_ref().map(|_| "..."))
326
+ .field("collection_format", &self.collection_format)
327
+ .field("use_shorthand", &self.use_shorthand)
328
+ .finish()
329
+ }
330
+ }
331
+
332
+ impl Default for SaveContext {
333
+ fn default() -> Self {
334
+ Self {
335
+ pre_save: None,
336
+ post_save: None,
337
+ collection_format: "object".to_string(),
338
+ use_shorthand: true,
339
+ }
340
+ }
341
+ }
342
+
343
+ impl SaveContext {
344
+ /// Create a new SaveContext with defaults.
345
+ pub fn new() -> Self {
346
+ Self::default()
347
+ }
348
+
349
+ /// Apply pre-processing to the object if a pre_save callback is set.
350
+ ///
351
+ /// # Arguments
352
+ /// * \`obj\` - The value to process before serialization.
353
+ ///
354
+ /// # Returns
355
+ /// The processed value, or the original if no callback is set.
356
+ pub fn process_object(&self, obj: serde_json::Value) -> serde_json::Value {
357
+ if let Some(ref f) = self.pre_save {
358
+ f(obj)
359
+ } else {
360
+ obj
361
+ }
362
+ }
363
+
364
+ /// Apply post-processing to the dictionary if a post_save callback is set.
365
+ ///
366
+ /// # Arguments
367
+ /// * \`data\` - The serialized value to process.
368
+ ///
369
+ /// # Returns
370
+ /// The processed value, or the original if no callback is set.
371
+ pub fn process_dict(&self, data: serde_json::Value) -> serde_json::Value {
372
+ if let Some(ref f) = self.post_save {
373
+ f(data)
374
+ } else {
375
+ data
376
+ }
377
+ }
378
+
379
+ /// Convert a value to a YAML string.
380
+ pub fn to_yaml(&self, data: &serde_json::Value) -> Result<String, serde_yaml::Error> {
381
+ serde_yaml::to_string(data)
382
+ }
383
+
384
+ /// Convert a value to a JSON string.
385
+ pub fn to_json(&self, data: &serde_json::Value, indent: bool) -> Result<String, serde_json::Error> {
386
+ if indent {
387
+ serde_json::to_string_pretty(data)
388
+ } else {
389
+ serde_json::to_string(data)
390
+ }
391
+ }
392
+ }
393
+ `;
394
+ }
395
+ /**
396
+ * Emit the root mod.rs file content (module declarations).
397
+ *
398
+ * @param rootModules - Module names emitted directly in the root (e.g. ["context"])
399
+ * @param groups - Group subfolder names (e.g. ["connection", "tools"])
400
+ */
401
+ function emitRustLib(rootModules, groups = []) {
402
+ let out = '// Code generated by Typra emitter; DO NOT EDIT.\n\n#![allow(unused_imports, dead_code, non_camel_case_types, unused_variables, clippy::all)]\n';
403
+ for (const module of rootModules) {
404
+ out += `\npub mod ${module};\npub use ${module}::*;\n`;
405
+ }
406
+ for (const group of groups) {
407
+ out += `\npub mod ${group};\npub use ${group}::*;\n`;
408
+ }
409
+ return `${out.trimEnd()}\n`;
410
+ }
411
+ /**
412
+ * Emit a per-group mod.rs file that declares and re-exports all modules in that group.
413
+ */
414
+ function emitRustGroupMod(moduleNames) {
415
+ let out = '// Code generated by Typra emitter; DO NOT EDIT.\n\n#![allow(unused_imports, dead_code, non_camel_case_types, unused_variables, clippy::all)]\n';
416
+ for (const module of moduleNames) {
417
+ out += `\npub mod ${module};\npub use ${module}::*;\n`;
418
+ }
419
+ return out;
420
+ }
421
+ /**
422
+ * Map a factory parameter type string to a Rust test value literal.
423
+ */
424
+ function factoryParamTestValue(typeStr) {
425
+ switch (typeStr) {
426
+ case "string": return '"test".to_string()';
427
+ case "boolean": return "true";
428
+ case "integer":
429
+ case "int32": return "42";
430
+ case "int64": return "42i64";
431
+ case "float":
432
+ case "float64": return "3.14";
433
+ case "unknown": return 'serde_json::json!("test")';
434
+ default: return 'serde_json::json!("test")';
435
+ }
436
+ }
437
+ function rustAssertionValue(node, key, value, delimiter) {
438
+ if (delimiter !== "" || typeof value !== "number" || !Number.isInteger(value)) {
439
+ return `${delimiter}${value}${delimiter}`;
440
+ }
441
+ const prop = node.properties.find(p => rustFieldName(p.name) === key);
442
+ const scalar = prop?.typeName.name;
443
+ if (scalar === "float" || scalar === "float32" || scalar === "float64" || scalar === "number" || scalar === "numeric") {
444
+ return `${value}.0`;
445
+ }
446
+ return `${value}`;
447
+ }
448
+ /**
449
+ * Emit an integration test file for a TypeSpec model type.
450
+ */
451
+ function emitRustTest(ctx) {
452
+ const { node, isAbstract, examples, coercions, factories, importPath, isPolymorphicBase } = ctx;
453
+ const typeName = node.typeName.name;
454
+ const snakeName = toSnakeCase(typeName);
455
+ let out = '';
456
+ // Collect enum types referenced in properties (for use imports)
457
+ const enumImports = new Set();
458
+ for (const prop of node.properties) {
459
+ if (prop.enumName && node.discriminator !== prop.name) {
460
+ enumImports.add(prop.enumName);
461
+ }
462
+ }
463
+ out += '// Code generated by Typra emitter; DO NOT EDIT.\n';
464
+ out += '\n';
465
+ out += '#![allow(unused_imports, dead_code, non_camel_case_types, unused_variables, clippy::all)]\n';
466
+ out += '\n';
467
+ out += `use ${importPath}::${typeName};\n`;
468
+ for (const enumName of [...enumImports].sort()) {
469
+ if (enumName !== typeName) {
470
+ out += `use ${importPath}::${enumName};\n`;
471
+ }
472
+ }
473
+ out += `use ${importPath}::context::{LoadContext, SaveContext};\n`;
474
+ out += '\n';
475
+ // Example tests (load JSON, load YAML, roundtrip)
476
+ for (let i = 0; i < examples.length; i++) {
477
+ const sample = examples[i];
478
+ const suffix = i === 0 ? '' : `_${i}`;
479
+ // JSON load test
480
+ out += '#[test]\n';
481
+ out += `fn test_${snakeName}_load_json${suffix}() {\n`;
482
+ out += ' let json = r####"\n';
483
+ for (const line of sample.json) {
484
+ out += `${line}\n`;
485
+ }
486
+ out += '"####;\n';
487
+ out += ' let ctx = LoadContext::default();\n';
488
+ out += ` let result = ${typeName}::from_json(json, &ctx);\n`;
489
+ out += ' assert!(result.is_ok(), "Failed to load from JSON: {:?}", result.err());\n';
490
+ if (!isAbstract) {
491
+ out += ' let instance = result.unwrap();\n';
492
+ if (sample.validations.length > 0) {
493
+ for (const v of sample.validations) {
494
+ if (v.isOptional) {
495
+ out += ` assert!(instance.${v.key}.is_some(), "Expected ${v.key} to be Some");\n`;
496
+ out += ` assert_eq!(instance.${v.key}.as_ref().unwrap(), &${rustAssertionValue(node, v.key, v.value, v.delimiter)});\n`;
497
+ }
498
+ else if (isPolymorphicBase && v.key === "kind") {
499
+ out += ` assert_eq!(instance.kind_str(), ${rustAssertionValue(node, v.key, v.value, v.delimiter)});\n`;
500
+ }
501
+ else {
502
+ out += ` assert_eq!(instance.${v.key}, ${rustAssertionValue(node, v.key, v.value, v.delimiter)});\n`;
503
+ }
504
+ }
505
+ }
506
+ else {
507
+ out += ' let _ = instance; // load succeeded, no scalar properties to validate\n';
508
+ }
509
+ }
510
+ out += '}\n';
511
+ out += '\n';
512
+ // YAML load test
513
+ out += '#[test]\n';
514
+ out += `fn test_${snakeName}_load_yaml${suffix}() {\n`;
515
+ out += ' let yaml = r####"\n';
516
+ for (const line of sample.yaml) {
517
+ out += `${line}\n`;
518
+ }
519
+ out += '"####;\n';
520
+ out += ' let ctx = LoadContext::default();\n';
521
+ out += ` let result = ${typeName}::from_yaml(yaml, &ctx);\n`;
522
+ out += ' assert!(result.is_ok(), "Failed to load from YAML: {:?}", result.err());\n';
523
+ if (!isAbstract) {
524
+ out += ' let instance = result.unwrap();\n';
525
+ if (sample.validations.length > 0) {
526
+ for (const v of sample.validations) {
527
+ if (v.isOptional) {
528
+ out += ` assert!(instance.${v.key}.is_some(), "Expected ${v.key} to be Some");\n`;
529
+ }
530
+ else if (isPolymorphicBase && v.key === "kind") {
531
+ out += ` assert_eq!(instance.kind_str(), ${rustAssertionValue(node, v.key, v.value, v.delimiter)});\n`;
532
+ }
533
+ else {
534
+ out += ` assert_eq!(instance.${v.key}, ${rustAssertionValue(node, v.key, v.value, v.delimiter)});\n`;
535
+ }
536
+ }
537
+ }
538
+ else {
539
+ out += ' let _ = instance; // load succeeded, no scalar properties to validate\n';
540
+ }
541
+ }
542
+ out += '}\n';
543
+ out += '\n';
544
+ // Roundtrip test
545
+ out += '#[test]\n';
546
+ out += `fn test_${snakeName}_roundtrip${suffix}() {\n`;
547
+ out += ' let json = r####"\n';
548
+ for (const line of sample.json) {
549
+ out += `${line}\n`;
550
+ }
551
+ out += '"####;\n';
552
+ out += ' let load_ctx = LoadContext::default();\n';
553
+ out += ` let result = ${typeName}::from_json(json, &load_ctx);\n`;
554
+ out += ' assert!(result.is_ok(), "Failed to load: {:?}", result.err());\n';
555
+ if (!isAbstract) {
556
+ out += ' let instance = result.unwrap();\n';
557
+ out += ' let save_ctx = SaveContext::default();\n';
558
+ out += ' let json_output = instance.to_json(&save_ctx);\n';
559
+ out += ' assert!(json_output.is_ok(), "Failed to serialize to JSON: {:?}", json_output.err());\n';
560
+ }
561
+ out += '}\n';
562
+ out += '\n';
563
+ }
564
+ // Coercion tests
565
+ for (let i = 0; i < coercions.length; i++) {
566
+ const alt = coercions[i];
567
+ const suffix = i === 0 ? '' : `_${i + 1}`;
568
+ out += '#[test]\n';
569
+ out += `fn test_${snakeName}_from_${alt.title.toLowerCase()}${suffix}() {\n`;
570
+ out += ` let value = serde_json::json!(${alt.value});\n`;
571
+ out += ' let ctx = LoadContext::default();\n';
572
+ out += ` let instance = ${typeName}::load_from_value(&value, &ctx);\n`;
573
+ if (!isAbstract) {
574
+ if (alt.validations.length > 0) {
575
+ for (const item of alt.validations) {
576
+ if (item.isOptional) {
577
+ out += ` assert!(instance.${item.key}.is_some());\n`;
578
+ }
579
+ else if (isPolymorphicBase && item.key === "kind") {
580
+ out += ` assert_eq!(instance.kind_str(), ${rustAssertionValue(node, item.key, item.value, item.delimiter)});\n`;
581
+ }
582
+ else {
583
+ out += ` assert_eq!(instance.${item.key}, ${rustAssertionValue(node, item.key, item.value, item.delimiter)});\n`;
584
+ }
585
+ }
586
+ }
587
+ else {
588
+ out += ' let _ = instance; // load succeeded, no scalar properties to validate\n';
589
+ }
590
+ }
591
+ else {
592
+ out += ' let _ = instance; // abstract type, load succeeded\n';
593
+ }
594
+ out += '}\n';
595
+ out += '\n';
596
+ }
597
+ // Factory tests
598
+ for (const factory of factories) {
599
+ const factorySnake = toSnakeCase(factory.name);
600
+ const paramEntries = Object.entries(factory.params);
601
+ const paramValues = paramEntries.map(([, pType]) => factoryParamTestValue(pType)).join(', ');
602
+ out += '#[test]\n';
603
+ out += `fn test_${snakeName}_factory_${factorySnake}() {\n`;
604
+ out += ` let instance = ${typeName}::${factorySnake}(${paramValues});\n`;
605
+ for (const [propName, value] of Object.entries(factory.sets)) {
606
+ if (value === true) {
607
+ out += ` assert!(instance.${toSnakeCase(propName)});\n`;
608
+ }
609
+ else if (value === false) {
610
+ out += ` assert!(!instance.${toSnakeCase(propName)});\n`;
611
+ }
612
+ }
613
+ for (const [pName] of paramEntries) {
614
+ const prop = node.properties.find(p => p.name === pName);
615
+ if (prop && prop.isOptional) {
616
+ out += ` assert!(instance.${toSnakeCase(pName)}.is_some());\n`;
617
+ }
618
+ }
619
+ out += '}\n';
620
+ out += '\n';
621
+ }
622
+ return out;
623
+ }
624
+ //# sourceMappingURL=driver.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Rust code emitter — Declaration IR → Rust source code.
3
+ *
4
+ * Replaces `file.rs.njk` + `_macros.njk` (~1,072 lines of Nunjucks templates)
5
+ * with a typed TypeScript function that walks the FileDecl tree.
6
+ *
7
+ * The emitter produces Rust code using the struct + enum pattern for
8
+ * polymorphic types. Output is post-processed by `cargo fmt`.
9
+ *
10
+ * Key Rust-specific patterns:
11
+ * - Polymorphic types use struct + XxxKind enum (not inheritance)
12
+ * - Variant-specific fields live on enum variants, not child classes
13
+ * - Polymorphic single-ref fields → serde_json::Value
14
+ * - Ownership: String, Option<T>, Vec<T>
15
+ * - Pattern matching for load/save of variant fields
16
+ *
17
+ * Structural blocks emitted (in order):
18
+ * 1. Header comment (auto-generated warning)
19
+ * 2. Imports (context, referenced types)
20
+ * 3. For each polymorphic type:
21
+ * a. XxxKind enum with inline struct variants
22
+ * b. impl Default for XxxKind
23
+ * 4. For each type:
24
+ * a. Struct definition with #[derive(Debug, Clone, Default)]
25
+ * b. impl block:
26
+ * - new(), from_json(), from_yaml()
27
+ * - load_from_value()
28
+ * - kind_str() (polymorphic only)
29
+ * - to_value(), to_json(), to_yaml()
30
+ * - to_wire() (when wire mappings exist)
31
+ * - Collection helpers
32
+ * - Factory methods
33
+ * - Method stubs (as trait)
34
+ */
35
+ import { FileDecl } from "../../ir/declarations.js";
36
+ import { ExprVisitor } from "../../ir/visitor.js";
37
+ /**
38
+ * Emit a complete Rust source file from a FileDecl.
39
+ *
40
+ * @param file - File declaration to emit
41
+ * @param visitor - Expression visitor
42
+ * @param polymorphicTypeNames - Set of type names that have polymorphic dispatch
43
+ * @param childToParent - Map from child variant name to parent type name
44
+ */
45
+ export declare function emitRustFile(file: FileDecl, visitor: ExprVisitor, polymorphicTypeNames: Set<string>, childToParent?: Map<string, string>): string;