ehds_logging/transformation.rs
1// SPDX-FileCopyrightText: Cadasto B.V.
2// SPDX-License-Identifier: BUSL-1.1
3
4//! The record of a semantic transformation an access applied.
5//!
6//! The eHealth Network General Guidelines (Release 3.1, Art 6(3)) ask that
7//! "Entities performing semantic transformations, such as `NCPeHs` in
8//! `MyHealth@EU`, shall enable logging sufficient for examination of events
9//! potentially leading to patient safety issues." A [`Transformation`] is
10//! that log for one mapping between openEHR and the European exchange
11//! format: which way it ran, the Art 14(1) category, the mapping and
12//! terminology versions in force, and how many elements it transcoded, left
13//! untranscoded (eHealth Network Guidelines on Patient Summary Art 10(7)) or
14//! dropped, each counted by its element path.
15//!
16//! A path names where an element sits in its model, such as the FHIR element
17//! path `AllergyIntolerance.code` or the openEHR RM path `/`, and never what
18//! the element holds: a transformation carries no clinical value, no code
19//! value and no patient identifier. It rides on the access record of the
20//! access that applied it, so the two are one record.
21
22use std::collections::BTreeMap;
23
24use crate::category::Category;
25
26/// Which way a transformation ran.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Direction {
29 /// From openEHR to the exchange format: data provided (Regulation (EU)
30 /// 2025/327 Annex II 2.1).
31 Provided,
32 /// From the exchange format to openEHR: data received (Annex II 2.2).
33 Received,
34}
35
36impl Direction {
37 /// The direction as a record writes it: `provided` or `received`.
38 #[must_use]
39 pub const fn code(self) -> &'static str {
40 match self {
41 Self::Provided => "provided",
42 Self::Received => "received",
43 }
44 }
45}
46
47/// The mapping a transformation ran, by the names and versions it was
48/// compiled from.
49#[derive(Debug, Clone, Default, PartialEq, Eq)]
50pub struct Mapping {
51 /// The FHIRconnect context mapping, or `None` when no mapping covered
52 /// the data.
53 pub context: Option<String>,
54 /// The openEHR template id the mapping maps.
55 pub template: String,
56 /// The canonical URL of the profile the mapping maps to, when one ran.
57 pub profile: Option<String>,
58 /// The file names of the model and context mappings.
59 pub files: Vec<String>,
60 /// The file name of the FHIR package the received data were held to,
61 /// for a received document.
62 pub package: Option<String>,
63}
64
65/// Elements of one fate, counted, with the count at each path that names
66/// one.
67///
68/// An element whose path cannot be told without quoting a value is counted
69/// with no path, so [`Elements::count`] is at least the sum of the paths'
70/// counts.
71#[derive(Debug, Clone, Default, PartialEq, Eq)]
72pub struct Elements {
73 count: usize,
74 paths: BTreeMap<String, usize>,
75}
76
77impl Elements {
78 /// The elements `paths` counts at each path, and `count` in all; a
79 /// `count` below the sum of the paths' counts is raised to it.
80 #[must_use]
81 pub fn new(count: usize, paths: BTreeMap<String, usize>) -> Self {
82 let named = paths
83 .values()
84 .fold(0_usize, |sum, held| sum.saturating_add(*held));
85 Self {
86 count: count.max(named),
87 paths,
88 }
89 }
90
91 /// Counts one element, at `path` when it names one.
92 pub fn add(&mut self, path: Option<&str>) {
93 self.count = self.count.saturating_add(1);
94 if let Some(path) = path {
95 let held = self.paths.entry(path.to_owned()).or_default();
96 *held = held.saturating_add(1);
97 }
98 }
99
100 /// Adds every element `other` counts.
101 pub fn extend(&mut self, other: &Self) {
102 self.count = self.count.saturating_add(other.count);
103 for (path, count) in &other.paths {
104 let held = self.paths.entry(path.clone()).or_default();
105 *held = held.saturating_add(*count);
106 }
107 }
108
109 /// The number of elements.
110 #[must_use]
111 pub const fn count(&self) -> usize {
112 self.count
113 }
114
115 /// The count at each path, in path order.
116 #[must_use]
117 pub const fn paths(&self) -> &BTreeMap<String, usize> {
118 &self.paths
119 }
120}
121
122/// One semantic transformation an access applied.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct Transformation {
125 /// Which way it ran.
126 pub direction: Direction,
127 /// The Art 14(1) category of the data it transformed, when it names one.
128 pub category: Option<Category>,
129 /// The mapping it ran.
130 pub mapping: Mapping,
131 /// The terminology releases and code system versions in force, each as
132 /// `<name>|<version>`.
133 pub terminology: Vec<String>,
134 /// The coded elements carried in a preferred code system.
135 pub transcoded: Elements,
136 /// The coded elements carried in no preferred code system, as text or in
137 /// another system (eHealth Network Guidelines on Patient Summary Art
138 /// 10(7)).
139 pub untranscoded: Elements,
140 /// The elements the mapping did not carry.
141 pub dropped: Elements,
142}