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}