xml_disassembler/
multi_level.rs1use serde_json::{Map, Value};
4
5use crate::builders::build_xml_string;
6use crate::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 if let Some(prefix) = file_pattern.split('-').next() {
64 prefix.to_string()
65 } else {
66 file_pattern.to_string()
67 }
68}
69
70pub async fn load_multi_level_config(dir_path: &std::path::Path) -> Option<MultiLevelConfig> {
72 let path = dir_path.join(".multi_level.json");
73 let content = tokio::fs::read_to_string(&path).await.ok()?;
74 serde_json::from_str(&content).ok()
75}
76
77pub async fn save_multi_level_config(
79 dir_path: &std::path::Path,
80 config: &MultiLevelConfig,
81) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
82 let path = dir_path.join(".multi_level.json");
83 let content = serde_json::to_string_pretty(config)?;
84 tokio::fs::write(path, content).await?;
85 Ok(())
86}
87
88pub async fn ensure_segment_files_structure(
92 dir_path: &std::path::Path,
93 document_root: &str,
94 inner_wrapper: &str,
95 xmlns: &str,
96) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
97 use crate::parsers::parse_xml_from_str;
98 use serde_json::Map;
99
100 let mut entries = Vec::new();
101 let mut read_dir = tokio::fs::read_dir(dir_path).await?;
102 while let Some(entry) = read_dir.next_entry().await? {
103 entries.push(entry);
104 }
105 entries.sort_by_key(|e| e.file_name());
107
108 for entry in entries {
109 let path = entry.path();
110 if !path.is_file() {
111 continue;
112 }
113 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
114 if !name.ends_with(".xml") {
115 continue;
116 }
117 let path_str = path.to_string_lossy();
118 let content = match tokio::fs::read_to_string(&path).await {
119 Ok(c) => c,
120 Err(_) => continue,
121 };
122 let parsed = match parse_xml_from_str(&content, &path_str) {
123 Some(p) => p,
124 None => continue,
125 };
126 let obj = match parsed.as_object() {
127 Some(o) => o,
128 None => continue,
129 };
130 let root_key = obj.keys().find(|k| *k != "?xml").cloned();
131 let Some(current_root_key) = root_key else {
132 continue;
133 };
134 let root_val = obj
135 .get(¤t_root_key)
136 .and_then(|v| v.as_object())
137 .cloned();
138 let Some(root_val) = root_val else {
139 continue;
140 };
141
142 let decl = obj.get("?xml").cloned().unwrap_or_else(|| {
143 let mut d = Map::new();
144 d.insert(
145 "@version".to_string(),
146 serde_json::Value::String("1.0".to_string()),
147 );
148 d.insert(
149 "@encoding".to_string(),
150 serde_json::Value::String("UTF-8".to_string()),
151 );
152 serde_json::Value::Object(d)
153 });
154
155 let non_attr_keys: Vec<&String> = root_val.keys().filter(|k| *k != "@xmlns").collect();
156 let single_inner = non_attr_keys.len() == 1 && non_attr_keys[0].as_str() == inner_wrapper;
157 let inner_content: serde_json::Value = if current_root_key == document_root && single_inner
158 {
159 let inner_obj = root_val
160 .get(inner_wrapper)
161 .and_then(|v| v.as_object())
162 .cloned()
163 .unwrap_or_else(Map::new);
164 let mut inner_clean = Map::new();
165 for (k, v) in &inner_obj {
166 if k != "@xmlns" {
167 inner_clean.insert(k.clone(), v.clone());
168 }
169 }
170 serde_json::Value::Object(inner_clean)
171 } else {
172 serde_json::Value::Object(root_val.clone())
173 };
174
175 let already_correct = current_root_key == document_root
176 && root_val.get("@xmlns").is_some()
177 && single_inner
178 && root_val
179 .get(inner_wrapper)
180 .and_then(|v| v.as_object())
181 .map(|o| !o.contains_key("@xmlns"))
182 .unwrap_or(true);
183 if already_correct {
184 continue;
185 }
186
187 let mut root_val_new = Map::new();
189 if !xmlns.is_empty() {
190 root_val_new.insert(
191 "@xmlns".to_string(),
192 serde_json::Value::String(xmlns.to_string()),
193 );
194 }
195 root_val_new.insert(inner_wrapper.to_string(), inner_content);
196
197 let mut top = Map::new();
198 top.insert("?xml".to_string(), decl);
199 top.insert(
200 document_root.to_string(),
201 serde_json::Value::Object(root_val_new),
202 );
203 let wrapped = serde_json::Value::Object(top);
204 let xml_string = build_xml_string(&wrapped);
205 tokio::fs::write(&path, xml_string).await?;
206 }
207 Ok(())
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use serde_json::json;
214
215 #[test]
216 fn path_segment_from_file_pattern_strips_suffix() {
217 assert_eq!(
218 path_segment_from_file_pattern("programProcesses-meta"),
219 "programProcesses"
220 );
221 }
222
223 #[test]
224 fn path_segment_from_file_pattern_no_dash() {
225 assert_eq!(path_segment_from_file_pattern("foo"), "foo");
226 }
227
228 #[test]
229 fn strip_root_and_build_xml_strips_child_not_root() {
230 let parsed = json!({
231 "?xml": { "@version": "1.0" },
232 "Root": {
233 "programProcesses": { "a": "1", "b": "2" },
234 "label": "x"
235 }
236 });
237 let out = strip_root_and_build_xml(&parsed, "programProcesses").unwrap();
238 assert!(out.contains("<Root>"));
239 assert!(out.contains("<a>1</a>"));
240 assert!(out.contains("<b>2</b>"));
241 assert!(out.contains("<label>x</label>"));
242 }
243
244 #[test]
245 fn strip_root_and_build_xml_strips_root_excludes_attributes() {
246 let parsed = json!({
247 "?xml": { "@version": "1.0" },
248 "LoyaltyProgramSetup": {
249 "@xmlns": "http://example.com",
250 "programProcesses": { "x": "1" }
251 }
252 });
253 let out = strip_root_and_build_xml(&parsed, "LoyaltyProgramSetup").unwrap();
254 assert!(!out.contains("@xmlns"));
255 assert!(out.contains("programProcesses"));
256 }
257
258 #[test]
259 fn capture_xmlns_from_root_returns_some() {
260 let parsed = json!({
261 "Root": { "@xmlns": "http://ns.example.com" }
262 });
263 assert_eq!(
264 capture_xmlns_from_root(&parsed),
265 Some("http://ns.example.com".to_string())
266 );
267 }
268
269 #[test]
270 fn capture_xmlns_from_root_returns_none_when_absent() {
271 let parsed = json!({ "Root": { "child": "x" } });
272 assert!(capture_xmlns_from_root(&parsed).is_none());
273 }
274
275 #[tokio::test]
276 async fn save_and_load_multi_level_config() {
277 let dir = tempfile::tempdir().unwrap();
278 let config = MultiLevelConfig {
279 rules: vec![crate::types::MultiLevelRule {
280 file_pattern: "test-meta".to_string(),
281 root_to_strip: "Root".to_string(),
282 unique_id_elements: "id".to_string(),
283 path_segment: "test".to_string(),
284 wrap_root_element: "Root".to_string(),
285 wrap_xmlns: "http://example.com".to_string(),
286 }],
287 };
288 save_multi_level_config(dir.path(), &config).await.unwrap();
289 let loaded = load_multi_level_config(dir.path()).await.unwrap();
290 assert_eq!(loaded.rules.len(), 1);
291 assert_eq!(loaded.rules[0].path_segment, "test");
292 }
293
294 #[tokio::test]
295 async fn load_multi_level_config_missing_file_returns_none() {
296 let dir = tempfile::tempdir().unwrap();
297 assert!(load_multi_level_config(dir.path()).await.is_none());
298 }
299
300 #[tokio::test]
301 async fn ensure_segment_files_structure_adds_xmlns_and_rewrites() {
302 let dir = tempfile::tempdir().unwrap();
303 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
304<Root>
305 <programProcesses><x>1</x></programProcesses>
306</Root>"#;
307 let path = dir.path().join("segment.xml");
308 tokio::fs::write(&path, xml).await.unwrap();
309 ensure_segment_files_structure(
310 dir.path(),
311 "Root",
312 "programProcesses",
313 "http://example.com",
314 )
315 .await
316 .unwrap();
317 let out = tokio::fs::read_to_string(&path).await.unwrap();
318 assert!(out.contains("http://example.com"));
319 assert!(out.contains("<programProcesses>"));
320 assert!(out.contains("<x>1</x>"));
321 }
322}