A human-friendly DSL for ATProto Lexicons
27
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 295 lines 12 kB view raw
1use mlf_codegen::{CodeGenerator, GeneratorContext, register_generator}; 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( 30 &self, 31 ty: &Type, 32 optional: bool, 33 ctx: &GeneratorContext, 34 ) -> Result<String, String> { 35 let base_type = match ty { 36 Type::Primitive { kind, .. } => match kind { 37 PrimitiveType::Null => "()".to_string(), // Unit type for null 38 PrimitiveType::Boolean => "bool".to_string(), 39 PrimitiveType::Integer => "i64".to_string(), 40 PrimitiveType::String => "String".to_string(), 41 PrimitiveType::Bytes => "Vec<u8>".to_string(), 42 PrimitiveType::Blob => "Vec<u8>".to_string(), // Annotation idea: @rustType("custom::BlobType") 43 }, 44 Type::Reference { path, .. } => { 45 let path_str = path.to_string(); 46 match path_str.as_str() { 47 // Map standard library types 48 "Datetime" => "String".to_string(), // ISO 8601 string, could use chrono::DateTime 49 "Did" | "AtUri" | "Cid" | "AtIdentifier" | "Handle" | "Nsid" | "Tid" 50 | "RecordKey" | "Uri" | "Language" => "String".to_string(), 51 _ => { 52 // Local reference - convert to PascalCase 53 self.to_pascal_case(&path.segments.last().unwrap().name) 54 } 55 } 56 } 57 Type::Array { inner, .. } => { 58 let inner_type = self.generate_type(inner, false, ctx)?; 59 format!("Vec<{}>", inner_type) 60 } 61 Type::Union { types, .. } => { 62 // Rust doesn't have direct union types, use an enum 63 // For now, generate a simple representation 64 // Annotation idea: @rustEnum to customize enum generation 65 if types.len() == 2 66 && matches!( 67 types[0], 68 Type::Primitive { 69 kind: PrimitiveType::Null, 70 .. 71 } 72 ) 73 { 74 // Special case: null | T becomes Option<T> 75 return self.generate_type(&types[1], true, ctx); 76 } else if types.len() == 2 77 && matches!( 78 types[1], 79 Type::Primitive { 80 kind: PrimitiveType::Null, 81 .. 82 } 83 ) 84 { 85 return self.generate_type(&types[0], true, ctx); 86 } 87 // Otherwise use serde_json::Value for flexibility 88 "serde_json::Value".to_string() 89 } 90 Type::Object { .. } => { 91 // Inline struct types aren't idiomatic in Rust 92 // We'd need to generate a named type 93 // For now, use serde_json::Value 94 // Annotation idea: @rustInlineStruct to force inline generation 95 "serde_json::Value".to_string() 96 } 97 Type::Parenthesized { inner, .. } => { 98 return self.generate_type(inner, optional, ctx); 99 } 100 Type::Constrained { base, .. } => { 101 return self.generate_type(base, optional, ctx); 102 } 103 Type::Unknown { .. } => "serde_json::Value".to_string(), 104 }; 105 106 if optional { 107 Ok(format!("Option<{}>", base_type)) 108 } else { 109 Ok(base_type) 110 } 111 } 112 113 fn generate_doc_comment(&self, docs: &[DocComment]) -> String { 114 if docs.is_empty() { 115 return String::new(); 116 } 117 118 let mut result = String::new(); 119 for doc in docs { 120 result.push_str("/// "); 121 result.push_str(&doc.text); 122 result.push('\n'); 123 } 124 result 125 } 126} 127 128impl CodeGenerator for RustGenerator { 129 fn name(&self) -> &'static str { 130 Self::NAME 131 } 132 133 fn description(&self) -> &'static str { 134 "Generate Rust structs and client code" 135 } 136 137 fn file_extension(&self) -> &'static str { 138 ".rs" 139 } 140 141 fn generate(&self, ctx: &GeneratorContext) -> Result<String, String> { 142 let mut output = String::new(); 143 144 // Header comment 145 writeln!(output, "// Generated from {}", ctx.namespace).unwrap(); 146 writeln!(output, "// Do not edit manually").unwrap(); 147 writeln!(output).unwrap(); 148 149 // Common imports 150 writeln!(output, "use serde::{{Deserialize, Serialize}};\n").unwrap(); 151 152 // Generate code for each item 153 for item in &ctx.lexicon.items { 154 match item { 155 Item::Record(record) => { 156 output.push_str(&self.generate_doc_comment(&record.docs)); 157 writeln!(output, "#[derive(Debug, Clone, Serialize, Deserialize)]").unwrap(); 158 writeln!( 159 output, 160 "pub struct {} {{", 161 self.to_pascal_case(&record.name.name) 162 ) 163 .unwrap(); 164 165 for field in &record.fields { 166 if !field.docs.is_empty() { 167 writeln!(output, " /// {}", field.docs[0].text).unwrap(); 168 } 169 170 // Use serde rename for camelCase fields 171 if field.name.name != self.to_snake_case(&field.name.name) { 172 writeln!(output, " #[serde(rename = \"{}\")]", field.name.name) 173 .unwrap(); 174 } 175 176 // Skip serializing None values for optional fields 177 if field.optional { 178 writeln!( 179 output, 180 " #[serde(skip_serializing_if = \"Option::is_none\")]" 181 ) 182 .unwrap(); 183 } 184 185 let field_type = self.generate_type(&field.ty, field.optional, ctx)?; 186 writeln!( 187 output, 188 " pub {}: {},", 189 self.to_snake_case(&field.name.name), 190 field_type 191 ) 192 .unwrap(); 193 } 194 195 writeln!(output, "}}\n").unwrap(); 196 } 197 Item::DefType(def) => { 198 output.push_str(&self.generate_doc_comment(&def.docs)); 199 200 match &def.ty { 201 Type::Object { fields, .. } => { 202 // Generate a struct for object types 203 writeln!(output, "#[derive(Debug, Clone, Serialize, Deserialize)]") 204 .unwrap(); 205 writeln!( 206 output, 207 "pub struct {} {{", 208 self.to_pascal_case(&def.name.name) 209 ) 210 .unwrap(); 211 212 for field in fields { 213 if !field.docs.is_empty() { 214 writeln!(output, " /// {}", field.docs[0].text).unwrap(); 215 } 216 217 if field.name.name != self.to_snake_case(&field.name.name) { 218 writeln!( 219 output, 220 " #[serde(rename = \"{}\")]", 221 field.name.name 222 ) 223 .unwrap(); 224 } 225 226 if field.optional { 227 writeln!( 228 output, 229 " #[serde(skip_serializing_if = \"Option::is_none\")]" 230 ) 231 .unwrap(); 232 } 233 234 let field_type = 235 self.generate_type(&field.ty, field.optional, ctx)?; 236 writeln!( 237 output, 238 " pub {}: {},", 239 self.to_snake_case(&field.name.name), 240 field_type 241 ) 242 .unwrap(); 243 } 244 245 writeln!(output, "}}\n").unwrap(); 246 } 247 _ => { 248 // Type alias 249 writeln!( 250 output, 251 "pub type {} = {};\n", 252 self.to_pascal_case(&def.name.name), 253 self.generate_type(&def.ty, false, ctx)? 254 ) 255 .unwrap(); 256 } 257 } 258 } 259 Item::InlineType(inline) => { 260 output.push_str(&self.generate_doc_comment(&inline.docs)); 261 writeln!( 262 output, 263 "pub type {} = {};\n", 264 self.to_pascal_case(&inline.name.name), 265 self.generate_type(&inline.ty, false, ctx)? 266 ) 267 .unwrap(); 268 } 269 Item::Token(token) => { 270 output.push_str(&self.generate_doc_comment(&token.docs)); 271 writeln!( 272 output, 273 "pub const {}: &str = \"{}\";\n", 274 token.name.name.to_uppercase(), 275 token.name.name 276 ) 277 .unwrap(); 278 } 279 Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => { 280 // TODO: Generate client methods 281 } 282 Item::Use(_) | Item::SelfItem(_) => { 283 // Skip `use` statements and the `self { }` item — both 284 // are lexicon-level metadata, not types to emit. 285 } 286 } 287 } 288 289 Ok(output) 290 } 291} 292 293// Register the Rust generator 294pub static RUST_GENERATOR: RustGenerator = RustGenerator; 295register_generator!(RUST_GENERATOR);