use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum Arg {
Ident(String),
Num(String),
Str(String),
}
impl Arg {
pub fn ident(v: impl Into<String>) -> Self { Self::Ident(v.into()) }
pub fn num(v: impl Into<String>) -> Self { Self::Num(v.into()) }
pub fn s(v: impl Into<String>) -> Self { Self::Str(v.into()) }
fn render(&self) -> String {
match self {
Self::Ident(s) => s.clone(),
Self::Num(s) => s.clone(),
Self::Str(s) => render_cmake_string(s),
}
}
}
fn render_cmake_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(r"\n"),
c => out.push(c),
}
}
out.push('"');
out
}
#[derive(Debug, Clone)]
pub enum Stmt {
Call { name: String, args: Vec<Arg> },
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 call(&mut self, name: impl Into<String>, args: impl IntoIterator<Item = Arg>) -> &mut Self {
self.stmts.push(Stmt::Call {
name: name.into(),
args: args.into_iter().collect(),
});
self
}
pub fn render(&self) -> String {
let mut out = String::new();
for s in &self.stmts {
match s {
Stmt::Call { name, args } => {
let _ = write!(out, "{name}(");
let parts: Vec<String> = args.iter().map(Arg::render).collect();
out.push_str(&parts.join(" "));
out.push_str(")\n");
}
Stmt::Blank => out.push('\n'),
Stmt::Comment(c) => { let _ = writeln!(out, "# {c}"); }
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ident_args_render_unquoted() {
let mut f = File::new();
f.call("project", [Arg::ident("demo")]);
assert_eq!(f.render(), "project(demo)\n");
}
#[test]
fn string_with_spaces_gets_quoted() {
let mut f = File::new();
f.call("set", [Arg::ident("DESC"), Arg::s("a description with spaces")]);
let out = f.render();
assert!(out.contains("\"a description with spaces\""));
}
#[test]
fn keyword_grouped_args() {
let mut f = File::new();
f.call("project", [
Arg::ident("demo"),
Arg::ident("VERSION"), Arg::num("0.1.0"),
Arg::ident("LANGUAGES"), Arg::ident("CXX"),
]);
assert_eq!(f.render(), "project(demo VERSION 0.1.0 LANGUAGES CXX)\n");
}
#[test]
fn quoted_string_escapes_quote_and_backslash() {
assert_eq!(render_cmake_string(r#"a"b\c"#), r#""a\"b\\c""#);
}
#[test]
fn full_cmakelists_shape() {
let mut f = File::new();
f.call("cmake_minimum_required", [Arg::ident("VERSION"), Arg::num("3.20")]);
f.call("project", [
Arg::ident("demo"),
Arg::ident("VERSION"), Arg::num("0.1.0"),
Arg::ident("LANGUAGES"), Arg::ident("CXX"),
]);
f.call("set", [Arg::ident("CMAKE_CXX_STANDARD"), Arg::num("20")]);
f.push(Stmt::Blank);
f.call("add_executable", [Arg::ident("demo"), Arg::ident("src/main.cpp")]);
let out = f.render();
assert!(out.starts_with("cmake_minimum_required(VERSION 3.20)\n"));
assert!(out.contains("project(demo VERSION 0.1.0 LANGUAGES CXX)\n"));
assert!(out.contains("set(CMAKE_CXX_STANDARD 20)\n"));
assert!(out.contains("add_executable(demo src/main.cpp)\n"));
}
}