hl7_3/message.rs
1//! The three-level structure every HL7 v3 message shares, whatever domain
2//! it carries: a transport wrapper, a control act wrapper, and a domain
3//! payload.
4//!
5//! ```
6//! let xml = r#"
7//! <QUQI_IN000001UV01 xmlns="urn:hl7-org:v3">
8//! <id root="2.16.840.1.113883.19.5" extension="MSG00001"/>
9//! <creationTime value="20260101120000"/>
10//! <interactionId root="2.16.840.1.113883.1.6" extension="QUQI_IN000001UV01"/>
11//! <processingCode code="P"/>
12//! <sender typeCode="SND"><device classCode="DEV" determinerCode="INSTANCE"/></sender>
13//! <receiver typeCode="RCV"><device classCode="DEV" determinerCode="INSTANCE"/></receiver>
14//! <controlActProcess classCode="CACT" moodCode="EVN">
15//! <code code="QUQI_TE000001UV01"/>
16//! <subject>
17//! <observation classCode="OBS" moodCode="EVN">
18//! <id root="2.16.840.1.113883.19.5" extension="1"/>
19//! </observation>
20//! </subject>
21//! </controlActProcess>
22//! </QUQI_IN000001UV01>
23//! "#;
24//! let message = hl7_3::message::parse(xml)?;
25//! assert_eq!(message.interaction_id.unwrap().extension.as_deref(), Some("QUQI_IN000001UV01"));
26//! assert_eq!(message.control_act.unwrap().code.unwrap().code, "QUQI_TE000001UV01");
27//! # Ok::<(), hl7_3::Error>(())
28//! ```
29
30use crate::Error;
31use crate::vocabulary::{Cd, Ii};
32use hl7_2_xml_lite_helper::Element;
33
34/// Level 1: the transport wrapper.
35///
36/// The root element's own tag is the interaction's wire name (analogous to
37/// [`hl7-2`](https://crates.io/crates/hl7-2)'s message structure ID) —
38/// this crate does not currently expose it as a field; read
39/// [`Element::name`] on the value [`parse`] was given if you need it.
40#[derive(Debug, Clone, PartialEq, Default)]
41pub struct Message {
42 /// This message's own identifier — not the identifier of anything it
43 /// carries.
44 pub id: Option<Ii>,
45 /// When the message was created, as raw text (see
46 /// [`crate::rim::Act::effective_time`] for why this crate doesn't
47 /// parse timestamps further yet).
48 pub creation_time: Option<String>,
49 /// Which interaction this is — the wire contract that names the
50 /// trigger event and the payload shape a receiver should expect. An
51 /// `II`, not a `CD`: `root` names the interaction catalog (almost
52 /// always `2.16.840.1.113883.1.6`) and `extension` names the specific
53 /// interaction (`"QUQI_IN000001UV01"`).
54 pub interaction_id: Option<Ii>,
55 /// The sender, read as a raw element rather than a modeled [`Device`]
56 /// — see `spec/index.md` §1 for why.
57 ///
58 /// [`Device`]: crate::rim::Entity
59 pub sender: Option<Element>,
60 /// The receiver, read the same way as `sender`.
61 pub receiver: Option<Element>,
62 /// Level 2 and 3: the control act wrapper and the domain payload it
63 /// carries, when the message has one.
64 pub control_act: Option<ControlAct>,
65}
66
67/// Level 2: the control act wrapper — identifies the real-world trigger
68/// event and carries level 3, the domain payload.
69#[derive(Debug, Clone, PartialEq, Default)]
70pub struct ControlAct {
71 /// Almost always `"CACT"` — kept as read, not assumed, since a
72 /// nonconforming sender is something a caller may want to notice
73 /// rather than have silently papered over.
74 pub class_code: String,
75 /// Almost always `"EVN"`, for the same reason.
76 pub mood_code: String,
77 /// The trigger event code — which real-world event caused this
78 /// message to be sent.
79 pub code: Option<Cd>,
80 /// Level 3: the domain-specific payload, read as a raw element.
81 ///
82 /// Its shape is defined by the interaction (`interaction_id`), not by
83 /// anything this crate knows in general — decode it with [`crate::rim`]
84 /// types yourself, matching what that interaction's schema says to
85 /// expect. This crate finds it (the first element under a `subject`
86 /// wrapper, the common case) and stops there; see `spec/index.md` §3
87 /// for the exact rule and its limits.
88 pub domain: Option<Element>,
89}
90
91/// Parse one HL7 v3 XML message into its three-level structure.
92///
93/// # Errors
94///
95/// [`Error::Xml`] when `xml_text` is not well-formed XML. This function
96/// does not fail when a wrapper element is missing — an absent `id`,
97/// `interactionId`, `sender`, or `controlActProcess` reads as `None`, the
98/// same way [`hl7-2`](https://crates.io/crates/hl7-2)'s generic mode
99/// degrades rather than rejecting; see `spec/index.md` §3.
100pub fn parse(xml_text: &str) -> Result<Message, Error> {
101 let root = hl7_2_xml_lite_helper::parse(xml_text).map_err(Error::Xml)?;
102 Ok(Message {
103 id: root.child("id").and_then(Ii::from_element),
104 creation_time: root
105 .child("creationTime")
106 .and_then(|time| time.attribute("value"))
107 .map(str::to_string),
108 interaction_id: root.child("interactionId").and_then(Ii::from_element),
109 sender: root
110 .child("sender")
111 .and_then(|wrapper| wrapper.children.first())
112 .cloned(),
113 receiver: root
114 .child("receiver")
115 .and_then(|wrapper| wrapper.children.first())
116 .cloned(),
117 control_act: root
118 .child("controlActProcess")
119 .map(control_act_from_element),
120 })
121}
122
123fn control_act_from_element(element: &Element) -> ControlAct {
124 ControlAct {
125 class_code: element
126 .attribute("classCode")
127 .unwrap_or_default()
128 .to_string(),
129 mood_code: element
130 .attribute("moodCode")
131 .unwrap_or_default()
132 .to_string(),
133 code: element.child("code").and_then(Cd::from_element),
134 domain: element
135 .child("subject")
136 .and_then(|subject| subject.children.first())
137 .cloned(),
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 const SAMPLE: &str = r#"
146 <QUQI_IN000001UV01 xmlns="urn:hl7-org:v3">
147 <id root="2.16.840.1.113883.19.5" extension="MSG00001"/>
148 <creationTime value="20260101120000"/>
149 <interactionId root="2.16.840.1.113883.1.6" extension="QUQI_IN000001UV01"/>
150 <sender typeCode="SND"><device classCode="DEV" determinerCode="INSTANCE"/></sender>
151 <receiver typeCode="RCV"><device classCode="DEV" determinerCode="INSTANCE"/></receiver>
152 <controlActProcess classCode="CACT" moodCode="EVN">
153 <code code="QUQI_TE000001UV01"/>
154 <subject>
155 <observation classCode="OBS" moodCode="EVN">
156 <id root="2.16.840.1.113883.19.5" extension="1"/>
157 </observation>
158 </subject>
159 </controlActProcess>
160 </QUQI_IN000001UV01>
161 "#;
162
163 #[test]
164 fn reads_the_transport_wrapper() {
165 let message = parse(SAMPLE).unwrap();
166 assert_eq!(message.id.unwrap().extension.as_deref(), Some("MSG00001"));
167 assert_eq!(message.creation_time.as_deref(), Some("20260101120000"));
168 assert_eq!(
169 message.interaction_id.unwrap().extension.as_deref(),
170 Some("QUQI_IN000001UV01")
171 );
172 assert_eq!(message.sender.unwrap().local_name(), "device");
173 }
174
175 #[test]
176 fn reads_the_control_act_wrapper_and_trigger_event() {
177 let message = parse(SAMPLE).unwrap();
178 let control_act = message.control_act.unwrap();
179 assert_eq!(control_act.class_code, "CACT");
180 assert_eq!(control_act.mood_code, "EVN");
181 assert_eq!(control_act.code.unwrap().code, "QUQI_TE000001UV01");
182 }
183
184 #[test]
185 fn reads_the_domain_payload_as_a_raw_element() {
186 let message = parse(SAMPLE).unwrap();
187 let domain = message.control_act.unwrap().domain.unwrap();
188 assert_eq!(domain.local_name(), "observation");
189 assert_eq!(domain.attribute("classCode"), Some("OBS"));
190 let act = crate::rim::Act::from_element(&domain);
191 assert_eq!(act.id[0].extension.as_deref(), Some("1"));
192 }
193
194 #[test]
195 fn missing_wrappers_read_as_none_not_an_error() {
196 let message = parse(r"<EMPTY_MESSAGE/>").unwrap();
197 assert_eq!(message.id, None);
198 assert_eq!(message.interaction_id, None);
199 assert_eq!(message.control_act, None);
200 }
201
202 #[test]
203 fn malformed_xml_is_an_error() {
204 assert!(matches!(parse("<unclosed>"), Err(Error::Xml(_))));
205 }
206}