use crate::error::Diagnostic;
use crate::lexer::Lexer;
use crate::parser::ast::{EnumDeclaration, SchemaDeclaration, Stmt, TypeName};
use crate::parser::Parser;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Lang {
Rust,
TypeScript,
Go,
}
impl Lang {
pub fn parse(s: &str) -> Option<Lang> {
match s {
"rust" | "rs" => Some(Lang::Rust),
"ts" | "typescript" => Some(Lang::TypeScript),
"go" | "golang" => Some(Lang::Go),
_ => None,
}
}
}
enum Decl<'a> {
Schema(&'a SchemaDeclaration<'a>),
Enum(&'a EnumDeclaration<'a>),
}
pub fn generate(src: &str, lang: Lang) -> Result<String, Diagnostic> {
let tokens = Lexer::new(src, 0).tokenize()?;
let module = Parser::new(tokens)
.parse_module()
.map_err(|mut ds| ds.remove(0))?;
let decls: Vec<Decl> = module
.stmts
.iter()
.filter_map(|s| match s {
Stmt::TypeDecl(t) => Some(Decl::Schema(t)),
Stmt::EnumDecl(e) => Some(Decl::Enum(e)),
_ => None,
})
.collect();
Ok(match lang {
Lang::Rust => rust(&decls),
Lang::TypeScript => typescript(&decls),
Lang::Go => go(&decls),
})
}
fn pascal(s: &str) -> String {
let mut out = String::new();
let mut upper = true;
for c in s.chars() {
if c.is_alphanumeric() {
if upper {
out.extend(c.to_uppercase());
} else {
out.push(c);
}
upper = false;
} else {
upper = true;
}
}
if out.is_empty() {
out.push_str("Empty");
} else if out.starts_with(|c: char| c.is_ascii_digit()) {
out.insert(0, 'N');
}
out
}
fn exported(s: &str) -> String {
pascal(s)
}
const HEADER: &str = "Generated by `aura types` — do not edit by hand.";
fn rust_ty(ty: &TypeName) -> String {
match ty {
TypeName::String => "String".into(),
TypeName::Int => "i64".into(),
TypeName::Float => "f64".into(),
TypeName::Bool => "bool".into(),
TypeName::List => "Vec<serde_json::Value>".into(),
TypeName::Object => "serde_json::Map<String, serde_json::Value>".into(),
TypeName::Custom(name) => (*name).to_string(),
}
}
fn rust(decls: &[Decl]) -> String {
let mut out = format!("// {HEADER}\n\nuse serde::{{Deserialize, Serialize}};\n");
for d in decls {
match d {
Decl::Enum(e) => {
out.push_str(&format!(
"\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum {} {{\n",
e.name
));
for m in &e.members {
out.push_str(&format!(
" #[serde(rename = \"{m}\")]\n {},\n",
pascal(m)
));
}
out.push_str("}\n");
}
Decl::Schema(s) => {
out.push_str(&format!(
"\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {} {{\n",
s.name
));
for f in &s.fields {
out.push_str(&format!(" pub {}: {},\n", f.name, rust_ty(&f.ty)));
}
out.push_str("}\n");
}
}
}
out
}
fn ts_ty(ty: &TypeName) -> String {
match ty {
TypeName::String => "string".into(),
TypeName::Int | TypeName::Float => "number".into(),
TypeName::Bool => "boolean".into(),
TypeName::List => "unknown[]".into(),
TypeName::Object => "Record<string, unknown>".into(),
TypeName::Custom(name) => (*name).to_string(),
}
}
fn typescript(decls: &[Decl]) -> String {
let mut out = format!("// {HEADER}\n");
for d in decls {
match d {
Decl::Enum(e) => {
let union = e
.members
.iter()
.map(|m| format!("\"{m}\""))
.collect::<Vec<_>>()
.join(" | ");
out.push_str(&format!("\nexport type {} = {union};\n", e.name));
}
Decl::Schema(s) => {
out.push_str(&format!("\nexport interface {} {{\n", s.name));
for f in &s.fields {
out.push_str(&format!(" {}: {};\n", f.name, ts_ty(&f.ty)));
}
out.push_str("}\n");
}
}
}
out
}
fn go_ty(ty: &TypeName) -> String {
match ty {
TypeName::String => "string".into(),
TypeName::Int => "int64".into(),
TypeName::Float => "float64".into(),
TypeName::Bool => "bool".into(),
TypeName::List => "[]any".into(),
TypeName::Object => "map[string]any".into(),
TypeName::Custom(name) => (*name).to_string(),
}
}
fn align_go(rows: &[Vec<String>]) -> String {
let cols = rows.iter().map(Vec::len).max().unwrap_or(0);
let widths: Vec<usize> = (0..cols)
.map(|i| {
rows.iter()
.filter(|r| i + 1 < r.len()) .map(|r| r[i].chars().count())
.max()
.unwrap_or(0)
})
.collect();
let mut out = String::new();
for row in rows {
out.push('\t');
for (i, cell) in row.iter().enumerate() {
out.push_str(cell);
if i + 1 < row.len() {
let pad = widths[i].saturating_sub(cell.chars().count());
out.push_str(&" ".repeat(pad + 1));
}
}
out.push('\n');
}
out
}
fn go(decls: &[Decl]) -> String {
let mut out = format!("// {HEADER}\n\npackage config\n");
for d in decls {
match d {
Decl::Enum(e) => {
out.push_str(&format!("\ntype {} string\n\nconst (\n", e.name));
let rows: Vec<Vec<String>> = e
.members
.iter()
.map(|m| {
vec![
format!("{}{}", e.name, pascal(m)),
e.name.to_string(),
format!("= \"{m}\""),
]
})
.collect();
out.push_str(&align_go(&rows));
out.push_str(")\n");
}
Decl::Schema(s) => {
out.push_str(&format!("\ntype {} struct {{\n", s.name));
let rows: Vec<Vec<String>> = s
.fields
.iter()
.map(|f| {
vec![
exported(f.name),
go_ty(&f.ty),
format!("`json:\"{}\"`", f.name),
]
})
.collect();
out.push_str(&align_go(&rows));
out.push_str("}\n");
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const SRC: &str = concat!(
"enum Tier\n \"frontend\"\n \"backend\"\nend\n",
"type Endpoint\n host: String\n port: Int = 443\nend\n",
"type Service\n",
" name: String\n",
" tier: Tier\n",
" endpoint: Endpoint\n",
" ratio: Float\n",
" enabled: Bool\n",
" tags: List\n",
" labels: Object\n",
"end\n",
);
#[test]
fn rust_output() {
let out = generate(SRC, Lang::Rust).unwrap();
assert!(out.contains("pub enum Tier {"));
assert!(out.contains("#[serde(rename = \"frontend\")]"));
assert!(out.contains(" Frontend,"));
assert!(out.contains("pub struct Service {"));
assert!(out.contains("pub tier: Tier,"));
assert!(out.contains("pub endpoint: Endpoint,"));
assert!(out.contains("pub port: i64,")); assert!(out.contains("pub tags: Vec<serde_json::Value>,"));
assert!(out.contains("pub labels: serde_json::Map<String, serde_json::Value>,"));
}
#[test]
fn typescript_output_uses_a_literal_union() {
let out = generate(SRC, Lang::TypeScript).unwrap();
assert!(out.contains("export type Tier = \"frontend\" | \"backend\";"));
assert!(out.contains("export interface Service {"));
assert!(out.contains(" tier: Tier;"));
assert!(out.contains(" ratio: number;"));
assert!(out.contains(" labels: Record<string, unknown>;"));
}
#[test]
fn go_output_uses_typed_constants_and_json_tags() {
let out = generate(SRC, Lang::Go).unwrap();
assert!(out.contains("type Tier string"));
assert!(out.contains("\tTierFrontend Tier = \"frontend\""));
assert!(out.contains("type Service struct {"));
assert!(
out.contains("\tTier Tier `json:\"tier\"`\n"),
"{out}"
);
assert!(
out.contains("\tLabels map[string]any `json:\"labels\"`\n"),
"{out}"
);
assert!(out.contains("\tTierBackend Tier = \"backend\"\n"), "{out}");
}
#[test]
fn member_names_are_sanitised() {
let src = "enum Region\n \"eu-central\"\n \"us.east\"\n \"2fa\"\nend\n";
let out = generate(src, Lang::Rust).unwrap();
assert!(out.contains(" EuCentral,"), "{out}");
assert!(out.contains(" UsEast,"), "{out}");
assert!(out.contains(" N2fa,"), "{out}"); assert!(out.contains("#[serde(rename = \"eu-central\")]"));
}
#[test]
fn a_manifest_without_declarations_emits_only_a_header() {
let out = generate("x: 1\n", Lang::TypeScript).unwrap();
assert!(out.starts_with("// Generated by"));
assert!(!out.contains("interface"));
}
#[test]
fn a_broken_manifest_is_an_error_not_a_panic() {
assert!(generate("type\n", Lang::Rust).is_err());
}
}