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 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);