operaton_task_worker/structures/
process_variables.rs

1use std::collections::HashMap;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Serialize, Deserialize)]
5#[serde(rename_all = "camelCase")]
6pub struct JsonValue {
7    data_format_name: String,
8
9    value: serde_json::Value,
10
11    string: bool,
12
13    object: bool,
14
15    boolean: bool,
16
17    number: bool,
18
19    array: bool,
20
21    #[serde(rename = "null")]
22    null_val: bool,
23
24    node_type: String,
25}
26
27#[derive(Debug, Serialize, Deserialize)]
28pub struct JsonVar {
29    #[serde(rename = "value")]
30    pub json_value: JsonValue,
31
32    #[serde(rename = "valueInfo")]
33    pub value_info: HashMap<String, serde_json::Value>,
34}
35
36#[derive(Debug, Serialize, Deserialize)]
37pub struct BoolVar {
38    pub value: bool,
39
40    #[serde(rename = "valueInfo")]
41    pub value_info: HashMap<String, serde_json::Value>,
42}
43
44#[derive(Debug, Serialize, Deserialize)]
45pub struct StringVar {
46    pub value: String,
47
48    #[serde(rename = "valueInfo")]
49    pub value_info: HashMap<String, serde_json::Value>,
50}
51
52#[derive(Debug)]
53pub enum ProcessInstanceVariable {
54    Json(JsonVar),
55    Boolean(BoolVar),
56    String(StringVar),
57}
58
59impl ProcessInstanceVariable {
60    pub fn as_bool(&self) -> Option<bool> {
61        match self {
62            ProcessInstanceVariable::Boolean(b) => Some(b.value),
63            _ => None,
64        }
65    }
66    pub fn as_str(&self) -> Option<&str> {
67        match self {
68            ProcessInstanceVariable::String(s) => Some(&s.value),
69            _ => None,
70        }
71    }
72    pub fn as_json(&self) -> Option<&serde_json::Value> {
73        match self {
74            ProcessInstanceVariable::Json(j) => Some(&j.json_value.value),
75            _ => None,
76        }
77    }
78}
79
80/// This represents an entry of the original JSON
81#[derive(Deserialize)]
82pub struct Entry {
83    #[serde(rename = "type")]
84    typ: String,
85
86    #[serde(default)]
87    #[allow(dead_code)]
88    name: String,
89
90    value: serde_json::Value,
91
92    #[serde(rename = "valueInfo")]
93    value_info: HashMap<String, serde_json::Value>,
94}
95
96impl<'de> Deserialize<'de> for ProcessInstanceVariable {
97    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
98    where
99        D: serde::Deserializer<'de>,
100    {
101        let map = HashMap::<String, Entry>::deserialize(deserializer)?;
102
103        // We expect only one entry in practice, but we'll take the first valid one
104        // Or collect all into Vec<Var> if you want multiple
105        for (_, entry) in map {
106            return match entry.typ.as_str() {
107                "Json" => {
108                    let json_var = JsonVar {
109                        json_value: serde_json::from_value(entry.value).map_err(serde::de::Error::custom)?,
110                        value_info: entry.value_info,
111                    };
112                    Ok(ProcessInstanceVariable::Json(json_var))
113                }
114                "Boolean" => {
115                    let bool_var = BoolVar {
116                        value: serde_json::from_value(entry.value).map_err(serde::de::Error::custom)?,
117                        value_info: entry.value_info,
118                    };
119                    Ok(ProcessInstanceVariable::Boolean(bool_var))
120                },
121                "String" => {
122                    let string_var = StringVar {
123                        value: serde_json::from_value(entry.value).map_err(serde::de::Error::custom)?,
124                        value_info: entry.value_info,
125                    };
126                    Ok(ProcessInstanceVariable::String(string_var))
127                },
128                _ => Err(serde::de::Error::custom(format!("unknown type: {}", entry.typ))),
129            };
130        }
131
132        Err(serde::de::Error::custom("no valid entries found"))
133    }
134}
135
136pub fn parse_process_instance_variables(json_str: &str) -> HashMap<String, ProcessInstanceVariable> {
137    // According to Camunda 7/Operaton, the variable endpoint usually returns an object map of
138    // name -> { type, value, valueInfo }. However, sometimes multiple JSON values can be returned
139    // as a JSON sequence (concatenated JSON values or an array). This function now handles:
140    // 1) A single JSON object map
141    // 2) A JSON array of such maps
142    // 3) A JSON array of entries (flat list with `name` inside)
143    // 4) A concatenated JSON sequence of such maps or entries
144
145    // Helper to convert an Entry into our enum and insert into result map
146    fn insert_entry(result: &mut HashMap<String, ProcessInstanceVariable>, name: String, entry: Entry) {
147        let parsed_var = match entry.typ.as_str() {
148            "Json" => ProcessInstanceVariable::Json(JsonVar {
149                json_value: serde_json::from_value(entry.value).unwrap_or_else(|e| {
150                    println!("Failed to parse JsonVar value for {}: {:#?}", name, e);
151                    // Fallback empty JSON details
152                    JsonValue {
153                        data_format_name: String::new(),
154                        value: serde_json::Value::Null,
155                        string: false,
156                        object: false,
157                        boolean: false,
158                        number: false,
159                        array: false,
160                        null_val: true,
161                        node_type: String::new(),
162                    }
163                }),
164                value_info: entry.value_info,
165            }),
166            "Boolean" => ProcessInstanceVariable::Boolean(BoolVar {
167                value: serde_json::from_value(entry.value).unwrap_or(false),
168                value_info: entry.value_info,
169            }),
170            "String" => ProcessInstanceVariable::String(StringVar {
171                value: serde_json::from_value(entry.value).unwrap_or_default(),
172                value_info: entry.value_info,
173            }),
174            _ => return,
175        };
176        result.insert(name, parsed_var);
177    }
178
179    let mut result: HashMap<String, ProcessInstanceVariable> = HashMap::new();
180
181    // Strategy 1: Try a single object map
182    if let Ok(parsed_map) = serde_json::from_str::<HashMap<String, Entry>>(json_str) {
183        for (name, entry) in parsed_map {
184            insert_entry(&mut result, name, entry);
185        }
186        return result;
187    }
188
189    // Strategy 2: Try an array of entries (flat list with `name` field)
190    if let Ok(entries) = serde_json::from_str::<Vec<Entry>>(json_str) {
191        for entry in entries.into_iter() {
192            let name = if !entry.name.is_empty() { entry.name.clone() } else { continue };
193            insert_entry(&mut result, name, entry);
194        }
195        return result;
196    }
197
198    // Strategy 3: Try an array of object maps
199    if let Ok(parsed_vec) = serde_json::from_str::<Vec<HashMap<String, Entry>>>(json_str) {
200        for map in parsed_vec.into_iter() {
201            for (name, entry) in map {
202                insert_entry(&mut result, name, entry);
203            }
204        }
205        return result;
206    }
207
208    // Strategy 4a: Stream/sequence of concatenated Entry values
209    let deser_entries = serde_json::Deserializer::from_str(json_str);
210    let mut stream_entries = deser_entries.into_iter::<Entry>();
211    let mut any_parsed = false;
212    while let Some(next) = stream_entries.next() {
213        match next {
214            Ok(entry) => {
215                if !entry.name.is_empty() {
216                    let name = entry.name.clone();
217                    insert_entry(&mut result, name, entry);
218                    any_parsed = true;
219                }
220            }
221            Err(e) => {
222                println!("Error while parsing JSON Entry sequence chunk: {:#?}", e);
223                break;
224            }
225        }
226    }
227    if any_parsed {
228        return result;
229    }
230
231    // Strategy 4b: Stream/sequence of concatenated map values
232    let deser_maps = serde_json::Deserializer::from_str(json_str);
233    let mut stream_maps = deser_maps.into_iter::<HashMap<String, Entry>>();
234    while let Some(next) = stream_maps.next() {
235        match next {
236            Ok(map) => {
237                any_parsed = true;
238                for (name, entry) in map {
239                    insert_entry(&mut result, name, entry);
240                }
241            }
242            Err(e) => {
243                // Stop streaming on error; we will report below if nothing parsed
244                println!("Error while parsing JSON map sequence chunk: {:#?}", e);
245                break;
246            }
247        }
248    }
249
250    if !any_parsed {
251        println!("Error while parsing \"{}\", ignoring it for now.", json_str);
252    }
253
254    result
255}
256
257#[cfg(test)]
258mod test {
259    use crate::structures::process_variables::parse_process_instance_variables;
260
261    #[test]
262    fn test_module_parsing() {
263        let response_string: &str = "{\"checklist_vj3ler\":{\"type\":\"Json\",\"value\":{\"dataFormatName\":\"application/json\",\"value\":false,\"string\":false,\"object\":false,\"boolean\":false,\"number\":false,\"array\":true,\"null\":false,\"nodeType\":\"ARRAY\"},\"valueInfo\":{}},\"checkbox_6ow5yg\":{\"type\":\"Boolean\",\"value\":true,\"valueInfo\":{}}}";
264        let variables = parse_process_instance_variables(response_string);
265        dbg!(&variables);
266        assert!(!variables.is_empty())
267    }
268
269    #[test]
270    fn test_module_parsing_complex_json_sequence() {
271        let response_string: &str = "[{\"type\":\"String\",\"value\":\"5x Vier Jahreszeiten\",\"valueInfo\":{},\"id\":\"f9bac09f-c5df-11f0-94e9-0242c0a80103\",\"name\":\"pizza_wishlist\",\"processDefinitionId\":\"OrderPizza:3:f2d157ce-c5df-11f0-94e9-0242c0a80103\",\"processInstanceId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"executionId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"caseInstanceId\":null,\"caseExecutionId\":null,\"taskId\":null,\"batchId\":null,\"activityInstanceId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"errorMessage\":null,\"tenantId\":null},{\"type\":\"String\",\"value\":\"JA\",\"valueInfo\":{},\"id\":\"f9bac0a2-c5df-11f0-94e9-0242c0a80103\",\"name\":\"mehrheit_will_pizza\",\"processDefinitionId\":\"OrderPizza:3:f2d157ce-c5df-11f0-94e9-0242c0a80103\",\"processInstanceId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"executionId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"caseInstanceId\":null,\"caseExecutionId\":null,\"taskId\":null,\"batchId\":null,\"activityInstanceId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"errorMessage\":null,\"tenantId\":null},{\"type\":\"String\",\"value\":\"JA\",\"valueInfo\":{},\"id\":\"f9bae7b5-c5df-11f0-94e9-0242c0a80103\",\"name\":\"MehrheitWillPizza\",\"processDefinitionId\":\"OrderPizza:3:f2d157ce-c5df-11f0-94e9-0242c0a80103\",\"processInstanceId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"executionId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"caseInstanceId\":null,\"caseExecutionId\":null,\"taskId\":null,\"batchId\":null,\"activityInstanceId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"errorMessage\":null,\"tenantId\":null},{\"type\":\"String\",\"value\":\"5x Vier Jahreszeiten\",\"valueInfo\":{},\"id\":\"f9bae7b7-c5df-11f0-94e9-0242c0a80103\",\"name\":\"Pizzawuensche\",\"processDefinitionId\":\"OrderPizza:3:f2d157ce-c5df-11f0-94e9-0242c0a80103\",\"processInstanceId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"executionId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"caseInstanceId\":null,\"caseExecutionId\":null,\"taskId\":null,\"batchId\":null,\"activityInstanceId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"errorMessage\":null,\"tenantId\":null},{\"type\":\"String\",\"value\":\"5x Vier Jahreszeiten\",\"valueInfo\":{},\"id\":\"f9bb0ecc-c5df-11f0-94e9-0242c0a80103\",\"name\":\"Bestellungen\",\"processDefinitionId\":\"OrderPizza:3:f2d157ce-c5df-11f0-94e9-0242c0a80103\",\"processInstanceId\":\"f2d4da42-c5df-11f0-94e9-0242c0a80103\",\"executionId\":\"f9bb0eca-c5df-11f0-94e9-0242c0a80103\",\"caseInstanceId\":null,\"caseExecutionId\":null,\"taskId\":null,\"batchId\":null,\"activityInstanceId\":\"ServiceTask_OrderPizza:f9bb0ecb-c5df-11f0-94e9-0242c0a80103\",\"errorMessage\":null,\"tenantId\":null}]";
272        let variables = parse_process_instance_variables(response_string);
273        dbg!(&variables);
274        assert!(!(variables.is_empty()))
275
276    }
277
278    #[test]
279    fn test_module_parsing_invalid() {
280        let response_string: &str = "{\"invalid\":}";
281    }
282}