@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.
- package/dist/src/cleanup/generated-file.d.ts +6 -0
- package/dist/src/cleanup/generated-file.js +61 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +110 -0
- package/dist/src/decorators.d.ts +56 -0
- package/dist/src/decorators.js +177 -0
- package/dist/src/emitter.d.ts +13 -0
- package/dist/src/emitter.js +137 -0
- package/dist/src/generate.d.ts +86 -0
- package/dist/src/generate.js +104 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +5 -0
- package/dist/src/ir/ast.d.ts +235 -0
- package/dist/src/ir/ast.js +589 -0
- package/dist/src/ir/declarations.d.ts +364 -0
- package/dist/src/ir/declarations.js +23 -0
- package/dist/src/ir/expansion.d.ts +140 -0
- package/dist/src/ir/expansion.js +407 -0
- package/dist/src/ir/lower.d.ts +53 -0
- package/dist/src/ir/lower.js +480 -0
- package/dist/src/ir/utilities.d.ts +12 -0
- package/dist/src/ir/utilities.js +39 -0
- package/dist/src/ir/visitor.d.ts +29 -0
- package/dist/src/ir/visitor.js +48 -0
- package/dist/src/languages/csharp/driver.d.ts +5 -0
- package/dist/src/languages/csharp/driver.js +315 -0
- package/dist/src/languages/csharp/emitter.d.ts +33 -0
- package/dist/src/languages/csharp/emitter.js +1140 -0
- package/dist/src/languages/csharp/scaffolding.d.ts +18 -0
- package/dist/src/languages/csharp/scaffolding.js +591 -0
- package/dist/src/languages/csharp/test-emitter.d.ts +43 -0
- package/dist/src/languages/csharp/test-emitter.js +274 -0
- package/dist/src/languages/csharp/visitor.d.ts +14 -0
- package/dist/src/languages/csharp/visitor.js +79 -0
- package/dist/src/languages/go/driver.d.ts +12 -0
- package/dist/src/languages/go/driver.js +128 -0
- package/dist/src/languages/go/emitter.d.ts +33 -0
- package/dist/src/languages/go/emitter.js +879 -0
- package/dist/src/languages/go/scaffolding.d.ts +18 -0
- package/dist/src/languages/go/scaffolding.js +53 -0
- package/dist/src/languages/go/test-emitter.d.ts +20 -0
- package/dist/src/languages/go/test-emitter.js +300 -0
- package/dist/src/languages/go/visitor.d.ts +14 -0
- package/dist/src/languages/go/visitor.js +78 -0
- package/dist/src/languages/markdown/driver.d.ts +19 -0
- package/dist/src/languages/markdown/driver.js +408 -0
- package/dist/src/languages/python/driver.d.ts +14 -0
- package/dist/src/languages/python/driver.js +372 -0
- package/dist/src/languages/python/emitter.d.ts +31 -0
- package/dist/src/languages/python/emitter.js +856 -0
- package/dist/src/languages/python/scaffolding.d.ts +33 -0
- package/dist/src/languages/python/scaffolding.js +279 -0
- package/dist/src/languages/python/test-emitter.d.ts +29 -0
- package/dist/src/languages/python/test-emitter.js +388 -0
- package/dist/src/languages/python/visitor.d.ts +14 -0
- package/dist/src/languages/python/visitor.js +65 -0
- package/dist/src/languages/rust/driver.d.ts +13 -0
- package/dist/src/languages/rust/driver.js +624 -0
- package/dist/src/languages/rust/emitter.d.ts +45 -0
- package/dist/src/languages/rust/emitter.js +1596 -0
- package/dist/src/languages/rust/visitor.d.ts +25 -0
- package/dist/src/languages/rust/visitor.js +153 -0
- package/dist/src/languages/typescript/driver.d.ts +8 -0
- package/dist/src/languages/typescript/driver.js +209 -0
- package/dist/src/languages/typescript/emitter.d.ts +42 -0
- package/dist/src/languages/typescript/emitter.js +904 -0
- package/dist/src/languages/typescript/scaffolding.d.ts +32 -0
- package/dist/src/languages/typescript/scaffolding.js +303 -0
- package/dist/src/languages/typescript/test-emitter.d.ts +23 -0
- package/dist/src/languages/typescript/test-emitter.js +204 -0
- package/dist/src/languages/typescript/visitor.d.ts +14 -0
- package/dist/src/languages/typescript/visitor.js +64 -0
- package/dist/src/lib.d.ts +33 -0
- package/dist/src/lib.js +101 -0
- package/dist/src/testing/index.d.ts +2 -0
- package/dist/src/testing/index.js +8 -0
- package/dist/src/testing/test-context.d.ts +63 -0
- package/dist/src/testing/test-context.js +355 -0
- package/fixtures/shapes/main.tsp +43 -0
- package/fixtures/tspconfig.yaml +13 -0
- package/package.json +76 -0
- 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;
|