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.keys().filter(|k| *k != "@xmlns").collect();
100 non_attr_keys.len() == 1 && non_attr_keys[0].as_str() == inner_wrapper
101}
102
103fn should_unwrap_inner_segment(
112 current_root_key: &str,
113 document_root: &str,
114 single_inner: bool,
115) -> bool {
116 current_root_key == document_root && single_inner
117}
118
119pub async fn ensure_segment_files_structure(
123 dir_path: &std::path::Path,
124 document_root: &str,
125 inner_wrapper: &str,
126 xmlns: &str,
127) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
128 use crate::xml::parsers::parse_xml_from_str;
129 use serde_json::Map;
130
131 let mut entries = Vec::new();
132 let mut read_dir = tokio::fs::read_dir(dir_path).await?;
133 while let Some(entry) = read_dir.next_entry().await? {
134 entries.push(entry);
135 }
136 entries.sort_by_key(|e| e.file_name());
138
139 for entry in entries {
140 let path = entry.path();
141 if !path.is_file() {
142 continue;
143 }
144 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
145 if !name.ends_with(".xml") {
146 continue;
147 }
148 let path_str = path.to_string_lossy();
149 let content = tokio::fs::read_to_string(&path).await.unwrap_or_default();
152 let Some(parsed) = parse_xml_from_str(&content, &path_str) else {
153 continue;
154 };
155 let obj = parsed.as_object().cloned().unwrap_or_default();
158 let Some(current_root_key) = obj.keys().find(|k| *k != "?xml").cloned() else {
159 continue;
160 };
161 let root_val = obj
162 .get(¤t_root_key)
163 .and_then(|v| v.as_object())
164 .cloned()
165 .unwrap_or_default();
166
167 let decl = obj.get("?xml").cloned().unwrap_or_else(|| {
168 let mut d = Map::new();
169 d.insert(
170 "@version".to_string(),
171 serde_json::Value::String("1.0".to_string()),
172 );
173 d.insert(
174 "@encoding".to_string(),
175 serde_json::Value::String("UTF-8".to_string()),
176 );
177 serde_json::Value::Object(d)
178 });
179
180 let single_inner = has_single_inner_wrapper(&root_val, inner_wrapper);
181 let inner_content: serde_json::Value =
182 if should_unwrap_inner_segment(¤t_root_key, document_root, single_inner) {
183 let inner_obj = root_val
184 .get(inner_wrapper)
185 .and_then(|v| v.as_object())
186 .cloned()
187 .unwrap_or_else(Map::new);
188 let mut inner_clean = Map::new();
189 for (k, v) in &inner_obj {
190 if k != "@xmlns" {
191 inner_clean.insert(k.clone(), v.clone());
192 }
193 }
194 serde_json::Value::Object(inner_clean)
195 } else {
196 let mut inner_clean = Map::new();
200 for (k, v) in &root_val {
201 if k != "@xmlns" {
202 inner_clean.insert(k.clone(), v.clone());
203 }
204 }
205 serde_json::Value::Object(inner_clean)
206 };
207
208 let already_correct = current_root_key == document_root
209 && root_val.get("@xmlns").is_some()
210 && single_inner
211 && root_val
212 .get(inner_wrapper)
213 .and_then(|v| v.as_object())
214 .map(|o| !o.contains_key("@xmlns"))
215 .unwrap_or(true);
216 if already_correct {
217 continue;
218 }
219
220 let mut root_val_new = Map::new();
222 if !xmlns.is_empty() {
223 root_val_new.insert(
224 "@xmlns".to_string(),
225 serde_json::Value::String(xmlns.to_string()),
226 );
227 }
228 root_val_new.insert(inner_wrapper.to_string(), inner_content);
229
230 let mut top = Map::new();
231 top.insert("?xml".to_string(), decl);
232 top.insert(
233 document_root.to_string(),
234 serde_json::Value::Object(root_val_new),
235 );
236 let wrapped = serde_json::Value::Object(top);
237 let xml_string = build_xml_string(&wrapped);
238 tokio::fs::write(&path, xml_string).await?;
239 }
240 Ok(())
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use serde_json::json;
247
248 #[test]
249 fn path_segment_from_file_pattern_strips_suffix() {
250 assert_eq!(
251 path_segment_from_file_pattern("programProcesses-meta"),
252 "programProcesses"
253 );
254 }
255
256 #[test]
257 fn path_segment_from_file_pattern_no_dash() {
258 assert_eq!(path_segment_from_file_pattern("foo"), "foo");
259 }
260
261 #[test]
262 fn strip_root_and_build_xml_strips_child_not_root() {
263 let parsed = json!({
264 "?xml": { "@version": "1.0" },
265 "Root": {
266 "programProcesses": { "a": "1", "b": "2" },
267 "label": "x"
268 }
269 });
270 let out = strip_root_and_build_xml(&parsed, "programProcesses").unwrap();
271 assert!(out.contains("<Root>"));
272 assert!(out.contains("<a>1</a>"));
273 assert!(out.contains("<b>2</b>"));
274 assert!(out.contains("<label>x</label>"));
275 }
276
277 #[test]
278 fn strip_root_and_build_xml_strips_root_excludes_attributes() {
279 let parsed = json!({
280 "?xml": { "@version": "1.0" },
281 "LoyaltyProgramSetup": {
282 "@xmlns": "http://example.com",
283 "programProcesses": { "x": "1" }
284 }
285 });
286 let out = strip_root_and_build_xml(&parsed, "LoyaltyProgramSetup").unwrap();
287 assert!(!out.contains("@xmlns"));
288 assert!(out.contains("programProcesses"));
289 }
290
291 #[test]
292 fn capture_xmlns_from_root_returns_some() {
293 let parsed = json!({
294 "Root": { "@xmlns": "http://ns.example.com" }
295 });
296 assert_eq!(
297 capture_xmlns_from_root(&parsed),
298 Some("http://ns.example.com".to_string())
299 );
300 }
301
302 #[test]
303 fn capture_xmlns_from_root_returns_none_when_absent() {
304 let parsed = json!({ "Root": { "child": "x" } });
305 assert!(capture_xmlns_from_root(&parsed).is_none());
306 }
307
308 #[tokio::test]
309 async fn save_and_load_multi_level_config() {
310 let dir = tempfile::tempdir().unwrap();
311 let config = MultiLevelConfig {
312 rules: vec![crate::xml::types::MultiLevelRule {
313 file_pattern: "test-meta".to_string(),
314 root_to_strip: "Root".to_string(),
315 unique_id_elements: "id".to_string(),
316 path_segment: "test".to_string(),
317 wrap_root_element: "Root".to_string(),
318 wrap_xmlns: "http://example.com".to_string(),
319 }],
320 };
321 save_multi_level_config(dir.path(), &config).await.unwrap();
322 let loaded = load_multi_level_config(dir.path()).await.unwrap();
323 assert_eq!(loaded.rules.len(), 1);
324 assert_eq!(loaded.rules[0].path_segment, "test");
325 }
326
327 #[tokio::test]
328 async fn load_multi_level_config_missing_file_returns_none() {
329 let dir = tempfile::tempdir().unwrap();
330 assert!(load_multi_level_config(dir.path()).await.is_none());
331 }
332
333 #[tokio::test]
334 async fn ensure_segment_files_structure_empty_xmlns_omits_xmlns_attribute() {
335 let dir = tempfile::tempdir().unwrap();
338 let xml = r#"<?xml version="1.0"?><Root><inner><x>1</x></inner></Root>"#;
339 let path = dir.path().join("seg.xml");
340 tokio::fs::write(&path, xml).await.unwrap();
341 ensure_segment_files_structure(
342 dir.path(),
343 "Root",
344 "inner",
345 "", )
347 .await
348 .unwrap();
349 let out = tokio::fs::read_to_string(&path).await.unwrap();
350 assert!(
351 !out.contains("xmlns"),
352 "empty xmlns must not emit an xmlns attribute: {out}"
353 );
354 assert!(
355 out.contains("<inner>"),
356 "inner wrapper must be present: {out}"
357 );
358 }
359
360 #[tokio::test]
361 async fn ensure_segment_files_structure_adds_xmlns_and_rewrites() {
362 let dir = tempfile::tempdir().unwrap();
363 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
364<Root>
365 <programProcesses><x>1</x></programProcesses>
366</Root>"#;
367 let path = dir.path().join("segment.xml");
368 tokio::fs::write(&path, xml).await.unwrap();
369 ensure_segment_files_structure(
370 dir.path(),
371 "Root",
372 "programProcesses",
373 "http://example.com",
374 )
375 .await
376 .unwrap();
377 let out = tokio::fs::read_to_string(&path).await.unwrap();
378 assert!(out.contains("http://example.com"));
379 assert!(out.contains("<programProcesses>"));
380 assert!(out.contains("<x>1</x>"));
381 }
382
383 #[tokio::test]
384 async fn ensure_segment_files_structure_skips_already_correct_files() {
385 let dir = tempfile::tempdir().unwrap();
387 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
388<Root xmlns="http://example.com"><programProcesses><x>1</x></programProcesses></Root>"#;
389 let path = dir.path().join("ok.xml");
390 tokio::fs::write(&path, xml).await.unwrap();
391 let before = tokio::fs::metadata(&path).await.unwrap().modified().ok();
392 ensure_segment_files_structure(
393 dir.path(),
394 "Root",
395 "programProcesses",
396 "http://example.com",
397 )
398 .await
399 .unwrap();
400 let after = tokio::fs::metadata(&path).await.unwrap().modified().ok();
401 assert_eq!(before, after, "already-correct files must be left as-is");
402 }
403
404 #[tokio::test]
405 async fn ensure_segment_files_structure_skips_non_xml_and_subdirs() {
406 let dir = tempfile::tempdir().unwrap();
407 tokio::fs::create_dir(dir.path().join("nested"))
408 .await
409 .unwrap();
410 tokio::fs::write(dir.path().join("notes.txt"), "hello")
411 .await
412 .unwrap();
413 tokio::fs::write(dir.path().join("broken.xml"), "<<not xml>")
414 .await
415 .unwrap();
416 ensure_segment_files_structure(
418 dir.path(),
419 "Root",
420 "programProcesses",
421 "http://example.com",
422 )
423 .await
424 .unwrap();
425 let raw = tokio::fs::read_to_string(dir.path().join("broken.xml"))
427 .await
428 .unwrap();
429 assert_eq!(raw, "<<not xml>");
430 }
431
432 #[tokio::test]
433 async fn ensure_segment_files_structure_skips_xml_missing_root() {
434 let dir = tempfile::tempdir().unwrap();
436 tokio::fs::write(dir.path().join("empty.xml"), "")
437 .await
438 .unwrap();
439 ensure_segment_files_structure(dir.path(), "Root", "programProcesses", "")
440 .await
441 .unwrap();
442 }
443
444 fn map_from(pairs: &[(&str, serde_json::Value)]) -> serde_json::Map<String, serde_json::Value> {
445 let mut m = serde_json::Map::new();
446 for (k, v) in pairs {
447 m.insert((*k).to_string(), v.clone());
448 }
449 m
450 }
451
452 #[test]
453 fn has_single_inner_wrapper_true_for_single_matching_child() {
454 let m = map_from(&[("inner", json!({"a": 1}))]);
455 assert!(has_single_inner_wrapper(&m, "inner"));
456 }
457
458 #[test]
459 fn has_single_inner_wrapper_true_when_only_attribute_is_xmlns_sibling() {
460 let m = map_from(&[
464 ("@xmlns", json!("http://example.com")),
465 ("inner", json!({"a": 1})),
466 ]);
467 assert!(has_single_inner_wrapper(&m, "inner"));
468 }
469
470 #[test]
471 fn has_single_inner_wrapper_false_when_multiple_non_attribute_children() {
472 let m = map_from(&[("inner", json!({})), ("other", json!({}))]);
473 assert!(!has_single_inner_wrapper(&m, "inner"));
474 }
475
476 #[test]
477 fn has_single_inner_wrapper_false_when_only_child_name_differs() {
478 let m = map_from(&[("notInner", json!({"a": 1}))]);
479 assert!(!has_single_inner_wrapper(&m, "inner"));
480 }
481
482 #[test]
483 fn has_single_inner_wrapper_false_when_empty() {
484 let m = serde_json::Map::new();
485 assert!(!has_single_inner_wrapper(&m, "inner"));
486 }
487
488 #[test]
489 fn should_unwrap_inner_segment_true_when_root_matches_and_single_inner() {
490 assert!(should_unwrap_inner_segment("Doc", "Doc", true));
495 }
496
497 #[test]
498 fn should_unwrap_inner_segment_false_when_current_root_differs() {
499 assert!(!should_unwrap_inner_segment("Other", "Doc", true));
504 }
505
506 #[test]
507 fn should_unwrap_inner_segment_false_when_not_single_inner() {
508 assert!(!should_unwrap_inner_segment("Doc", "Doc", false));
511 }
512
513 #[tokio::test]
514 async fn ensure_segment_files_structure_else_branch_when_root_differs_from_document_root() {
515 let dir = tempfile::tempdir().unwrap();
518 let xml = r#"<Item><child>x</child></Item>"#;
520 let path = dir.path().join("item.xml");
521 tokio::fs::write(&path, xml).await.unwrap();
522 ensure_segment_files_structure(
523 dir.path(),
524 "Root", "child",
526 "http://example.com",
527 )
528 .await
529 .unwrap();
530 let out = tokio::fs::read_to_string(&path).await.unwrap();
531 assert!(out.contains("<Root"), "expected Root element: {out}");
533 assert!(
534 out.contains("http://example.com"),
535 "expected xmlns attribute: {out}"
536 );
537 }
538
539 #[tokio::test]
540 async fn ensure_segment_files_structure_else_branch_multiple_children() {
541 let dir = tempfile::tempdir().unwrap();
543 let xml = r#"<Root><a>1</a><b>2</b></Root>"#;
545 let path = dir.path().join("multi.xml");
546 tokio::fs::write(&path, xml).await.unwrap();
547 ensure_segment_files_structure(
548 dir.path(),
549 "Root",
550 "inner", "http://example.com",
552 )
553 .await
554 .unwrap();
555 let out = tokio::fs::read_to_string(&path).await.unwrap();
556 assert!(
557 out.contains("<Root"),
558 "Root element must be in output: {out}"
559 );
560 }
561}