use crate::haystack::encoding::zinc::encode::ToZinc;
use crate::haystack::val::{Dict, Grid, Value};
const INDENT: &str = " ";
const NL_INDENT: &str = "\n ";
#[derive(Debug, Clone, Copy)]
pub struct TrioWriterOptions {
pub multiline_strings: bool,
}
enum TrioPart {
Dict(Dict),
Comment(String),
NewLine,
}
pub struct TrioWriter {
parts: Vec<TrioPart>,
options: Option<TrioWriterOptions>,
}
impl Default for TrioWriter {
fn default() -> Self {
Self::new()
}
}
impl TrioWriter {
pub fn new() -> Self {
TrioWriter {
parts: Vec::new(),
options: None,
}
}
pub fn with_options(options: TrioWriterOptions) -> Self {
TrioWriter {
parts: Vec::new(),
options: Some(options),
}
}
pub fn add_dict(&mut self, dict: Dict) -> &mut Self {
self.parts.push(TrioPart::Dict(dict));
self
}
pub fn add_grid(&mut self, grid: &Grid) -> &mut Self {
for row in &grid.rows {
self.parts.push(TrioPart::Dict(row.clone()));
}
self
}
pub fn add_comment(&mut self, text: &str) -> &mut Self {
self.parts.push(TrioPart::Comment(text.to_string()));
self
}
pub fn add_newline(&mut self) -> &mut Self {
self.parts.push(TrioPart::NewLine);
self
}
pub fn to_trio<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
let mut first = true;
for part in &self.parts {
if !first {
writer.write_all(b"\n")?;
}
first = false;
match part {
TrioPart::Comment(text) => {
if text.is_empty() {
writer.write_all(b"//")?;
} else {
writer.write_all(b"// ")?;
writer.write_all(text.as_bytes())?;
}
}
TrioPart::Dict(dict) => {
let s = Self::dict_to_trio(dict, self.options.as_ref());
writer.write_all(s.as_bytes())?;
writer.write_all(b"\n---")?;
}
TrioPart::NewLine => {}
}
}
Ok(())
}
pub fn to_trio_string(&self) -> String {
let mut buf = Vec::new();
self.to_trio(&mut buf)
.expect("writing Trio to Vec<u8> cannot fail");
String::from_utf8(buf).expect("Trio output is always valid UTF-8")
}
pub fn dict_to_trio(dict: &Dict, options: Option<&TrioWriterOptions>) -> String {
let multiline = options.is_some_and(|o| o.multiline_strings);
dict.iter()
.map(|(name, value)| encode_tag(name, value, multiline))
.collect::<Vec<_>>()
.join("\n")
}
}
impl std::fmt::Display for TrioWriter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_trio_string())
}
}
fn encode_tag(name: &str, value: &Value, multiline_strings: bool) -> String {
match value {
Value::Marker => name.to_string(),
Value::Grid(grid) => {
let zinc = grid
.to_zinc_string()
.expect("Grid Zinc encoding should never fail for well-formed data");
let indented = zinc.trim().replace('\n', NL_INDENT);
format!("{}:Zinc:\n{}{}", name, INDENT, indented)
}
Value::Str(s) if multiline_strings => {
let indented = s.value.replace('\n', NL_INDENT);
format!("{}: \n{}{}", name, INDENT, indented)
}
_ => {
let zinc = value
.to_zinc_string()
.expect("Zinc encoding should never fail for scalar/collection values");
format!("{}: {}", name, zinc)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dict;
use crate::haystack::val::*;
fn make_dict() -> Dict {
dict! {
"marker" => Value::make_marker(),
"str" => Value::make_str("A string"),
"list" => Value::make_list(vec![Value::make_bool(true)]),
"dict" => Value::make_dict(dict! { "foo" => Value::make_str("bar") })
}
}
const MAKE_DICT_TRIO: &str = concat!(
"dict: {foo:\"bar\"}\n",
"list: [T]\n",
"marker\n",
"str: \"A string\"",
);
fn to_str(writer: &TrioWriter) -> String {
writer.to_trio_string()
}
#[test]
fn test_add_dict() {
let mut writer = TrioWriter::new();
writer.add_dict(make_dict());
let expected = format!("{}\n---", MAKE_DICT_TRIO);
assert_eq!(to_str(&writer), expected);
}
#[test]
fn test_to_trio_writer() {
let mut writer = TrioWriter::new();
writer.add_dict(make_dict());
let mut buf = Vec::new();
writer.to_trio(&mut buf).expect("write ok");
assert_eq!(String::from_utf8(buf).unwrap(), to_str(&writer));
}
#[test]
fn test_add_dict_with_inner_grid() {
let mut writer = TrioWriter::new();
let dict = dict! {
"grid" => Value::make_grid_from_dicts(vec![
dict! { "foo" => Value::make_str("bar"), "boo" => Value::make_int(1) }
])
};
writer.add_dict(dict);
let expected = concat!(
"grid:Zinc:\n",
" ver:\"3.0\"\n",
" boo,foo\n",
" 1,\"bar\"\n",
"---"
);
assert_eq!(to_str(&writer), expected);
}
#[test]
fn test_add_grid_writes_all_rows() {
let mut writer = TrioWriter::new();
let grid = Grid::make_from_dicts(vec![make_dict(), make_dict()]);
writer.add_grid(&grid);
let expected = format!("{}\n---\n{}\n---", MAKE_DICT_TRIO, MAKE_DICT_TRIO);
assert_eq!(to_str(&writer), expected);
}
#[test]
fn test_add_comment_single() {
let mut writer = TrioWriter::new();
writer.add_comment("Hello");
assert_eq!(to_str(&writer), "// Hello");
}
#[test]
fn test_add_comment_empty() {
let mut writer = TrioWriter::new();
writer.add_comment("");
assert_eq!(to_str(&writer), "//");
}
#[test]
fn test_add_comment_multiple() {
let mut writer = TrioWriter::new();
writer
.add_comment("This is a comment")
.add_comment("This is another comment");
assert_eq!(
to_str(&writer),
"// This is a comment\n// This is another comment"
);
}
#[test]
fn test_add_newline_between_comments() {
let mut writer = TrioWriter::new();
writer
.add_comment("This is a comment")
.add_newline()
.add_comment("This is another comment");
assert_eq!(
to_str(&writer),
"// This is a comment\n\n// This is another comment"
);
}
#[test]
fn test_full_document() {
let mut writer = TrioWriter::new();
writer
.add_comment("")
.add_comment("Copyright J2 Innovations")
.add_comment("")
.add_newline()
.add_dict(make_dict())
.add_newline()
.add_comment("The second dict...")
.add_dict(make_dict());
let expected = format!(
concat!(
"//\n",
"// Copyright J2 Innovations\n",
"//\n",
"\n",
"{}\n---\n",
"\n",
"// The second dict...\n",
"{}\n---"
),
MAKE_DICT_TRIO, MAKE_DICT_TRIO
);
assert_eq!(to_str(&writer), expected);
}
#[test]
fn test_dict_to_trio_basic() {
let trio = TrioWriter::dict_to_trio(&make_dict(), None);
assert_eq!(trio, MAKE_DICT_TRIO);
}
#[test]
fn test_dict_to_trio_multiline_string() {
let dict = dict! {
"bool" => Value::make_bool(true),
"num" => Value::make_number(42.0),
"str" => Value::make_str("{\n \"foo\": \"bar\"\n}")
};
let opts = TrioWriterOptions {
multiline_strings: true,
};
let trio = TrioWriter::dict_to_trio(&dict, Some(&opts));
let expected = concat!(
"bool: T\n",
"num: 42\n",
"str: \n",
" {\n",
" \"foo\": \"bar\"\n",
" }",
);
assert_eq!(trio, expected);
}
#[test]
fn test_dict_to_trio_multiline_empty_string() {
let dict = dict! {
"bool" => Value::make_bool(true),
"num" => Value::make_number(42.0),
"str" => Value::make_str("")
};
let opts = TrioWriterOptions {
multiline_strings: true,
};
let trio = TrioWriter::dict_to_trio(&dict, Some(&opts));
assert_eq!(trio, "bool: T\nnum: 42\nstr: \n ");
}
#[test]
fn test_dict_to_trio_null_value() {
let dict = dict! { "nothing" => Value::Null };
let trio = TrioWriter::dict_to_trio(&dict, None);
assert_eq!(trio, "nothing: N");
}
#[test]
fn test_dict_to_trio_na_value() {
let dict = dict! { "na" => Value::make_na() };
let trio = TrioWriter::dict_to_trio(&dict, None);
assert_eq!(trio, "na: NA");
}
#[test]
fn test_to_string_delegates_to_to_trio_string() {
let mut writer = TrioWriter::new();
writer.add_comment("test");
assert_eq!(writer.to_string(), writer.to_trio_string());
}
}