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