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 trace_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) = trace_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 let mut m = Message::new(Arc::new(dv(json!({}))));
336 set_nested_value(&mut m.context, "data", dv(initial));
337 m
338 }
339
340 #[test]
341 fn test_map_config_from_json() {
342 let input = json!({
343 "mappings": [
344 { "path": "data.field1", "logic": {"var": "data.source"} },
345 { "path": "data.field2", "logic": "static_value" }
346 ]
347 });
348
349 let config = MapConfig::from_json(&input).unwrap();
350 assert_eq!(config.mappings.len(), 2);
351 assert_eq!(config.mappings[0].path, "data.field1");
352 assert_eq!(config.mappings[1].path, "data.field2");
353 }
354
355 #[test]
356 fn test_map_config_missing_mappings() {
357 assert!(MapConfig::from_json(&json!({})).is_err());
358 }
359
360 #[test]
361 fn test_map_config_invalid_mappings() {
362 assert!(MapConfig::from_json(&json!({"mappings": "not_an_array"})).is_err());
363 }
364
365 #[test]
366 fn test_map_config_missing_path() {
367 let input = json!({"mappings": [{"logic": {"var": "data.source"}}]});
368 assert!(MapConfig::from_json(&input).is_err());
369 }
370
371 #[test]
372 fn test_map_config_missing_logic() {
373 let input = json!({"mappings": [{"path": "data.field1"}]});
374 assert!(MapConfig::from_json(&input).is_err());
375 }
376
377 fn compile_mappings(engine: &Arc<Engine>, config: &mut MapConfig) {
381 for mapping in &mut config.mappings {
382 mapping.compiled_logic = Some(engine.compile_arc(&mapping.logic).unwrap());
383 }
384 }
385
386 #[test]
387 fn test_map_metadata_assignment() {
388 let engine = Arc::new(Engine::builder().with_templating(true).build());
389
390 let mut message = fresh_message(json!({
391 "SwiftMT": { "message_type": "103" }
392 }));
393
394 let mut config = MapConfig {
395 mappings: vec![MapMapping {
396 path: "metadata.SwiftMT.message_type".to_string(),
397 logic: json!({"var": "data.SwiftMT.message_type"}),
398 ..Default::default()
399 }],
400 };
401 compile_mappings(&engine, &mut config);
402
403 let result = config.execute(&mut message, &engine);
404 assert!(result.is_ok());
405
406 let (outcome, changes) = result.unwrap();
407 assert_eq!(outcome, TaskOutcome::Success);
408 assert_eq!(changes.len(), 1);
409
410 assert_eq!(
411 message.context["metadata"]
412 .get("SwiftMT")
413 .and_then(|v| v.get("message_type")),
414 Some(&dv(json!("103")))
415 );
416 }
417
418 #[test]
419 fn test_map_null_values_skip_assignment() {
420 let engine = Arc::new(Engine::builder().with_templating(true).build());
421
422 let mut message = fresh_message(json!({ "existing_field": "should_remain" }));
423 set_nested_value(
424 &mut message.context,
425 "metadata",
426 dv(json!({"existing_meta": "should_remain"})),
427 );
428
429 let mut config = MapConfig {
430 mappings: vec![
431 MapMapping {
432 path: "data.new_field".to_string(),
433 logic: json!({"var": "data.non_existent_field"}),
434 ..Default::default()
435 },
436 MapMapping {
437 path: "metadata.new_meta".to_string(),
438 logic: json!({"var": "data.another_non_existent"}),
439 ..Default::default()
440 },
441 MapMapping {
442 path: "data.actual_field".to_string(),
443 logic: json!("actual_value"),
444 ..Default::default()
445 },
446 ],
447 };
448 compile_mappings(&engine, &mut config);
449
450 let result = config.execute(&mut message, &engine);
451 assert!(result.is_ok());
452
453 let (outcome, changes) = result.unwrap();
454 assert_eq!(outcome, TaskOutcome::Success);
455 assert_eq!(changes.len(), 1);
456 assert_eq!(changes[0].path.as_ref(), "data.actual_field");
457
458 assert_eq!(message.context["data"].get("new_field"), None);
459 assert_eq!(message.context["metadata"].get("new_meta"), None);
460
461 assert_eq!(
462 message.context["data"].get("existing_field"),
463 Some(&dv(json!("should_remain")))
464 );
465 assert_eq!(
466 message.context["metadata"].get("existing_meta"),
467 Some(&dv(json!("should_remain")))
468 );
469
470 assert_eq!(
471 message.context["data"].get("actual_field"),
472 Some(&dv(json!("actual_value")))
473 );
474 }
475
476 #[test]
477 fn test_map_execute_with_trace_captures_context_snapshots() {
478 let engine = Arc::new(Engine::builder().with_templating(true).build());
479
480 let mut message = fresh_message(json!({ "first": "Alice", "last": "Smith" }));
481
482 let mut config = MapConfig {
483 mappings: vec![
484 MapMapping {
485 path: "data.full_name".to_string(),
486 logic: json!({"cat": [{"var": "data.first"}, " ", {"var": "data.last"}]}),
487 ..Default::default()
488 },
489 MapMapping {
490 path: "data.greeting".to_string(),
491 logic: json!({"cat": ["Hello, ", {"var": "data.full_name"}]}),
492 ..Default::default()
493 },
494 ],
495 };
496 compile_mappings(&engine, &mut config);
497
498 let mut context_snapshots: Vec<Value> = Vec::new();
499 let result = with_arena(|arena| {
500 let mut arena_ctx = ArenaContext::from_owned(&message.context, arena);
501 config.execute_in_arena(
502 &mut message,
503 &mut arena_ctx,
504 &engine,
505 Some(&mut context_snapshots),
506 )
507 });
508 assert!(result.is_ok());
509
510 let (outcome, changes) = result.unwrap();
511 assert_eq!(outcome, TaskOutcome::Success);
512 assert_eq!(changes.len(), 2);
513 assert_eq!(context_snapshots.len(), 2);
514
515 assert!(context_snapshots[0]["data"].get("full_name").is_none());
517 assert_eq!(
518 context_snapshots[1]["data"].get("full_name"),
519 Some(&json!("Alice Smith"))
520 );
521 }
522
523 #[test]
524 fn test_map_multiple_fields_including_metadata() {
525 let engine = Arc::new(Engine::builder().with_templating(true).build());
526
527 let mut message = fresh_message(json!({
528 "ISO20022_MX": {
529 "document": {
530 "TxInf": {
531 "OrgnlGrpInf": { "OrgnlMsgNmId": "pacs.008.001.08" }
532 }
533 }
534 },
535 "SwiftMT": { "message_type": "103" }
536 }));
537
538 let mut config = MapConfig {
539 mappings: vec![
540 MapMapping {
541 path: "data.SwiftMT.message_type".to_string(),
542 logic: json!("103"),
543 ..Default::default()
544 },
545 MapMapping {
546 path: "metadata.SwiftMT.message_type".to_string(),
547 logic: json!({"var": "data.SwiftMT.message_type"}),
548 ..Default::default()
549 },
550 MapMapping {
551 path: "temp_data.original_msg_type".to_string(),
552 logic: json!({"var": "data.ISO20022_MX.document.TxInf.OrgnlGrpInf.OrgnlMsgNmId"}),
553 ..Default::default()
554 },
555 ],
556 };
557 compile_mappings(&engine, &mut config);
558
559 let result = config.execute(&mut message, &engine);
560 assert!(result.is_ok());
561
562 let (outcome, changes) = result.unwrap();
563 assert_eq!(outcome, TaskOutcome::Success);
564 assert_eq!(changes.len(), 3);
565
566 assert_eq!(
567 message.context["data"]
568 .get("SwiftMT")
569 .and_then(|v| v.get("message_type")),
570 Some(&dv(json!("103")))
571 );
572 assert_eq!(
573 message.context["metadata"]
574 .get("SwiftMT")
575 .and_then(|v| v.get("message_type")),
576 Some(&dv(json!("103")))
577 );
578 assert_eq!(
579 message.context["temp_data"].get("original_msg_type"),
580 Some(&dv(json!("pacs.008.001.08")))
581 );
582 }
583}