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 182 lines 7.1 kB view raw
1use mlf_codegen::{CodeGenerator, GeneratorContext, register_generator}; 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" 28 | "RecordKey" | "Uri" | "Language" => "string".to_string(), 29 _ => { 30 // Local or cross-file reference 31 path.segments.last().unwrap().name.clone() 32 } 33 }) 34 } 35 Type::Array { inner, .. } => { 36 let inner_type = self.generate_type(inner, ctx)?; 37 Ok(format!("{}[]", inner_type)) 38 } 39 Type::Union { types, .. } => { 40 let type_strings: Result<Vec<_>, _> = 41 types.iter().map(|t| self.generate_type(t, ctx)).collect(); 42 Ok(type_strings?.join(" | ")) 43 } 44 Type::Object { fields, .. } => { 45 let mut obj = String::from("{\n"); 46 for field in fields { 47 if !field.docs.is_empty() { 48 obj.push_str(" /** "); 49 obj.push_str(&field.docs[0].text); 50 obj.push_str(" */\n"); 51 } 52 obj.push_str(" "); 53 obj.push_str(&field.name.name); 54 if field.optional { 55 obj.push('?'); 56 } 57 obj.push_str(": "); 58 obj.push_str(&self.generate_type(&field.ty, ctx)?); 59 obj.push_str(";\n"); 60 } 61 obj.push('}'); 62 Ok(obj) 63 } 64 Type::Parenthesized { inner, .. } => { 65 let inner_type = self.generate_type(inner, ctx)?; 66 Ok(format!("({})", inner_type)) 67 } 68 Type::Constrained { base, .. } => { 69 // For constrained types, just use the base type 70 // Annotations like @min, @max could be added for runtime validation libraries 71 self.generate_type(base, ctx) 72 } 73 Type::Unknown { .. } => Ok("unknown".to_string()), 74 } 75 } 76 77 fn generate_doc_comment(&self, docs: &[DocComment]) -> String { 78 if docs.is_empty() { 79 return String::new(); 80 } 81 82 let mut result = String::from("/**\n"); 83 for doc in docs { 84 result.push_str(" * "); 85 result.push_str(&doc.text); 86 result.push('\n'); 87 } 88 result.push_str(" */\n"); 89 result 90 } 91} 92 93impl CodeGenerator for TypeScriptGenerator { 94 fn name(&self) -> &'static str { 95 Self::NAME 96 } 97 98 fn description(&self) -> &'static str { 99 "Generate TypeScript type definitions and client code" 100 } 101 102 fn file_extension(&self) -> &'static str { 103 ".ts" 104 } 105 106 fn generate(&self, ctx: &GeneratorContext) -> Result<String, String> { 107 let mut output = String::new(); 108 109 // Header comment 110 writeln!(output, "/**").unwrap(); 111 writeln!(output, " * Generated from {}", ctx.namespace).unwrap(); 112 writeln!(output, " * Do not edit manually").unwrap(); 113 writeln!(output, " */").unwrap(); 114 writeln!(output).unwrap(); 115 116 // Generate code for each item 117 for item in &ctx.lexicon.items { 118 match item { 119 Item::Record(record) => { 120 output.push_str(&self.generate_doc_comment(&record.docs)); 121 writeln!(output, "export interface {} {{", record.name.name).unwrap(); 122 123 for field in &record.fields { 124 if !field.docs.is_empty() { 125 write!(output, " /** {} */\n", field.docs[0].text).unwrap(); 126 } 127 write!(output, " {}", field.name.name).unwrap(); 128 if field.optional { 129 write!(output, "?").unwrap(); 130 } 131 writeln!(output, ": {};", self.generate_type(&field.ty, ctx)?).unwrap(); 132 } 133 134 writeln!(output, "}}\n").unwrap(); 135 } 136 Item::DefType(def) => { 137 output.push_str(&self.generate_doc_comment(&def.docs)); 138 writeln!( 139 output, 140 "export type {} = {};\n", 141 def.name.name, 142 self.generate_type(&def.ty, ctx)? 143 ) 144 .unwrap(); 145 } 146 Item::InlineType(inline) => { 147 output.push_str(&self.generate_doc_comment(&inline.docs)); 148 writeln!( 149 output, 150 "export type {} = {};\n", 151 inline.name.name, 152 self.generate_type(&inline.ty, ctx)? 153 ) 154 .unwrap(); 155 } 156 Item::Token(token) => { 157 output.push_str(&self.generate_doc_comment(&token.docs)); 158 writeln!( 159 output, 160 "export const {} = Symbol('{}');\n", 161 token.name.name, token.name.name 162 ) 163 .unwrap(); 164 } 165 Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) => { 166 // TODO: Generate client methods for these 167 // Annotation idea: @clientMethod for custom generation 168 } 169 Item::Use(_) | Item::SelfItem(_) => { 170 // Skip `use` statements and the `self { }` item — both 171 // are lexicon-level metadata, not types to emit. 172 } 173 } 174 } 175 176 Ok(output) 177 } 178} 179 180// Register the TypeScript generator 181pub static TYPESCRIPT_GENERATOR: TypeScriptGenerator = TypeScriptGenerator; 182register_generator!(TYPESCRIPT_GENERATOR);