A human-friendly DSL for ATProto Lexicons
1use mlf_codegen::{CodeGenerator, GeneratorContext, register_generator};
2use mlf_lang::ast::*;
3use std::fmt::Write;
4
5pub struct GoGenerator;
6
7impl GoGenerator {
8 pub const NAME: &'static str = "go";
9
10 fn generate_type(
11 &self,
12 ty: &Type,
13 optional: bool,
14 ctx: &GeneratorContext,
15 ) -> Result<String, String> {
16 let base_type = match ty {
17 Type::Primitive { kind, .. } => match kind {
18 PrimitiveType::Null => "interface{}",
19 PrimitiveType::Boolean => "bool",
20 PrimitiveType::Integer => "int64",
21 PrimitiveType::String => "string",
22 PrimitiveType::Bytes => "[]byte",
23 PrimitiveType::Blob => "[]byte", // Annotation idea: @goType("custom.BlobType")
24 }
25 .to_string(),
26 Type::Reference { path, .. } => {
27 let path_str = path.to_string();
28 match path_str.as_str() {
29 // Map standard library types
30 "Datetime" => "string".to_string(), // ISO 8601 string
31 "Did" | "AtUri" | "Cid" | "AtIdentifier" | "Handle" | "Nsid" | "Tid"
32 | "RecordKey" | "Uri" | "Language" => "string".to_string(),
33 _ => {
34 // Local reference
35 path.segments.last().unwrap().name.clone()
36 }
37 }
38 }
39 Type::Array { inner, .. } => {
40 let inner_type = self.generate_type(inner, false, ctx)?;
41 format!("[]{}", inner_type)
42 }
43 Type::Union { .. } => {
44 // Go doesn't have union types, use interface{}
45 // Annotation idea: @goUnion to generate type switch helpers
46 "interface{}".to_string()
47 }
48 Type::Object { fields, .. } => {
49 let mut obj = String::from("struct {\n");
50 for field in fields {
51 let field_name = self.capitalize(&field.name.name);
52 let field_type = self.generate_type(&field.ty, field.optional, ctx)?;
53 let json_name = field.name.name.clone();
54
55 if !field.docs.is_empty() {
56 write!(obj, "\t\t// {}\n", field.docs[0].text).unwrap();
57 }
58 write!(
59 obj,
60 "\t\t{} {} `json:\"{}",
61 field_name, field_type, json_name
62 )
63 .unwrap();
64 if field.optional {
65 write!(obj, ",omitempty").unwrap();
66 }
67 writeln!(obj, "\"`").unwrap();
68 }
69 obj.push_str("\t}");
70 obj
71 }
72 Type::Parenthesized { inner, .. } => {
73 return self.generate_type(inner, optional, ctx);
74 }
75 Type::Constrained { base, .. } => {
76 return self.generate_type(base, optional, ctx);
77 }
78 Type::Unknown { .. } => "interface{}".to_string(),
79 };
80
81 // For optional fields, use pointer types
82 if optional {
83 Ok(format!("*{}", base_type))
84 } else {
85 Ok(base_type)
86 }
87 }
88
89 fn capitalize(&self, s: &str) -> String {
90 let mut chars = s.chars();
91 match chars.next() {
92 None => String::new(),
93 Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
94 }
95 }
96
97 fn generate_doc_comment(&self, docs: &[DocComment]) -> String {
98 if docs.is_empty() {
99 return String::new();
100 }
101
102 let mut result = String::new();
103 for doc in docs {
104 result.push_str("// ");
105 result.push_str(&doc.text);
106 result.push('\n');
107 }
108 result
109 }
110}
111
112impl CodeGenerator for GoGenerator {
113 fn name(&self) -> &'static str {
114 Self::NAME
115 }
116
117 fn description(&self) -> &'static str {
118 "Generate Go structs and client code"
119 }
120
121 fn file_extension(&self) -> &'static str {
122 ".go"
123 }
124
125 fn generate(&self, ctx: &GeneratorContext) -> Result<String, String> {
126 let mut output = String::new();
127
128 // Header comment
129 writeln!(output, "// Generated from {}", ctx.namespace).unwrap();
130 writeln!(output, "// Do not edit manually").unwrap();
131 writeln!(output).unwrap();
132
133 // Package name (use last segment of namespace)
134 let package_name = ctx.namespace.split('.').last().unwrap_or("lexicon");
135 writeln!(output, "package {}\n", package_name).unwrap();
136
137 // Generate code for each item
138 for item in &ctx.lexicon.items {
139 match item {
140 Item::Record(record) => {
141 output.push_str(&self.generate_doc_comment(&record.docs));
142 writeln!(
143 output,
144 "type {} struct {{",
145 self.capitalize(&record.name.name)
146 )
147 .unwrap();
148
149 for field in &record.fields {
150 let field_name = self.capitalize(&field.name.name);
151 let field_type = self.generate_type(&field.ty, field.optional, ctx)?;
152 let json_name = &field.name.name;
153
154 if !field.docs.is_empty() {
155 writeln!(output, "\t// {}", field.docs[0].text).unwrap();
156 }
157 write!(
158 output,
159 "\t{} {} `json:\"{}\"",
160 field_name, field_type, json_name
161 )
162 .unwrap();
163 if field.optional {
164 write!(output, ",omitempty").unwrap();
165 }
166 writeln!(output, "`").unwrap();
167 }
168
169 writeln!(output, "}}\n").unwrap();
170 }
171 Item::DefType(def) => {
172 output.push_str(&self.generate_doc_comment(&def.docs));
173 let type_name = self.capitalize(&def.name.name);
174
175 match &def.ty {
176 Type::Object { .. } => {
177 // Object types become structs
178 writeln!(
179 output,
180 "type {} {}\n",
181 type_name,
182 self.generate_type(&def.ty, false, ctx)?
183 )
184 .unwrap();
185 }
186 _ => {
187 // Other types become type aliases
188 writeln!(
189 output,
190 "type {} {}\n",
191 type_name,
192 self.generate_type(&def.ty, false, ctx)?
193 )
194 .unwrap();
195 }
196 }
197 }
198 Item::InlineType(inline) => {
199 output.push_str(&self.generate_doc_comment(&inline.docs));
200 writeln!(
201 output,
202 "type {} {}\n",
203 self.capitalize(&inline.name.name),
204 self.generate_type(&inline.ty, false, ctx)?
205 )
206 .unwrap();
207 }
208 Item::Token(token) => {
209 output.push_str(&self.generate_doc_comment(&token.docs));
210 let const_name = token.name.name.to_uppercase();
211 writeln!(output, "const {} = \"{}\"\n", const_name, token.name.name).unwrap();
212 }
213 Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => {
214 // TODO: Generate client methods
215 }
216 Item::Use(_) | Item::SelfItem(_) => {
217 // Skip `use` statements and the `self { }` item — both
218 // are lexicon-level metadata, not types to emit.
219 }
220 }
221 }
222
223 Ok(output)
224 }
225}
226
227// Register the Go generator
228pub static GO_GENERATOR: GoGenerator = GoGenerator;
229register_generator!(GO_GENERATOR);