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    const HEX: &[u8; 16] = b"0123456789abcdef";
16    let mut s = String::with_capacity(8);
17    for b in result.iter().take(4) {
18        s.push(HEX[(b >> 4) as usize] as char);
19        s.push(HEX[(b & 0xf) as usize] as char);
20    }
21    s
22}
23
24fn is_object(value: &Value) -> bool {
25    value.is_object() && !value.is_array()
26}
27
28/// Extract string from a value - handles both direct strings and objects with #text (XML leaf elements).
29fn value_as_string(value: &Value) -> Option<String> {
30    if let Some(s) = value.as_str() {
31        return Some(s.to_string());
32    }
33    if let Some(obj) = value.as_object() {
34        if let Some(text) = obj.get("#text").and_then(|v| v.as_str()) {
35            return Some(text.to_string());
36        }
37    }
38    None
39}
40
41fn find_direct_field_match(element: &XmlElement, field_names: &[&str]) -> Option<String> {
42    let obj = element.as_object()?;
43    for name in field_names {
44        if let Some(value) = obj.get(*name) {
45            if let Some(s) = value_as_string(value) {
46                return Some(s);
47            }
48        }
49    }
50    None
51}
52
53fn find_nested_field_match(element: &XmlElement, unique_id_elements: &str) -> Option<String> {
54    let obj = element.as_object()?;
55    for (_, child) in obj {
56        if is_object(child) {
57            let result = parse_unique_id_element(child, Some(unique_id_elements));
58            if !result.is_empty() {
59                return Some(result);
60            }
61        }
62    }
63    None
64}
65
66/// Get a unique ID for an element, using configured fields or a hash.
67pub fn parse_unique_id_element(element: &XmlElement, unique_id_elements: Option<&str>) -> String {
68    if let Some(ids) = unique_id_elements {
69        let field_names: Vec<&str> = ids.split(',').map(|s| s.trim()).collect();
70        find_direct_field_match(element, &field_names)
71            .or_else(|| find_nested_field_match(element, ids))
72            .unwrap_or_else(|| create_short_hash(element))
73    } else {
74        create_short_hash(element)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use serde_json::json;
82
83    #[test]
84    fn finds_direct_field() {
85        let el = json!({ "name": "Get_Info", "label": "Get Info" });
86        assert_eq!(parse_unique_id_element(&el, Some("name")), "Get_Info");
87    }
88
89    #[test]
90    fn finds_deeply_nested_field() {
91        // value before connector so we find elementReference (matches TS iteration order)
92        let el = json!({
93            "value": { "elementReference": "accts.accounts" },
94            "connector": { "targetReference": "X" }
95        });
96        assert_eq!(
97            parse_unique_id_element(&el, Some("elementReference")),
98            "accts.accounts"
99        );
100    }
101
102    #[test]
103    fn finds_id_in_grandchild() {
104        let el = json!({
105            "wrapper": {
106                "inner": { "name": "NestedName" }
107            }
108        });
109        assert_eq!(parse_unique_id_element(&el, Some("name")), "NestedName");
110    }
111
112    #[test]
113    fn finds_name_from_text_object() {
114        // XML parser stores leaf elements as { "#text": "value" }
115        let el = json!({
116            "name": { "#text": "Get_Info" },
117            "label": { "#text": "Get Info" },
118            "actionName": { "#text": "GetFirstFromCollection" }
119        });
120        assert_eq!(parse_unique_id_element(&el, Some("name")), "Get_Info");
121        assert_eq!(
122            parse_unique_id_element(&el, Some("actionName")),
123            "GetFirstFromCollection"
124        );
125    }
126}