use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum Expr {
String(String),
Int(i64),
Bool(bool),
Atom(String),
Ident(String),
List(Vec<Expr>),
KeywordList(Vec<(String, Expr)>),
Tuple(Vec<Expr>),
Call { 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 b(v: bool) -> Self { Self::Bool(v) }
pub fn atom(v: impl Into<String>) -> Self { Self::Atom(v.into()) }
pub fn ident(v: impl Into<String>) -> Self { Self::Ident(v.into()) }
pub fn list(items: impl IntoIterator<Item = Expr>) -> Self {
Self::List(items.into_iter().collect())
}
pub fn kwlist(pairs: impl IntoIterator<Item = (String, Expr)>) -> Self {
Self::KeywordList(pairs.into_iter().collect())
}
pub fn tuple(items: impl IntoIterator<Item = Expr>) -> Self {
Self::Tuple(items.into_iter().collect())
}
pub fn call(name: impl Into<String>, args: impl IntoIterator<Item = Expr>) -> Self {
Self::Call { name: name.into(), args: args.into_iter().collect() }
}
fn render(&self, indent: usize) -> String {
let pad_outer = " ".repeat(indent);
let pad_inner = " ".repeat(indent + 1);
match self {
Self::String(s) => render_elixir_string(s),
Self::Int(n) => n.to_string(),
Self::Bool(true) => "true".into(),
Self::Bool(false) => "false".into(),
Self::Atom(a) => { let mut s = String::from(":"); s.push_str(a); s }
Self::Ident(s) => s.clone(),
Self::List(items) if items.is_empty() => "[]".into(),
Self::List(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(']'); 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
}
}
Self::KeywordList(items) if items.is_empty() => "[]".into(),
Self::KeywordList(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::Tuple(items) => {
let parts: Vec<String> = items.iter().map(|v| v.render(indent + 1)).collect();
let mut s = String::from("{");
s.push_str(&parts.join(", "));
s.push('}');
s
}
Self::Call { name, args } => {
let parts: Vec<String> = args.iter().map(|a| a.render(indent + 1)).collect();
let mut s = String::from(name);
s.push('(');
s.push_str(&parts.join(", "));
s.push(')');
s
}
}
}
}
fn render_elixir_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(r"\#"), '\n' => out.push_str("\\n"),
c => out.push(c),
}
}
out.push('"');
out
}
#[derive(Debug, Clone)]
pub enum Stmt {
Use(String),
Def { name: String, body: Expr },
Defp { name: String, body: Expr },
Blank,
Comment(String),
}
#[derive(Debug, Clone)]
pub struct Module {
pub name: String,
pub body: Vec<Stmt>,
}
impl Module {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into(), body: Vec::new() }
}
pub fn push(&mut self, s: Stmt) -> &mut Self { self.body.push(s); self }
pub fn render(&self) -> String {
let mut out = String::new();
let _ = writeln!(out, "defmodule {} do", self.name);
let pad = " ";
for s in &self.body {
match s {
Stmt::Use(m) => { let _ = writeln!(out, "{pad}use {m}"); }
Stmt::Def { name, body } => {
let _ = writeln!(out, "{pad}def {name}, do: {}", body.render(1));
}
Stmt::Defp { name, body } => {
let _ = writeln!(out, "{pad}defp {name}, do: {}", body.render(1));
}
Stmt::Blank => out.push('\n'),
Stmt::Comment(c) => { let _ = writeln!(out, "{pad}# {c}"); }
}
}
out.push_str("end\n");
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn atom_renders_with_colon_prefix() {
assert_eq!(Expr::atom("ok").render(0), ":ok");
}
#[test]
fn tuple_renders_with_braces() {
let e = Expr::tuple(vec![Expr::atom("cowboy"), Expr::s("~> 2.10")]);
assert_eq!(e.render(0), "{:cowboy, \"~> 2.10\"}");
}
#[test]
fn keyword_list_indents_per_pair() {
let e = Expr::kwlist(vec![
("app".into(), Expr::atom("demo")),
("version".into(), Expr::s("0.1.0")),
]);
let out = e.render(0);
assert!(out.starts_with("[\n"));
assert!(out.contains(" app: :demo,"));
assert!(out.contains(" version: \"0.1.0\""));
assert!(out.ends_with(']'));
}
#[test]
fn module_with_use_and_def() {
let mut m = Module::new("Demo");
m.push(Stmt::Use("Mix.Project".into()));
m.push(Stmt::Blank);
m.push(Stmt::Def { name: "project".into(), body: Expr::list([]) });
let out = m.render();
assert!(out.starts_with("defmodule Demo do\n"));
assert!(out.contains(" use Mix.Project\n"));
assert!(out.contains(" def project, do: []\n"));
assert!(out.ends_with("end\n"));
}
#[test]
fn string_defangs_hash_interpolation() {
assert_eq!(render_elixir_string("a#{b}"), r#""a\#{b}""#);
}
}