use mlf_codegen::{register_generator, CodeGenerator, GeneratorContext}; use mlf_lang::ast::*; use std::fmt::Write; pub struct RustGenerator; impl RustGenerator { pub const NAME: &'static str = "rust"; fn to_snake_case(&self, s: &str) -> String { let mut result = String::new(); for (i, ch) in s.chars().enumerate() { if ch.is_uppercase() && i > 0 { result.push('_'); } result.push(ch.to_lowercase().next().unwrap()); } result } fn to_pascal_case(&self, s: &str) -> String { let mut chars = s.chars(); match chars.next() { None => String::new(), Some(f) => f.to_uppercase().collect::() + chars.as_str(), } } fn generate_type(&self, ty: &Type, optional: bool, ctx: &GeneratorContext) -> Result { let base_type = match ty { Type::Primitive { kind, .. } => match kind { PrimitiveType::Null => "()".to_string(), // Unit type for null PrimitiveType::Boolean => "bool".to_string(), PrimitiveType::Integer => "i64".to_string(), PrimitiveType::String => "String".to_string(), PrimitiveType::Bytes => "Vec".to_string(), PrimitiveType::Blob => "Vec".to_string(), // Annotation idea: @rustType("custom::BlobType") }, Type::Reference { path, .. } => { let path_str = path.to_string(); match path_str.as_str() { // Map standard library types "Datetime" => "String".to_string(), // ISO 8601 string, could use chrono::DateTime "Did" | "AtUri" | "Cid" | "AtIdentifier" | "Handle" | "Nsid" | "Tid" | "RecordKey" | "Uri" | "Language" => { "String".to_string() } _ => { // Local reference - convert to PascalCase self.to_pascal_case(&path.segments.last().unwrap().name) } } } Type::Array { inner, .. } => { let inner_type = self.generate_type(inner, false, ctx)?; format!("Vec<{}>", inner_type) } Type::Union { types, .. } => { // Rust doesn't have direct union types, use an enum // For now, generate a simple representation // Annotation idea: @rustEnum to customize enum generation if types.len() == 2 && matches!(types[0], Type::Primitive { kind: PrimitiveType::Null, .. }) { // Special case: null | T becomes Option return self.generate_type(&types[1], true, ctx); } else if types.len() == 2 && matches!(types[1], Type::Primitive { kind: PrimitiveType::Null, .. }) { return self.generate_type(&types[0], true, ctx); } // Otherwise use serde_json::Value for flexibility "serde_json::Value".to_string() } Type::Object { .. } => { // Inline struct types aren't idiomatic in Rust // We'd need to generate a named type // For now, use serde_json::Value // Annotation idea: @rustInlineStruct to force inline generation "serde_json::Value".to_string() } Type::Parenthesized { inner, .. } => { return self.generate_type(inner, optional, ctx); } Type::Constrained { base, .. } => { return self.generate_type(base, optional, ctx); } Type::Unknown { .. } => "serde_json::Value".to_string(), }; if optional { Ok(format!("Option<{}>", base_type)) } else { Ok(base_type) } } fn generate_doc_comment(&self, docs: &[DocComment]) -> String { if docs.is_empty() { return String::new(); } let mut result = String::new(); for doc in docs { result.push_str("/// "); result.push_str(&doc.text); result.push('\n'); } result } } impl CodeGenerator for RustGenerator { fn name(&self) -> &'static str { Self::NAME } fn description(&self) -> &'static str { "Generate Rust structs and client code" } fn file_extension(&self) -> &'static str { ".rs" } fn generate(&self, ctx: &GeneratorContext) -> Result { let mut output = String::new(); // Header comment writeln!(output, "// Generated from {}", ctx.namespace).unwrap(); writeln!(output, "// Do not edit manually").unwrap(); writeln!(output).unwrap(); // Common imports writeln!(output, "use serde::{{Deserialize, Serialize}};\n").unwrap(); // Generate code for each item for item in &ctx.lexicon.items { match item { Item::Record(record) => { output.push_str(&self.generate_doc_comment(&record.docs)); writeln!(output, "#[derive(Debug, Clone, Serialize, Deserialize)]").unwrap(); writeln!(output, "pub struct {} {{", self.to_pascal_case(&record.name.name)).unwrap(); for field in &record.fields { if !field.docs.is_empty() { writeln!(output, " /// {}", field.docs[0].text).unwrap(); } // Use serde rename for camelCase fields if field.name.name != self.to_snake_case(&field.name.name) { writeln!(output, " #[serde(rename = \"{}\")]", field.name.name).unwrap(); } // Skip serializing None values for optional fields if field.optional { writeln!(output, " #[serde(skip_serializing_if = \"Option::is_none\")]").unwrap(); } let field_type = self.generate_type(&field.ty, field.optional, ctx)?; writeln!(output, " pub {}: {},", self.to_snake_case(&field.name.name), field_type).unwrap(); } writeln!(output, "}}\n").unwrap(); } Item::DefType(def) => { output.push_str(&self.generate_doc_comment(&def.docs)); match &def.ty { Type::Object { fields, .. } => { // Generate a struct for object types writeln!(output, "#[derive(Debug, Clone, Serialize, Deserialize)]").unwrap(); writeln!(output, "pub struct {} {{", self.to_pascal_case(&def.name.name)).unwrap(); for field in fields { if !field.docs.is_empty() { writeln!(output, " /// {}", field.docs[0].text).unwrap(); } if field.name.name != self.to_snake_case(&field.name.name) { writeln!(output, " #[serde(rename = \"{}\")]", field.name.name).unwrap(); } if field.optional { writeln!(output, " #[serde(skip_serializing_if = \"Option::is_none\")]").unwrap(); } let field_type = self.generate_type(&field.ty, field.optional, ctx)?; writeln!(output, " pub {}: {},", self.to_snake_case(&field.name.name), field_type).unwrap(); } writeln!(output, "}}\n").unwrap(); } _ => { // Type alias writeln!( output, "pub type {} = {};\n", self.to_pascal_case(&def.name.name), self.generate_type(&def.ty, false, ctx)? ).unwrap(); } } } Item::InlineType(inline) => { output.push_str(&self.generate_doc_comment(&inline.docs)); writeln!( output, "pub type {} = {};\n", self.to_pascal_case(&inline.name.name), self.generate_type(&inline.ty, false, ctx)? ).unwrap(); } Item::Token(token) => { output.push_str(&self.generate_doc_comment(&token.docs)); writeln!(output, "pub const {}: &str = \"{}\";\n", token.name.name.to_uppercase(), token.name.name ).unwrap(); } Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => { // TODO: Generate client methods } Item::Use(_) => { // Skip use statements } } } Ok(output) } } // Register the Rust generator pub static RUST_GENERATOR: RustGenerator = RustGenerator; register_generator!(RUST_GENERATOR);