use crate::value::Value;
#[derive(Debug, Clone)]
pub struct DumpOptions {
pub indent: usize,
pub sort_keys: bool,
}
impl Default for DumpOptions {
fn default() -> Self {
DumpOptions {
indent: 4,
sort_keys: false,
}
}
}
pub fn dumps(value: &Value, options: &DumpOptions) -> String {
let mut lines = Vec::new();
render_value(value, 0, options, &mut lines);
if lines.is_empty() {
String::new()
} else {
lines.join("\n") + "\n"
}
}
pub fn dump<W: std::io::Write>(
value: &Value,
options: &DumpOptions,
mut writer: W,
) -> Result<(), std::io::Error> {
let s = dumps(value, options);
writer.write_all(s.as_bytes())
}
fn render_value(value: &Value, depth: usize, options: &DumpOptions, lines: &mut Vec<String>) {
match value {
Value::String(s) => render_string(s, depth, lines),
Value::List(items) => render_list(items, depth, options, lines),
Value::Dict(pairs) => render_dict(pairs, depth, options, lines),
}
}
fn render_string(s: &str, depth: usize, lines: &mut Vec<String>) {
let indent = " ".repeat(depth);
if s.is_empty() {
lines.push(format!("{}>", indent));
} else {
for line in s.split('\n') {
lines.push(format!("{}> {}", indent, line));
}
}
}
fn render_list(items: &[Value], depth: usize, options: &DumpOptions, lines: &mut Vec<String>) {
let indent = " ".repeat(depth);
if items.is_empty() {
lines.push(format!("{}[]", indent));
return;
}
for item in items {
match item {
Value::String(s) if s.is_empty() => {
lines.push(format!("{}-", indent));
}
Value::String(s) if !value_needs_multiline(s) => {
lines.push(format!("{}- {}", indent, s));
}
_ => {
lines.push(format!("{}-", indent));
render_value(item, depth + options.indent, options, lines);
}
}
}
}
fn render_dict(
pairs: &[(String, Value)],
depth: usize,
options: &DumpOptions,
lines: &mut Vec<String>,
) {
let indent = " ".repeat(depth);
if pairs.is_empty() {
lines.push(format!("{}{{}}", indent));
return;
}
let pairs: Vec<&(String, Value)> = if options.sort_keys {
let mut sorted: Vec<&(String, Value)> = pairs.iter().collect();
sorted.sort_by(|a, b| a.0.cmp(&b.0));
sorted
} else {
pairs.iter().collect()
};
for (key, value) in pairs {
let key_needs_multiline = key_requires_multiline(key);
if key_needs_multiline {
for key_line in key.split('\n') {
if key_line.is_empty() {
lines.push(format!("{}:", indent));
} else {
lines.push(format!("{}: {}", indent, key_line));
}
}
render_value(value, depth + options.indent, options, lines);
} else {
match value {
Value::String(s) if s.is_empty() => {
lines.push(format!("{}{}:", indent, key));
}
Value::String(s) if !value_needs_multiline(s) => {
lines.push(format!("{}{}: {}", indent, key, s));
}
_ => {
lines.push(format!("{}{}:", indent, key));
render_value(value, depth + options.indent, options, lines);
}
}
}
}
}
fn key_requires_multiline(key: &str) -> bool {
if key.is_empty() || key.contains('\n') {
return true;
}
if key != key.trim() {
return true;
}
if key.contains(": ") || key.ends_with(':') {
return true;
}
if key.starts_with("- ")
|| key == "-"
|| key.starts_with("> ")
|| key == ">"
|| key.starts_with(": ")
|| key == ":"
|| key.starts_with('#')
|| key.starts_with('[')
|| key.starts_with('{')
{
return true;
}
false
}
fn value_needs_multiline(s: &str) -> bool {
if s.contains('\n') {
return true;
}
if !s.is_empty() && s != s.trim() {
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dump_simple_string() {
let v = Value::String("hello".to_string());
assert_eq!(dumps(&v, &DumpOptions::default()), "> hello\n");
}
#[test]
fn test_dump_multiline_string() {
let v = Value::String("line 1\nline 2".to_string());
assert_eq!(dumps(&v, &DumpOptions::default()), "> line 1\n> line 2\n");
}
#[test]
fn test_dump_empty_string() {
let v = Value::String(String::new());
assert_eq!(dumps(&v, &DumpOptions::default()), ">\n");
}
#[test]
fn test_dump_simple_list() {
let v = Value::List(vec![
Value::String("a".to_string()),
Value::String("b".to_string()),
]);
assert_eq!(dumps(&v, &DumpOptions::default()), "- a\n- b\n");
}
#[test]
fn test_dump_empty_list() {
let v = Value::List(vec![]);
assert_eq!(dumps(&v, &DumpOptions::default()), "[]\n");
}
#[test]
fn test_dump_simple_dict() {
let v = Value::Dict(vec![
("name".to_string(), Value::String("John".to_string())),
("age".to_string(), Value::String("30".to_string())),
]);
assert_eq!(
dumps(&v, &DumpOptions::default()),
"name: John\nage: 30\n"
);
}
#[test]
fn test_dump_empty_dict() {
let v = Value::Dict(vec![]);
assert_eq!(dumps(&v, &DumpOptions::default()), "{}\n");
}
#[test]
fn test_dump_nested() {
let v = Value::Dict(vec![(
"items".to_string(),
Value::List(vec![
Value::String("a".to_string()),
Value::String("b".to_string()),
]),
)]);
assert_eq!(
dumps(&v, &DumpOptions::default()),
"items:\n - a\n - b\n"
);
}
#[test]
fn test_dump_multiline_key() {
let v = Value::Dict(vec![(
"".to_string(),
Value::String("value".to_string()),
)]);
let result = dumps(&v, &DumpOptions::default());
assert_eq!(result, ":\n > value\n");
}
#[test]
fn test_dump_sorted_keys() {
let v = Value::Dict(vec![
("b".to_string(), Value::String("2".to_string())),
("a".to_string(), Value::String("1".to_string())),
]);
let opts = DumpOptions {
sort_keys: true,
..Default::default()
};
assert_eq!(dumps(&v, &opts), "a: 1\nb: 2\n");
}
#[test]
fn test_roundtrip_simple() {
use crate::parser::{loads, Top};
let input = "name: John\nage: 30\n";
let v = loads(input, Top::Any).unwrap().unwrap();
let output = dumps(&v, &DumpOptions::default());
let v2 = loads(&output, Top::Any).unwrap().unwrap();
assert_eq!(v, v2);
}
}