1use crate::engine::error::{DataflowError, Result};
20use crate::engine::executor::{ArenaContext, with_arena};
21use crate::engine::functions::path_template::{ContextRoot, ParamCtx, PathTemplate, ResolvedPath};
22use crate::engine::message::{Change, Message};
23use crate::engine::task_outcome::TaskOutcome;
24use crate::engine::utils::{
25 describe_kind, get_nested_value_parts, get_nested_value_parts_mut, set_nested_value_parts,
26 strip_hash_prefix,
27};
28use datalogic_rs::{Engine, Logic};
29use datavalue::{DataValue, OwnedDataValue};
30use log::{debug, error};
31use serde::de::Error as _;
32use serde::{Deserialize, Deserializer};
33use serde_json::Value;
34use std::sync::Arc;
35
36#[derive(Debug, Clone, Deserialize)]
38pub struct MapConfig {
39 pub mappings: Vec<MapMapping>,
41}
42
43#[derive(Debug, Clone, Default)]
57pub struct MapMapping {
58 pub path: PathTemplate<ContextRoot>,
68
69 pub logic: Value,
74
75 pub unset: bool,
84
85 pub on_null: OnNull,
88
89 pub mode: MapMode,
93
94 #[doc(hidden)]
99 pub compiled_logic: Option<Arc<Logic>>,
100}
101
102#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
107#[serde(rename_all = "lowercase")]
108#[non_exhaustive]
109pub enum OnNull {
110 #[default]
115 Skip,
116 Unset,
119}
120
121#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
125#[serde(rename_all = "lowercase")]
126#[non_exhaustive]
127pub enum MapMode {
128 #[default]
130 Set,
131 Append,
140 Extend,
143}
144
145#[derive(Deserialize)]
151pub(crate) struct AuthoredMapping {
152 path: PathTemplate<ContextRoot>,
153 #[serde(default, deserialize_with = "present")]
156 logic: Option<Value>,
157 #[serde(default)]
158 unset: bool,
159 #[serde(default)]
162 on_null: Option<OnNull>,
163 #[serde(default)]
165 mode: Option<MapMode>,
166}
167
168fn present<'de, D: Deserializer<'de>>(d: D) -> std::result::Result<Option<Value>, D::Error> {
171 Value::deserialize(d).map(Some)
172}
173
174pub(crate) struct MappingProblem {
176 pub(crate) field: &'static str,
178 pub(crate) message: &'static str,
179}
180
181impl AuthoredMapping {
182 pub(crate) fn problem(&self) -> Option<MappingProblem> {
184 let problem = |field, message| Some(MappingProblem { field, message });
185 match (&self.logic, self.unset) {
186 (None, false) => {
187 return problem(
188 "logic",
189 "a mapping needs `logic`, or `\"unset\": true` to remove the path",
190 );
191 }
192 (Some(_), true) => {
193 return problem(
194 "unset",
195 "`unset` removes the path and takes no `logic` — to remove it only \
196 when the result is null, keep `logic` and use `\"on_null\": \"unset\"`",
197 );
198 }
199 _ => {}
200 }
201 if self.logic.is_none() && self.on_null.is_some() {
202 return problem(
203 "on_null",
204 "`on_null` says what a null `logic` result does, and this mapping has no `logic`",
205 );
206 }
207 if self.logic.is_none() && self.mode.is_some() {
208 return problem(
209 "mode",
210 "`mode` says how a `logic` result is written, and this mapping has no `logic`",
211 );
212 }
213 let literal_root = self
214 .path
215 .as_json()
216 .as_str()
217 .is_some_and(|path| is_context_root(&path.split('.').collect::<Vec<_>>()));
218 if literal_root && (self.unset || self.on_null == Some(OnNull::Unset)) {
219 return problem(
220 "path",
221 "a context root (`data`, `metadata`, `temp_data`) cannot be removed — \
222 remove the keys under it instead",
223 );
224 }
225 if literal_root && matches!(self.mode, Some(MapMode::Append | MapMode::Extend)) {
226 return problem(
227 "path",
228 "a context root (`data`, `metadata`, `temp_data`) is an object and cannot be \
229 appended to — name an array under it instead",
230 );
231 }
232 None
233 }
234
235 fn into_mapping(self) -> MapMapping {
236 MapMapping {
237 path: self.path,
238 logic: self.logic.unwrap_or(Value::Null),
239 unset: self.unset,
240 on_null: self.on_null.unwrap_or_default(),
241 mode: self.mode.unwrap_or_default(),
242 compiled_logic: None,
243 }
244 }
245}
246
247impl<'de> Deserialize<'de> for MapMapping {
248 fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
249 let authored = AuthoredMapping::deserialize(d)?;
250 if let Some(problem) = authored.problem() {
251 return Err(D::Error::custom(format_args!(
252 "mapping `{}`: {}",
253 problem.field, problem.message
254 )));
255 }
256 Ok(authored.into_mapping())
257 }
258}
259
260fn is_context_root<P: AsRef<str>>(parts: &[P]) -> bool {
263 matches!(parts, [only] if matches!(strip_hash_prefix(only.as_ref()), "data" | "metadata" | "temp_data"))
264}
265
266impl MapMapping {
267 pub(crate) fn describe_path(&self) -> String {
274 self.path
275 .constant_path()
276 .map(str::to_string)
277 .unwrap_or_else(|| self.path.as_json().to_string())
278 }
279}
280
281impl MapConfig {
282 pub fn from_json(input: &Value) -> Result<Self> {
284 let mappings = input.get("mappings").ok_or_else(|| {
285 DataflowError::Validation("Missing 'mappings' array in input".to_string())
286 })?;
287
288 let mappings_arr = mappings
289 .as_array()
290 .ok_or_else(|| DataflowError::Validation("'mappings' must be an array".to_string()))?;
291
292 let mut parsed_mappings = Vec::new();
293
294 for mapping in mappings_arr {
295 if mapping.get("path").is_none() {
296 return Err(DataflowError::Validation(
297 "Missing 'path' in mapping".to_string(),
298 ));
299 }
300 let parsed = MapMapping::deserialize(mapping)
303 .map_err(|e| DataflowError::Validation(e.to_string()))?;
304 parsed_mappings.push(parsed);
305 }
306
307 Ok(Self {
308 mappings: parsed_mappings,
309 })
310 }
311
312 pub fn execute(
318 &self,
319 message: &mut Message,
320 engine: &Arc<Engine>,
321 ) -> Result<(TaskOutcome, Vec<Change>)> {
322 with_arena(|arena| {
326 let mut arena_ctx = ArenaContext::from_owned(&message.context, arena);
327 self.execute_in_arena(message, &mut arena_ctx, engine, None)
328 })
329 }
330
331 pub(crate) fn execute_in_arena<'arena>(
345 &'arena self,
346 message: &mut Message,
347 arena_ctx: &mut ArenaContext<'arena>,
348 engine: &Arc<Engine>,
349 mut mapping_snapshots: Option<&mut Vec<Value>>,
350 ) -> Result<(TaskOutcome, Vec<Change>)> {
351 let mut changes = if message.capture_changes {
354 Vec::with_capacity(self.mappings.len())
355 } else {
356 Vec::new()
357 };
358 let mut errors_encountered = false;
359
360 debug!("Map: Executing {} mappings", self.mappings.len());
361
362 let arena = arena_ctx.arena();
363 for mapping in &self.mappings {
364 debug!("Processing mapping to path: {}", mapping.describe_path());
365
366 if let Some(buf) = mapping_snapshots.as_deref_mut() {
370 buf.push(Value::from(&message.context));
371 }
372
373 if mapping.unset {
374 let ctx_av = arena_ctx.as_data_value();
377 let Some(resolved) = resolve_destination(mapping, engine, ctx_av, arena) else {
378 errors_encountered = true;
379 continue;
380 };
381 if !remove_at(message, arena_ctx, &resolved, &mut changes) {
382 errors_encountered = true;
383 }
384 continue;
385 }
386
387 let compiled_logic = match &mapping.compiled_logic {
392 Some(logic) => logic,
393 None => {
394 error!(
395 "Map: Logic not compiled for mapping to {}",
396 mapping.describe_path()
397 );
398 errors_encountered = true;
399 continue;
400 }
401 };
402
403 let ctx_av = arena_ctx.as_data_value();
404 let result_av = match engine.evaluate(compiled_logic, ctx_av, arena) {
405 Ok(av) => av,
406 Err(e) => {
407 error!(
408 "Map: Error evaluating logic for path {}: {:?}",
409 mapping.describe_path(),
410 e
411 );
412 errors_encountered = true;
413 continue;
414 }
415 };
416
417 let transformed_value = result_av.to_owned();
418 debug!(
419 "Map: Evaluated logic for path {} resulted in: {:?}",
420 mapping.describe_path(),
421 transformed_value
422 );
423
424 if matches!(transformed_value, OwnedDataValue::Null) {
425 match mapping.on_null {
426 OnNull::Skip => {
427 debug!(
428 "Map: Skipping mapping for path {} as result is null",
429 mapping.describe_path()
430 );
431 }
432 OnNull::Unset => {
433 let Some(resolved) = resolve_destination(mapping, engine, ctx_av, arena)
434 else {
435 errors_encountered = true;
436 continue;
437 };
438 if !remove_at(message, arena_ctx, &resolved, &mut changes) {
439 errors_encountered = true;
440 }
441 }
442 }
443 continue;
444 }
445
446 let Some(resolved) = resolve_destination(mapping, engine, ctx_av, arena) else {
452 errors_encountered = true;
453 continue;
454 };
455 if mapping.mode != MapMode::Set {
456 if !append_at(
457 message,
458 arena_ctx,
459 mapping.mode,
460 &resolved,
461 result_av,
462 transformed_value,
463 &mut changes,
464 ) {
465 errors_encountered = true;
466 }
467 continue;
468 }
469 let (path_arc, parts) = (&resolved.0, &*resolved.1);
470
471 if message.capture_changes {
472 let old_value = get_nested_value_parts(&message.context, parts)
476 .cloned()
477 .unwrap_or(OwnedDataValue::Null);
478 let new_value = transformed_value.clone();
479
480 changes.push(Change {
481 path: Arc::clone(path_arc),
482 old_value,
483 new_value,
484 removed: false,
485 });
486 }
487 arena_ctx.apply_mutation_parts_write_through(
492 &mut message.context,
493 parts,
494 *result_av,
495 |ctx| {
496 apply_mapping_parts(ctx, parts, path_arc, transformed_value);
497 },
498 );
499 debug!("Successfully mapped to path: {path_arc}");
500 }
501
502 let outcome = if errors_encountered {
503 TaskOutcome::Status(500)
504 } else {
505 TaskOutcome::Success
506 };
507 Ok((outcome, changes))
508 }
509}
510
511fn resolve_destination<'a>(
516 mapping: &'a MapMapping,
517 engine: &'a Engine,
518 ctx_av: datavalue::DataValue<'a>,
519 arena: &'a datalogic_rs::bumpalo::Bump,
520) -> Option<std::borrow::Cow<'a, ResolvedPath>> {
521 match mapping
522 .path
523 .resolve_in_arena(ParamCtx::new(engine, ctx_av, arena))
524 {
525 Ok(pair) => Some(pair),
526 Err(e) => {
527 error!(
528 "Map: Error resolving destination path {}: {:?}",
529 mapping.describe_path(),
530 e
531 );
532 None
533 }
534 }
535}
536
537fn remove_at(
544 message: &mut Message,
545 arena_ctx: &mut ArenaContext<'_>,
546 resolved: &ResolvedPath,
547 changes: &mut Vec<Change>,
548) -> bool {
549 let (path_arc, parts) = (&resolved.0, &*resolved.1);
550 if is_context_root(parts) {
551 error!("Map: Refusing to remove context root {path_arc}");
552 return false;
553 }
554 match arena_ctx.apply_removal_parts(&mut message.context, parts) {
555 Some(old_value) => {
556 if message.capture_changes {
557 changes.push(Change {
558 path: Arc::clone(path_arc),
559 old_value,
560 new_value: OwnedDataValue::Null,
561 removed: true,
562 });
563 }
564 debug!("Map: Removed {path_arc}");
565 }
566 None => debug!("Map: Nothing to remove at {path_arc}"),
567 }
568 true
569}
570
571fn append_at<'a>(
585 message: &mut Message,
586 arena_ctx: &mut ArenaContext<'a>,
587 mode: MapMode,
588 resolved: &ResolvedPath,
589 result_av: &'a DataValue<'a>,
590 result: OwnedDataValue,
591 changes: &mut Vec<Change>,
592) -> bool {
593 let (path_arc, parts) = (&resolved.0, &*resolved.1);
594 if is_context_root(parts) {
595 error!("Map: Refusing to append to context root {path_arc}");
596 return false;
597 }
598 let (items_av, items): (&'a [DataValue<'a>], Vec<OwnedDataValue>) = match (mode, result) {
599 (MapMode::Extend, OwnedDataValue::Array(items)) => match result_av {
600 DataValue::Array(items_av) => (items_av, items),
601 _ => unreachable!("the owned result was converted from this arena value"),
602 },
603 (MapMode::Extend, other) => {
604 error!(
605 "Map: `extend` needs an array result for {path_arc}, got {}",
606 describe_kind(&other)
607 );
608 return false;
609 }
610 (_, value) => (std::slice::from_ref(result_av), vec![value]),
611 };
612 let start = match get_nested_value_parts(&message.context, parts) {
613 None | Some(OwnedDataValue::Null) => 0,
614 Some(OwnedDataValue::Array(existing)) => existing.len(),
615 Some(other) => {
616 error!(
617 "Map: Cannot append to {path_arc}: it holds {}, not an array",
618 describe_kind(other)
619 );
620 return false;
621 }
622 };
623 if message.capture_changes {
624 for (i, item) in items.iter().enumerate() {
625 changes.push(Change {
626 path: Arc::from(format!("{path_arc}.{}", start + i)),
627 old_value: OwnedDataValue::Null,
628 new_value: item.clone(),
629 removed: false,
630 });
631 }
632 }
633
634 let old_av: Option<&[DataValue<'a>]> = if start == 0 {
635 Some(&[])
636 } else {
637 match arena_ctx.cached_at(parts) {
638 Some(DataValue::Array(old)) if old.len() == start => Some(old),
639 _ => None,
640 }
641 };
642 match old_av {
643 Some(old) => {
644 let arena = arena_ctx.arena();
645 let new_av = DataValue::Array(arena.alloc_slice_fill_with(
646 old.len() + items_av.len(),
647 |i| {
648 if i < old.len() {
649 old[i]
650 } else {
651 items_av[i - old.len()]
652 }
653 },
654 ));
655 arena_ctx.apply_mutation_parts_write_through(
656 &mut message.context,
657 parts,
658 new_av,
659 |ctx| append_owned(ctx, parts, items),
660 );
661 }
662 None => {
665 append_owned(&mut message.context, parts, items);
666 arena_ctx.refresh_for_path_parts(&message.context, parts);
667 }
668 }
669 debug!("Map: Appended to {path_arc}");
670 true
671}
672
673fn append_owned(context: &mut OwnedDataValue, parts: &[Arc<str>], items: Vec<OwnedDataValue>) {
677 match get_nested_value_parts_mut(context, parts) {
678 Some(OwnedDataValue::Array(existing)) => existing.extend(items),
679 _ => set_nested_value_parts(context, parts, OwnedDataValue::Array(items)),
680 }
681}
682
683fn apply_mapping_parts(
687 context: &mut OwnedDataValue,
688 parts: &[Arc<str>],
689 full_path: &str,
690 new_value: OwnedDataValue,
691) {
692 if parts.len() == 1 && matches!(full_path, "data" | "metadata" | "temp_data") {
693 merge_root_field(context, full_path, new_value);
694 } else {
695 set_nested_value_parts(context, parts, new_value);
696 }
697}
698
699fn merge_root_field(context: &mut OwnedDataValue, path: &str, new_value: OwnedDataValue) {
703 let OwnedDataValue::Object(ctx_pairs) = context else {
704 *context = wrap_root(path, new_value);
706 return;
707 };
708
709 let slot_idx = ctx_pairs.iter().position(|(k, _)| k == path);
710 match slot_idx {
711 Some(idx) => {
712 let slot = &mut ctx_pairs[idx].1;
713 match (slot, new_value) {
714 (OwnedDataValue::Object(existing), OwnedDataValue::Object(new_pairs)) => {
715 for (k, v) in new_pairs {
716 if let Some(s) = existing.iter_mut().find(|(ek, _)| ek == &k) {
717 s.1 = v;
718 } else {
719 existing.push((k, v));
720 }
721 }
722 }
723 (slot, new) => *slot = new,
724 }
725 }
726 None => {
727 ctx_pairs.push((path.to_string(), new_value));
728 }
729 }
730}
731
732fn wrap_root(path: &str, value: OwnedDataValue) -> OwnedDataValue {
735 OwnedDataValue::Object(vec![(path.to_string(), value)])
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741 use crate::engine::message::Message;
742 use crate::engine::utils::set_nested_value;
743 use serde_json::json;
744
745 fn dv(v: serde_json::Value) -> OwnedDataValue {
746 OwnedDataValue::from(&v)
747 }
748
749 fn fresh_message(initial: serde_json::Value) -> Message {
750 Message::builder().data(dv(initial)).build()
752 }
753
754 #[test]
755 fn test_map_config_from_json() {
756 let input = json!({
757 "mappings": [
758 { "path": "data.field1", "logic": {"var": "data.source"} },
759 { "path": "data.field2", "logic": "static_value" }
760 ]
761 });
762
763 let config = MapConfig::from_json(&input).unwrap();
764 assert_eq!(config.mappings.len(), 2);
765 assert_eq!(config.mappings[0].path.as_json(), &json!("data.field1"));
766 assert_eq!(config.mappings[1].path.as_json(), &json!("data.field2"));
767 }
768
769 #[test]
770 fn test_map_config_missing_mappings() {
771 assert!(MapConfig::from_json(&json!({})).is_err());
772 }
773
774 #[test]
775 fn test_map_config_invalid_mappings() {
776 assert!(MapConfig::from_json(&json!({"mappings": "not_an_array"})).is_err());
777 }
778
779 #[test]
780 fn test_map_config_missing_path() {
781 let input = json!({"mappings": [{"logic": {"var": "data.source"}}]});
782 assert!(MapConfig::from_json(&input).is_err());
783 }
784
785 #[test]
786 fn test_map_config_missing_logic() {
787 let input = json!({"mappings": [{"path": "data.field1"}]});
788 assert!(MapConfig::from_json(&input).is_err());
789 }
790
791 fn compile_mappings(engine: &Arc<Engine>, config: &mut MapConfig) {
795 for mapping in &mut config.mappings {
796 mapping.compiled_logic = Some(engine.compile_arc(&mapping.logic).unwrap());
797 }
798 }
799
800 #[test]
801 fn test_map_metadata_assignment() {
802 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
803
804 let mut message = fresh_message(json!({
805 "SwiftMT": { "message_type": "103" }
806 }));
807
808 let mut config = MapConfig {
809 mappings: vec![MapMapping {
810 path: PathTemplate::from("metadata.SwiftMT.message_type"),
811 logic: json!({"var": "data.SwiftMT.message_type"}),
812 ..Default::default()
813 }],
814 };
815 compile_mappings(&engine, &mut config);
816
817 let result = config.execute(&mut message, &engine);
818 assert!(result.is_ok());
819
820 let (outcome, changes) = result.unwrap();
821 assert_eq!(outcome, TaskOutcome::Success);
822 assert_eq!(changes.len(), 1);
823
824 assert_eq!(
825 message.context["metadata"]
826 .get("SwiftMT")
827 .and_then(|v| v.get("message_type")),
828 Some(&dv(json!("103")))
829 );
830 }
831
832 #[test]
833 fn test_map_null_values_skip_assignment() {
834 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
835
836 let mut message = fresh_message(json!({ "existing_field": "should_remain" }));
837 set_nested_value(
838 &mut message.context,
839 "metadata",
840 dv(json!({"existing_meta": "should_remain"})),
841 );
842
843 let mut config = MapConfig {
844 mappings: vec![
845 MapMapping {
846 path: PathTemplate::from("data.new_field"),
847 logic: json!({"var": "data.non_existent_field"}),
848 ..Default::default()
849 },
850 MapMapping {
851 path: PathTemplate::from("metadata.new_meta"),
852 logic: json!({"var": "data.another_non_existent"}),
853 ..Default::default()
854 },
855 MapMapping {
856 path: PathTemplate::from("data.actual_field"),
857 logic: json!("actual_value"),
858 ..Default::default()
859 },
860 ],
861 };
862 compile_mappings(&engine, &mut config);
863
864 let result = config.execute(&mut message, &engine);
865 assert!(result.is_ok());
866
867 let (outcome, changes) = result.unwrap();
868 assert_eq!(outcome, TaskOutcome::Success);
869 assert_eq!(changes.len(), 1);
870 assert_eq!(changes[0].path.as_ref(), "data.actual_field");
871
872 assert_eq!(message.context["data"].get("new_field"), None);
873 assert_eq!(message.context["metadata"].get("new_meta"), None);
874
875 assert_eq!(
876 message.context["data"].get("existing_field"),
877 Some(&dv(json!("should_remain")))
878 );
879 assert_eq!(
880 message.context["metadata"].get("existing_meta"),
881 Some(&dv(json!("should_remain")))
882 );
883
884 assert_eq!(
885 message.context["data"].get("actual_field"),
886 Some(&dv(json!("actual_value")))
887 );
888 }
889
890 #[test]
891 fn test_map_execute_with_trace_captures_context_snapshots() {
892 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
893
894 let mut message = fresh_message(json!({ "first": "Alice", "last": "Smith" }));
895
896 let mut config = MapConfig {
897 mappings: vec![
898 MapMapping {
899 path: PathTemplate::from("data.full_name"),
900 logic: json!({"cat": [{"var": "data.first"}, " ", {"var": "data.last"}]}),
901 ..Default::default()
902 },
903 MapMapping {
904 path: PathTemplate::from("data.greeting"),
905 logic: json!({"cat": ["Hello, ", {"var": "data.full_name"}]}),
906 ..Default::default()
907 },
908 ],
909 };
910 compile_mappings(&engine, &mut config);
911
912 let mut context_snapshots: Vec<Value> = Vec::new();
913 let result = with_arena(|arena| {
914 let mut arena_ctx = ArenaContext::from_owned(&message.context, arena);
915 config.execute_in_arena(
916 &mut message,
917 &mut arena_ctx,
918 &engine,
919 Some(&mut context_snapshots),
920 )
921 });
922 assert!(result.is_ok());
923
924 let (outcome, changes) = result.unwrap();
925 assert_eq!(outcome, TaskOutcome::Success);
926 assert_eq!(changes.len(), 2);
927 assert_eq!(context_snapshots.len(), 2);
928
929 assert!(context_snapshots[0]["data"].get("full_name").is_none());
931 assert_eq!(
932 context_snapshots[1]["data"].get("full_name"),
933 Some(&json!("Alice Smith"))
934 );
935 }
936
937 fn parse(mapping: serde_json::Value) -> std::result::Result<MapMapping, String> {
938 MapMapping::deserialize(&mapping).map_err(|e| e.to_string())
939 }
940
941 #[test]
942 fn the_rules_between_mapping_keys() {
943 let unset = parse(json!({"path": "temp_data.x", "unset": true})).unwrap();
944 assert!(unset.unset);
945 assert_eq!(unset.logic, Value::Null);
946 assert_eq!(unset.on_null, OnNull::Skip);
947
948 let on_null =
949 parse(json!({"path": "temp_data.x", "logic": 1, "on_null": "unset"})).unwrap();
950 assert_eq!(on_null.on_null, OnNull::Unset);
951
952 assert!(parse(json!({"path": "data.x", "logic": null})).is_ok());
954
955 for (bad, names) in [
956 (json!({"path": "data.x"}), "`logic`"),
957 (
958 json!({"path": "data.x", "logic": 1, "unset": true}),
959 "`unset`",
960 ),
961 (
962 json!({"path": "data.x", "logic": null, "unset": true}),
963 "`unset`",
964 ),
965 (
966 json!({"path": "data.x", "unset": true, "on_null": "skip"}),
967 "`on_null`",
968 ),
969 (json!({"path": "data", "unset": true}), "`path`"),
970 (json!({"path": "#temp_data", "unset": true}), "`path`"),
971 (
972 json!({"path": "metadata", "logic": 1, "on_null": "unset"}),
973 "`path`",
974 ),
975 (
976 json!({"path": "data.x", "unset": true, "mode": "append"}),
977 "`mode`",
978 ),
979 (
980 json!({"path": "data", "logic": 1, "mode": "append"}),
981 "`path`",
982 ),
983 (
984 json!({"path": "temp_data", "logic": [1], "mode": "extend"}),
985 "`path`",
986 ),
987 ] {
988 let err = parse(bad.clone()).expect_err(&format!("{bad} should be refused"));
989 assert!(err.contains(&format!("mapping {names}")), "{bad}: {err}");
990 }
991
992 let append = parse(json!({"path": "data.log", "logic": 1, "mode": "append"})).unwrap();
993 assert_eq!(append.mode, MapMode::Append);
994 assert_eq!(
995 parse(json!({"path": "data.x", "logic": 1})).unwrap().mode,
996 MapMode::Set
997 );
998
999 assert!(parse(json!({"path": "data", "logic": {"a": 1}})).is_ok());
1002 assert!(parse(json!({"path": "data", "logic": {"a": 1}, "mode": "set"})).is_ok());
1003 assert!(parse(json!({"path": "data", "logic": 1, "on_null": "skip"})).is_ok());
1004 }
1005
1006 #[test]
1007 fn from_json_applies_the_same_rules() {
1008 assert!(
1009 MapConfig::from_json(&json!({"mappings": [{"path": "data.x", "unset": true}]}))
1010 .unwrap()
1011 .mappings[0]
1012 .unset
1013 );
1014 assert!(
1015 MapConfig::from_json(
1016 &json!({"mappings": [{"path": "data.x", "logic": 1, "unset": true}]})
1017 )
1018 .is_err()
1019 );
1020 }
1021
1022 #[test]
1026 fn removals_keep_the_arena_cache_in_step() {
1027 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
1028 let mut message = fresh_message(json!({
1029 "a": {"b": {"c": 1, "d": 2}},
1030 "items": [1, 2, 3],
1031 "top": 1,
1032 "7": "hashed"
1033 }));
1034 set_nested_value(&mut message.context, "extra_root", dv(json!({"k": 1})));
1035
1036 let mut config = MapConfig {
1037 mappings: [
1038 "data.a.b.c",
1039 "data.items.1",
1040 "data.top",
1041 "data.#7",
1042 "extra_root",
1043 "data.not.there",
1044 ]
1045 .into_iter()
1046 .map(|path| MapMapping {
1047 path: PathTemplate::from(path),
1048 unset: true,
1049 ..Default::default()
1050 })
1051 .chain([MapMapping {
1052 path: PathTemplate::from("data.a.b.d"),
1053 logic: json!({"var": "data.nope"}),
1054 on_null: OnNull::Unset,
1055 ..Default::default()
1056 }])
1057 .collect(),
1058 };
1059 compile_mappings(&engine, &mut config);
1060
1061 let (outcome, changes) = config.execute(&mut message, &engine).unwrap();
1062 assert_eq!(outcome, TaskOutcome::Success);
1063 assert_eq!(
1064 Value::from(&message.context["data"]),
1065 json!({"a": {"b": {}}, "items": [1, 3]})
1066 );
1067 assert!(message.context.get("extra_root").is_none());
1068 assert_eq!(changes.len(), 6, "the absent path records nothing");
1069 assert!(changes.iter().all(|c| c.removed));
1070 }
1071
1072 #[test]
1077 fn appends_keep_the_arena_cache_in_step() {
1078 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
1079 let mut message = fresh_message(json!({
1080 "nul": null,
1081 "a": {"b": {"list": [1]}},
1082 "nested": [[1]],
1083 "top": 5
1084 }));
1085 let m = |path: &str, logic: Value, mode: MapMode| MapMapping {
1086 path: PathTemplate::from(path),
1087 logic,
1088 mode,
1089 ..Default::default()
1090 };
1091 let mut config = MapConfig {
1092 mappings: vec![
1093 m("data.log", json!({"id": 0}), MapMode::Append),
1094 m("data.log", json!({"id": 1}), MapMode::Append),
1095 m("data.log", json!([2, 3]), MapMode::Extend),
1096 m("data.seen", json!({"var": "data.log"}), MapMode::Set),
1097 m("data.log", json!([4]), MapMode::Append),
1099 m("data.nul", json!("x"), MapMode::Append),
1100 m("data.a.b.list", json!(2), MapMode::Append),
1101 m(
1102 "data.a.b.seen",
1103 json!({"var": "data.a.b.list"}),
1104 MapMode::Set,
1105 ),
1106 m("data.nested.0", json!(2), MapMode::Append),
1107 m(
1108 "data.nested_seen",
1109 json!({"var": "data.nested"}),
1110 MapMode::Set,
1111 ),
1112 m("data.empty", json!([]), MapMode::Extend),
1113 m(
1114 "data.skipped",
1115 json!({"var": "data.missing"}),
1116 MapMode::Append,
1117 ),
1118 ],
1119 };
1120 compile_mappings(&engine, &mut config);
1121
1122 with_arena(|arena| {
1123 let mut arena_ctx = ArenaContext::from_owned(&message.context, arena);
1124 let (outcome, changes) = config
1125 .execute_in_arena(&mut message, &mut arena_ctx, &engine, None)
1126 .unwrap();
1127 assert_eq!(outcome, TaskOutcome::Success);
1128 assert_eq!(
1129 arena_ctx.as_data_value().to_owned(),
1130 ArenaContext::from_owned(&message.context, arena)
1131 .as_data_value()
1132 .to_owned(),
1133 "the cache matches a rebuild"
1134 );
1135 let paths: Vec<&str> = changes
1136 .iter()
1137 .filter(|c| c.path.starts_with("data.log"))
1138 .map(|c| c.path.as_ref())
1139 .collect();
1140 assert_eq!(
1141 paths,
1142 [
1143 "data.log.0",
1144 "data.log.1",
1145 "data.log.2",
1146 "data.log.3",
1147 "data.log.4"
1148 ]
1149 );
1150 assert!(changes.iter().all(|c| c.old_value == OwnedDataValue::Null));
1151 });
1152
1153 let log = json!([{"id": 0}, {"id": 1}, 2, 3, [4]]);
1154 assert_eq!(
1155 Value::from(&message.context["data"]),
1156 json!({
1157 "nul": ["x"],
1158 "a": {"b": {"list": [1, 2], "seen": [1, 2]}},
1159 "nested": [[1, 2]],
1160 "top": 5,
1161 "log": log,
1162 "seen": [{"id": 0}, {"id": 1}, 2, 3],
1163 "nested_seen": [[1, 2]],
1164 "empty": []
1165 })
1166 );
1167 }
1168
1169 #[test]
1170 fn an_append_to_a_non_array_or_an_extend_of_a_non_array_fails_the_mapping() {
1171 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
1172 for (mapping, data) in [
1173 (
1174 json!({"path": "data.top", "logic": 1, "mode": "append"}),
1175 json!({"top": 5}),
1176 ),
1177 (
1178 json!({"path": "data.obj", "logic": [1], "mode": "extend"}),
1179 json!({"obj": {"k": 1}}),
1180 ),
1181 (
1182 json!({"path": "data.log", "logic": 1, "mode": "extend"}),
1183 json!({"log": []}),
1184 ),
1185 (
1186 json!({"path": {"cat": ["da", "ta"]}, "logic": 1, "mode": "append"}),
1187 json!({}),
1188 ),
1189 ] {
1190 let mut message = fresh_message(data.clone());
1191 let mut config = MapConfig::from_json(&json!({"mappings": [mapping]})).unwrap();
1192 compile_mappings(&engine, &mut config);
1193 for m in &mut config.mappings {
1194 m.path
1195 .compile(
1196 &crate::engine::functions::TemplateCompiler::new(engine.clone()),
1197 "path",
1198 )
1199 .unwrap();
1200 }
1201 let (outcome, changes) = config.execute(&mut message, &engine).unwrap();
1202 assert_eq!(outcome, TaskOutcome::Status(500), "{mapping}");
1203 assert!(changes.is_empty(), "{mapping}");
1204 assert_eq!(Value::from(&message.context["data"]), data, "{mapping}");
1205 }
1206 }
1207
1208 #[test]
1209 fn test_map_multiple_fields_including_metadata() {
1210 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
1211
1212 let mut message = fresh_message(json!({
1213 "ISO20022_MX": {
1214 "document": {
1215 "TxInf": {
1216 "OrgnlGrpInf": { "OrgnlMsgNmId": "pacs.008.001.08" }
1217 }
1218 }
1219 },
1220 "SwiftMT": { "message_type": "103" }
1221 }));
1222
1223 let mut config = MapConfig {
1224 mappings: vec![
1225 MapMapping {
1226 path: PathTemplate::from("data.SwiftMT.message_type"),
1227 logic: json!("103"),
1228 ..Default::default()
1229 },
1230 MapMapping {
1231 path: PathTemplate::from("metadata.SwiftMT.message_type"),
1232 logic: json!({"var": "data.SwiftMT.message_type"}),
1233 ..Default::default()
1234 },
1235 MapMapping {
1236 path: PathTemplate::from("temp_data.original_msg_type"),
1237 logic: json!({"var": "data.ISO20022_MX.document.TxInf.OrgnlGrpInf.OrgnlMsgNmId"}),
1238 ..Default::default()
1239 },
1240 ],
1241 };
1242 compile_mappings(&engine, &mut config);
1243
1244 let result = config.execute(&mut message, &engine);
1245 assert!(result.is_ok());
1246
1247 let (outcome, changes) = result.unwrap();
1248 assert_eq!(outcome, TaskOutcome::Success);
1249 assert_eq!(changes.len(), 3);
1250
1251 assert_eq!(
1252 message.context["data"]
1253 .get("SwiftMT")
1254 .and_then(|v| v.get("message_type")),
1255 Some(&dv(json!("103")))
1256 );
1257 assert_eq!(
1258 message.context["metadata"]
1259 .get("SwiftMT")
1260 .and_then(|v| v.get("message_type")),
1261 Some(&dv(json!("103")))
1262 );
1263 assert_eq!(
1264 message.context["temp_data"].get("original_msg_type"),
1265 Some(&dv(json!("pacs.008.001.08")))
1266 );
1267 }
1268}