use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum Expr {
String(String),
Symbol(String),
Array(Vec<Expr>),
Hash(Vec<(String, Expr)>),
Raw(String),
}
impl Expr {
pub fn s(v: impl Into<String>) -> Self { Self::String(v.into()) }
pub fn sym(v: impl Into<String>) -> Self { Self::Symbol(v.into()) }
pub fn arr(items: impl IntoIterator<Item = Expr>) -> Self {
Self::Array(items.into_iter().collect())
}
pub fn raw(v: impl Into<String>) -> Self { Self::Raw(v.into()) }
fn render(&self) -> String {
match self {
Self::String(s) => render_single_quoted(s),
Self::Symbol(s) => {
let mut out = String::from(":");
out.push_str(s);
out
}
Self::Array(items) => {
let mut out = String::from("[");
for (i, v) in items.iter().enumerate() {
if i > 0 { out.push_str(", "); }
out.push_str(&v.render());
}
out.push(']');
out
}
Self::Hash(pairs) => {
let mut out = String::from("{ ");
for (i, (k, v)) in pairs.iter().enumerate() {
if i > 0 { out.push_str(", "); }
out.push_str(k);
out.push_str(": ");
out.push_str(&v.render());
}
out.push_str(" }");
out
}
Self::Raw(s) => s.clone(),
}
}
}
fn render_single_quoted(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(r"\'"),
c => out.push(c),
}
}
out.push('\'');
out
}
#[derive(Debug, Clone)]
pub enum Stmt {
Assign { receiver: String, attr: String, value: Expr },
MethodCall { receiver: String, method: String, args: Vec<Expr> },
Blank,
Comment(String),
}
#[derive(Debug, Clone)]
pub struct Block {
pub object: String,
pub param: String,
pub body: Vec<Stmt>,
}
impl Block {
pub fn new(object: impl Into<String>, param: impl Into<String>) -> Self {
Self { object: object.into(), param: param.into(), body: Vec::new() }
}
pub fn push(&mut self, stmt: Stmt) -> &mut Self {
self.body.push(stmt);
self
}
pub fn render(&self) -> String {
let mut out = String::new();
let _ = writeln!(out, "{} do |{}|", self.object, self.param);
for stmt in &self.body {
match stmt {
Stmt::Assign { receiver, attr, value } => {
let _ = writeln!(out, " {receiver}.{attr} = {}", value.render());
}
Stmt::MethodCall { receiver, method, args } => {
let arg_text: Vec<String> = args.iter().map(Expr::render).collect();
let _ = writeln!(out, " {receiver}.{method} {}", arg_text.join(", "));
}
Stmt::Blank => out.push('\n'),
Stmt::Comment(c) => {
let _ = writeln!(out, " # {c}");
}
}
}
out.push_str("end\n");
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_quoted_escapes_quote_and_backslash() {
assert_eq!(render_single_quoted(r"it's \fine"), r"'it\'s \\fine'");
}
#[test]
fn symbol_renders_with_colon() {
assert_eq!(Expr::sym("ok").render(), ":ok");
}
#[test]
fn array_of_strings_renders_inline() {
let e = Expr::arr([Expr::s("a"), Expr::s("b")]);
assert_eq!(e.render(), "['a', 'b']");
}
#[test]
fn block_renders_assign_and_method_call() {
let mut b = Block::new("Gem::Specification.new", "spec");
b.push(Stmt::Assign {
receiver: "spec".into(), attr: "name".into(),
value: Expr::s("demo"),
});
b.push(Stmt::MethodCall {
receiver: "spec".into(), method: "add_dependency".into(),
args: vec![Expr::s("rake"), Expr::s(">= 13")],
});
let out = b.render();
assert!(out.starts_with("Gem::Specification.new do |spec|\n"));
assert!(out.contains(" spec.name = 'demo'\n"));
assert!(out.contains(" spec.add_dependency 'rake', '>= 13'\n"));
assert!(out.ends_with("end\n"));
}
}