xml_disassembler/parsers/
parse_unique_id.rs1use serde_json::Value;
4use sha2::{Digest, Sha256};
5
6use crate::types::XmlElement;
7
8fn 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
34fn 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
74pub 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}