1use crate::engine::error::{DataflowError, Result};
19use crate::engine::executor::{ArenaContext, with_arena};
20use crate::engine::functions::path_template::{ContextRoot, ParamCtx, PathTemplate, ResolvedPath};
21use crate::engine::message::{Change, Message};
22use crate::engine::task_outcome::TaskOutcome;
23use crate::engine::utils::{get_nested_value_parts, set_nested_value_parts, strip_hash_prefix};
24use datalogic_rs::{Engine, Logic};
25use datavalue::OwnedDataValue;
26use log::{debug, error};
27use serde::de::Error as _;
28use serde::{Deserialize, Deserializer};
29use serde_json::Value;
30use std::sync::Arc;
31
32#[derive(Debug, Clone, Deserialize)]
34pub struct MapConfig {
35 pub mappings: Vec<MapMapping>,
37}
38
39#[derive(Debug, Clone, Default)]
53pub struct MapMapping {
54 pub path: PathTemplate<ContextRoot>,
64
65 pub logic: Value,
70
71 pub unset: bool,
80
81 pub on_null: OnNull,
84
85 #[doc(hidden)]
90 pub compiled_logic: Option<Arc<Logic>>,
91}
92
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
98#[serde(rename_all = "lowercase")]
99#[non_exhaustive]
100pub enum OnNull {
101 #[default]
106 Skip,
107 Unset,
110}
111
112#[derive(Deserialize)]
118pub(crate) struct AuthoredMapping {
119 path: PathTemplate<ContextRoot>,
120 #[serde(default, deserialize_with = "present")]
123 logic: Option<Value>,
124 #[serde(default)]
125 unset: bool,
126 #[serde(default)]
129 on_null: Option<OnNull>,
130}
131
132fn present<'de, D: Deserializer<'de>>(d: D) -> std::result::Result<Option<Value>, D::Error> {
135 Value::deserialize(d).map(Some)
136}
137
138pub(crate) struct MappingProblem {
140 pub(crate) field: &'static str,
142 pub(crate) message: &'static str,
143}
144
145impl AuthoredMapping {
146 pub(crate) fn problem(&self) -> Option<MappingProblem> {
148 let problem = |field, message| Some(MappingProblem { field, message });
149 match (&self.logic, self.unset) {
150 (None, false) => {
151 return problem(
152 "logic",
153 "a mapping needs `logic`, or `\"unset\": true` to remove the path",
154 );
155 }
156 (Some(_), true) => {
157 return problem(
158 "unset",
159 "`unset` removes the path and takes no `logic` — to remove it only \
160 when the result is null, keep `logic` and use `\"on_null\": \"unset\"`",
161 );
162 }
163 _ => {}
164 }
165 if self.logic.is_none() && self.on_null.is_some() {
166 return problem(
167 "on_null",
168 "`on_null` says what a null `logic` result does, and this mapping has no `logic`",
169 );
170 }
171 let removes = self.unset || self.on_null == Some(OnNull::Unset);
172 if removes
173 && let Some(path) = self.path.as_json().as_str()
174 && is_context_root(&path.split('.').collect::<Vec<_>>())
175 {
176 return problem(
177 "path",
178 "a context root (`data`, `metadata`, `temp_data`) cannot be removed — \
179 remove the keys under it instead",
180 );
181 }
182 None
183 }
184
185 fn into_mapping(self) -> MapMapping {
186 MapMapping {
187 path: self.path,
188 logic: self.logic.unwrap_or(Value::Null),
189 unset: self.unset,
190 on_null: self.on_null.unwrap_or_default(),
191 compiled_logic: None,
192 }
193 }
194}
195
196impl<'de> Deserialize<'de> for MapMapping {
197 fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
198 let authored = AuthoredMapping::deserialize(d)?;
199 if let Some(problem) = authored.problem() {
200 return Err(D::Error::custom(format_args!(
201 "mapping `{}`: {}",
202 problem.field, problem.message
203 )));
204 }
205 Ok(authored.into_mapping())
206 }
207}
208
209fn is_context_root<P: AsRef<str>>(parts: &[P]) -> bool {
212 matches!(parts, [only] if matches!(strip_hash_prefix(only.as_ref()), "data" | "metadata" | "temp_data"))
213}
214
215impl MapMapping {
216 pub(crate) fn describe_path(&self) -> String {
223 self.path
224 .constant_path()
225 .map(str::to_string)
226 .unwrap_or_else(|| self.path.as_json().to_string())
227 }
228}
229
230impl MapConfig {
231 pub fn from_json(input: &Value) -> Result<Self> {
233 let mappings = input.get("mappings").ok_or_else(|| {
234 DataflowError::Validation("Missing 'mappings' array in input".to_string())
235 })?;
236
237 let mappings_arr = mappings
238 .as_array()
239 .ok_or_else(|| DataflowError::Validation("'mappings' must be an array".to_string()))?;
240
241 let mut parsed_mappings = Vec::new();
242
243 for mapping in mappings_arr {
244 if mapping.get("path").is_none() {
245 return Err(DataflowError::Validation(
246 "Missing 'path' in mapping".to_string(),
247 ));
248 }
249 let parsed = MapMapping::deserialize(mapping)
252 .map_err(|e| DataflowError::Validation(e.to_string()))?;
253 parsed_mappings.push(parsed);
254 }
255
256 Ok(Self {
257 mappings: parsed_mappings,
258 })
259 }
260
261 pub fn execute(
267 &self,
268 message: &mut Message,
269 engine: &Arc<Engine>,
270 ) -> Result<(TaskOutcome, Vec<Change>)> {
271 with_arena(|arena| {
275 let mut arena_ctx = ArenaContext::from_owned(&message.context, arena);
276 self.execute_in_arena(message, &mut arena_ctx, engine, None)
277 })
278 }
279
280 pub(crate) fn execute_in_arena<'arena>(
294 &'arena self,
295 message: &mut Message,
296 arena_ctx: &mut ArenaContext<'arena>,
297 engine: &Arc<Engine>,
298 mut mapping_snapshots: Option<&mut Vec<Value>>,
299 ) -> Result<(TaskOutcome, Vec<Change>)> {
300 let mut changes = if message.capture_changes {
303 Vec::with_capacity(self.mappings.len())
304 } else {
305 Vec::new()
306 };
307 let mut errors_encountered = false;
308
309 debug!("Map: Executing {} mappings", self.mappings.len());
310
311 let arena = arena_ctx.arena();
312 for mapping in &self.mappings {
313 debug!("Processing mapping to path: {}", mapping.describe_path());
314
315 if let Some(buf) = mapping_snapshots.as_deref_mut() {
319 buf.push(Value::from(&message.context));
320 }
321
322 if mapping.unset {
323 let ctx_av = arena_ctx.as_data_value();
326 let Some(resolved) = resolve_destination(mapping, engine, ctx_av, arena) else {
327 errors_encountered = true;
328 continue;
329 };
330 if !remove_at(message, arena_ctx, &resolved, &mut changes) {
331 errors_encountered = true;
332 }
333 continue;
334 }
335
336 let compiled_logic = match &mapping.compiled_logic {
341 Some(logic) => logic,
342 None => {
343 error!(
344 "Map: Logic not compiled for mapping to {}",
345 mapping.describe_path()
346 );
347 errors_encountered = true;
348 continue;
349 }
350 };
351
352 let ctx_av = arena_ctx.as_data_value();
353 let result_av = match engine.evaluate(compiled_logic, ctx_av, arena) {
354 Ok(av) => av,
355 Err(e) => {
356 error!(
357 "Map: Error evaluating logic for path {}: {:?}",
358 mapping.describe_path(),
359 e
360 );
361 errors_encountered = true;
362 continue;
363 }
364 };
365
366 let transformed_value = result_av.to_owned();
367 debug!(
368 "Map: Evaluated logic for path {} resulted in: {:?}",
369 mapping.describe_path(),
370 transformed_value
371 );
372
373 if matches!(transformed_value, OwnedDataValue::Null) {
374 match mapping.on_null {
375 OnNull::Skip => {
376 debug!(
377 "Map: Skipping mapping for path {} as result is null",
378 mapping.describe_path()
379 );
380 }
381 OnNull::Unset => {
382 let Some(resolved) = resolve_destination(mapping, engine, ctx_av, arena)
383 else {
384 errors_encountered = true;
385 continue;
386 };
387 if !remove_at(message, arena_ctx, &resolved, &mut changes) {
388 errors_encountered = true;
389 }
390 }
391 }
392 continue;
393 }
394
395 let Some(resolved) = resolve_destination(mapping, engine, ctx_av, arena) else {
401 errors_encountered = true;
402 continue;
403 };
404 let (path_arc, parts) = (&resolved.0, &*resolved.1);
405
406 if message.capture_changes {
407 let old_value = get_nested_value_parts(&message.context, parts)
411 .cloned()
412 .unwrap_or(OwnedDataValue::Null);
413 let new_value = transformed_value.clone();
414
415 changes.push(Change {
416 path: Arc::clone(path_arc),
417 old_value,
418 new_value,
419 removed: false,
420 });
421 }
422 arena_ctx.apply_mutation_parts_write_through(
427 &mut message.context,
428 parts,
429 *result_av,
430 |ctx| {
431 apply_mapping_parts(ctx, parts, path_arc, transformed_value);
432 },
433 );
434 debug!("Successfully mapped to path: {path_arc}");
435 }
436
437 let outcome = if errors_encountered {
438 TaskOutcome::Status(500)
439 } else {
440 TaskOutcome::Success
441 };
442 Ok((outcome, changes))
443 }
444}
445
446fn resolve_destination<'a>(
451 mapping: &'a MapMapping,
452 engine: &'a Engine,
453 ctx_av: datavalue::DataValue<'a>,
454 arena: &'a datalogic_rs::bumpalo::Bump,
455) -> Option<std::borrow::Cow<'a, ResolvedPath>> {
456 match mapping
457 .path
458 .resolve_in_arena(ParamCtx::new(engine, ctx_av, arena))
459 {
460 Ok(pair) => Some(pair),
461 Err(e) => {
462 error!(
463 "Map: Error resolving destination path {}: {:?}",
464 mapping.describe_path(),
465 e
466 );
467 None
468 }
469 }
470}
471
472fn remove_at(
479 message: &mut Message,
480 arena_ctx: &mut ArenaContext<'_>,
481 resolved: &ResolvedPath,
482 changes: &mut Vec<Change>,
483) -> bool {
484 let (path_arc, parts) = (&resolved.0, &*resolved.1);
485 if is_context_root(parts) {
486 error!("Map: Refusing to remove context root {path_arc}");
487 return false;
488 }
489 match arena_ctx.apply_removal_parts(&mut message.context, parts) {
490 Some(old_value) => {
491 if message.capture_changes {
492 changes.push(Change {
493 path: Arc::clone(path_arc),
494 old_value,
495 new_value: OwnedDataValue::Null,
496 removed: true,
497 });
498 }
499 debug!("Map: Removed {path_arc}");
500 }
501 None => debug!("Map: Nothing to remove at {path_arc}"),
502 }
503 true
504}
505
506fn apply_mapping_parts(
510 context: &mut OwnedDataValue,
511 parts: &[Arc<str>],
512 full_path: &str,
513 new_value: OwnedDataValue,
514) {
515 if parts.len() == 1 && matches!(full_path, "data" | "metadata" | "temp_data") {
516 merge_root_field(context, full_path, new_value);
517 } else {
518 set_nested_value_parts(context, parts, new_value);
519 }
520}
521
522fn merge_root_field(context: &mut OwnedDataValue, path: &str, new_value: OwnedDataValue) {
526 let OwnedDataValue::Object(ctx_pairs) = context else {
527 *context = wrap_root(path, new_value);
529 return;
530 };
531
532 let slot_idx = ctx_pairs.iter().position(|(k, _)| k == path);
533 match slot_idx {
534 Some(idx) => {
535 let slot = &mut ctx_pairs[idx].1;
536 match (slot, new_value) {
537 (OwnedDataValue::Object(existing), OwnedDataValue::Object(new_pairs)) => {
538 for (k, v) in new_pairs {
539 if let Some(s) = existing.iter_mut().find(|(ek, _)| ek == &k) {
540 s.1 = v;
541 } else {
542 existing.push((k, v));
543 }
544 }
545 }
546 (slot, new) => *slot = new,
547 }
548 }
549 None => {
550 ctx_pairs.push((path.to_string(), new_value));
551 }
552 }
553}
554
555fn wrap_root(path: &str, value: OwnedDataValue) -> OwnedDataValue {
558 OwnedDataValue::Object(vec![(path.to_string(), value)])
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564 use crate::engine::message::Message;
565 use crate::engine::utils::set_nested_value;
566 use serde_json::json;
567
568 fn dv(v: serde_json::Value) -> OwnedDataValue {
569 OwnedDataValue::from(&v)
570 }
571
572 fn fresh_message(initial: serde_json::Value) -> Message {
573 Message::builder().data(dv(initial)).build()
575 }
576
577 #[test]
578 fn test_map_config_from_json() {
579 let input = json!({
580 "mappings": [
581 { "path": "data.field1", "logic": {"var": "data.source"} },
582 { "path": "data.field2", "logic": "static_value" }
583 ]
584 });
585
586 let config = MapConfig::from_json(&input).unwrap();
587 assert_eq!(config.mappings.len(), 2);
588 assert_eq!(config.mappings[0].path.as_json(), &json!("data.field1"));
589 assert_eq!(config.mappings[1].path.as_json(), &json!("data.field2"));
590 }
591
592 #[test]
593 fn test_map_config_missing_mappings() {
594 assert!(MapConfig::from_json(&json!({})).is_err());
595 }
596
597 #[test]
598 fn test_map_config_invalid_mappings() {
599 assert!(MapConfig::from_json(&json!({"mappings": "not_an_array"})).is_err());
600 }
601
602 #[test]
603 fn test_map_config_missing_path() {
604 let input = json!({"mappings": [{"logic": {"var": "data.source"}}]});
605 assert!(MapConfig::from_json(&input).is_err());
606 }
607
608 #[test]
609 fn test_map_config_missing_logic() {
610 let input = json!({"mappings": [{"path": "data.field1"}]});
611 assert!(MapConfig::from_json(&input).is_err());
612 }
613
614 fn compile_mappings(engine: &Arc<Engine>, config: &mut MapConfig) {
618 for mapping in &mut config.mappings {
619 mapping.compiled_logic = Some(engine.compile_arc(&mapping.logic).unwrap());
620 }
621 }
622
623 #[test]
624 fn test_map_metadata_assignment() {
625 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
626
627 let mut message = fresh_message(json!({
628 "SwiftMT": { "message_type": "103" }
629 }));
630
631 let mut config = MapConfig {
632 mappings: vec![MapMapping {
633 path: PathTemplate::from("metadata.SwiftMT.message_type"),
634 logic: json!({"var": "data.SwiftMT.message_type"}),
635 ..Default::default()
636 }],
637 };
638 compile_mappings(&engine, &mut config);
639
640 let result = config.execute(&mut message, &engine);
641 assert!(result.is_ok());
642
643 let (outcome, changes) = result.unwrap();
644 assert_eq!(outcome, TaskOutcome::Success);
645 assert_eq!(changes.len(), 1);
646
647 assert_eq!(
648 message.context["metadata"]
649 .get("SwiftMT")
650 .and_then(|v| v.get("message_type")),
651 Some(&dv(json!("103")))
652 );
653 }
654
655 #[test]
656 fn test_map_null_values_skip_assignment() {
657 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
658
659 let mut message = fresh_message(json!({ "existing_field": "should_remain" }));
660 set_nested_value(
661 &mut message.context,
662 "metadata",
663 dv(json!({"existing_meta": "should_remain"})),
664 );
665
666 let mut config = MapConfig {
667 mappings: vec![
668 MapMapping {
669 path: PathTemplate::from("data.new_field"),
670 logic: json!({"var": "data.non_existent_field"}),
671 ..Default::default()
672 },
673 MapMapping {
674 path: PathTemplate::from("metadata.new_meta"),
675 logic: json!({"var": "data.another_non_existent"}),
676 ..Default::default()
677 },
678 MapMapping {
679 path: PathTemplate::from("data.actual_field"),
680 logic: json!("actual_value"),
681 ..Default::default()
682 },
683 ],
684 };
685 compile_mappings(&engine, &mut config);
686
687 let result = config.execute(&mut message, &engine);
688 assert!(result.is_ok());
689
690 let (outcome, changes) = result.unwrap();
691 assert_eq!(outcome, TaskOutcome::Success);
692 assert_eq!(changes.len(), 1);
693 assert_eq!(changes[0].path.as_ref(), "data.actual_field");
694
695 assert_eq!(message.context["data"].get("new_field"), None);
696 assert_eq!(message.context["metadata"].get("new_meta"), None);
697
698 assert_eq!(
699 message.context["data"].get("existing_field"),
700 Some(&dv(json!("should_remain")))
701 );
702 assert_eq!(
703 message.context["metadata"].get("existing_meta"),
704 Some(&dv(json!("should_remain")))
705 );
706
707 assert_eq!(
708 message.context["data"].get("actual_field"),
709 Some(&dv(json!("actual_value")))
710 );
711 }
712
713 #[test]
714 fn test_map_execute_with_trace_captures_context_snapshots() {
715 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
716
717 let mut message = fresh_message(json!({ "first": "Alice", "last": "Smith" }));
718
719 let mut config = MapConfig {
720 mappings: vec![
721 MapMapping {
722 path: PathTemplate::from("data.full_name"),
723 logic: json!({"cat": [{"var": "data.first"}, " ", {"var": "data.last"}]}),
724 ..Default::default()
725 },
726 MapMapping {
727 path: PathTemplate::from("data.greeting"),
728 logic: json!({"cat": ["Hello, ", {"var": "data.full_name"}]}),
729 ..Default::default()
730 },
731 ],
732 };
733 compile_mappings(&engine, &mut config);
734
735 let mut context_snapshots: Vec<Value> = Vec::new();
736 let result = with_arena(|arena| {
737 let mut arena_ctx = ArenaContext::from_owned(&message.context, arena);
738 config.execute_in_arena(
739 &mut message,
740 &mut arena_ctx,
741 &engine,
742 Some(&mut context_snapshots),
743 )
744 });
745 assert!(result.is_ok());
746
747 let (outcome, changes) = result.unwrap();
748 assert_eq!(outcome, TaskOutcome::Success);
749 assert_eq!(changes.len(), 2);
750 assert_eq!(context_snapshots.len(), 2);
751
752 assert!(context_snapshots[0]["data"].get("full_name").is_none());
754 assert_eq!(
755 context_snapshots[1]["data"].get("full_name"),
756 Some(&json!("Alice Smith"))
757 );
758 }
759
760 fn parse(mapping: serde_json::Value) -> std::result::Result<MapMapping, String> {
761 MapMapping::deserialize(&mapping).map_err(|e| e.to_string())
762 }
763
764 #[test]
765 fn the_rules_between_mapping_keys() {
766 let unset = parse(json!({"path": "temp_data.x", "unset": true})).unwrap();
767 assert!(unset.unset);
768 assert_eq!(unset.logic, Value::Null);
769 assert_eq!(unset.on_null, OnNull::Skip);
770
771 let on_null =
772 parse(json!({"path": "temp_data.x", "logic": 1, "on_null": "unset"})).unwrap();
773 assert_eq!(on_null.on_null, OnNull::Unset);
774
775 assert!(parse(json!({"path": "data.x", "logic": null})).is_ok());
777
778 for (bad, names) in [
779 (json!({"path": "data.x"}), "`logic`"),
780 (
781 json!({"path": "data.x", "logic": 1, "unset": true}),
782 "`unset`",
783 ),
784 (
785 json!({"path": "data.x", "logic": null, "unset": true}),
786 "`unset`",
787 ),
788 (
789 json!({"path": "data.x", "unset": true, "on_null": "skip"}),
790 "`on_null`",
791 ),
792 (json!({"path": "data", "unset": true}), "`path`"),
793 (json!({"path": "#temp_data", "unset": true}), "`path`"),
794 (
795 json!({"path": "metadata", "logic": 1, "on_null": "unset"}),
796 "`path`",
797 ),
798 ] {
799 let err = parse(bad.clone()).expect_err(&format!("{bad} should be refused"));
800 assert!(err.contains(&format!("mapping {names}")), "{bad}: {err}");
801 }
802
803 assert!(parse(json!({"path": "data", "logic": {"a": 1}})).is_ok());
805 assert!(parse(json!({"path": "data", "logic": 1, "on_null": "skip"})).is_ok());
806 }
807
808 #[test]
809 fn from_json_applies_the_same_rules() {
810 assert!(
811 MapConfig::from_json(&json!({"mappings": [{"path": "data.x", "unset": true}]}))
812 .unwrap()
813 .mappings[0]
814 .unset
815 );
816 assert!(
817 MapConfig::from_json(
818 &json!({"mappings": [{"path": "data.x", "logic": 1, "unset": true}]})
819 )
820 .is_err()
821 );
822 }
823
824 #[test]
828 fn removals_keep_the_arena_cache_in_step() {
829 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
830 let mut message = fresh_message(json!({
831 "a": {"b": {"c": 1, "d": 2}},
832 "items": [1, 2, 3],
833 "top": 1,
834 "7": "hashed"
835 }));
836 set_nested_value(&mut message.context, "extra_root", dv(json!({"k": 1})));
837
838 let mut config = MapConfig {
839 mappings: [
840 "data.a.b.c",
841 "data.items.1",
842 "data.top",
843 "data.#7",
844 "extra_root",
845 "data.not.there",
846 ]
847 .into_iter()
848 .map(|path| MapMapping {
849 path: PathTemplate::from(path),
850 unset: true,
851 ..Default::default()
852 })
853 .chain([MapMapping {
854 path: PathTemplate::from("data.a.b.d"),
855 logic: json!({"var": "data.nope"}),
856 on_null: OnNull::Unset,
857 ..Default::default()
858 }])
859 .collect(),
860 };
861 compile_mappings(&engine, &mut config);
862
863 let (outcome, changes) = config.execute(&mut message, &engine).unwrap();
864 assert_eq!(outcome, TaskOutcome::Success);
865 assert_eq!(
866 Value::from(&message.context["data"]),
867 json!({"a": {"b": {}}, "items": [1, 3]})
868 );
869 assert!(message.context.get("extra_root").is_none());
870 assert_eq!(changes.len(), 6, "the absent path records nothing");
871 assert!(changes.iter().all(|c| c.removed));
872 }
873
874 #[test]
875 fn test_map_multiple_fields_including_metadata() {
876 let engine = Arc::new(crate::engine::compiler::datalogic_engine_builder().build());
877
878 let mut message = fresh_message(json!({
879 "ISO20022_MX": {
880 "document": {
881 "TxInf": {
882 "OrgnlGrpInf": { "OrgnlMsgNmId": "pacs.008.001.08" }
883 }
884 }
885 },
886 "SwiftMT": { "message_type": "103" }
887 }));
888
889 let mut config = MapConfig {
890 mappings: vec![
891 MapMapping {
892 path: PathTemplate::from("data.SwiftMT.message_type"),
893 logic: json!("103"),
894 ..Default::default()
895 },
896 MapMapping {
897 path: PathTemplate::from("metadata.SwiftMT.message_type"),
898 logic: json!({"var": "data.SwiftMT.message_type"}),
899 ..Default::default()
900 },
901 MapMapping {
902 path: PathTemplate::from("temp_data.original_msg_type"),
903 logic: json!({"var": "data.ISO20022_MX.document.TxInf.OrgnlGrpInf.OrgnlMsgNmId"}),
904 ..Default::default()
905 },
906 ],
907 };
908 compile_mappings(&engine, &mut config);
909
910 let result = config.execute(&mut message, &engine);
911 assert!(result.is_ok());
912
913 let (outcome, changes) = result.unwrap();
914 assert_eq!(outcome, TaskOutcome::Success);
915 assert_eq!(changes.len(), 3);
916
917 assert_eq!(
918 message.context["data"]
919 .get("SwiftMT")
920 .and_then(|v| v.get("message_type")),
921 Some(&dv(json!("103")))
922 );
923 assert_eq!(
924 message.context["metadata"]
925 .get("SwiftMT")
926 .and_then(|v| v.get("message_type")),
927 Some(&dv(json!("103")))
928 );
929 assert_eq!(
930 message.context["temp_data"].get("original_msg_type"),
931 Some(&dv(json!("pacs.008.001.08")))
932 );
933 }
934}