1use serde_json::{Map, Value};
4
5use crate::xml::builders::build_xml_string;
6use crate::xml::types::{MultiLevelConfig, XmlElement};
7
8pub fn strip_root_and_build_xml(parsed: &XmlElement, element_to_strip: &str) -> Option<String> {
13 let obj = parsed.as_object()?;
14 let root_key = obj.keys().find(|k| *k != "?xml")?.clone();
15 let root_val = obj.get(&root_key)?.as_object()?;
16 let decl = obj.get("?xml").cloned().unwrap_or_else(|| {
17 let mut d = Map::new();
18 d.insert("@version".to_string(), Value::String("1.0".to_string()));
19 d.insert("@encoding".to_string(), Value::String("UTF-8".to_string()));
20 Value::Object(d)
21 });
22
23 if root_key == element_to_strip {
24 let mut new_obj = Map::new();
26 new_obj.insert("?xml".to_string(), decl);
27 for (k, v) in root_val {
28 if !k.starts_with('@') {
29 new_obj.insert(k.clone(), v.clone());
30 }
31 }
32 return Some(build_xml_string(&Value::Object(new_obj)));
33 }
34
35 let inner = root_val.get(element_to_strip)?.as_object()?;
37 let mut new_root_val = Map::new();
38 for (k, v) in root_val {
39 if k != element_to_strip {
40 new_root_val.insert(k.clone(), v.clone());
41 }
42 }
43 for (k, v) in inner {
44 new_root_val.insert(k.clone(), v.clone());
45 }
46 let mut new_obj = Map::new();
47 new_obj.insert("?xml".to_string(), decl);
48 new_obj.insert(root_key, Value::Object(new_root_val));
49 Some(build_xml_string(&Value::Object(new_obj)))
50}
51
52pub fn capture_xmlns_from_root(parsed: &XmlElement) -> Option<String> {
54 let obj = parsed.as_object()?;
55 let root_key = obj.keys().find(|k| *k != "?xml")?.clone();
56 let root_val = obj.get(&root_key)?.as_object()?;
57 let xmlns = root_val.get("@xmlns")?.as_str()?;
58 Some(xmlns.to_string())
59}
60
61pub fn path_segment_from_file_pattern(file_pattern: &str) -> String {
63 file_pattern
66 .split('-')
67 .next()
68 .unwrap_or(file_pattern)
69 .to_string()
70}
71
72pub async fn load_multi_level_config(dir_path: &std::path::Path) -> Option<MultiLevelConfig> {
74 let path = dir_path.join(".multi_level.json");
75 let content = tokio::fs::read_to_string(&path).await.ok()?;
76 serde_json::from_str(&content).ok()
77}
78
79pub async fn save_multi_level_config(
81 dir_path: &std::path::Path,
82 config: &MultiLevelConfig,
83) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
84 let path = dir_path.join(".multi_level.json");
85 let content = serde_json::to_string_pretty(config)?;
86 tokio::fs::write(path, content).await?;
87 Ok(())
88}
89
90fn has_single_inner_wrapper(
96 root_val: &serde_json::Map<String, serde_json::Value>,
97 inner_wrapper: &str,
98) -> bool {
99 let non_attr_keys: Vec<&String> = root_val
104 .keys()
105 .filter(|k| *k != "@xmlns" && !k.starts_with('#'))
106 .collect();
107 non_attr_keys.len() == 1 && non_attr_keys[0].as_str() == inner_wrapper
108}
109
110fn should_unwrap_inner_segment(
119 current_root_key: &str,
120 document_root: &str,
121 single_inner: bool,
122) -> bool {
123 current_root_key == document_root && single_inner
124}
125
126pub async fn ensure_segment_files_structure(
130 dir_path: &std::path::Path,
131 document_root: &str,
132 inner_wrapper: &str,
133 xmlns: &str,
134) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
135 use crate::xml::parsers::parse_xml_from_str;
136 use serde_json::Map;
137
138 let mut entries = Vec::new();
139 let mut read_dir = tokio::fs::read_dir(dir_path).await?;
140 while let Some(entry) = read_dir.next_entry().await? {
141 entries.push(entry);
142 }
143 entries.sort_by_key(|e| e.file_name());
145
146 for entry in entries {
147 let path = entry.path();
148 if !path.is_file() {
149 continue;
150 }
151 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
152 if !name.ends_with(".xml") {
153 continue;
154 }
155 let path_str = path.to_string_lossy();
156 let content = tokio::fs::read_to_string(&path).await.unwrap_or_default();
159 let Some(parsed) = parse_xml_from_str(&content, &path_str) else {
160 continue;
161 };
162 let obj = parsed.as_object().cloned().unwrap_or_default();
165 let Some(current_root_key) = obj.keys().find(|k| *k != "?xml").cloned() else {
166 continue;
167 };
168 let root_val = obj
169 .get(¤t_root_key)
170 .and_then(|v| v.as_object())
171 .cloned()
172 .unwrap_or_default();
173
174 let decl = obj.get("?xml").cloned().unwrap_or_else(|| {
175 let mut d = Map::new();
176 d.insert(
177 "@version".to_string(),
178 serde_json::Value::String("1.0".to_string()),
179 );
180 d.insert(
181 "@encoding".to_string(),
182 serde_json::Value::String("UTF-8".to_string()),
183 );
184 serde_json::Value::Object(d)
185 });
186
187 let single_inner = has_single_inner_wrapper(&root_val, inner_wrapper);
188 let inner_content: serde_json::Value =
189 if should_unwrap_inner_segment(¤t_root_key, document_root, single_inner) {
190 let inner_obj = root_val
191 .get(inner_wrapper)
192 .and_then(|v| v.as_object())
193 .cloned()
194 .unwrap_or_else(Map::new);
195 let mut inner_clean = Map::new();
196 for (k, v) in &inner_obj {
197 if k != "@xmlns" {
198 inner_clean.insert(k.clone(), v.clone());
199 }
200 }
201 serde_json::Value::Object(inner_clean)
202 } else {
203 let mut inner_clean = Map::new();
207 for (k, v) in &root_val {
208 if k != "@xmlns" {
209 inner_clean.insert(k.clone(), v.clone());
210 }
211 }
212 serde_json::Value::Object(inner_clean)
213 };
214
215 let already_correct = current_root_key == document_root
216 && root_val.get("@xmlns").is_some()
217 && single_inner
218 && root_val
219 .get(inner_wrapper)
220 .and_then(|v| v.as_object())
221 .map(|o| !o.contains_key("@xmlns"))
222 .unwrap_or(true);
223 if already_correct {
224 continue;
225 }
226
227 let mut root_val_new = Map::new();
229 if !xmlns.is_empty() {
230 root_val_new.insert(
231 "@xmlns".to_string(),
232 serde_json::Value::String(xmlns.to_string()),
233 );
234 }
235 root_val_new.insert(inner_wrapper.to_string(), inner_content);
236
237 let mut top = Map::new();
238 top.insert("?xml".to_string(), decl);
239 top.insert(
240 document_root.to_string(),
241 serde_json::Value::Object(root_val_new),
242 );
243 let wrapped = serde_json::Value::Object(top);
244 let xml_string = build_xml_string(&wrapped);
245 tokio::fs::write(&path, xml_string).await?;
246 }
247 Ok(())
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use serde_json::json;
254
255 #[test]
256 fn path_segment_from_file_pattern_strips_suffix() {
257 assert_eq!(
258 path_segment_from_file_pattern("programProcesses-meta"),
259 "programProcesses"
260 );
261 }
262
263 #[test]
264 fn path_segment_from_file_pattern_no_dash() {
265 assert_eq!(path_segment_from_file_pattern("foo"), "foo");
266 }
267
268 #[test]
269 fn strip_root_and_build_xml_strips_child_not_root() {
270 let parsed = json!({
271 "?xml": { "@version": "1.0" },
272 "Root": {
273 "programProcesses": { "a": "1", "b": "2" },
274 "label": "x"
275 }
276 });
277 let out = strip_root_and_build_xml(&parsed, "programProcesses").unwrap();
278 assert!(out.contains("<Root>"));
279 assert!(out.contains("<a>1</a>"));
280 assert!(out.contains("<b>2</b>"));
281 assert!(out.contains("<label>x</label>"));
282 }
283
284 #[test]
285 fn strip_root_and_build_xml_strips_root_excludes_attributes() {
286 let parsed = json!({
287 "?xml": { "@version": "1.0" },
288 "LoyaltyProgramSetup": {
289 "@xmlns": "http://example.com",
290 "programProcesses": { "x": "1" }
291 }
292 });
293 let out = strip_root_and_build_xml(&parsed, "LoyaltyProgramSetup").unwrap();
294 assert!(!out.contains("@xmlns"));
295 assert!(out.contains("programProcesses"));
296 }
297
298 #[test]
299 fn capture_xmlns_from_root_returns_some() {
300 let parsed = json!({
301 "Root": { "@xmlns": "http://ns.example.com" }
302 });
303 assert_eq!(
304 capture_xmlns_from_root(&parsed),
305 Some("http://ns.example.com".to_string())
306 );
307 }
308
309 #[test]
310 fn capture_xmlns_from_root_returns_none_when_absent() {
311 let parsed = json!({ "Root": { "child": "x" } });
312 assert!(capture_xmlns_from_root(&parsed).is_none());
313 }
314
315 #[tokio::test]
316 async fn save_and_load_multi_level_config() {
317 let dir = tempfile::tempdir().unwrap();
318 let config = MultiLevelConfig {
319 rules: vec![crate::xml::types::MultiLevelRule {
320 file_pattern: "test-meta".to_string(),
321 root_to_strip: "Root".to_string(),
322 unique_id_elements: "id".to_string(),
323 path_segment: "test".to_string(),
324 wrap_root_element: "Root".to_string(),
325 wrap_xmlns: "http://example.com".to_string(),
326 }],
327 };
328 save_multi_level_config(dir.path(), &config).await.unwrap();
329 let loaded = load_multi_level_config(dir.path()).await.unwrap();
330 assert_eq!(loaded.rules.len(), 1);
331 assert_eq!(loaded.rules[0].path_segment, "test");
332 }
333
334 #[tokio::test]
335 async fn load_multi_level_config_missing_file_returns_none() {
336 let dir = tempfile::tempdir().unwrap();
337 assert!(load_multi_level_config(dir.path()).await.is_none());
338 }
339
340 #[tokio::test]
341 async fn ensure_segment_files_structure_empty_xmlns_omits_xmlns_attribute() {
342 let dir = tempfile::tempdir().unwrap();
345 let xml = r#"<?xml version="1.0"?><Root><inner><x>1</x></inner></Root>"#;
346 let path = dir.path().join("seg.xml");
347 tokio::fs::write(&path, xml).await.unwrap();
348 ensure_segment_files_structure(
349 dir.path(),
350 "Root",
351 "inner",
352 "", )
354 .await
355 .unwrap();
356 let out = tokio::fs::read_to_string(&path).await.unwrap();
357 assert!(
358 !out.contains("xmlns"),
359 "empty xmlns must not emit an xmlns attribute: {out}"
360 );
361 assert!(
362 out.contains("<inner>"),
363 "inner wrapper must be present: {out}"
364 );
365 }
366
367 #[tokio::test]
368 async fn ensure_segment_files_structure_adds_xmlns_and_rewrites() {
369 let dir = tempfile::tempdir().unwrap();
370 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
371<Root>
372 <programProcesses><x>1</x></programProcesses>
373</Root>"#;
374 let path = dir.path().join("segment.xml");
375 tokio::fs::write(&path, xml).await.unwrap();
376 ensure_segment_files_structure(
377 dir.path(),
378 "Root",
379 "programProcesses",
380 "http://example.com",
381 )
382 .await
383 .unwrap();
384 let out = tokio::fs::read_to_string(&path).await.unwrap();
385 assert!(out.contains("http://example.com"));
386 assert!(out.contains("<programProcesses>"));
387 assert!(out.contains("<x>1</x>"));
388 }
389
390 #[tokio::test]
391 async fn ensure_segment_files_structure_skips_already_correct_files() {
392 let dir = tempfile::tempdir().unwrap();
394 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
395<Root xmlns="http://example.com"><programProcesses><x>1</x></programProcesses></Root>"#;
396 let path = dir.path().join("ok.xml");
397 tokio::fs::write(&path, xml).await.unwrap();
398 let before = tokio::fs::metadata(&path).await.unwrap().modified().ok();
399 ensure_segment_files_structure(
400 dir.path(),
401 "Root",
402 "programProcesses",
403 "http://example.com",
404 )
405 .await
406 .unwrap();
407 let after = tokio::fs::metadata(&path).await.unwrap().modified().ok();
408 assert_eq!(before, after, "already-correct files must be left as-is");
409 }
410
411 #[tokio::test]
412 async fn ensure_segment_files_structure_skips_non_xml_and_subdirs() {
413 let dir = tempfile::tempdir().unwrap();
414 tokio::fs::create_dir(dir.path().join("nested"))
415 .await
416 .unwrap();
417 tokio::fs::write(dir.path().join("notes.txt"), "hello")
418 .await
419 .unwrap();
420 tokio::fs::write(dir.path().join("broken.xml"), "<<not xml>")
421 .await
422 .unwrap();
423 ensure_segment_files_structure(
425 dir.path(),
426 "Root",
427 "programProcesses",
428 "http://example.com",
429 )
430 .await
431 .unwrap();
432 let raw = tokio::fs::read_to_string(dir.path().join("broken.xml"))
434 .await
435 .unwrap();
436 assert_eq!(raw, "<<not xml>");
437 }
438
439 #[tokio::test]
440 async fn ensure_segment_files_structure_skips_xml_missing_root() {
441 let dir = tempfile::tempdir().unwrap();
443 tokio::fs::write(dir.path().join("empty.xml"), "")
444 .await
445 .unwrap();
446 ensure_segment_files_structure(dir.path(), "Root", "programProcesses", "")
447 .await
448 .unwrap();
449 }
450
451 fn map_from(pairs: &[(&str, serde_json::Value)]) -> serde_json::Map<String, serde_json::Value> {
452 let mut m = serde_json::Map::new();
453 for (k, v) in pairs {
454 m.insert((*k).to_string(), v.clone());
455 }
456 m
457 }
458
459 #[test]
460 fn has_single_inner_wrapper_true_for_single_matching_child() {
461 let m = map_from(&[("inner", json!({"a": 1}))]);
462 assert!(has_single_inner_wrapper(&m, "inner"));
463 }
464
465 #[test]
466 fn has_single_inner_wrapper_true_when_only_attribute_is_xmlns_sibling() {
467 let m = map_from(&[
471 ("@xmlns", json!("http://example.com")),
472 ("inner", json!({"a": 1})),
473 ]);
474 assert!(has_single_inner_wrapper(&m, "inner"));
475 }
476
477 #[test]
478 fn has_single_inner_wrapper_true_when_sibling_is_internal_compact_marker() {
479 let m = map_from(&[
486 ("@xmlns", json!("http://example.com")),
487 ("inner", json!({"a": 1})),
488 ("#compact", json!(true)),
489 ]);
490 assert!(has_single_inner_wrapper(&m, "inner"));
491 }
492
493 #[test]
494 fn has_single_inner_wrapper_false_when_multiple_non_attribute_children() {
495 let m = map_from(&[("inner", json!({})), ("other", json!({}))]);
496 assert!(!has_single_inner_wrapper(&m, "inner"));
497 }
498
499 #[test]
500 fn has_single_inner_wrapper_false_when_only_child_name_differs() {
501 let m = map_from(&[("notInner", json!({"a": 1}))]);
502 assert!(!has_single_inner_wrapper(&m, "inner"));
503 }
504
505 #[test]
506 fn has_single_inner_wrapper_false_when_empty() {
507 let m = serde_json::Map::new();
508 assert!(!has_single_inner_wrapper(&m, "inner"));
509 }
510
511 #[test]
512 fn should_unwrap_inner_segment_true_when_root_matches_and_single_inner() {
513 assert!(should_unwrap_inner_segment("Doc", "Doc", true));
518 }
519
520 #[test]
521 fn should_unwrap_inner_segment_false_when_current_root_differs() {
522 assert!(!should_unwrap_inner_segment("Other", "Doc", true));
527 }
528
529 #[test]
530 fn should_unwrap_inner_segment_false_when_not_single_inner() {
531 assert!(!should_unwrap_inner_segment("Doc", "Doc", false));
534 }
535
536 #[tokio::test]
537 async fn ensure_segment_files_structure_else_branch_when_root_differs_from_document_root() {
538 let dir = tempfile::tempdir().unwrap();
541 let xml = r#"<Item><child>x</child></Item>"#;
543 let path = dir.path().join("item.xml");
544 tokio::fs::write(&path, xml).await.unwrap();
545 ensure_segment_files_structure(
546 dir.path(),
547 "Root", "child",
549 "http://example.com",
550 )
551 .await
552 .unwrap();
553 let out = tokio::fs::read_to_string(&path).await.unwrap();
554 assert!(out.contains("<Root"), "expected Root element: {out}");
556 assert!(
557 out.contains("http://example.com"),
558 "expected xmlns attribute: {out}"
559 );
560 }
561
562 #[tokio::test]
563 async fn ensure_segment_files_structure_else_branch_multiple_children() {
564 let dir = tempfile::tempdir().unwrap();
566 let xml = r#"<Root><a>1</a><b>2</b></Root>"#;
568 let path = dir.path().join("multi.xml");
569 tokio::fs::write(&path, xml).await.unwrap();
570 ensure_segment_files_structure(
571 dir.path(),
572 "Root",
573 "inner", "http://example.com",
575 )
576 .await
577 .unwrap();
578 let out = tokio::fs::read_to_string(&path).await.unwrap();
579 assert!(
580 out.contains("<Root"),
581 "Root element must be in output: {out}"
582 );
583 }
584}