config_disassembler/xml/
multi_level.rs1use 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
90pub async fn ensure_segment_files_structure(
94 dir_path: &std::path::Path,
95 document_root: &str,
96 inner_wrapper: &str,
97 xmlns: &str,
98) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
99 use crate::xml::parsers::parse_xml_from_str;
100 use serde_json::Map;
101
102 let mut entries = Vec::new();
103 let mut read_dir = tokio::fs::read_dir(dir_path).await?;
104 while let Some(entry) = read_dir.next_entry().await? {
105 entries.push(entry);
106 }
107 entries.sort_by_key(|e| e.file_name());
109
110 for entry in entries {
111 let path = entry.path();
112 if !path.is_file() {
113 continue;
114 }
115 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
116 if !name.ends_with(".xml") {
117 continue;
118 }
119 let path_str = path.to_string_lossy();
120 let content = tokio::fs::read_to_string(&path).await.unwrap_or_default();
123 let Some(parsed) = parse_xml_from_str(&content, &path_str) else {
124 continue;
125 };
126 let obj = parsed.as_object().cloned().unwrap_or_default();
129 let Some(current_root_key) = obj.keys().find(|k| *k != "?xml").cloned() else {
130 continue;
131 };
132 let root_val = obj
133 .get(¤t_root_key)
134 .and_then(|v| v.as_object())
135 .cloned()
136 .unwrap_or_default();
137
138 let decl = obj.get("?xml").cloned().unwrap_or_else(|| {
139 let mut d = Map::new();
140 d.insert(
141 "@version".to_string(),
142 serde_json::Value::String("1.0".to_string()),
143 );
144 d.insert(
145 "@encoding".to_string(),
146 serde_json::Value::String("UTF-8".to_string()),
147 );
148 serde_json::Value::Object(d)
149 });
150
151 let non_attr_keys: Vec<&String> = root_val.keys().filter(|k| *k != "@xmlns").collect();
152 let single_inner = non_attr_keys.len() == 1 && non_attr_keys[0].as_str() == inner_wrapper;
153 let inner_content: serde_json::Value = if current_root_key == document_root && single_inner
154 {
155 let inner_obj = root_val
156 .get(inner_wrapper)
157 .and_then(|v| v.as_object())
158 .cloned()
159 .unwrap_or_else(Map::new);
160 let mut inner_clean = Map::new();
161 for (k, v) in &inner_obj {
162 if k != "@xmlns" {
163 inner_clean.insert(k.clone(), v.clone());
164 }
165 }
166 serde_json::Value::Object(inner_clean)
167 } else {
168 let mut inner_clean = Map::new();
172 for (k, v) in &root_val {
173 if k != "@xmlns" {
174 inner_clean.insert(k.clone(), v.clone());
175 }
176 }
177 serde_json::Value::Object(inner_clean)
178 };
179
180 let already_correct = current_root_key == document_root
181 && root_val.get("@xmlns").is_some()
182 && single_inner
183 && root_val
184 .get(inner_wrapper)
185 .and_then(|v| v.as_object())
186 .map(|o| !o.contains_key("@xmlns"))
187 .unwrap_or(true);
188 if already_correct {
189 continue;
190 }
191
192 let mut root_val_new = Map::new();
194 if !xmlns.is_empty() {
195 root_val_new.insert(
196 "@xmlns".to_string(),
197 serde_json::Value::String(xmlns.to_string()),
198 );
199 }
200 root_val_new.insert(inner_wrapper.to_string(), inner_content);
201
202 let mut top = Map::new();
203 top.insert("?xml".to_string(), decl);
204 top.insert(
205 document_root.to_string(),
206 serde_json::Value::Object(root_val_new),
207 );
208 let wrapped = serde_json::Value::Object(top);
209 let xml_string = build_xml_string(&wrapped);
210 tokio::fs::write(&path, xml_string).await?;
211 }
212 Ok(())
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use serde_json::json;
219
220 #[test]
221 fn path_segment_from_file_pattern_strips_suffix() {
222 assert_eq!(
223 path_segment_from_file_pattern("programProcesses-meta"),
224 "programProcesses"
225 );
226 }
227
228 #[test]
229 fn path_segment_from_file_pattern_no_dash() {
230 assert_eq!(path_segment_from_file_pattern("foo"), "foo");
231 }
232
233 #[test]
234 fn strip_root_and_build_xml_strips_child_not_root() {
235 let parsed = json!({
236 "?xml": { "@version": "1.0" },
237 "Root": {
238 "programProcesses": { "a": "1", "b": "2" },
239 "label": "x"
240 }
241 });
242 let out = strip_root_and_build_xml(&parsed, "programProcesses").unwrap();
243 assert!(out.contains("<Root>"));
244 assert!(out.contains("<a>1</a>"));
245 assert!(out.contains("<b>2</b>"));
246 assert!(out.contains("<label>x</label>"));
247 }
248
249 #[test]
250 fn strip_root_and_build_xml_strips_root_excludes_attributes() {
251 let parsed = json!({
252 "?xml": { "@version": "1.0" },
253 "LoyaltyProgramSetup": {
254 "@xmlns": "http://example.com",
255 "programProcesses": { "x": "1" }
256 }
257 });
258 let out = strip_root_and_build_xml(&parsed, "LoyaltyProgramSetup").unwrap();
259 assert!(!out.contains("@xmlns"));
260 assert!(out.contains("programProcesses"));
261 }
262
263 #[test]
264 fn capture_xmlns_from_root_returns_some() {
265 let parsed = json!({
266 "Root": { "@xmlns": "http://ns.example.com" }
267 });
268 assert_eq!(
269 capture_xmlns_from_root(&parsed),
270 Some("http://ns.example.com".to_string())
271 );
272 }
273
274 #[test]
275 fn capture_xmlns_from_root_returns_none_when_absent() {
276 let parsed = json!({ "Root": { "child": "x" } });
277 assert!(capture_xmlns_from_root(&parsed).is_none());
278 }
279
280 #[tokio::test]
281 async fn save_and_load_multi_level_config() {
282 let dir = tempfile::tempdir().unwrap();
283 let config = MultiLevelConfig {
284 rules: vec![crate::xml::types::MultiLevelRule {
285 file_pattern: "test-meta".to_string(),
286 root_to_strip: "Root".to_string(),
287 unique_id_elements: "id".to_string(),
288 path_segment: "test".to_string(),
289 wrap_root_element: "Root".to_string(),
290 wrap_xmlns: "http://example.com".to_string(),
291 }],
292 };
293 save_multi_level_config(dir.path(), &config).await.unwrap();
294 let loaded = load_multi_level_config(dir.path()).await.unwrap();
295 assert_eq!(loaded.rules.len(), 1);
296 assert_eq!(loaded.rules[0].path_segment, "test");
297 }
298
299 #[tokio::test]
300 async fn load_multi_level_config_missing_file_returns_none() {
301 let dir = tempfile::tempdir().unwrap();
302 assert!(load_multi_level_config(dir.path()).await.is_none());
303 }
304
305 #[tokio::test]
306 async fn ensure_segment_files_structure_adds_xmlns_and_rewrites() {
307 let dir = tempfile::tempdir().unwrap();
308 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
309<Root>
310 <programProcesses><x>1</x></programProcesses>
311</Root>"#;
312 let path = dir.path().join("segment.xml");
313 tokio::fs::write(&path, xml).await.unwrap();
314 ensure_segment_files_structure(
315 dir.path(),
316 "Root",
317 "programProcesses",
318 "http://example.com",
319 )
320 .await
321 .unwrap();
322 let out = tokio::fs::read_to_string(&path).await.unwrap();
323 assert!(out.contains("http://example.com"));
324 assert!(out.contains("<programProcesses>"));
325 assert!(out.contains("<x>1</x>"));
326 }
327
328 #[tokio::test]
329 async fn ensure_segment_files_structure_skips_already_correct_files() {
330 let dir = tempfile::tempdir().unwrap();
332 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
333<Root xmlns="http://example.com"><programProcesses><x>1</x></programProcesses></Root>"#;
334 let path = dir.path().join("ok.xml");
335 tokio::fs::write(&path, xml).await.unwrap();
336 let before = tokio::fs::metadata(&path).await.unwrap().modified().ok();
337 ensure_segment_files_structure(
338 dir.path(),
339 "Root",
340 "programProcesses",
341 "http://example.com",
342 )
343 .await
344 .unwrap();
345 let after = tokio::fs::metadata(&path).await.unwrap().modified().ok();
346 assert_eq!(before, after, "already-correct files must be left as-is");
347 }
348
349 #[tokio::test]
350 async fn ensure_segment_files_structure_skips_non_xml_and_subdirs() {
351 let dir = tempfile::tempdir().unwrap();
352 tokio::fs::create_dir(dir.path().join("nested"))
353 .await
354 .unwrap();
355 tokio::fs::write(dir.path().join("notes.txt"), "hello")
356 .await
357 .unwrap();
358 tokio::fs::write(dir.path().join("broken.xml"), "<<not xml>")
359 .await
360 .unwrap();
361 ensure_segment_files_structure(
363 dir.path(),
364 "Root",
365 "programProcesses",
366 "http://example.com",
367 )
368 .await
369 .unwrap();
370 let raw = tokio::fs::read_to_string(dir.path().join("broken.xml"))
372 .await
373 .unwrap();
374 assert_eq!(raw, "<<not xml>");
375 }
376
377 #[tokio::test]
378 async fn ensure_segment_files_structure_skips_xml_missing_root() {
379 let dir = tempfile::tempdir().unwrap();
381 tokio::fs::write(dir.path().join("empty.xml"), "")
382 .await
383 .unwrap();
384 ensure_segment_files_structure(dir.path(), "Root", "programProcesses", "")
385 .await
386 .unwrap();
387 }
388}