1use crate::error::Diagnostic;
12use crate::lexer::Lexer;
13use crate::parser::ast::{EnumDeclaration, SchemaDeclaration, Stmt, TypeName};
14use crate::parser::Parser;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Lang {
18 Rust,
19 TypeScript,
20 Go,
21}
22
23impl Lang {
24 pub fn parse(s: &str) -> Option<Lang> {
25 match s {
26 "rust" | "rs" => Some(Lang::Rust),
27 "ts" | "typescript" => Some(Lang::TypeScript),
28 "go" | "golang" => Some(Lang::Go),
29 _ => None,
30 }
31 }
32}
33
34enum Decl<'a> {
36 Schema(&'a SchemaDeclaration<'a>),
37 Enum(&'a EnumDeclaration<'a>),
38}
39
40pub fn generate(src: &str, lang: Lang) -> Result<String, Diagnostic> {
42 let tokens = Lexer::new(src, 0).tokenize()?;
43 let module = Parser::new(tokens)
44 .parse_module()
45 .map_err(|mut ds| ds.remove(0))?;
46
47 let decls: Vec<Decl> = module
48 .stmts
49 .iter()
50 .filter_map(|s| match s {
51 Stmt::TypeDecl(t) => Some(Decl::Schema(t)),
52 Stmt::EnumDecl(e) => Some(Decl::Enum(e)),
53 _ => None,
54 })
55 .collect();
56
57 Ok(match lang {
60 Lang::Rust => rust(&decls),
61 Lang::TypeScript => typescript(&decls),
62 Lang::Go => go(&decls),
63 })
64}
65
66fn pascal(s: &str) -> String {
72 let mut out = String::new();
73 let mut upper = true;
74 for c in s.chars() {
75 if c.is_alphanumeric() {
76 if upper {
77 out.extend(c.to_uppercase());
78 } else {
79 out.push(c);
80 }
81 upper = false;
82 } else {
83 upper = true;
84 }
85 }
86 if out.is_empty() {
87 out.push_str("Empty");
88 } else if out.starts_with(|c: char| c.is_ascii_digit()) {
89 out.insert(0, 'N');
90 }
91 out
92}
93
94fn exported(s: &str) -> String {
97 pascal(s)
98}
99
100const HEADER: &str = "Generated by `aura types` — do not edit by hand.";
101
102fn rust_ty(ty: &TypeName) -> String {
105 match ty {
106 TypeName::String => "String".into(),
107 TypeName::Int => "i64".into(),
108 TypeName::Float => "f64".into(),
109 TypeName::Bool => "bool".into(),
110 TypeName::List => "Vec<serde_json::Value>".into(),
111 TypeName::Object => "serde_json::Map<String, serde_json::Value>".into(),
112 TypeName::Custom(name) => (*name).to_string(),
113 }
114}
115
116fn rust(decls: &[Decl]) -> String {
117 let mut out = format!("// {HEADER}\n\nuse serde::{{Deserialize, Serialize}};\n");
118 for d in decls {
119 match d {
120 Decl::Enum(e) => {
121 out.push_str(&format!(
122 "\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum {} {{\n",
123 e.name
124 ));
125 for m in &e.members {
126 out.push_str(&format!(
127 " #[serde(rename = \"{m}\")]\n {},\n",
128 pascal(m)
129 ));
130 }
131 out.push_str("}\n");
132 }
133 Decl::Schema(s) => {
134 out.push_str(&format!(
135 "\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {} {{\n",
136 s.name
137 ));
138 for f in &s.fields {
139 out.push_str(&format!(" pub {}: {},\n", f.name, rust_ty(&f.ty)));
140 }
141 out.push_str("}\n");
142 }
143 }
144 }
145 out
146}
147
148fn ts_ty(ty: &TypeName) -> String {
151 match ty {
152 TypeName::String => "string".into(),
153 TypeName::Int | TypeName::Float => "number".into(),
154 TypeName::Bool => "boolean".into(),
155 TypeName::List => "unknown[]".into(),
156 TypeName::Object => "Record<string, unknown>".into(),
157 TypeName::Custom(name) => (*name).to_string(),
158 }
159}
160
161fn typescript(decls: &[Decl]) -> String {
162 let mut out = format!("// {HEADER}\n");
163 for d in decls {
164 match d {
165 Decl::Enum(e) => {
167 let union = e
168 .members
169 .iter()
170 .map(|m| format!("\"{m}\""))
171 .collect::<Vec<_>>()
172 .join(" | ");
173 out.push_str(&format!("\nexport type {} = {union};\n", e.name));
174 }
175 Decl::Schema(s) => {
176 out.push_str(&format!("\nexport interface {} {{\n", s.name));
177 for f in &s.fields {
178 out.push_str(&format!(" {}: {};\n", f.name, ts_ty(&f.ty)));
179 }
180 out.push_str("}\n");
181 }
182 }
183 }
184 out
185}
186
187fn go_ty(ty: &TypeName) -> String {
190 match ty {
191 TypeName::String => "string".into(),
192 TypeName::Int => "int64".into(),
193 TypeName::Float => "float64".into(),
194 TypeName::Bool => "bool".into(),
195 TypeName::List => "[]any".into(),
196 TypeName::Object => "map[string]any".into(),
197 TypeName::Custom(name) => (*name).to_string(),
198 }
199}
200
201fn align_go(rows: &[Vec<String>]) -> String {
206 let cols = rows.iter().map(Vec::len).max().unwrap_or(0);
207 let widths: Vec<usize> = (0..cols)
208 .map(|i| {
209 rows.iter()
210 .filter(|r| i + 1 < r.len()) .map(|r| r[i].chars().count())
212 .max()
213 .unwrap_or(0)
214 })
215 .collect();
216 let mut out = String::new();
217 for row in rows {
218 out.push('\t');
219 for (i, cell) in row.iter().enumerate() {
220 out.push_str(cell);
221 if i + 1 < row.len() {
222 let pad = widths[i].saturating_sub(cell.chars().count());
223 out.push_str(&" ".repeat(pad + 1));
224 }
225 }
226 out.push('\n');
227 }
228 out
229}
230
231fn go(decls: &[Decl]) -> String {
232 let mut out = format!("// {HEADER}\n\npackage config\n");
233 for d in decls {
234 match d {
235 Decl::Enum(e) => {
237 out.push_str(&format!("\ntype {} string\n\nconst (\n", e.name));
238 let rows: Vec<Vec<String>> = e
239 .members
240 .iter()
241 .map(|m| {
242 vec![
243 format!("{}{}", e.name, pascal(m)),
244 e.name.to_string(),
245 format!("= \"{m}\""),
246 ]
247 })
248 .collect();
249 out.push_str(&align_go(&rows));
250 out.push_str(")\n");
251 }
252 Decl::Schema(s) => {
253 out.push_str(&format!("\ntype {} struct {{\n", s.name));
254 let rows: Vec<Vec<String>> = s
255 .fields
256 .iter()
257 .map(|f| {
258 vec![
259 exported(f.name),
260 go_ty(&f.ty),
261 format!("`json:\"{}\"`", f.name),
262 ]
263 })
264 .collect();
265 out.push_str(&align_go(&rows));
266 out.push_str("}\n");
267 }
268 }
269 }
270 out
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 const SRC: &str = concat!(
278 "enum Tier\n \"frontend\"\n \"backend\"\nend\n",
279 "type Endpoint\n host: String\n port: Int = 443\nend\n",
280 "type Service\n",
281 " name: String\n",
282 " tier: Tier\n",
283 " endpoint: Endpoint\n",
284 " ratio: Float\n",
285 " enabled: Bool\n",
286 " tags: List\n",
287 " labels: Object\n",
288 "end\n",
289 );
290
291 #[test]
292 fn rust_output() {
293 let out = generate(SRC, Lang::Rust).unwrap();
294 assert!(out.contains("pub enum Tier {"));
295 assert!(out.contains("#[serde(rename = \"frontend\")]"));
296 assert!(out.contains(" Frontend,"));
297 assert!(out.contains("pub struct Service {"));
298 assert!(out.contains("pub tier: Tier,"));
299 assert!(out.contains("pub endpoint: Endpoint,"));
300 assert!(out.contains("pub port: i64,")); assert!(out.contains("pub tags: Vec<serde_json::Value>,"));
302 assert!(out.contains("pub labels: serde_json::Map<String, serde_json::Value>,"));
303 }
304
305 #[test]
306 fn typescript_output_uses_a_literal_union() {
307 let out = generate(SRC, Lang::TypeScript).unwrap();
308 assert!(out.contains("export type Tier = \"frontend\" | \"backend\";"));
309 assert!(out.contains("export interface Service {"));
310 assert!(out.contains(" tier: Tier;"));
311 assert!(out.contains(" ratio: number;"));
312 assert!(out.contains(" labels: Record<string, unknown>;"));
313 }
314
315 #[test]
316 fn go_output_uses_typed_constants_and_json_tags() {
317 let out = generate(SRC, Lang::Go).unwrap();
318 assert!(out.contains("type Tier string"));
319 assert!(out.contains("\tTierFrontend Tier = \"frontend\""));
320 assert!(out.contains("type Service struct {"));
321 assert!(
323 out.contains("\tTier Tier `json:\"tier\"`\n"),
324 "{out}"
325 );
326 assert!(
327 out.contains("\tLabels map[string]any `json:\"labels\"`\n"),
328 "{out}"
329 );
330 assert!(out.contains("\tTierBackend Tier = \"backend\"\n"), "{out}");
332 }
333
334 #[test]
335 fn member_names_are_sanitised() {
336 let src = "enum Region\n \"eu-central\"\n \"us.east\"\n \"2fa\"\nend\n";
338 let out = generate(src, Lang::Rust).unwrap();
339 assert!(out.contains(" EuCentral,"), "{out}");
340 assert!(out.contains(" UsEast,"), "{out}");
341 assert!(out.contains(" N2fa,"), "{out}"); assert!(out.contains("#[serde(rename = \"eu-central\")]"));
344 }
345
346 #[test]
347 fn a_manifest_without_declarations_emits_only_a_header() {
348 let out = generate("x: 1\n", Lang::TypeScript).unwrap();
349 assert!(out.starts_with("// Generated by"));
350 assert!(!out.contains("interface"));
351 }
352
353 #[test]
354 fn a_broken_manifest_is_an_error_not_a_panic() {
355 assert!(generate("type\n", Lang::Rust).is_err());
356 }
357}