use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum Expr {
String(String),
Int(i64),
Bool(bool),
Table(Vec<(String, Expr)>),
Array(Vec<Expr>),
}
impl Expr {
pub fn s(v: impl Into<String>) -> Self { Self::String(v.into()) }
pub fn i(v: i64) -> Self { Self::Int(v) }
pub fn b(v: bool) -> Self { Self::Bool(v) }
pub fn table() -> Self { Self::Table(Vec::new()) }
pub fn arr(items: impl IntoIterator<Item = Expr>) -> Self {
Self::Array(items.into_iter().collect())
}
pub fn insert(&mut self, k: impl Into<String>, v: Expr) -> &mut Self {
if let Self::Table(items) = self {
let key = k.into();
if let Some(idx) = items.iter().position(|(ek, _)| ek == &key) {
items[idx].1 = v;
} else {
items.push((key, v));
}
}
self
}
fn render(&self, indent: usize) -> String {
let pad_outer = " ".repeat(indent);
let pad_inner = " ".repeat(indent + 1);
match self {
Self::String(s) => render_lua_string(s),
Self::Int(n) => n.to_string(),
Self::Bool(true) => "true".into(),
Self::Bool(false) => "false".into(),
Self::Table(items) if items.is_empty() => "{}".into(),
Self::Table(items) => {
let mut out = String::from("{\n");
for (i, (k, v)) in items.iter().enumerate() {
let _ = write!(out, "{pad_inner}{k} = {}", v.render(indent + 1));
if i + 1 < items.len() { out.push(','); }
out.push('\n');
}
let _ = write!(out, "{pad_outer}}}");
out
}
Self::Array(items) if items.is_empty() => "{}".into(),
Self::Array(items) => {
let parts: Vec<String> = items.iter().map(|v| v.render(indent + 1)).collect();
let inline = {
let mut s = String::from("{ ");
s.push_str(&parts.join(", "));
s.push_str(" }");
s
};
if inline.len() <= 80 && !inline.contains('\n') {
inline
} else {
let mut out = String::from("{\n");
for (i, p) in parts.iter().enumerate() {
let _ = write!(out, "{pad_inner}{p}");
if i + 1 < parts.len() { out.push(','); }
out.push('\n');
}
let _ = write!(out, "{pad_outer}}}");
out
}
}
}
}
}
fn render_lua_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\\' => out.push_str(r"\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c => out.push(c),
}
}
out.push('"');
out
}
#[derive(Debug, Clone)]
pub enum Stmt {
Set { key: String, value: Expr },
Blank,
Comment(String),
}
#[derive(Debug, Clone, Default)]
pub struct File { pub stmts: Vec<Stmt> }
impl File {
pub fn new() -> Self { Self::default() }
pub fn push(&mut self, s: Stmt) -> &mut Self { self.stmts.push(s); self }
pub fn render(&self) -> String {
let mut out = String::new();
for s in &self.stmts {
match s {
Stmt::Set { key, value } => {
let _ = writeln!(out, "{key} = {}", value.render(0));
}
Stmt::Blank => out.push('\n'),
Stmt::Comment(c) => { let _ = writeln!(out, "-- {c}"); }
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_table_inline() {
assert_eq!(Expr::table().render(0), "{}");
}
#[test]
fn array_short_inline() {
let e = Expr::arr([Expr::s("a"), Expr::s("b")]);
assert_eq!(e.render(0), "{ \"a\", \"b\" }");
}
#[test]
fn table_multi_line_with_comma_per_line() {
let mut t = Expr::table();
t.insert("name", Expr::s("demo"));
t.insert("version", Expr::s("1"));
let out = t.render(0);
assert!(out.contains("name = \"demo\","));
assert!(out.contains("version = \"1\""));
}
#[test]
fn file_renders_set_lines() {
let mut f = File::new();
f.push(Stmt::Set { key: "package".into(), value: Expr::s("demo") });
assert_eq!(f.render(), "package = \"demo\"\n");
}
}