A human-friendly DSL for ATProto Lexicons
Something went wrong. Try again.
8.0 kB · 205 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206use mlf_codegen::{register_generator, CodeGenerator, GeneratorContext};use mlf_lang::ast::*;use std::fmt::Write;
pub struct GoGenerator;
impl GoGenerator { pub const NAME: &'static str = "go";
fn generate_type(&self, ty: &Type, optional: bool, ctx: &GeneratorContext) -> Result<String, String> { let base_type = match ty { Type::Primitive { kind, .. } => match kind { PrimitiveType::Null => "interface{}", PrimitiveType::Boolean => "bool", PrimitiveType::Integer => "int64", PrimitiveType::String => "string", PrimitiveType::Bytes => "[]byte", PrimitiveType::Blob => "[]byte", // Annotation idea: @goType("custom.BlobType") }.to_string(), 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 "Did" | "AtUri" | "Cid" | "AtIdentifier" | "Handle" | "Nsid" | "Tid" | "RecordKey" | "Uri" | "Language" => { "string".to_string() } _ => { // Local reference path.segments.last().unwrap().name.clone() } } } Type::Array { inner, .. } => { let inner_type = self.generate_type(inner, false, ctx)?; format!("[]{}", inner_type) } Type::Union { .. } => { // Go doesn't have union types, use interface{} // Annotation idea: @goUnion to generate type switch helpers "interface{}".to_string() } Type::Object { fields, .. } => { let mut obj = String::from("struct {\n"); for field in fields { let field_name = self.capitalize(&field.name.name); let field_type = self.generate_type(&field.ty, field.optional, ctx)?; let json_name = field.name.name.clone();
if !field.docs.is_empty() { write!(obj, "\t\t// {}\n", field.docs[0].text).unwrap(); } write!(obj, "\t\t{} {} `json:\"{}", field_name, field_type, json_name).unwrap(); if field.optional { write!(obj, ",omitempty").unwrap(); } writeln!(obj, "\"`").unwrap(); } obj.push_str("\t}"); obj } Type::Parenthesized { inner, .. } => { return self.generate_type(inner, optional, ctx); } Type::Constrained { base, .. } => { return self.generate_type(base, optional, ctx); } Type::Unknown { .. } => "interface{}".to_string(), };
// For optional fields, use pointer types if optional { Ok(format!("*{}", base_type)) } else { Ok(base_type) } }
fn capitalize(&self, s: &str) -> String { let mut chars = s.chars(); match chars.next() { None => String::new(), Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(), } }
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 GoGenerator { fn name(&self) -> &'static str { Self::NAME }
fn description(&self) -> &'static str { "Generate Go structs and client code" }
fn file_extension(&self) -> &'static str { ".go" }
fn generate(&self, ctx: &GeneratorContext) -> Result<String, String> { let mut output = String::new();
// Header comment writeln!(output, "// Generated from {}", ctx.namespace).unwrap(); writeln!(output, "// Do not edit manually").unwrap(); writeln!(output).unwrap();
// Package name (use last segment of namespace) let package_name = ctx.namespace.split('.').last().unwrap_or("lexicon"); writeln!(output, "package {}\n", package_name).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, "type {} struct {{", self.capitalize(&record.name.name)).unwrap();
for field in &record.fields { let field_name = self.capitalize(&field.name.name); let field_type = self.generate_type(&field.ty, field.optional, ctx)?; let json_name = &field.name.name;
if !field.docs.is_empty() { writeln!(output, "\t// {}", field.docs[0].text).unwrap(); } write!(output, "\t{} {} `json:\"{}\"", field_name, field_type, json_name).unwrap(); if field.optional { write!(output, ",omitempty").unwrap(); } writeln!(output, "`").unwrap(); }
writeln!(output, "}}\n").unwrap(); } Item::DefType(def) => { output.push_str(&self.generate_doc_comment(&def.docs)); let type_name = self.capitalize(&def.name.name);
match &def.ty { Type::Object { .. } => { // Object types become structs writeln!( output, "type {} {}\n", type_name, self.generate_type(&def.ty, false, ctx)? ).unwrap(); } _ => { // Other types become type aliases writeln!( output, "type {} {}\n", type_name, self.generate_type(&def.ty, false, ctx)? ).unwrap(); } } } Item::InlineType(inline) => { output.push_str(&self.generate_doc_comment(&inline.docs)); writeln!( output, "type {} {}\n", self.capitalize(&inline.name.name), self.generate_type(&inline.ty, false, ctx)? ).unwrap(); } Item::Token(token) => { output.push_str(&self.generate_doc_comment(&token.docs)); let const_name = token.name.name.to_uppercase(); writeln!(output, "const {} = \"{}\"\n", const_name, token.name.name).unwrap(); } Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => { // TODO: Generate client methods } Item::Use(_) => { // Skip use statements } } }
Ok(output) }}
// Register the Go generatorpub static GO_GENERATOR: GoGenerator = GoGenerator;register_generator!(GO_GENERATOR);