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