dataflow_rs/engine/functions/
map.rs1use 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#[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 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 for mapping in &self.mappings {
74 let context_arc = message.get_context_arc();
77 debug!("Processing mapping to path: {}", mapping.path);
78
79 let compiled_logic = match mapping.logic_index {
81 Some(index) => {
82 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 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 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 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 if mapping.path == "data"
144 || mapping.path == "metadata"
145 || mapping.path == "temp_data"
146 {
147 if let Value::Object(new_map) = transformed_value {
149 if let Value::Object(existing_map) = &mut message.context[&mapping.path]
151 {
152 for (key, value) in new_map {
154 existing_map.insert(key, value);
155 }
156 } else {
157 message.context[&mapping.path] = Value::Object(new_map);
159 }
160 } else {
161 message.context[&mapping.path] = transformed_value;
163 }
164 } else {
165 set_nested_value(&mut message.context, &mapping.path, transformed_value);
167 }
168 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 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 let datalogic = Arc::new(DataLogic::with_preserve_structure());
262
263 let mut message = Message::new(Arc::new(json!({})));
265 message.context["data"] = json!({
266 "SwiftMT": {
267 "message_type": "103"
268 }
269 });
270
271 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 let logic_cache = vec![datalogic.compile(&config.mappings[0].logic).unwrap()];
282
283 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 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 let datalogic = Arc::new(DataLogic::with_preserve_structure());
304
305 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 let config = MapConfig {
316 mappings: vec![
317 MapMapping {
318 path: "data.new_field".to_string(),
319 logic: json!({"var": "data.non_existent_field"}), logic_index: Some(0),
321 },
322 MapMapping {
323 path: "metadata.new_meta".to_string(),
324 logic: json!({"var": "data.another_non_existent"}), logic_index: Some(1),
326 },
327 MapMapping {
328 path: "data.actual_field".to_string(),
329 logic: json!("actual_value"), logic_index: Some(2),
331 },
332 ],
333 };
334
335 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 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 assert_eq!(changes.len(), 1);
350 assert_eq!(changes[0].path.as_ref(), "data.actual_field");
351
352 assert_eq!(message.context["data"].get("new_field"), None);
354 assert_eq!(message.context["metadata"].get("new_meta"), None);
355
356 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 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 let datalogic = Arc::new(DataLogic::with_preserve_structure());
377
378 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 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 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 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 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}