A human-friendly DSL for ATProto Lexicons
at main 177 lines 6.9 kB view raw
1use mlf_codegen::{register_generator, CodeGenerator, GeneratorContext}; 2use mlf_lang::ast::*; 3use std::fmt::Write; 4 5pub struct TypeScriptGenerator; 6 7impl TypeScriptGenerator { 8 pub const NAME: &'static str = "typescript"; 9 10 fn generate_type(&self, ty: &Type, ctx: &GeneratorContext) -> Result<String, String> { 11 match ty { 12 Type::Primitive { kind, .. } => Ok(match kind { 13 PrimitiveType::Null => "null".to_string(), 14 PrimitiveType::Boolean => "boolean".to_string(), 15 PrimitiveType::Integer => "number".to_string(), 16 PrimitiveType::String => "string".to_string(), 17 PrimitiveType::Bytes => "Uint8Array".to_string(), 18 PrimitiveType::Blob => "Blob".to_string(), // Annotation idea: @tsType("CustomBlobType") 19 }), 20 Type::Reference { path, .. } => { 21 // Check if it's from the standard library 22 let path_str = path.to_string(); 23 24 // Map standard library types to TypeScript types 25 Ok(match path_str.as_str() { 26 "Datetime" => "string".to_string(), // ISO 8601 27 "Did" | "AtUri" | "Cid" | "AtIdentifier" | "Handle" | "Nsid" | "Tid" | "RecordKey" | "Uri" | "Language" => { 28 "string".to_string() 29 } 30 _ => { 31 // Local or cross-file reference 32 path.segments.last().unwrap().name.clone() 33 } 34 }) 35 } 36 Type::Array { inner, .. } => { 37 let inner_type = self.generate_type(inner, ctx)?; 38 Ok(format!("{}[]", inner_type)) 39 } 40 Type::Union { types, .. } => { 41 let type_strings: Result<Vec<_>, _> = types 42 .iter() 43 .map(|t| self.generate_type(t, ctx)) 44 .collect(); 45 Ok(type_strings?.join(" | ")) 46 } 47 Type::Object { fields, .. } => { 48 let mut obj = String::from("{\n"); 49 for field in fields { 50 if !field.docs.is_empty() { 51 obj.push_str(" /** "); 52 obj.push_str(&field.docs[0].text); 53 obj.push_str(" */\n"); 54 } 55 obj.push_str(" "); 56 obj.push_str(&field.name.name); 57 if field.optional { 58 obj.push('?'); 59 } 60 obj.push_str(": "); 61 obj.push_str(&self.generate_type(&field.ty, ctx)?); 62 obj.push_str(";\n"); 63 } 64 obj.push('}'); 65 Ok(obj) 66 } 67 Type::Parenthesized { inner, .. } => { 68 let inner_type = self.generate_type(inner, ctx)?; 69 Ok(format!("({})", inner_type)) 70 } 71 Type::Constrained { base, .. } => { 72 // For constrained types, just use the base type 73 // Annotations like @min, @max could be added for runtime validation libraries 74 self.generate_type(base, ctx) 75 } 76 Type::Unknown { .. } => Ok("unknown".to_string()), 77 } 78 } 79 80 fn generate_doc_comment(&self, docs: &[DocComment]) -> String { 81 if docs.is_empty() { 82 return String::new(); 83 } 84 85 let mut result = String::from("/**\n"); 86 for doc in docs { 87 result.push_str(" * "); 88 result.push_str(&doc.text); 89 result.push('\n'); 90 } 91 result.push_str(" */\n"); 92 result 93 } 94} 95 96impl CodeGenerator for TypeScriptGenerator { 97 fn name(&self) -> &'static str { 98 Self::NAME 99 } 100 101 fn description(&self) -> &'static str { 102 "Generate TypeScript type definitions and client code" 103 } 104 105 fn file_extension(&self) -> &'static str { 106 ".ts" 107 } 108 109 fn generate(&self, ctx: &GeneratorContext) -> Result<String, String> { 110 let mut output = String::new(); 111 112 // Header comment 113 writeln!(output, "/**").unwrap(); 114 writeln!(output, " * Generated from {}", ctx.namespace).unwrap(); 115 writeln!(output, " * Do not edit manually").unwrap(); 116 writeln!(output, " */").unwrap(); 117 writeln!(output).unwrap(); 118 119 // Generate code for each item 120 for item in &ctx.lexicon.items { 121 match item { 122 Item::Record(record) => { 123 output.push_str(&self.generate_doc_comment(&record.docs)); 124 writeln!(output, "export interface {} {{", record.name.name).unwrap(); 125 126 for field in &record.fields { 127 if !field.docs.is_empty() { 128 write!(output, " /** {} */\n", field.docs[0].text).unwrap(); 129 } 130 write!(output, " {}", field.name.name).unwrap(); 131 if field.optional { 132 write!(output, "?").unwrap(); 133 } 134 writeln!(output, ": {};", self.generate_type(&field.ty, ctx)?).unwrap(); 135 } 136 137 writeln!(output, "}}\n").unwrap(); 138 } 139 Item::DefType(def) => { 140 output.push_str(&self.generate_doc_comment(&def.docs)); 141 writeln!( 142 output, 143 "export type {} = {};\n", 144 def.name.name, 145 self.generate_type(&def.ty, ctx)? 146 ).unwrap(); 147 } 148 Item::InlineType(inline) => { 149 output.push_str(&self.generate_doc_comment(&inline.docs)); 150 writeln!( 151 output, 152 "export type {} = {};\n", 153 inline.name.name, 154 self.generate_type(&inline.ty, ctx)? 155 ).unwrap(); 156 } 157 Item::Token(token) => { 158 output.push_str(&self.generate_doc_comment(&token.docs)); 159 writeln!(output, "export const {} = Symbol('{}');\n", token.name.name, token.name.name).unwrap(); 160 } 161 Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => { 162 // TODO: Generate client methods for these 163 // Annotation idea: @clientMethod for custom generation 164 } 165 Item::Use(_) => { 166 // Skip use statements in output 167 } 168 } 169 } 170 171 Ok(output) 172 } 173} 174 175// Register the TypeScript generator 176pub static TYPESCRIPT_GENERATOR: TypeScriptGenerator = TypeScriptGenerator; 177register_generator!(TYPESCRIPT_GENERATOR);