1use crate::engine::error::{DataflowError, Result};
17use crate::engine::executor::{ArenaContext, with_arena};
18use crate::engine::message::{Change, Message};
19use crate::engine::task_outcome::TaskOutcome;
20use crate::engine::utils::{get_nested_value_parts, set_nested_value_parts};
21use datalogic_rs::{Engine, Logic};
22use datavalue::OwnedDataValue;
23use log::{debug, error};
24use serde::Deserialize;
25use serde_json::Value;
26use std::sync::Arc;
27
28#[derive(Debug, Clone, Deserialize)]
30pub struct MapConfig {
31 pub mappings: Vec<MapMapping>,
33}
34
35#[derive(Debug, Clone, Deserialize, Default)]
37pub struct MapMapping {
38 pub path: String,
41
42 pub logic: Value,
45
46 #[doc(hidden)]
50 #[serde(skip)]
51 pub compiled_logic: Option<Arc<Logic>>,
52
53 #[doc(hidden)]
58 #[serde(skip)]
59 pub path_arc: Arc<str>,
60
61 #[doc(hidden)]
67 #[serde(skip)]
68 pub path_parts: Arc<[Arc<str>]>,
69}
70
71impl MapConfig {
72 pub fn from_json(input: &Value) -> Result<Self> {
74 let mappings = input.get("mappings").ok_or_else(|| {
75 DataflowError::Validation("Missing 'mappings' array in input".to_string())
76 })?;
77
78 let mappings_arr = mappings
79 .as_array()
80 .ok_or_else(|| DataflowError::Validation("'mappings' must be an array".to_string()))?;
81
82 let mut parsed_mappings = Vec::new();
83
84 for mapping in mappings_arr {
85 let path = mapping
86 .get("path")
87 .and_then(Value::as_str)
88 .ok_or_else(|| DataflowError::Validation("Missing 'path' in mapping".to_string()))?
89 .to_string();
90
91 let logic = mapping
92 .get("logic")
93 .ok_or_else(|| DataflowError::Validation("Missing 'logic' in mapping".to_string()))?
94 .clone();
95
96 parsed_mappings.push(MapMapping {
97 path_arc: Arc::from(path.as_str()),
98 path_parts: Arc::from(Vec::<Arc<str>>::new().into_boxed_slice()),
99 path,
100 logic,
101 compiled_logic: None,
102 });
103 }
104
105 Ok(MapConfig {
106 mappings: parsed_mappings,
107 })
108 }
109
110 pub fn execute(
116 &self,
117 message: &mut Message,
118 engine: &Arc<Engine>,
119 ) -> Result<(TaskOutcome, Vec<Change>)> {
120 with_arena(|arena| {
124 let mut arena_ctx = ArenaContext::from_owned(&message.context, arena);
125 self.execute_in_arena(message, &mut arena_ctx, engine, None)
126 })
127 }
128
129 pub(crate) fn execute_in_arena<'arena>(
143 &'arena self,
144 message: &mut Message,
145 arena_ctx: &mut ArenaContext<'arena>,
146 engine: &Arc<Engine>,
147 mut mapping_snapshots: Option<&mut Vec<Value>>,
148 ) -> Result<(TaskOutcome, Vec<Change>)> {
149 let mut changes = if message.capture_changes {
152 Vec::with_capacity(self.mappings.len())
153 } else {
154 Vec::new()
155 };
156 let mut errors_encountered = false;
157
158 debug!("Map: Executing {} mappings", self.mappings.len());
159
160 let arena = arena_ctx.arena();
161 for mapping in &self.mappings {
162 debug!("Processing mapping to path: {}", mapping.path);
163
164 if let Some(buf) = mapping_snapshots.as_deref_mut() {
168 buf.push(Value::from(&message.context));
169 }
170
171 let compiled_logic = match &mapping.compiled_logic {
176 Some(logic) => logic,
177 None => {
178 error!("Map: Logic not compiled for mapping to {}", mapping.path);
179 errors_encountered = true;
180 continue;
181 }
182 };
183
184 let ctx_av = arena_ctx.as_data_value();
185 let result_av = match engine.evaluate(compiled_logic, ctx_av, arena) {
186 Ok(av) => av,
187 Err(e) => {
188 error!(
189 "Map: Error evaluating logic for path {}: {:?}",
190 mapping.path, e
191 );
192 errors_encountered = true;
193 continue;
194 }
195 };
196
197 let transformed_value = result_av.to_owned();
198 debug!(
199 "Map: Evaluated logic for path {} resulted in: {:?}",
200 mapping.path, transformed_value
201 );
202
203 if matches!(transformed_value, OwnedDataValue::Null) {
204 debug!(
205 "Map: Skipping mapping for path {} as result is null",
206 mapping.path
207 );
208 continue;
209 }
210
211 let fallback_parts: Vec<Arc<str>>;
216 let parts: &[Arc<str>] = if mapping.path_parts.is_empty() && !mapping.path.is_empty() {
217 fallback_parts = mapping.path.split('.').map(Arc::from).collect();
218 &fallback_parts
219 } else {
220 &mapping.path_parts
221 };
222 let path_arc: Arc<str> = if mapping.path_arc.is_empty() && !mapping.path.is_empty() {
223 Arc::from(mapping.path.as_str())
224 } else {
225 Arc::clone(&mapping.path_arc)
226 };
227
228 if message.capture_changes {
229 let old_value = get_nested_value_parts(&message.context, parts)
233 .cloned()
234 .unwrap_or(OwnedDataValue::Null);
235 let new_value = transformed_value.clone();
236
237 changes.push(Change {
238 path: path_arc,
239 old_value,
240 new_value,
241 });
242 }
243 arena_ctx.apply_mutation_parts_write_through(
248 &mut message.context,
249 parts,
250 *result_av,
251 |ctx| {
252 apply_mapping_parts(ctx, parts, &mapping.path, transformed_value);
253 },
254 );
255 debug!("Successfully mapped to path: {}", mapping.path);
256 }
257
258 let outcome = if errors_encountered {
259 TaskOutcome::Status(500)
260 } else {
261 TaskOutcome::Success
262 };
263 Ok((outcome, changes))
264 }
265}
266
267fn apply_mapping_parts(
271 context: &mut OwnedDataValue,
272 parts: &[Arc<str>],
273 full_path: &str,
274 new_value: OwnedDataValue,
275) {
276 if parts.len() == 1 && matches!(full_path, "data" | "metadata" | "temp_data") {
277 merge_root_field(context, full_path, new_value);
278 } else {
279 set_nested_value_parts(context, parts, new_value);
280 }
281}
282
283fn merge_root_field(context: &mut OwnedDataValue, path: &str, new_value: OwnedDataValue) {
287 let OwnedDataValue::Object(ctx_pairs) = context else {
288 *context = wrap_root(path, new_value);
290 return;
291 };
292
293 let slot_idx = ctx_pairs.iter().position(|(k, _)| k == path);
294 match slot_idx {
295 Some(idx) => {
296 let slot = &mut ctx_pairs[idx].1;
297 match (slot, new_value) {
298 (OwnedDataValue::Object(existing), OwnedDataValue::Object(new_pairs)) => {
299 for (k, v) in new_pairs {
300 if let Some(s) = existing.iter_mut().find(|(ek, _)| ek == &k) {
301 s.1 = v;
302 } else {
303 existing.push((k, v));
304 }
305 }
306 }
307 (slot, new) => *slot = new,
308 }
309 }
310 None => {
311 ctx_pairs.push((path.to_string(), new_value));
312 }
313 }
314}
315
316fn wrap_root(path: &str, value: OwnedDataValue) -> OwnedDataValue {
319 OwnedDataValue::Object(vec![(path.to_string(), value)])
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::engine::message::Message;
326 use crate::engine::utils::set_nested_value;
327 use serde_json::json;
328
329 fn dv(v: serde_json::Value) -> OwnedDataValue {
330 OwnedDataValue::from(&v)
331 }
332
333 fn fresh_message(initial: serde_json::Value) -> Message {
334 Message::builder().data(dv(initial)).build()
336 }
337
338 #[test]
339 fn test_map_config_from_json() {
340 let input = json!({
341 "mappings": [
342 { "path": "data.field1", "logic": {"var": "data.source"} },
343 { "path": "data.field2", "logic": "static_value" }
344 ]
345 });
346
347 let config = MapConfig::from_json(&input).unwrap();
348 assert_eq!(config.mappings.len(), 2);
349 assert_eq!(config.mappings[0].path, "data.field1");
350 assert_eq!(config.mappings[1].path, "data.field2");
351 }
352
353 #[test]
354 fn test_map_config_missing_mappings() {
355 assert!(MapConfig::from_json(&json!({})).is_err());
356 }
357
358 #[test]
359 fn test_map_config_invalid_mappings() {
360 assert!(MapConfig::from_json(&json!({"mappings": "not_an_array"})).is_err());
361 }
362
363 #[test]
364 fn test_map_config_missing_path() {
365 let input = json!({"mappings": [{"logic": {"var": "data.source"}}]});
366 assert!(MapConfig::from_json(&input).is_err());
367 }
368
369 #[test]
370 fn test_map_config_missing_logic() {
371 let input = json!({"mappings": [{"path": "data.field1"}]});
372 assert!(MapConfig::from_json(&input).is_err());
373 }
374
375 fn compile_mappings(engine: &Arc<Engine>, config: &mut MapConfig) {
379 for mapping in &mut config.mappings {
380 mapping.compiled_logic = Some(engine.compile_arc(&mapping.logic).unwrap());
381 }
382 }
383
384 #[test]
385 fn test_map_metadata_assignment() {
386 let engine = Arc::new(Engine::builder().with_templating(true).build());
387
388 let mut message = fresh_message(json!({
389 "SwiftMT": { "message_type": "103" }
390 }));
391
392 let mut config = MapConfig {
393 mappings: vec![MapMapping {
394 path: "metadata.SwiftMT.message_type".to_string(),
395 logic: json!({"var": "data.SwiftMT.message_type"}),
396 ..Default::default()
397 }],
398 };
399 compile_mappings(&engine, &mut config);
400
401 let result = config.execute(&mut message, &engine);
402 assert!(result.is_ok());
403
404 let (outcome, changes) = result.unwrap();
405 assert_eq!(outcome, TaskOutcome::Success);
406 assert_eq!(changes.len(), 1);
407
408 assert_eq!(
409 message.context["metadata"]
410 .get("SwiftMT")
411 .and_then(|v| v.get("message_type")),
412 Some(&dv(json!("103")))
413 );
414 }
415
416 #[test]
417 fn test_map_null_values_skip_assignment() {
418 let engine = Arc::new(Engine::builder().with_templating(true).build());
419
420 let mut message = fresh_message(json!({ "existing_field": "should_remain" }));
421 set_nested_value(
422 &mut message.context,
423 "metadata",
424 dv(json!({"existing_meta": "should_remain"})),
425 );
426
427 let mut config = MapConfig {
428 mappings: vec![
429 MapMapping {
430 path: "data.new_field".to_string(),
431 logic: json!({"var": "data.non_existent_field"}),
432 ..Default::default()
433 },
434 MapMapping {
435 path: "metadata.new_meta".to_string(),
436 logic: json!({"var": "data.another_non_existent"}),
437 ..Default::default()
438 },
439 MapMapping {
440 path: "data.actual_field".to_string(),
441 logic: json!("actual_value"),
442 ..Default::default()
443 },
444 ],
445 };
446 compile_mappings(&engine, &mut config);
447
448 let result = config.execute(&mut message, &engine);
449 assert!(result.is_ok());
450
451 let (outcome, changes) = result.unwrap();
452 assert_eq!(outcome, TaskOutcome::Success);
453 assert_eq!(changes.len(), 1);
454 assert_eq!(changes[0].path.as_ref(), "data.actual_field");
455
456 assert_eq!(message.context["data"].get("new_field"), None);
457 assert_eq!(message.context["metadata"].get("new_meta"), None);
458
459 assert_eq!(
460 message.context["data"].get("existing_field"),
461 Some(&dv(json!("should_remain")))
462 );
463 assert_eq!(
464 message.context["metadata"].get("existing_meta"),
465 Some(&dv(json!("should_remain")))
466 );
467
468 assert_eq!(
469 message.context["data"].get("actual_field"),
470 Some(&dv(json!("actual_value")))
471 );
472 }
473
474 #[test]
475 fn test_map_execute_with_trace_captures_context_snapshots() {
476 let engine = Arc::new(Engine::builder().with_templating(true).build());
477
478 let mut message = fresh_message(json!({ "first": "Alice", "last": "Smith" }));
479
480 let mut config = MapConfig {
481 mappings: vec![
482 MapMapping {
483 path: "data.full_name".to_string(),
484 logic: json!({"cat": [{"var": "data.first"}, " ", {"var": "data.last"}]}),
485 ..Default::default()
486 },
487 MapMapping {
488 path: "data.greeting".to_string(),
489 logic: json!({"cat": ["Hello, ", {"var": "data.full_name"}]}),
490 ..Default::default()
491 },
492 ],
493 };
494 compile_mappings(&engine, &mut config);
495
496 let mut context_snapshots: Vec<Value> = Vec::new();
497 let result = with_arena(|arena| {
498 let mut arena_ctx = ArenaContext::from_owned(&message.context, arena);
499 config.execute_in_arena(
500 &mut message,
501 &mut arena_ctx,
502 &engine,
503 Some(&mut context_snapshots),
504 )
505 });
506 assert!(result.is_ok());
507
508 let (outcome, changes) = result.unwrap();
509 assert_eq!(outcome, TaskOutcome::Success);
510 assert_eq!(changes.len(), 2);
511 assert_eq!(context_snapshots.len(), 2);
512
513 assert!(context_snapshots[0]["data"].get("full_name").is_none());
515 assert_eq!(
516 context_snapshots[1]["data"].get("full_name"),
517 Some(&json!("Alice Smith"))
518 );
519 }
520
521 #[test]
522 fn test_map_multiple_fields_including_metadata() {
523 let engine = Arc::new(Engine::builder().with_templating(true).build());
524
525 let mut message = fresh_message(json!({
526 "ISO20022_MX": {
527 "document": {
528 "TxInf": {
529 "OrgnlGrpInf": { "OrgnlMsgNmId": "pacs.008.001.08" }
530 }
531 }
532 },
533 "SwiftMT": { "message_type": "103" }
534 }));
535
536 let mut config = MapConfig {
537 mappings: vec![
538 MapMapping {
539 path: "data.SwiftMT.message_type".to_string(),
540 logic: json!("103"),
541 ..Default::default()
542 },
543 MapMapping {
544 path: "metadata.SwiftMT.message_type".to_string(),
545 logic: json!({"var": "data.SwiftMT.message_type"}),
546 ..Default::default()
547 },
548 MapMapping {
549 path: "temp_data.original_msg_type".to_string(),
550 logic: json!({"var": "data.ISO20022_MX.document.TxInf.OrgnlGrpInf.OrgnlMsgNmId"}),
551 ..Default::default()
552 },
553 ],
554 };
555 compile_mappings(&engine, &mut config);
556
557 let result = config.execute(&mut message, &engine);
558 assert!(result.is_ok());
559
560 let (outcome, changes) = result.unwrap();
561 assert_eq!(outcome, TaskOutcome::Success);
562 assert_eq!(changes.len(), 3);
563
564 assert_eq!(
565 message.context["data"]
566 .get("SwiftMT")
567 .and_then(|v| v.get("message_type")),
568 Some(&dv(json!("103")))
569 );
570 assert_eq!(
571 message.context["metadata"]
572 .get("SwiftMT")
573 .and_then(|v| v.get("message_type")),
574 Some(&dv(json!("103")))
575 );
576 assert_eq!(
577 message.context["temp_data"].get("original_msg_type"),
578 Some(&dv(json!("pacs.008.001.08")))
579 );
580 }
581}