use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum Expr {
String(String),
Ident(String),
Call { name: String, args: Vec<Expr> },
Infix { op: String, parts: Vec<Expr> },
}
impl Expr {
pub fn s(v: impl Into<String>) -> Self { Self::String(v.into()) }
pub fn ident(v: impl Into<String>) -> Self { Self::Ident(v.into()) }
pub fn call(name: impl Into<String>, args: impl IntoIterator<Item = Expr>) -> Self {
Self::Call { name: name.into(), args: args.into_iter().collect() }
}
pub fn infix(op: impl Into<String>, parts: impl IntoIterator<Item = Expr>) -> Self {
Self::Infix { op: op.into(), parts: parts.into_iter().collect() }
}
fn render(&self) -> String {
match self {
Self::String(s) => render_scala_string(s),
Self::Ident(s) => s.clone(),
Self::Call { name, args } => {
let mut out = String::from(name);
out.push('(');
let parts: Vec<String> = args.iter().map(Expr::render).collect();
out.push_str(&parts.join(", "));
out.push(')');
out
}
Self::Infix { op, parts } => {
let rendered: Vec<String> = parts.iter().map(Expr::render).collect();
let sep = {
let mut s = String::from(" ");
s.push_str(op);
s.push(' ');
s
};
rendered.join(&sep)
}
}
}
}
fn render_scala_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("\\\""),
'$' => 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 },
Add { key: String, value: Expr },
Append { 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 } => {
write_op(&mut out, key, ":=", value);
}
Stmt::Add { key, value } => {
write_op(&mut out, key, "+=", value);
}
Stmt::Append { key, value } => {
write_op(&mut out, key, "++=", value);
}
Stmt::Blank => out.push('\n'),
Stmt::Comment(c) => {
let _ = writeln!(out, "// {c}");
}
}
}
out
}
}
fn write_op(out: &mut String, key: &str, op: &str, value: &Expr) {
let val_str = value.render();
let break_multi = matches!(value,
Expr::Call { name, args } if name == "Seq" && args.len() >= 2
);
if break_multi {
if let Expr::Call { name, args } = value {
let _ = writeln!(out, "{key} {op} {name}(");
for (i, a) in args.iter().enumerate() {
let _ = write!(out, " {}", a.render());
if i + 1 < args.len() { out.push(','); }
out.push('\n');
}
let _ = writeln!(out, ")");
return;
}
}
let _ = writeln!(out, "{key} {op} {val_str}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_escapes_quote_and_dollar() {
assert_eq!(render_scala_string(r#"a"$b"#), r#""a\"\$b""#);
}
#[test]
fn infix_renders_with_spaced_operator() {
let e = Expr::infix("%%", vec![Expr::s("org"), Expr::s("lib")]);
assert_eq!(e.render(), r#""org" %% "lib""#);
}
#[test]
fn chained_infix_left_associative() {
let e = Expr::infix(
"%",
vec![
Expr::infix("%%", vec![Expr::s("org"), Expr::s("lib")]),
Expr::s("ver"),
],
);
let out = e.render();
assert!(out.contains(r#""org" %% "lib""#));
assert!(out.ends_with(r#" % "ver""#));
}
#[test]
fn set_renders_with_colon_equals() {
let mut f = File::new();
f.push(Stmt::Set { key: "name".into(), value: Expr::s("demo") });
assert_eq!(f.render(), "name := \"demo\"\n");
}
#[test]
fn append_seq_multiline_when_two_or_more() {
let mut f = File::new();
f.push(Stmt::Append {
key: "libraryDependencies".into(),
value: Expr::call("Seq", vec![
Expr::infix("%", vec![
Expr::infix("%%", vec![Expr::s("a"), Expr::s("b")]),
Expr::s("1"),
]),
Expr::infix("%", vec![
Expr::infix("%%", vec![Expr::s("c"), Expr::s("d")]),
Expr::s("2"),
]),
]),
});
let out = f.render();
assert!(out.starts_with("libraryDependencies ++= Seq(\n"));
assert!(out.contains(" \"a\" %% \"b\" % \"1\","));
assert!(out.contains(" \"c\" %% \"d\" % \"2\""));
assert!(out.ends_with(")\n"));
}
}