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