Thanks to visit codestin.com
Credit goes to docs.rs

dataflow_rs/engine/functions/
map.rs

1use crate::engine::error::{DataflowError, Result};
2use crate::engine::message::{Change, Message};
3use crate::engine::utils::{get_nested_value, set_nested_value};
4use datalogic_rs::{CompiledLogic, DataLogic};
5use log::{debug, error};
6use serde::Deserialize;
7use serde_json::Value;
8use std::sync::Arc;
9
10/// Pre-parsed configuration for map function
11#[derive(Debug, Clone, Deserialize)]
12pub struct MapConfig {
13    pub mappings: Vec<MapMapping>,
14}
15
16#[derive(Debug, Clone, Deserialize)]
17pub struct MapMapping {
18    pub path: String,
19    pub logic: Value,
20    #[serde(skip)]
21    pub logic_index: Option<usize>,
22}
23
24impl MapConfig {
25    pub fn from_json(input: &Value) -> Result<Self> {
26        let mappings = input.get("mappings").ok_or_else(|| {
27            DataflowError::Validation("Missing 'mappings' array in input".to_string())
28        })?;
29
30        let mappings_arr = mappings
31            .as_array()
32            .ok_or_else(|| DataflowError::Validation("'mappings' must be an array".to_string()))?;
33
34        let mut parsed_mappings = Vec::new();
35
36        for mapping in mappings_arr {
37            let path = mapping
38                .get("path")
39                .and_then(Value::as_str)
40                .ok_or_else(|| DataflowError::Validation("Missing 'path' in mapping".to_string()))?
41                .to_string();
42
43            let logic = mapping
44                .get("logic")
45                .ok_or_else(|| DataflowError::Validation("Missing 'logic' in mapping".to_string()))?
46                .clone();
47
48            parsed_mappings.push(MapMapping {
49                path,
50                logic,
51                logic_index: None,
52            });
53        }
54
55        Ok(MapConfig {
56            mappings: parsed_mappings,
57        })
58    }
59
60    /// Execute the map transformations using pre-compiled logic
61    pub fn execute(
62        &self,
63        message: &mut Message,
64        datalogic: &Arc<DataLogic>,
65        logic_cache: &[Arc<CompiledLogic>],
66    ) -> Result<(usize, Vec<Change>)> {
67        let mut changes = Vec::new();
68        let mut errors_encountered = false;
69
70        debug!("Map: Executing {} mappings", self.mappings.len());
71
72        // Process each mapping
73        for mapping in &self.mappings {
74            // Get or create context Arc for this iteration
75            // This ensures we use cached Arc when available, or create fresh one after modifications
76            let context_arc = message.get_context_arc();
77            debug!("Processing mapping to path: {}", mapping.path);
78
79            // Get the compiled logic from cache with proper bounds checking
80            let compiled_logic = match mapping.logic_index {
81                Some(index) => {
82                    // Ensure index is valid before accessing
83                    if index >= logic_cache.len() {
84                        error!(
85                            "Map: Logic index {} out of bounds (cache size: {}) for mapping to {}",
86                            index,
87                            logic_cache.len(),
88                            mapping.path
89                        );
90                        errors_encountered = true;
91                        continue;
92                    }
93                    &logic_cache[index]
94                }
95                None => {
96                    error!(
97                        "Map: Logic not compiled (no index) for mapping to {}",
98                        mapping.path
99                    );
100                    errors_encountered = true;
101                    continue;
102                }
103            };
104
105            // Evaluate the transformation logic using DataLogic v4
106            // DataLogic v4 is thread-safe with Arc<CompiledLogic>, no spawn_blocking needed
107            let result = datalogic.evaluate(compiled_logic, Arc::clone(&context_arc));
108
109            match result {
110                Ok(transformed_value) => {
111                    debug!(
112                        "Map: Evaluated logic for path {} resulted in: {:?}",
113                        mapping.path, transformed_value
114                    );
115
116                    // Skip mapping if the result is null
117                    if transformed_value.is_null() {
118                        debug!(
119                            "Map: Skipping mapping for path {} as result is null",
120                            mapping.path
121                        );
122                        continue;
123                    }
124
125                    // Get old value from the appropriate location in context
126                    let old_value = get_nested_value(&message.context, &mapping.path);
127
128                    let old_value_arc = Arc::new(old_value.cloned().unwrap_or(Value::Null));
129                    let new_value_arc = Arc::new(transformed_value.clone());
130
131                    debug!(
132                        "Recording change for path '{}': old={:?}, new={:?}",
133                        mapping.path, old_value_arc, new_value_arc
134                    );
135                    changes.push(Change {
136                        path: Arc::from(mapping.path.as_str()),
137                        old_value: old_value_arc,
138                        new_value: Arc::clone(&new_value_arc),
139                    });
140
141                    // Update the context directly with the transformed value
142                    // Check if we're replacing a root field (data, metadata, or temp_data)
143                    if mapping.path == "data"
144                        || mapping.path == "metadata"
145                        || mapping.path == "temp_data"
146                    {
147                        // Merge with existing field instead of replacing entirely
148                        if let Value::Object(new_map) = transformed_value {
149                            // If new value is an object, merge its fields
150                            if let Value::Object(existing_map) = &mut message.context[&mapping.path]
151                            {
152                                // Merge new fields into existing object
153                                for (key, value) in new_map {
154                                    existing_map.insert(key, value);
155                                }
156                            } else {
157                                // If existing is not an object, replace with new object
158                                message.context[&mapping.path] = Value::Object(new_map);
159                            }
160                        } else {
161                            // If new value is not an object, replace entirely
162                            message.context[&mapping.path] = transformed_value;
163                        }
164                    } else {
165                        // Set nested value in context
166                        set_nested_value(&mut message.context, &mapping.path, transformed_value);
167                    }
168                    // Invalidate the cached context Arc since we modified the context
169                    // The next iteration (if any) will create a fresh Arc when needed
170                    message.invalidate_context_cache();
171                    debug!("Successfully mapped to path: {}", mapping.path);
172                }
173                Err(e) => {
174                    error!(
175                        "Map: Error evaluating logic for path {}: {:?}",
176                        mapping.path, e
177                    );
178                    errors_encountered = true;
179                }
180            }
181        }
182
183        // Return appropriate status based on results
184        let status = if errors_encountered { 500 } else { 200 };
185        Ok((status, changes))
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::engine::message::Message;
193    use serde_json::json;
194
195    #[test]
196    fn test_map_config_from_json() {
197        let input = json!({
198            "mappings": [
199                {
200                    "path": "data.field1",
201                    "logic": {"var": "data.source"}
202                },
203                {
204                    "path": "data.field2",
205                    "logic": "static_value"
206                }
207            ]
208        });
209
210        let config = MapConfig::from_json(&input).unwrap();
211        assert_eq!(config.mappings.len(), 2);
212        assert_eq!(config.mappings[0].path, "data.field1");
213        assert_eq!(config.mappings[1].path, "data.field2");
214    }
215
216    #[test]
217    fn test_map_config_missing_mappings() {
218        let input = json!({});
219        let result = MapConfig::from_json(&input);
220        assert!(result.is_err());
221    }
222
223    #[test]
224    fn test_map_config_invalid_mappings() {
225        let input = json!({
226            "mappings": "not_an_array"
227        });
228        let result = MapConfig::from_json(&input);
229        assert!(result.is_err());
230    }
231
232    #[test]
233    fn test_map_config_missing_path() {
234        let input = json!({
235            "mappings": [
236                {
237                    "logic": {"var": "data.source"}
238                }
239            ]
240        });
241        let result = MapConfig::from_json(&input);
242        assert!(result.is_err());
243    }
244
245    #[test]
246    fn test_map_config_missing_logic() {
247        let input = json!({
248            "mappings": [
249                {
250                    "path": "data.field1"
251                }
252            ]
253        });
254        let result = MapConfig::from_json(&input);
255        assert!(result.is_err());
256    }
257
258    #[test]
259    fn test_map_metadata_assignment() {
260        // Test that metadata field assignments work correctly
261        let datalogic = Arc::new(DataLogic::with_preserve_structure());
262
263        // Create test message
264        let mut message = Message::new(Arc::new(json!({})));
265        message.context["data"] = json!({
266            "SwiftMT": {
267                "message_type": "103"
268            }
269        });
270
271        // Create mapping config that assigns from data to metadata
272        let config = MapConfig {
273            mappings: vec![MapMapping {
274                path: "metadata.SwiftMT.message_type".to_string(),
275                logic: json!({"var": "data.SwiftMT.message_type"}),
276                logic_index: Some(0),
277            }],
278        };
279
280        // Compile the logic
281        let logic_cache = vec![datalogic.compile(&config.mappings[0].logic).unwrap()];
282
283        // Execute the mapping
284        let result = config.execute(&mut message, &datalogic, &logic_cache);
285        assert!(result.is_ok());
286
287        let (status, changes) = result.unwrap();
288        assert_eq!(status, 200);
289        assert_eq!(changes.len(), 1);
290
291        // Verify metadata was updated
292        assert_eq!(
293            message.context["metadata"]
294                .get("SwiftMT")
295                .and_then(|v| v.get("message_type")),
296            Some(&json!("103"))
297        );
298    }
299
300    #[test]
301    fn test_map_null_values_skip_assignment() {
302        // Test that null evaluation results skip the mapping entirely
303        let datalogic = Arc::new(DataLogic::with_preserve_structure());
304
305        // Create test message with existing values
306        let mut message = Message::new(Arc::new(json!({})));
307        message.context["data"] = json!({
308            "existing_field": "should_remain"
309        });
310        message.context["metadata"] = json!({
311            "existing_meta": "should_remain"
312        });
313
314        // Create mapping config that would return null
315        let config = MapConfig {
316            mappings: vec![
317                MapMapping {
318                    path: "data.new_field".to_string(),
319                    logic: json!({"var": "data.non_existent_field"}), // This will return null
320                    logic_index: Some(0),
321                },
322                MapMapping {
323                    path: "metadata.new_meta".to_string(),
324                    logic: json!({"var": "data.another_non_existent"}), // This will return null
325                    logic_index: Some(1),
326                },
327                MapMapping {
328                    path: "data.actual_field".to_string(),
329                    logic: json!("actual_value"), // This will succeed
330                    logic_index: Some(2),
331                },
332            ],
333        };
334
335        // Compile the logic
336        let logic_cache = vec![
337            datalogic.compile(&config.mappings[0].logic).unwrap(),
338            datalogic.compile(&config.mappings[1].logic).unwrap(),
339            datalogic.compile(&config.mappings[2].logic).unwrap(),
340        ];
341
342        // Execute the mapping
343        let result = config.execute(&mut message, &datalogic, &logic_cache);
344        assert!(result.is_ok());
345
346        let (status, changes) = result.unwrap();
347        assert_eq!(status, 200);
348        // Only one change should be recorded (the non-null mapping)
349        assert_eq!(changes.len(), 1);
350        assert_eq!(changes[0].path.as_ref(), "data.actual_field");
351
352        // Verify that null mappings were skipped (fields don't exist)
353        assert_eq!(message.context["data"].get("new_field"), None);
354        assert_eq!(message.context["metadata"].get("new_meta"), None);
355
356        // Verify existing fields remain unchanged
357        assert_eq!(
358            message.context["data"].get("existing_field"),
359            Some(&json!("should_remain"))
360        );
361        assert_eq!(
362            message.context["metadata"].get("existing_meta"),
363            Some(&json!("should_remain"))
364        );
365
366        // Verify the successful mapping
367        assert_eq!(
368            message.context["data"].get("actual_field"),
369            Some(&json!("actual_value"))
370        );
371    }
372
373    #[test]
374    fn test_map_multiple_fields_including_metadata() {
375        // Test mapping to data, metadata, and temp_data in one task
376        let datalogic = Arc::new(DataLogic::with_preserve_structure());
377
378        // Create test message
379        let mut message = Message::new(Arc::new(json!({})));
380        message.context["data"] = json!({
381            "ISO20022_MX": {
382                "document": {
383                    "TxInf": {
384                        "OrgnlGrpInf": {
385                            "OrgnlMsgNmId": "pacs.008.001.08"
386                        }
387                    }
388                }
389            },
390            "SwiftMT": {
391                "message_type": "103"
392            }
393        });
394
395        // Create mapping config with multiple mappings
396        let mut config = MapConfig {
397            mappings: vec![
398                MapMapping {
399                    path: "data.SwiftMT.message_type".to_string(),
400                    logic: json!("103"),
401                    logic_index: None,
402                },
403                MapMapping {
404                    path: "metadata.SwiftMT.message_type".to_string(),
405                    logic: json!({"var": "data.SwiftMT.message_type"}),
406                    logic_index: None,
407                },
408                MapMapping {
409                    path: "temp_data.original_msg_type".to_string(),
410                    logic: json!({"var": "data.ISO20022_MX.document.TxInf.OrgnlGrpInf.OrgnlMsgNmId"}),
411                    logic_index: None,
412                },
413            ],
414        };
415
416        // Compile the logic and set indices
417        let mut logic_cache = Vec::new();
418        for (i, mapping) in config.mappings.iter_mut().enumerate() {
419            logic_cache.push(datalogic.compile(&mapping.logic).unwrap());
420            mapping.logic_index = Some(i);
421        }
422
423        // Execute the mapping
424        let result = config.execute(&mut message, &datalogic, &logic_cache);
425        assert!(result.is_ok());
426
427        let (status, changes) = result.unwrap();
428        assert_eq!(status, 200);
429        assert_eq!(changes.len(), 3);
430
431        // Verify all fields were updated correctly
432        assert_eq!(
433            message.context["data"]
434                .get("SwiftMT")
435                .and_then(|v| v.get("message_type")),
436            Some(&json!("103"))
437        );
438        assert_eq!(
439            message.context["metadata"]
440                .get("SwiftMT")
441                .and_then(|v| v.get("message_type")),
442            Some(&json!("103"))
443        );
444        assert_eq!(
445            message.context["temp_data"].get("original_msg_type"),
446            Some(&json!("pacs.008.001.08"))
447        );
448    }
449}