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