A human-friendly DSL for ATProto Lexicons
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);