use super::{read_file, write_output};
use crate::error::CliError;
use hedl_c14n::{canonicalize_with_config, CanonicalConfig};
use hedl_core::{parse, Document, Item};
pub fn format(
file: &str,
output: Option<&str>,
check: bool,
ditto: bool,
with_counts: bool,
) -> Result<(), CliError> {
let content = read_file(file)?;
let mut doc =
parse(content.as_bytes()).map_err(|e| CliError::parse(format!("Parse error: {e}")))?;
if with_counts {
add_count_hints_to_doc(&mut doc);
}
let mut config = CanonicalConfig::default();
config.use_ditto = ditto;
let canonical = canonicalize_with_config(&doc, &config)
.map_err(|e| CliError::canonicalization(format!("Canonicalization error: {e}")))?;
if check {
let normalized_original = content.replace("\r\n", "\n");
if canonical.trim() != normalized_original.trim() {
return Err(CliError::NotCanonical);
}
println!("File is in canonical form");
Ok(())
} else {
write_output(&canonical, output)
}
}
fn add_count_hints_to_doc(doc: &mut Document) {
for item in doc.root.values_mut() {
add_count_hints_to_item(item);
}
}
fn add_count_hints_to_item(item: &mut Item) {
match item {
Item::List(list) => {
list.count_hint = Some(list.rows.len());
for node in &mut list.rows {
add_child_count_to_node(node);
}
}
Item::Object(map) => {
for nested_item in map.values_mut() {
add_count_hints_to_item(nested_item);
}
}
Item::Scalar(_) => {
}
}
}
fn add_child_count_to_node(node: &mut hedl_core::Node) {
let total_children: usize = node
.children()
.map_or(0, |c| c.values().map(std::vec::Vec::len).sum());
if total_children > 0 {
node.child_count = total_children.min(u16::MAX as usize) as u16;
if let Some(children) = node.children_mut() {
for child_list in children.values_mut() {
for child_node in child_list {
add_child_count_to_node(child_node);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use hedl_core::{MatrixList, Node, Value};
#[test]
fn test_add_count_hints_to_empty_list() {
let list = MatrixList::new("Team", vec!["id".to_string(), "name".to_string()]);
assert_eq!(list.count_hint, None);
let mut item = Item::List(list);
add_count_hints_to_item(&mut item);
if let Item::List(list) = item {
assert_eq!(list.count_hint, Some(0));
} else {
panic!("Expected List item");
}
}
#[test]
fn test_add_count_hints_to_list_with_rows() {
let mut list = MatrixList::new("Team", vec!["id".to_string(), "name".to_string()]);
list.add_row(Node::new(
"Team",
"t1",
vec![Value::String("Team 1".into())],
));
list.add_row(Node::new(
"Team",
"t2",
vec![Value::String("Team 2".into())],
));
list.add_row(Node::new(
"Team",
"t3",
vec![Value::String("Team 3".into())],
));
assert_eq!(list.count_hint, None);
let mut item = Item::List(list);
add_count_hints_to_item(&mut item);
if let Item::List(list) = item {
assert_eq!(list.count_hint, Some(3));
assert_eq!(list.rows.len(), 3);
} else {
panic!("Expected List item");
}
}
#[test]
fn test_add_count_hints_overwrites_existing() {
let mut list =
MatrixList::with_count_hint("Team", vec!["id".to_string(), "name".to_string()], 5);
list.add_row(Node::new(
"Team",
"t1",
vec![Value::String("Team 1".into())],
));
list.add_row(Node::new(
"Team",
"t2",
vec![Value::String("Team 2".into())],
));
assert_eq!(list.count_hint, Some(5));
let mut item = Item::List(list);
add_count_hints_to_item(&mut item);
if let Item::List(list) = item {
assert_eq!(list.count_hint, Some(2)); assert_eq!(list.rows.len(), 2);
} else {
panic!("Expected List item");
}
}
#[test]
fn test_add_count_hints_to_nested_objects() {
use std::collections::BTreeMap;
let mut list1 = MatrixList::new("Team", vec!["id".to_string()]);
list1.add_row(Node::new("Team", "t1", vec![]));
let mut list2 = MatrixList::new("Player", vec!["id".to_string()]);
list2.add_row(Node::new("Player", "p1", vec![]));
list2.add_row(Node::new("Player", "p2", vec![]));
let mut inner_map = BTreeMap::new();
inner_map.insert("teams".to_string(), Item::List(list1));
let mut outer_map = BTreeMap::new();
outer_map.insert("sports".to_string(), Item::Object(inner_map));
outer_map.insert("players".to_string(), Item::List(list2));
let mut item = Item::Object(outer_map);
add_count_hints_to_item(&mut item);
if let Item::Object(map) = item {
if let Some(Item::Object(sports)) = map.get("sports") {
if let Some(Item::List(teams)) = sports.get("teams") {
assert_eq!(teams.count_hint, Some(1));
} else {
panic!("Expected teams list in sports");
}
} else {
panic!("Expected sports object");
}
if let Some(Item::List(players)) = map.get("players") {
assert_eq!(players.count_hint, Some(2));
} else {
panic!("Expected players list");
}
} else {
panic!("Expected Object item");
}
}
#[test]
fn test_add_count_hints_to_scalar() {
let mut item = Item::Scalar(Value::String("test".into()));
add_count_hints_to_item(&mut item);
assert!(matches!(item, Item::Scalar(_)));
}
#[test]
fn test_add_count_hints_to_empty_object() {
use std::collections::BTreeMap;
let mut item = Item::Object(BTreeMap::new());
add_count_hints_to_item(&mut item);
assert!(matches!(item, Item::Object(_)));
}
#[test]
fn test_add_count_hints_document() {
let mut doc = Document::new((2, 0));
let mut list1 = MatrixList::new("Team", vec!["id".to_string()]);
list1.add_row(Node::new("Team", "t1", vec![]));
list1.add_row(Node::new("Team", "t2", vec![]));
let mut list2 = MatrixList::new("Player", vec!["id".to_string()]);
list2.add_row(Node::new("Player", "p1", vec![]));
doc.root.insert("teams".to_string(), Item::List(list1));
doc.root.insert("players".to_string(), Item::List(list2));
add_count_hints_to_doc(&mut doc);
if let Some(Item::List(teams)) = doc.root.get("teams") {
assert_eq!(teams.count_hint, Some(2));
} else {
panic!("Expected teams list");
}
if let Some(Item::List(players)) = doc.root.get("players") {
assert_eq!(players.count_hint, Some(1));
} else {
panic!("Expected players list");
}
}
}