Skip to main content

xml_disassembler/parsers/
parse_unique_id.rs

1//! Parse unique ID from XML element for file naming.
2
3use serde_json::Value;
4use sha2::{Digest, Sha256};
5
6use crate::types::XmlElement;
7
8/// Cache for stringified elements - we use a simple approach in Rust.
9/// For full equivalence we could use a type with interior mutability and weak refs.
10fn create_short_hash(element: &XmlElement) -> String {
11    let stringified = serde_json::to_string(element).unwrap_or_default();
12    let mut hasher = Sha256::new();
13    hasher.update(stringified.as_bytes());
14    let result = hasher.finalize();
15    format!("{:x}", result)[..8].to_string()
16}
17
18fn is_object(value: &Value) -> bool {
19    value.is_object() && !value.is_array()
20}
21
22fn find_direct_field_match(element: &XmlElement, field_names: &[&str]) -> Option<String> {
23    let obj = element.as_object()?;
24    for name in field_names {
25        if let Some(value) = obj.get(*name) {
26            if let Some(s) = value.as_str() {
27                return Some(s.to_string());
28            }
29        }
30    }
31    None
32}
33
34/// Returns true if the string looks like a hash fallback (8 hex chars) rather than a real ID.
35fn looks_like_hash(s: &str) -> bool {
36    s.len() == 8 && s.chars().all(|c| c.is_ascii_hexdigit())
37}
38
39fn find_nested_field_match(element: &XmlElement, unique_id_elements: &str) -> Option<String> {
40    let obj = element.as_object()?;
41    let mut hash_fallback: Option<String> = None;
42    for (_, child) in obj {
43        if is_object(child) {
44            let result = parse_unique_id_element(child, Some(unique_id_elements));
45            if !result.is_empty() {
46                if looks_like_hash(&result) {
47                    if hash_fallback.is_none() {
48                        hash_fallback = Some(result);
49                    }
50                } else {
51                    return Some(result);
52                }
53            }
54        } else if let Some(arr) = child.as_array() {
55            for item in arr {
56                if is_object(item) {
57                    let result = parse_unique_id_element(item, Some(unique_id_elements));
58                    if !result.is_empty() {
59                        if looks_like_hash(&result) {
60                            if hash_fallback.is_none() {
61                                hash_fallback = Some(result);
62                            }
63                        } else {
64                            return Some(result);
65                        }
66                    }
67                }
68            }
69        }
70    }
71    hash_fallback
72}
73
74/// Get a unique ID for an element, using configured fields or a hash.
75pub fn parse_unique_id_element(element: &XmlElement, unique_id_elements: Option<&str>) -> String {
76    if let Some(ids) = unique_id_elements {
77        let field_names: Vec<&str> = ids.split(',').map(|s| s.trim()).collect();
78        find_direct_field_match(element, &field_names)
79            .or_else(|| find_nested_field_match(element, ids))
80            .unwrap_or_else(|| create_short_hash(element))
81    } else {
82        create_short_hash(element)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use serde_json::json;
90
91    #[test]
92    fn finds_direct_field() {
93        let el = json!({ "name": "Get_Info", "label": "Get Info" });
94        assert_eq!(parse_unique_id_element(&el, Some("name")), "Get_Info");
95    }
96
97    #[test]
98    fn finds_deeply_nested_field() {
99        let el = json!({
100            "connector": { "targetReference": "X" },
101            "value": { "elementReference": "accts.accounts" }
102        });
103        assert_eq!(
104            parse_unique_id_element(&el, Some("elementReference")),
105            "accts.accounts"
106        );
107    }
108
109    #[test]
110    fn prefers_real_id_over_hash_from_first_child() {
111        let el = json!({
112            "connector": { "targetReference": "Update_If_Existing" },
113            "value": { "elementReference": "accts.accounts" }
114        });
115        let result = parse_unique_id_element(&el, Some("elementReference"));
116        assert_eq!(result, "accts.accounts");
117    }
118
119    #[test]
120    fn finds_id_in_array_element() {
121        let el = json!({
122            "items": [
123                { "other": "x" },
124                { "name": "NestedName" }
125            ]
126        });
127        assert_eq!(parse_unique_id_element(&el, Some("name")), "NestedName");
128    }
129}