hedl_cli/commands/
format.rs1use super::{read_file, write_output};
21use crate::error::CliError;
22use hedl_c14n::{canonicalize_with_config, CanonicalConfig};
23use hedl_core::{parse, Document, Item};
24
25pub fn format(
83 file: &str,
84 output: Option<&str>,
85 check: bool,
86 ditto: bool,
87 with_counts: bool,
88) -> Result<(), CliError> {
89 let content = read_file(file)?;
90
91 let mut doc =
92 parse(content.as_bytes()).map_err(|e| CliError::parse(format!("Parse error: {e}")))?;
93
94 if with_counts {
96 add_count_hints_to_doc(&mut doc);
97 }
98
99 let mut config = CanonicalConfig::default();
100 config.use_ditto = ditto;
101
102 let canonical = canonicalize_with_config(&doc, &config)
103 .map_err(|e| CliError::canonicalization(format!("Canonicalization error: {e}")))?;
104
105 if check {
106 let normalized_original = content.replace("\r\n", "\n");
108 if canonical.trim() != normalized_original.trim() {
109 return Err(CliError::NotCanonical);
110 }
111 println!("File is in canonical form");
112 Ok(())
113 } else {
114 write_output(&canonical, output)
115 }
116}
117
118fn add_count_hints_to_doc(doc: &mut Document) {
120 for item in doc.root.values_mut() {
121 add_count_hints_to_item(item);
122 }
123}
124
125fn add_count_hints_to_item(item: &mut Item) {
127 match item {
128 Item::List(list) => {
129 list.count_hint = Some(list.rows.len());
131
132 for node in &mut list.rows {
134 add_child_count_to_node(node);
135 }
136 }
137 Item::Object(map) => {
138 for nested_item in map.values_mut() {
140 add_count_hints_to_item(nested_item);
141 }
142 }
143 Item::Scalar(_) => {
144 }
146 }
147}
148
149fn add_child_count_to_node(node: &mut hedl_core::Node) {
151 let total_children: usize = node
153 .children()
154 .map_or(0, |c| c.values().map(std::vec::Vec::len).sum());
155
156 if total_children > 0 {
157 node.child_count = total_children.min(u16::MAX as usize) as u16;
158
159 if let Some(children) = node.children_mut() {
161 for child_list in children.values_mut() {
162 for child_node in child_list {
163 add_child_count_to_node(child_node);
164 }
165 }
166 }
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use hedl_core::{MatrixList, Node, Value};
174
175 #[test]
176 fn test_add_count_hints_to_empty_list() {
177 let list = MatrixList::new("Team", vec!["id".to_string(), "name".to_string()]);
178 assert_eq!(list.count_hint, None);
179
180 let mut item = Item::List(list);
181 add_count_hints_to_item(&mut item);
182
183 if let Item::List(list) = item {
184 assert_eq!(list.count_hint, Some(0));
185 } else {
186 panic!("Expected List item");
187 }
188 }
189
190 #[test]
191 fn test_add_count_hints_to_list_with_rows() {
192 let mut list = MatrixList::new("Team", vec!["id".to_string(), "name".to_string()]);
193 list.add_row(Node::new(
194 "Team",
195 "t1",
196 vec![Value::String("Team 1".into())],
197 ));
198 list.add_row(Node::new(
199 "Team",
200 "t2",
201 vec![Value::String("Team 2".into())],
202 ));
203 list.add_row(Node::new(
204 "Team",
205 "t3",
206 vec![Value::String("Team 3".into())],
207 ));
208 assert_eq!(list.count_hint, None);
209
210 let mut item = Item::List(list);
211 add_count_hints_to_item(&mut item);
212
213 if let Item::List(list) = item {
214 assert_eq!(list.count_hint, Some(3));
215 assert_eq!(list.rows.len(), 3);
216 } else {
217 panic!("Expected List item");
218 }
219 }
220
221 #[test]
222 fn test_add_count_hints_overwrites_existing() {
223 let mut list =
224 MatrixList::with_count_hint("Team", vec!["id".to_string(), "name".to_string()], 5);
225 list.add_row(Node::new(
226 "Team",
227 "t1",
228 vec![Value::String("Team 1".into())],
229 ));
230 list.add_row(Node::new(
231 "Team",
232 "t2",
233 vec![Value::String("Team 2".into())],
234 ));
235 assert_eq!(list.count_hint, Some(5)); let mut item = Item::List(list);
238 add_count_hints_to_item(&mut item);
239
240 if let Item::List(list) = item {
241 assert_eq!(list.count_hint, Some(2)); assert_eq!(list.rows.len(), 2);
243 } else {
244 panic!("Expected List item");
245 }
246 }
247
248 #[test]
249 fn test_add_count_hints_to_nested_objects() {
250 use std::collections::BTreeMap;
251
252 let mut list1 = MatrixList::new("Team", vec!["id".to_string()]);
253 list1.add_row(Node::new("Team", "t1", vec![]));
254
255 let mut list2 = MatrixList::new("Player", vec!["id".to_string()]);
256 list2.add_row(Node::new("Player", "p1", vec![]));
257 list2.add_row(Node::new("Player", "p2", vec![]));
258
259 let mut inner_map = BTreeMap::new();
260 inner_map.insert("teams".to_string(), Item::List(list1));
261
262 let mut outer_map = BTreeMap::new();
263 outer_map.insert("sports".to_string(), Item::Object(inner_map));
264 outer_map.insert("players".to_string(), Item::List(list2));
265
266 let mut item = Item::Object(outer_map);
267 add_count_hints_to_item(&mut item);
268
269 if let Item::Object(map) = item {
271 if let Some(Item::Object(sports)) = map.get("sports") {
273 if let Some(Item::List(teams)) = sports.get("teams") {
274 assert_eq!(teams.count_hint, Some(1));
275 } else {
276 panic!("Expected teams list in sports");
277 }
278 } else {
279 panic!("Expected sports object");
280 }
281
282 if let Some(Item::List(players)) = map.get("players") {
284 assert_eq!(players.count_hint, Some(2));
285 } else {
286 panic!("Expected players list");
287 }
288 } else {
289 panic!("Expected Object item");
290 }
291 }
292
293 #[test]
294 fn test_add_count_hints_to_scalar() {
295 let mut item = Item::Scalar(Value::String("test".into()));
296 add_count_hints_to_item(&mut item);
297 assert!(matches!(item, Item::Scalar(_)));
299 }
300
301 #[test]
302 fn test_add_count_hints_to_empty_object() {
303 use std::collections::BTreeMap;
304
305 let mut item = Item::Object(BTreeMap::new());
306 add_count_hints_to_item(&mut item);
307 assert!(matches!(item, Item::Object(_)));
309 }
310
311 #[test]
312 fn test_add_count_hints_document() {
313 let mut doc = Document::new((2, 0));
314
315 let mut list1 = MatrixList::new("Team", vec!["id".to_string()]);
316 list1.add_row(Node::new("Team", "t1", vec![]));
317 list1.add_row(Node::new("Team", "t2", vec![]));
318
319 let mut list2 = MatrixList::new("Player", vec!["id".to_string()]);
320 list2.add_row(Node::new("Player", "p1", vec![]));
321
322 doc.root.insert("teams".to_string(), Item::List(list1));
323 doc.root.insert("players".to_string(), Item::List(list2));
324
325 add_count_hints_to_doc(&mut doc);
326
327 if let Some(Item::List(teams)) = doc.root.get("teams") {
329 assert_eq!(teams.count_hint, Some(2));
330 } else {
331 panic!("Expected teams list");
332 }
333
334 if let Some(Item::List(players)) = doc.root.get("players") {
335 assert_eq!(players.count_hint, Some(1));
336 } else {
337 panic!("Expected players list");
338 }
339 }
340}