use std::collections::BTreeMap;
use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum Value {
String(String),
Int(i64),
Bool(bool),
Array(Vec<Value>),
InlineTable(BTreeMap<String, Value>),
}
impl Value {
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 arr(vs: impl IntoIterator<Item = Value>) -> Self {
Self::Array(vs.into_iter().collect())
}
pub fn render_inline(&self) -> String {
self.render()
}
fn render(&self) -> String {
match self {
Self::String(s) => render_basic_string(s),
Self::Int(n) => n.to_string(),
Self::Bool(true) => "true".to_string(),
Self::Bool(false) => "false".to_string(),
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::InlineTable(map) => {
let mut out = String::from("{ ");
for (i, (k, v)) in map.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&render_bare_key(k));
out.push_str(" = ");
out.push_str(&v.render());
}
out.push_str(" }");
out
}
}
}
}
fn render_basic_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("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
use std::fmt::Write as _;
let _ = write!(out, "\\u{:04X}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
fn render_bare_key(k: &str) -> String {
if !k.is_empty()
&& k.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
k.to_string()
} else {
render_basic_string(k)
}
}
#[derive(Debug, Default)]
pub struct Document {
pub root: BTreeMap<String, Value>,
pub tables: Vec<Table>,
pub table_arrays: Vec<Table>,
}
#[derive(Debug, Default)]
pub struct Table {
pub header: String,
pub keys: BTreeMap<String, Value>,
}
impl Document {
pub fn new() -> Self {
Self::default()
}
pub fn root_key(&mut self, k: impl Into<String>, v: Value) -> &mut Self {
self.root.insert(k.into(), v);
self
}
pub fn table(&mut self, header: impl Into<String>) -> &mut Table {
let header = header.into();
if let Some(idx) = self.tables.iter().position(|t| t.header == header) {
return &mut self.tables[idx];
}
self.tables.push(Table {
header,
keys: BTreeMap::new(),
});
self.tables.last_mut().unwrap()
}
pub fn array_table(&mut self, header: impl Into<String>) -> &mut Table {
self.table_arrays.push(Table {
header: header.into(),
keys: BTreeMap::new(),
});
self.table_arrays.last_mut().unwrap()
}
pub fn render(&self) -> String {
let mut out = String::new();
for (k, v) in &self.root {
let _ = writeln!(out, "{} = {}", render_bare_key(k), v.render());
}
for table in &self.tables {
if !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
let _ = writeln!(out, "[{}]", table.header);
for (k, v) in &table.keys {
let _ = writeln!(out, "{} = {}", render_bare_key(k), v.render());
}
}
for table in &self.table_arrays {
if !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
let _ = writeln!(out, "[[{}]]", table.header);
for (k, v) in &table.keys {
let _ = writeln!(out, "{} = {}", render_bare_key(k), v.render());
}
}
out
}
}
impl Table {
pub fn key(&mut self, k: impl Into<String>, v: Value) -> &mut Self {
self.keys.insert(k.into(), v);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_string_escapes_quotes() {
assert_eq!(render_basic_string(r#"hello "world""#), r#""hello \"world\"""#);
}
#[test]
fn basic_string_escapes_backslash() {
assert_eq!(render_basic_string(r"path\to\file"), r#""path\\to\\file""#);
}
#[test]
fn document_renders_table() {
let mut doc = Document::new();
doc.table("package").key("name", Value::s("demo")).key("version", Value::s("0.1.0"));
let out = doc.render();
assert!(out.contains("[package]"));
assert!(out.contains(r#"name = "demo""#));
assert!(out.contains(r#"version = "0.1.0""#));
}
#[test]
fn quotes_in_description_dont_break_output() {
let mut doc = Document::new();
doc.table("package").key("description", Value::s(r#"A "quoted" thing"#));
let out = doc.render();
assert!(out.contains(r#"description = "A \"quoted\" thing""#));
}
#[test]
fn array_of_tables_renders_double_brackets() {
let mut doc = Document::new();
doc.array_table("depends-on").key("gnat", Value::s(">=11"));
doc.array_table("depends-on").key("alr", Value::s("~1.2"));
let out = doc.render();
assert!(out.contains("[[depends-on]]"), "missing [[depends-on]]");
assert!(out.matches("[[depends-on]]").count() == 2, "should repeat header per entry");
assert!(out.contains(r#"gnat = ">=11""#), "missing gnat key");
assert!(out.contains(r#"alr = "~1.2""#), "missing alr key");
}
}