Skip to main content

ehds_logging/
emergency.rs

1// SPDX-FileCopyrightText: Cadasto B.V.
2// SPDX-License-Identifier: BUSL-1.1
3
4//! Emergency access: an access the accessor declared, through its purpose of
5//! use, as made to protect the vital interests of the data subject.
6//!
7//! Regulation (EU) 2025/327 Art 11(5) lets a healthcare provider or health
8//! professional be granted access to data a natural person restricted
9//! under Art 8 "where necessary in order to protect the vital interests of
10//! the data subject", and asks that "such cases shall be logged in a clear
11//! and understandable format and shall be easily accessible for the data
12//! subject". The deployment declares which purposes of use assert such an
13//! access, as [`EmergencyPurposes`], and a record whose accessor declared
14//! one of them carries an [`Emergency`] naming the purposes that marked it.
15//!
16//! The mark states what the accessor asserted, never what the recording
17//! system inferred: it is read from the declared purposes alone. Whether
18//! restricted data were released is the decision of the system that holds
19//! them, and Art 8 keeps the fact of a restriction from the recording
20//! system, so the mark does not say that restricted data were reached.
21
22use std::collections::BTreeSet;
23
24use crate::record::Purpose;
25
26/// The purposes of use a deployment declares as asserting an emergency
27/// access (Art 11(5)), such as the HL7 v3 `ActReason` code `BTG`, "break
28/// the glass".
29///
30/// A declared purpose matches a purpose the accessor declared only when the
31/// two codes are equal and the two systems are equal, an absent system
32/// matching only an absent one.
33#[derive(Debug, Clone, Default, PartialEq, Eq)]
34pub struct EmergencyPurposes {
35    purposes: BTreeSet<Purpose>,
36}
37
38impl EmergencyPurposes {
39    /// The set of `purposes`.
40    ///
41    /// # Errors
42    ///
43    /// [`EmergencyError::EmptyCode`] for a purpose with an empty code, and
44    /// [`EmergencyError::EmptySystem`] for one that names an empty system.
45    pub fn declare(purposes: &[Purpose]) -> Result<Self, EmergencyError> {
46        for (index, purpose) in purposes.iter().enumerate() {
47            if purpose.code.trim().is_empty() {
48                return Err(EmergencyError::EmptyCode { index });
49            }
50            if purpose
51                .system
52                .as_deref()
53                .is_some_and(|system| system.trim().is_empty())
54            {
55                return Err(EmergencyError::EmptySystem { index });
56            }
57        }
58        Ok(Self {
59            purposes: purposes.iter().cloned().collect(),
60        })
61    }
62
63    /// Whether the set declares no purpose, so no access is marked.
64    #[must_use]
65    pub fn is_empty(&self) -> bool {
66        self.purposes.is_empty()
67    }
68
69    /// The declared purposes, in order.
70    pub fn purposes(&self) -> impl Iterator<Item = &Purpose> {
71        self.purposes.iter()
72    }
73
74    /// The mark of an access whose accessor declared `declared`: the
75    /// declared purposes this set holds, or `None` when it holds none of
76    /// them.
77    #[must_use]
78    pub fn mark(&self, declared: &[Purpose]) -> Option<Emergency> {
79        let marking: BTreeSet<&Purpose> = declared
80            .iter()
81            .filter(|purpose| self.purposes.contains(*purpose))
82            .collect();
83        (!marking.is_empty()).then(|| Emergency {
84            purposes: marking.into_iter().cloned().collect(),
85            consent_set_aside: BTreeSet::new(),
86        })
87    }
88}
89
90/// The mark of an emergency access (Art 11(5)).
91///
92/// It names the purposes of use the accessor declared that the deployment
93/// declares as asserting one, and the members whose consent pre-filter
94/// denial the recording system set aside for it.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct Emergency {
97    purposes: Vec<Purpose>,
98    consent_set_aside: BTreeSet<String>,
99}
100
101impl Emergency {
102    /// The purposes that marked the access, in order, each once.
103    #[must_use]
104    pub fn purposes(&self) -> &[Purpose] {
105        &self.purposes
106    }
107
108    /// This mark, naming `members`, each a member a consent pre-filter in
109    /// front of the systems that hold the data denied, whose denial the
110    /// recording system set aside for the emergency and asked all the same.
111    ///
112    /// The member, never the recording system, decided whether to release
113    /// anything; the mark says it was asked, not that it released data.
114    #[must_use]
115    pub fn with_consent_set_aside(mut self, members: impl IntoIterator<Item = String>) -> Self {
116        self.consent_set_aside.extend(members);
117        self
118    }
119
120    /// The members whose consent pre-filter denial was set aside, in order.
121    #[must_use]
122    pub fn consent_set_aside(&self) -> &BTreeSet<String> {
123        &self.consent_set_aside
124    }
125}
126
127/// Why a set of emergency purposes cannot be declared.
128#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
129#[non_exhaustive]
130pub enum EmergencyError {
131    /// A purpose has an empty code.
132    #[error("emergency purpose {index} has an empty code")]
133    EmptyCode {
134        /// The position of the purpose, from zero.
135        index: usize,
136    },
137    /// A purpose names an empty system.
138    #[error("emergency purpose {index} names an empty system")]
139    EmptySystem {
140        /// The position of the purpose, from zero.
141        index: usize,
142    },
143}