A human-friendly DSL for ATProto Lexicons
at main 231 lines 9.7 kB view raw
1use mlf_codegen::{register_generator, CodeGenerator, GeneratorContext}; 2use mlf_lang::ast::*; 3use std::fmt::Write; 4 5pub struct RustGenerator; 6 7impl RustGenerator { 8 pub const NAME: &'static str = "rust"; 9 10 fn to_snake_case(&self, s: &str) -> String { 11 let mut result = String::new(); 12 for (i, ch) in s.chars().enumerate() { 13 if ch.is_uppercase() && i > 0 { 14 result.push('_'); 15 } 16 result.push(ch.to_lowercase().next().unwrap()); 17 } 18 result 19 } 20 21 fn to_pascal_case(&self, s: &str) -> String { 22 let mut chars = s.chars(); 23 match chars.next() { 24 None => String::new(), 25 Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(), 26 } 27 } 28 29 fn generate_type(&self, ty: &Type, optional: bool, ctx: &GeneratorContext) -> Result<String, String> { 30 let base_type = match ty { 31 Type::Primitive { kind, .. } => match kind { 32 PrimitiveType::Null => "()".to_string(), // Unit type for null 33 PrimitiveType::Boolean => "bool".to_string(), 34 PrimitiveType::Integer => "i64".to_string(), 35 PrimitiveType::String => "String".to_string(), 36 PrimitiveType::Bytes => "Vec<u8>".to_string(), 37 PrimitiveType::Blob => "Vec<u8>".to_string(), // Annotation idea: @rustType("custom::BlobType") 38 }, 39 Type::Reference { path, .. } => { 40 let path_str = path.to_string(); 41 match path_str.as_str() { 42 // Map standard library types 43 "Datetime" => "String".to_string(), // ISO 8601 string, could use chrono::DateTime 44 "Did" | "AtUri" | "Cid" | "AtIdentifier" | "Handle" | "Nsid" | "Tid" | "RecordKey" | "Uri" | "Language" => { 45 "String".to_string() 46 } 47 _ => { 48 // Local reference - convert to PascalCase 49 self.to_pascal_case(&path.segments.last().unwrap().name) 50 } 51 } 52 } 53 Type::Array { inner, .. } => { 54 let inner_type = self.generate_type(inner, false, ctx)?; 55 format!("Vec<{}>", inner_type) 56 } 57 Type::Union { types, .. } => { 58 // Rust doesn't have direct union types, use an enum 59 // For now, generate a simple representation 60 // Annotation idea: @rustEnum to customize enum generation 61 if types.len() == 2 && matches!(types[0], Type::Primitive { kind: PrimitiveType::Null, .. }) { 62 // Special case: null | T becomes Option<T> 63 return self.generate_type(&types[1], true, ctx); 64 } else if types.len() == 2 && matches!(types[1], Type::Primitive { kind: PrimitiveType::Null, .. }) { 65 return self.generate_type(&types[0], true, ctx); 66 } 67 // Otherwise use serde_json::Value for flexibility 68 "serde_json::Value".to_string() 69 } 70 Type::Object { .. } => { 71 // Inline struct types aren't idiomatic in Rust 72 // We'd need to generate a named type 73 // For now, use serde_json::Value 74 // Annotation idea: @rustInlineStruct to force inline generation 75 "serde_json::Value".to_string() 76 } 77 Type::Parenthesized { inner, .. } => { 78 return self.generate_type(inner, optional, ctx); 79 } 80 Type::Constrained { base, .. } => { 81 return self.generate_type(base, optional, ctx); 82 } 83 Type::Unknown { .. } => "serde_json::Value".to_string(), 84 }; 85 86 if optional { 87 Ok(format!("Option<{}>", base_type)) 88 } else { 89 Ok(base_type) 90 } 91 } 92 93 fn generate_doc_comment(&self, docs: &[DocComment]) -> String { 94 if docs.is_empty() { 95 return String::new(); 96 } 97 98 let mut result = String::new(); 99 for doc in docs { 100 result.push_str("/// "); 101 result.push_str(&doc.text); 102 result.push('\n'); 103 } 104 result 105 } 106} 107 108impl CodeGenerator for RustGenerator { 109 fn name(&self) -> &'static str { 110 Self::NAME 111 } 112 113 fn description(&self) -> &'static str { 114 "Generate Rust structs and client code" 115 } 116 117 fn file_extension(&self) -> &'static str { 118 ".rs" 119 } 120 121 fn generate(&self, ctx: &GeneratorContext) -> Result<String, String> { 122 let mut output = String::new(); 123 124 // Header comment 125 writeln!(output, "// Generated from {}", ctx.namespace).unwrap(); 126 writeln!(output, "// Do not edit manually").unwrap(); 127 writeln!(output).unwrap(); 128 129 // Common imports 130 writeln!(output, "use serde::{{Deserialize, Serialize}};\n").unwrap(); 131 132 // Generate code for each item 133 for item in &ctx.lexicon.items { 134 match item { 135 Item::Record(record) => { 136 output.push_str(&self.generate_doc_comment(&record.docs)); 137 writeln!(output, "#[derive(Debug, Clone, Serialize, Deserialize)]").unwrap(); 138 writeln!(output, "pub struct {} {{", self.to_pascal_case(&record.name.name)).unwrap(); 139 140 for field in &record.fields { 141 if !field.docs.is_empty() { 142 writeln!(output, " /// {}", field.docs[0].text).unwrap(); 143 } 144 145 // Use serde rename for camelCase fields 146 if field.name.name != self.to_snake_case(&field.name.name) { 147 writeln!(output, " #[serde(rename = \"{}\")]", field.name.name).unwrap(); 148 } 149 150 // Skip serializing None values for optional fields 151 if field.optional { 152 writeln!(output, " #[serde(skip_serializing_if = \"Option::is_none\")]").unwrap(); 153 } 154 155 let field_type = self.generate_type(&field.ty, field.optional, ctx)?; 156 writeln!(output, " pub {}: {},", self.to_snake_case(&field.name.name), field_type).unwrap(); 157 } 158 159 writeln!(output, "}}\n").unwrap(); 160 } 161 Item::DefType(def) => { 162 output.push_str(&self.generate_doc_comment(&def.docs)); 163 164 match &def.ty { 165 Type::Object { fields, .. } => { 166 // Generate a struct for object types 167 writeln!(output, "#[derive(Debug, Clone, Serialize, Deserialize)]").unwrap(); 168 writeln!(output, "pub struct {} {{", self.to_pascal_case(&def.name.name)).unwrap(); 169 170 for field in fields { 171 if !field.docs.is_empty() { 172 writeln!(output, " /// {}", field.docs[0].text).unwrap(); 173 } 174 175 if field.name.name != self.to_snake_case(&field.name.name) { 176 writeln!(output, " #[serde(rename = \"{}\")]", field.name.name).unwrap(); 177 } 178 179 if field.optional { 180 writeln!(output, " #[serde(skip_serializing_if = \"Option::is_none\")]").unwrap(); 181 } 182 183 let field_type = self.generate_type(&field.ty, field.optional, ctx)?; 184 writeln!(output, " pub {}: {},", self.to_snake_case(&field.name.name), field_type).unwrap(); 185 } 186 187 writeln!(output, "}}\n").unwrap(); 188 } 189 _ => { 190 // Type alias 191 writeln!( 192 output, 193 "pub type {} = {};\n", 194 self.to_pascal_case(&def.name.name), 195 self.generate_type(&def.ty, false, ctx)? 196 ).unwrap(); 197 } 198 } 199 } 200 Item::InlineType(inline) => { 201 output.push_str(&self.generate_doc_comment(&inline.docs)); 202 writeln!( 203 output, 204 "pub type {} = {};\n", 205 self.to_pascal_case(&inline.name.name), 206 self.generate_type(&inline.ty, false, ctx)? 207 ).unwrap(); 208 } 209 Item::Token(token) => { 210 output.push_str(&self.generate_doc_comment(&token.docs)); 211 writeln!(output, "pub const {}: &str = \"{}\";\n", 212 token.name.name.to_uppercase(), 213 token.name.name 214 ).unwrap(); 215 } 216 Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => { 217 // TODO: Generate client methods 218 } 219 Item::Use(_) => { 220 // Skip use statements 221 } 222 } 223 } 224 225 Ok(output) 226 } 227} 228 229// Register the Rust generator 230pub static RUST_GENERATOR: RustGenerator = RustGenerator; 231register_generator!(RUST_GENERATOR);