use crate::model::CclObject;
use crate::Entry;
pub fn print(entries: &[Entry]) -> String {
entries
.iter()
.map(|entry| {
if entry.key.is_empty() {
format!("= {}", entry.value)
} else {
format!("{} = {}", entry.key, entry.value)
}
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn round_trip(input: &str) -> crate::Result<bool> {
let entries1 = crate::parse(input)?;
let printed = print(&entries1);
let entries2 = crate::parse(&printed)?;
Ok(entries1 == entries2)
}
#[derive(Debug, Clone)]
pub struct PrinterConfig {
pub indent_size: usize,
pub use_bare_list_syntax: bool,
}
impl Default for PrinterConfig {
fn default() -> Self {
Self {
indent_size: 2,
use_bare_list_syntax: true,
}
}
}
impl PrinterConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_indent_size(mut self, size: usize) -> Self {
self.indent_size = size;
self
}
pub fn with_bare_list_syntax(mut self, use_bare: bool) -> Self {
self.use_bare_list_syntax = use_bare;
self
}
}
#[derive(Debug, Clone)]
pub struct CclPrinter {
config: PrinterConfig,
}
impl Default for CclPrinter {
fn default() -> Self {
Self::new()
}
}
impl CclPrinter {
pub fn new() -> Self {
Self {
config: PrinterConfig::default(),
}
}
pub fn with_config(config: PrinterConfig) -> Self {
Self { config }
}
pub fn print(&self, model: &CclObject) -> String {
let mut output = String::new();
self.print_object(model, 0, &mut output);
output.trim_end_matches('\n').to_string()
}
fn print_object(&self, model: &CclObject, indent: usize, output: &mut String) {
let indent_str = " ".repeat(indent);
for (key, value) in model.iter_all() {
self.print_entry(key, value, &indent_str, indent, output);
}
}
fn print_entry(
&self,
key: &str,
value: &CclObject,
indent_str: &str,
indent: usize,
output: &mut String,
) {
if key.is_empty() && value.is_empty() {
output.push('\n');
return;
}
if key.starts_with("/=") && value.is_empty() {
output.push_str(indent_str);
output.push_str(key);
output.push('\n');
return;
}
if value.is_empty() {
output.push_str(indent_str);
output.push_str(key);
output.push_str(" =\n");
} else if self.is_string_value(value) {
let string_value = value.keys().next().unwrap();
output.push_str(indent_str);
if key.is_empty() {
output.push_str("= ");
} else {
output.push_str(key);
output.push_str(" = ");
}
output.push_str(string_value);
output.push('\n');
} else if self.is_list_value(value) {
self.print_list(key, value, indent_str, indent, output);
} else {
output.push_str(indent_str);
output.push_str(key);
output.push_str(" =\n");
self.print_object(value, indent + self.config.indent_size, output);
}
}
fn is_string_value(&self, value: &CclObject) -> bool {
if value.len() != 1 {
return false;
}
if let Some((_, child)) = value.iter().next() {
return child.is_empty();
}
false
}
fn is_list_value(&self, value: &CclObject) -> bool {
if value.len() < 2 {
return false;
}
value.values().all(|child| child.is_empty())
}
fn print_list(
&self,
key: &str,
value: &CclObject,
indent_str: &str,
indent: usize,
output: &mut String,
) {
let child_indent_str = " ".repeat(indent + self.config.indent_size);
if key.is_empty() || (self.config.use_bare_list_syntax && !key.is_empty()) {
if !key.is_empty() {
output.push_str(indent_str);
output.push_str(key);
output.push_str(" =\n");
}
for item_key in value.keys() {
if key.is_empty() {
output.push_str(indent_str);
} else {
output.push_str(&child_indent_str);
}
output.push_str("= ");
output.push_str(item_key);
output.push('\n');
}
} else {
for item_key in value.keys() {
output.push_str(indent_str);
output.push_str(key);
output.push_str(" = ");
output.push_str(item_key);
output.push('\n');
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::load;
#[test]
fn test_print_basic() {
let input = "key = value\nnested =\n sub = val";
let entries = crate::parse(input).unwrap();
let output = print(&entries);
assert_eq!(output, "key = value\nnested = \n sub = val");
}
#[test]
fn test_print_empty_keys_lists() {
let input = "= item1\n= item2\nregular = value";
let entries = crate::parse(input).unwrap();
let output = print(&entries);
assert_eq!(output, "= item1\n= item2\nregular = value");
}
#[test]
fn test_print_nested_structures() {
let input = "config =\n host = localhost\n port = 8080\n db =\n name = mydb\n user = admin";
let entries = crate::parse(input).unwrap();
let output = print(&entries);
assert_eq!(
output,
"config = \n host = localhost\n port = 8080\n db =\n name = mydb\n user = admin"
);
}
#[test]
fn test_print_multiline_values() {
let input = "script =\n #!/bin/bash\n echo hello\n exit 0";
let entries = crate::parse(input).unwrap();
let output = print(&entries);
assert_eq!(output, "script = \n #!/bin/bash\n echo hello\n exit 0");
}
#[test]
fn test_print_empty_value() {
let input = "empty_section =\n\nother = value";
let entries = crate::parse(input).unwrap();
let output = print(&entries);
assert_eq!(output, "empty_section = \nother = value");
}
#[test]
fn test_print_deeply_nested() {
let input =
"level1 =\n level2 =\n level3 =\n level4 =\n deep = value\n = deep_item";
let entries = crate::parse(input).unwrap();
let output = print(&entries);
assert_eq!(
output,
"level1 = \n level2 =\n level3 =\n level4 =\n deep = value\n = deep_item"
);
}
#[test]
fn test_print_mixed_content() {
let input =
"name = Alice\n= first item\nconfig =\n port = 3000\n= second item\nfinal = value";
let entries = crate::parse(input).unwrap();
let output = print(&entries);
assert_eq!(
output,
"name = Alice\n= first item\nconfig = \n port = 3000\n= second item\nfinal = value"
);
}
#[test]
fn test_print_complex_nesting() {
let input = "app =\n = item1\n config =\n = nested_item\n db =\n host = localhost\n = db_item\n = item2";
let entries = crate::parse(input).unwrap();
let output = print(&entries);
assert_eq!(
output,
"app = \n = item1\n config =\n = nested_item\n db =\n host = localhost\n = db_item\n = item2"
);
}
#[test]
fn test_round_trip_basic() {
assert!(round_trip("key = value\nnested =\n sub = val").unwrap());
}
#[test]
fn test_round_trip_empty_keys() {
assert!(round_trip("= item1\n= item2\nregular = value").unwrap());
}
#[test]
fn test_round_trip_nested() {
assert!(round_trip("config =\n host = localhost\n port = 8080\n db =\n name = mydb\n user = admin").unwrap());
}
#[test]
fn test_round_trip_multiline() {
assert!(round_trip("script =\n #!/bin/bash\n echo hello\n exit 0").unwrap());
}
#[test]
fn test_round_trip_deeply_nested() {
assert!(round_trip("level1 =\n level2 =\n level3 =\n level4 =\n deep = value\n = deep_item").unwrap());
}
#[test]
fn test_round_trip_mixed_content() {
let input =
"name = Alice\n= first item\nconfig =\n port = 3000\n= second item\nfinal = value";
let entries = crate::parse(input).unwrap();
let printed = print(&entries);
assert_eq!(
printed,
"name = Alice\n= first item\nconfig = \n port = 3000\n= second item\nfinal = value"
);
assert!(round_trip(input).unwrap());
}
#[test]
fn test_round_trip_empty_value() {
assert!(round_trip("empty_section =\n\nother = value").unwrap());
}
#[test]
fn test_simple_key_value() {
let ccl = "name = Alice\nage = 42";
let model = load(ccl).unwrap();
let printer = CclPrinter::new();
let output = printer.print(&model);
assert_eq!(output, "name = Alice\nage = 42");
}
#[test]
fn test_nested_object() {
let ccl = "server =\n host = localhost\n port = 8080";
let model = load(ccl).unwrap();
let printer = CclPrinter::new();
let output = printer.print(&model);
assert_eq!(output, "server =\n host = localhost\n port = 8080");
}
#[test]
fn test_list_with_bare_syntax() {
let ccl = "servers =\n = web1\n = web2";
let model = load(ccl).unwrap();
let printer = CclPrinter::new();
let output = printer.print(&model);
assert!(output.contains("servers ="));
assert!(output.contains("= web1") || output.contains("= web2"));
}
#[test]
fn test_list_with_duplicate_keys_syntax() {
let ccl = "servers = web1\nservers = web2\nservers = web3";
let model = load(ccl).unwrap();
let printer = CclPrinter::with_config(PrinterConfig::new().with_bare_list_syntax(false));
let output = printer.print(&model);
assert!(output.contains("servers = "));
}
#[test]
fn test_comment_preservation() {
let ccl = "/= This is a comment\nname = Alice";
let model = load(ccl).unwrap();
let printer = CclPrinter::new();
let output = printer.print(&model);
assert!(output.contains("/"));
}
#[test]
fn test_deeply_nested() {
let ccl = "level1 =\n level2 =\n level3 = value";
let model = load(ccl).unwrap();
let printer = CclPrinter::new();
let output = printer.print(&model);
assert!(output.contains("level1 ="));
assert!(output.contains(" level2 ="));
assert!(output.contains(" level3 = value"));
}
#[test]
fn test_custom_indent_size() {
let ccl = "parent =\n child = value";
let model = load(ccl).unwrap();
let printer = CclPrinter::with_config(PrinterConfig::new().with_indent_size(4));
let output = printer.print(&model);
assert!(output.contains("parent =\n child = value"));
}
#[test]
fn test_empty_model() {
let ccl = "";
let model = load(ccl).unwrap();
let printer = CclPrinter::new();
let output = printer.print(&model);
assert!(output.is_empty());
}
#[test]
fn test_mixed_nested_and_lists() {
let ccl = "config =\n server = web1\n server = web2\n port = 80";
let model = load(ccl).unwrap();
let printer = CclPrinter::new();
let output = printer.print(&model);
assert!(output.contains("config ="));
assert!(output.contains("port = 80"));
}
#[test]
fn test_from_list_uses_correct_indentation() {
use crate::CclObject;
let mut model = CclObject::new();
let map = model.inner_mut();
map.insert(
"package".to_string(),
vec![CclObject::from_list(vec!["brew", "scoop", "nix"])],
);
let printer = CclPrinter::new();
let output = printer.print(&model);
assert!(output.contains("package =\n"));
assert!(output.contains(" = brew\n")); assert!(output.contains(" = scoop\n"));
assert!(output.contains(" = nix"));
assert!(!output.contains(" = "));
}
}