Skip to main content

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}