Skip to main content

xml_disassembler/
multi_level.rs

1//! Multi-level disassembly: strip a root element and re-disassemble with different unique-id elements.
2
3use serde_json::{Map, Value};
4
5use crate::builders::build_xml_string;
6use crate::types::{MultiLevelConfig, XmlElement};
7
8/// Strip the given element and build a new XML string.
9/// - If it is the root element: its inner content becomes the new document (with ?xml preserved).
10/// - If it is a child of the root (e.g. programProcesses under LoyaltyProgramSetup): unwrap it so
11///   its inner content becomes the direct children of the root; the root element is kept.
12pub 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        // Strip the root: new doc = ?xml + inner content of root
25        let mut new_obj = Map::new();
26        new_obj.insert("?xml".to_string(), decl);
27        for (k, v) in root_val {
28            new_obj.insert(k.clone(), v.clone());
29        }
30        return Some(build_xml_string(&Value::Object(new_obj)));
31    }
32
33    // Strip a child of the root: unwrap it so its inner content becomes direct children of the root
34    let inner = root_val.get(element_to_strip)?.as_object()?;
35    let mut new_root_val = Map::new();
36    for (k, v) in root_val {
37        if k != element_to_strip {
38            new_root_val.insert(k.clone(), v.clone());
39        }
40    }
41    for (k, v) in inner {
42        new_root_val.insert(k.clone(), v.clone());
43    }
44    let mut new_obj = Map::new();
45    new_obj.insert("?xml".to_string(), decl);
46    new_obj.insert(root_key, Value::Object(new_root_val));
47    Some(build_xml_string(&Value::Object(new_obj)))
48}
49
50/// Capture xmlns from the root element (e.g. LoyaltyProgramSetup) for later wrap.
51pub fn capture_xmlns_from_root(parsed: &XmlElement) -> Option<String> {
52    let obj = parsed.as_object()?;
53    let root_key = obj.keys().find(|k| *k != "?xml")?.clone();
54    let root_val = obj.get(&root_key)?.as_object()?;
55    let xmlns = root_val.get("@xmlns")?.as_str()?;
56    Some(xmlns.to_string())
57}
58
59/// Derive path_segment from file_pattern (e.g. "programProcesses-meta" -> "programProcesses").
60pub fn path_segment_from_file_pattern(file_pattern: &str) -> String {
61    if let Some(prefix) = file_pattern.split('-').next() {
62        prefix.to_string()
63    } else {
64        file_pattern.to_string()
65    }
66}
67
68/// Load multi-level config from a directory (reads .multi_level.json).
69pub async fn load_multi_level_config(dir_path: &std::path::Path) -> Option<MultiLevelConfig> {
70    let path = dir_path.join(".multi_level.json");
71    let content = tokio::fs::read_to_string(&path).await.ok()?;
72    serde_json::from_str(&content).ok()
73}
74
75/// Persist multi-level config to a directory.
76pub async fn save_multi_level_config(
77    dir_path: &std::path::Path,
78    config: &MultiLevelConfig,
79) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
80    let path = dir_path.join(".multi_level.json");
81    let content = serde_json::to_string_pretty(config)?;
82    tokio::fs::write(path, content).await?;
83    Ok(())
84}
85
86/// Ensure all XML files in a segment directory have structure:
87/// document_root (with xmlns) > inner_wrapper (no xmlns) > content.
88/// Used after inner-level reassembly for multi-level (e.g. LoyaltyProgramSetup > programProcesses).
89pub async fn ensure_segment_files_structure(
90    dir_path: &std::path::Path,
91    document_root: &str,
92    inner_wrapper: &str,
93    xmlns: &str,
94) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
95    use crate::parsers::parse_xml_from_str;
96    use serde_json::Map;
97
98    let mut entries = Vec::new();
99    let mut read_dir = tokio::fs::read_dir(dir_path).await?;
100    while let Some(entry) = read_dir.next_entry().await? {
101        entries.push(entry);
102    }
103    // Sort for deterministic cross-platform ordering
104    entries.sort_by_key(|e| e.file_name());
105
106    for entry in entries {
107        let path = entry.path();
108        if !path.is_file() {
109            continue;
110        }
111        let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
112        if !name.ends_with(".xml") {
113            continue;
114        }
115        let path_str = path.to_string_lossy();
116        let content = match tokio::fs::read_to_string(&path).await {
117            Ok(c) => c,
118            Err(_) => continue,
119        };
120        let parsed = match parse_xml_from_str(&content, &path_str) {
121            Some(p) => p,
122            None => continue,
123        };
124        let obj = match parsed.as_object() {
125            Some(o) => o,
126            None => continue,
127        };
128        let root_key = obj.keys().find(|k| *k != "?xml").cloned();
129        let Some(current_root_key) = root_key else {
130            continue;
131        };
132        let root_val = obj
133            .get(&current_root_key)
134            .and_then(|v| v.as_object())
135            .cloned();
136        let Some(root_val) = root_val else {
137            continue;
138        };
139
140        let decl = obj.get("?xml").cloned().unwrap_or_else(|| {
141            let mut d = Map::new();
142            d.insert(
143                "@version".to_string(),
144                serde_json::Value::String("1.0".to_string()),
145            );
146            d.insert(
147                "@encoding".to_string(),
148                serde_json::Value::String("UTF-8".to_string()),
149            );
150            serde_json::Value::Object(d)
151        });
152
153        let non_attr_keys: Vec<&String> = root_val.keys().filter(|k| *k != "@xmlns").collect();
154        let single_inner = non_attr_keys.len() == 1 && non_attr_keys[0].as_str() == inner_wrapper;
155        let inner_content: serde_json::Value = if current_root_key == document_root && single_inner
156        {
157            let inner_obj = root_val
158                .get(inner_wrapper)
159                .and_then(|v| v.as_object())
160                .cloned()
161                .unwrap_or_else(Map::new);
162            let mut inner_clean = Map::new();
163            for (k, v) in &inner_obj {
164                if k != "@xmlns" {
165                    inner_clean.insert(k.clone(), v.clone());
166                }
167            }
168            serde_json::Value::Object(inner_clean)
169        } else {
170            serde_json::Value::Object(root_val.clone())
171        };
172
173        let already_correct = current_root_key == document_root
174            && root_val.get("@xmlns").is_some()
175            && single_inner
176            && root_val
177                .get(inner_wrapper)
178                .and_then(|v| v.as_object())
179                .map(|o| !o.contains_key("@xmlns"))
180                .unwrap_or(true);
181        if already_correct {
182            continue;
183        }
184
185        // Build document_root (with @xmlns only on root) > inner_wrapper (no xmlns) > content
186        let mut root_val_new = Map::new();
187        if !xmlns.is_empty() {
188            root_val_new.insert(
189                "@xmlns".to_string(),
190                serde_json::Value::String(xmlns.to_string()),
191            );
192        }
193        root_val_new.insert(inner_wrapper.to_string(), inner_content);
194
195        let mut top = Map::new();
196        top.insert("?xml".to_string(), decl);
197        top.insert(
198            document_root.to_string(),
199            serde_json::Value::Object(root_val_new),
200        );
201        let wrapped = serde_json::Value::Object(top);
202        let xml_string = build_xml_string(&wrapped);
203        tokio::fs::write(&path, xml_string).await?;
204    }
205    Ok(())
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn path_segment_from_file_pattern_strips_suffix() {
214        assert_eq!(
215            path_segment_from_file_pattern("programProcesses-meta"),
216            "programProcesses"
217        );
218    }
219
220    #[test]
221    fn path_segment_from_file_pattern_no_dash() {
222        assert_eq!(path_segment_from_file_pattern("foo"), "foo");
223    }
224}