use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum Expr {
String(String),
Int(i64),
Ident(String),
BacktickIdent(String),
Call { receiver: Option<Box<Expr>>, name: String, args: 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 ident(v: impl Into<String>) -> Self { Self::Ident(v.into()) }
pub fn backtick(v: impl Into<String>) -> Self { Self::BacktickIdent(v.into()) }
pub fn call(name: impl Into<String>, args: impl IntoIterator<Item = Expr>) -> Self {
Self::Call { receiver: None, name: name.into(), args: args.into_iter().collect() }
}
pub fn method(receiver: Expr, name: impl Into<String>, args: impl IntoIterator<Item = Expr>) -> Self {
Self::Call {
receiver: Some(Box::new(receiver)),
name: name.into(),
args: args.into_iter().collect(),
}
}
fn render(&self) -> String {
match self {
Self::String(s) => render_kotlin_string(s),
Self::Int(n) => n.to_string(),
Self::Ident(s) => s.clone(),
Self::BacktickIdent(s) => {
let mut out = String::with_capacity(s.len() + 2);
out.push('`');
out.push_str(s);
out.push('`');
out
}
Self::Call { receiver, name, args } => {
let mut out = String::new();
if let Some(r) = receiver {
out.push_str(&r.render());
out.push('.');
}
out.push_str(name);
out.push('(');
let parts: Vec<String> = args.iter().map(Expr::render).collect();
out.push_str(&parts.join(", "));
out.push(')');
out
}
}
}
}
fn render_kotlin_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 {
Assign { name: String, value: Expr },
Expr(Expr),
Block { name: String, body: Vec<Stmt>, inline: bool },
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();
render_at(&self.stmts, 0, &mut out);
out
}
}
fn is_inlinable(stmt: &Stmt) -> bool {
matches!(stmt, Stmt::Expr(_) | Stmt::Assign { .. })
}
fn render_inline_part(stmt: &Stmt) -> Option<String> {
match stmt {
Stmt::Expr(e) => Some(e.render()),
Stmt::Assign { name, value } => {
let mut a = String::from(name);
a.push_str(" = ");
a.push_str(&value.render());
Some(a)
}
_ => None,
}
}
fn render_at(stmts: &[Stmt], depth: usize, out: &mut String) {
let pad = " ".repeat(depth);
for s in stmts {
match s {
Stmt::Assign { name, value } => {
let _ = writeln!(out, "{pad}{name} = {}", value.render());
}
Stmt::Expr(e) => {
let _ = writeln!(out, "{pad}{}", e.render());
}
Stmt::Block { name, body, inline: true }
if body.len() <= 2 && body.iter().all(is_inlinable) =>
{
let parts: Vec<String> = body.iter().filter_map(render_inline_part).collect();
let _ = writeln!(out, "{pad}{name} {{ {} }}", parts.join("; "));
}
Stmt::Block { name, body, .. } => {
let _ = writeln!(out, "{pad}{name} {{");
render_at(body, depth + 1, out);
let _ = writeln!(out, "{pad}}}");
}
Stmt::Blank => out.push('\n'),
Stmt::Comment(c) => {
let _ = writeln!(out, "{pad}// {c}");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_escapes_dollar_for_interp_safety() {
assert_eq!(render_kotlin_string("$home"), r#""\$home""#);
}
#[test]
fn backtick_ident_wraps_with_backticks() {
assert_eq!(Expr::backtick("java-library").render(), "`java-library`");
}
#[test]
fn method_chain_renders_with_dot() {
let e = Expr::method(Expr::ident("JavaLanguageVersion"), "of", vec![Expr::i(17)]);
assert_eq!(e.render(), "JavaLanguageVersion.of(17)");
}
#[test]
fn inline_block_collapses_short_body() {
let mut f = File::new();
f.push(Stmt::Block {
name: "plugins".into(),
body: vec![Stmt::Expr(Expr::backtick("java-library"))],
inline: true,
});
let out = f.render();
assert_eq!(out, "plugins { `java-library` }\n");
}
#[test]
fn multi_line_block_indents_4spaces() {
let mut f = File::new();
f.push(Stmt::Block {
name: "dependencies".into(),
body: vec![
Stmt::Expr(Expr::call("implementation", vec![Expr::s("g:n:v")])),
Stmt::Expr(Expr::call("testImplementation", vec![Expr::s("g2:n2:v2")])),
],
inline: false,
});
let out = f.render();
assert!(out.starts_with("dependencies {\n"));
assert!(out.contains(" implementation(\"g:n:v\")\n"));
assert!(out.contains(" testImplementation(\"g2:n2:v2\")\n"));
assert!(out.ends_with("}\n"));
}
}