Skip to main content

hl7_3/
rim.rs

1//! The Reference Information Model (RIM): the six backbone classes every
2//! HL7 v3 domain payload is built from.
3//!
4//! Where HL7 v2 names segments and fields per message type, HL7 v3 names
5//! six classes once and reuses them everywhere:
6//!
7//! - [`Act`] — something that happened, is happening, or is intended to
8//!   happen: an observation, a procedure, an administration of a
9//!   substance. `moodCode` is what makes this one class cover both "this
10//!   was done" (`EVN`) and "please do this" (`RQO`) — an intent and its
11//!   eventual fulfillment are the same kind of thing at different moods,
12//!   not two different classes.
13//! - [`Entity`] — a physical thing: a person, an organization, a device,
14//!   a place. `determinerCode` distinguishes one specific instance
15//!   (`INSTANCE`) from a kind of thing in general (`KIND`).
16//! - [`Role`] — a competency one [`Entity`] has with respect to another:
17//!   *this* person as *patient*, *that* organization as *provider*. The
18//!   same [`Entity`] plays different [`Role`]s in different messages.
19//! - [`Participation`] — links an [`Act`] to the [`Role`] that took part
20//!   in it, and how (`AUT` author, `PRF` performer, `SBJ` subject, ...).
21//! - [`ActRelationship`] — links two [`Act`]s (`COMP` component, `RSON`
22//!   reason, `PERT` pertains to, ...), which is how a single act becomes
23//!   a document made of sections made of entries.
24//! - [`RoleLink`] — links two [`Role`]s, less common than the other five.
25//!
26//! Every attribute below is read straight off the matching XML element or
27//! attribute by [`crate::message::parse`]'s domain-payload walk, or can be
28//! read directly from a [`hl7_2_xml_lite_helper::Element`] with the
29//! `from_element` methods here. Nothing here validates that a `classCode`
30//! or `moodCode` is one of the values its vocabulary domain actually
31//! allows — see `spec/index.md` §6 for why that is future work, not a
32//! missing check.
33
34use crate::vocabulary::{Cd, Ii};
35use hl7_2_xml_lite_helper::Element;
36
37/// Something that happened, is happening, or is intended to happen.
38#[derive(Debug, Clone, PartialEq, Eq, Default)]
39pub struct Act {
40    /// What kind of act this is (`ActClass`: `OBS` observation, `PROC`
41    /// procedure, `SBADM` substance administration, ...).
42    pub class_code: String,
43    /// Where the act sits between intent and fact (`ActMood`: `EVN` it
44    /// happened, `INT` it is intended, `RQO` it is requested, ...).
45    pub mood_code: String,
46    /// This act's own identifiers.
47    pub id: Vec<Ii>,
48    /// What the act is, more specifically than `classCode` alone says.
49    pub code: Option<Cd>,
50    /// Where the act stands (`ActStatus`: `active`, `completed`,
51    /// `cancelled`, ...).
52    pub status_code: Option<Cd>,
53    /// When the act happened, happens, or is meant to happen. Carried as
54    /// the raw text HL7 v3's `TS`/`IVL<TS>` types serialize to — see
55    /// `spec/index.md` §1 for why this crate doesn't parse it further yet.
56    pub effective_time: Option<String>,
57    /// Free text describing the act, when the message carries one
58    /// alongside or instead of `code`.
59    pub text: Option<String>,
60}
61
62/// A physical thing: a person, organization, device, or place.
63#[derive(Debug, Clone, PartialEq, Eq, Default)]
64pub struct Entity {
65    /// What kind of thing this is (`EntityClass`: `PSN` person, `ORG`
66    /// organization, `DEV` device, `PLC` place, ...).
67    pub class_code: String,
68    /// Whether this element names one specific entity (`INSTANCE`) or a
69    /// kind of entity in general (`KIND`).
70    pub determiner_code: Option<String>,
71    /// This entity's own identifiers.
72    pub id: Vec<Ii>,
73    /// What kind of entity this is, more specifically than `classCode`
74    /// alone says (a device's model, an organization's type).
75    pub code: Option<Cd>,
76    /// A name for this entity, when it has one as free text (a person's or
77    /// organization's name is usually structured in real messages; this
78    /// crate reads it as text — see `spec/index.md` §1).
79    pub name: Option<String>,
80}
81
82/// A competency one [`Entity`] has with respect to another: this person as
83/// patient, that organization as provider.
84#[derive(Debug, Clone, PartialEq, Eq, Default)]
85pub struct Role {
86    /// What kind of role this is (`RoleClass`: `PAT` patient, `PROV`
87    /// provider, `ASSIGNED`, ...).
88    pub class_code: String,
89    /// This role's own identifiers — distinct from the identifiers of the
90    /// [`Entity`] playing it.
91    pub id: Vec<Ii>,
92    /// What the role is, more specifically than `classCode` alone says.
93    pub code: Option<Cd>,
94    /// Where the role stands (`RoleStatus`: `active`, `terminated`, ...).
95    pub status_code: Option<Cd>,
96    /// When this role applies, as raw text (see [`Act::effective_time`]).
97    pub effective_time: Option<String>,
98}
99
100/// Links an [`Act`] to the [`Role`] that took part in it, and how.
101#[derive(Debug, Clone, PartialEq, Eq, Default)]
102pub struct Participation {
103    /// How the role took part (`ParticipationType`: `AUT` author, `PRF`
104    /// performer, `SBJ` subject, `LOC` location, ...).
105    pub type_code: String,
106    /// When this participation applies, as raw text.
107    pub time: Option<String>,
108    /// A finer-grained statement of the role's function in this act, when
109    /// `typeCode` alone doesn't say enough.
110    pub function_code: Option<Cd>,
111}
112
113/// Links two [`Act`]s — the mechanism a single top-level act becomes a
114/// document built of sections built of entries.
115#[derive(Debug, Clone, PartialEq, Eq, Default)]
116pub struct ActRelationship {
117    /// How the two acts relate (`ActRelationshipType`: `COMP` component,
118    /// `RSON` reason, `PERT` pertains to, ...).
119    pub type_code: String,
120    /// Whether the relationship reads in the stated direction (`false`,
121    /// the default) or reversed (`true`).
122    pub inversion_ind: Option<bool>,
123}
124
125/// Links two [`Role`]s. Less common in practice than the other five
126/// backbone classes.
127#[derive(Debug, Clone, PartialEq, Eq, Default)]
128pub struct RoleLink {
129    /// How the two roles relate (`RoleLinkType`).
130    pub type_code: String,
131}
132
133impl Act {
134    /// Read an [`Act`] from its XML element: `classCode`/`moodCode`
135    /// attributes, an `id` child per identifier, a `code` child, a
136    /// `statusCode` child, an `effectiveTime` child's `value` attribute,
137    /// and a `text` child's text.
138    #[must_use]
139    pub fn from_element(element: &Element) -> Act {
140        Act {
141            class_code: element
142                .attribute("classCode")
143                .unwrap_or_default()
144                .to_string(),
145            mood_code: element
146                .attribute("moodCode")
147                .unwrap_or_default()
148                .to_string(),
149            id: element
150                .children_named("id")
151                .filter_map(Ii::from_element)
152                .collect(),
153            code: element.child("code").and_then(Cd::from_element),
154            status_code: element.child("statusCode").and_then(Cd::from_element),
155            effective_time: element
156                .child("effectiveTime")
157                .and_then(|time| time.attribute("value"))
158                .map(str::to_string),
159            text: element
160                .child("text")
161                .and_then(Element::text_opt)
162                .map(str::to_string),
163        }
164    }
165}
166
167impl Entity {
168    /// Read an [`Entity`] from its XML element, the same way
169    /// [`Act::from_element`] reads an `Act`.
170    #[must_use]
171    pub fn from_element(element: &Element) -> Entity {
172        Entity {
173            class_code: element
174                .attribute("classCode")
175                .unwrap_or_default()
176                .to_string(),
177            determiner_code: element.attribute("determinerCode").map(str::to_string),
178            id: element
179                .children_named("id")
180                .filter_map(Ii::from_element)
181                .collect(),
182            code: element.child("code").and_then(Cd::from_element),
183            name: element
184                .child("name")
185                .and_then(Element::text_opt)
186                .map(str::to_string),
187        }
188    }
189}
190
191impl Role {
192    /// Read a [`Role`] from its XML element.
193    #[must_use]
194    pub fn from_element(element: &Element) -> Role {
195        Role {
196            class_code: element
197                .attribute("classCode")
198                .unwrap_or_default()
199                .to_string(),
200            id: element
201                .children_named("id")
202                .filter_map(Ii::from_element)
203                .collect(),
204            code: element.child("code").and_then(Cd::from_element),
205            status_code: element.child("statusCode").and_then(Cd::from_element),
206            effective_time: element
207                .child("effectiveTime")
208                .and_then(|time| time.attribute("value"))
209                .map(str::to_string),
210        }
211    }
212}
213
214impl Participation {
215    /// Read a [`Participation`] from its XML element.
216    #[must_use]
217    pub fn from_element(element: &Element) -> Participation {
218        Participation {
219            type_code: element
220                .attribute("typeCode")
221                .unwrap_or_default()
222                .to_string(),
223            time: element
224                .child("time")
225                .and_then(|time| time.attribute("value"))
226                .map(str::to_string),
227            function_code: element.child("functionCode").and_then(Cd::from_element),
228        }
229    }
230}
231
232impl ActRelationship {
233    /// Read an [`ActRelationship`] from its XML element.
234    #[must_use]
235    pub fn from_element(element: &Element) -> ActRelationship {
236        ActRelationship {
237            type_code: element
238                .attribute("typeCode")
239                .unwrap_or_default()
240                .to_string(),
241            inversion_ind: element
242                .attribute("inversionInd")
243                .map(|value| value == "true"),
244        }
245    }
246}
247
248impl RoleLink {
249    /// Read a [`RoleLink`] from its XML element.
250    #[must_use]
251    pub fn from_element(element: &Element) -> RoleLink {
252        RoleLink {
253            type_code: element
254                .attribute("typeCode")
255                .unwrap_or_default()
256                .to_string(),
257        }
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn act_reads_class_mood_id_code_status_and_time() {
267        let element = hl7_2_xml_lite_helper::parse(
268            r#"<observation classCode="OBS" moodCode="EVN">
269                 <id root="2.16.840.1.113883.19.5" extension="1"/>
270                 <code code="8302-2" codeSystem="2.16.840.1.113883.6.1" displayName="Height"/>
271                 <statusCode code="completed"/>
272                 <effectiveTime value="20260101"/>
273               </observation>"#,
274        )
275        .unwrap();
276        let act = Act::from_element(&element);
277        assert_eq!(act.class_code, "OBS");
278        assert_eq!(act.mood_code, "EVN");
279        assert_eq!(act.id.len(), 1);
280        assert_eq!(act.id[0].extension.as_deref(), Some("1"));
281        assert_eq!(act.code.unwrap().code, "8302-2");
282        assert_eq!(act.status_code.unwrap().code, "completed");
283        assert_eq!(act.effective_time.as_deref(), Some("20260101"));
284    }
285
286    #[test]
287    fn act_with_no_optional_children_still_reads() {
288        let element =
289            hl7_2_xml_lite_helper::parse(r#"<act classCode="ACT" moodCode="EVN"/>"#).unwrap();
290        let act = Act::from_element(&element);
291        assert_eq!(act.id, Vec::new());
292        assert_eq!(act.code, None);
293        assert_eq!(act.status_code, None);
294        assert_eq!(act.effective_time, None);
295        assert_eq!(act.text, None);
296    }
297
298    #[test]
299    fn entity_reads_class_determiner_and_name() {
300        let element = hl7_2_xml_lite_helper::parse(
301            r#"<representedOrganization classCode="ORG" determinerCode="INSTANCE">
302                 <id root="2.16.840.1.113883.19.5"/>
303                 <name>Acme Clinic</name>
304               </representedOrganization>"#,
305        )
306        .unwrap();
307        let entity = Entity::from_element(&element);
308        assert_eq!(entity.class_code, "ORG");
309        assert_eq!(entity.determiner_code.as_deref(), Some("INSTANCE"));
310        assert_eq!(entity.name.as_deref(), Some("Acme Clinic"));
311    }
312
313    #[test]
314    fn role_reads_class_and_status() {
315        let element = hl7_2_xml_lite_helper::parse(
316            r#"<patient classCode="PAT">
317                 <id root="2.16.840.1.113883.19.5" extension="12345"/>
318                 <statusCode code="active"/>
319               </patient>"#,
320        )
321        .unwrap();
322        let role = Role::from_element(&element);
323        assert_eq!(role.class_code, "PAT");
324        assert_eq!(role.id[0].extension.as_deref(), Some("12345"));
325        assert_eq!(role.status_code.unwrap().code, "active");
326    }
327
328    #[test]
329    fn participation_reads_type_and_function() {
330        let element = hl7_2_xml_lite_helper::parse(
331            r#"<author typeCode="AUT"><time value="20260101"/></author>"#,
332        )
333        .unwrap();
334        let participation = Participation::from_element(&element);
335        assert_eq!(participation.type_code, "AUT");
336        assert_eq!(participation.time.as_deref(), Some("20260101"));
337    }
338
339    #[test]
340    fn act_relationship_reads_type_and_inversion() {
341        let element =
342            hl7_2_xml_lite_helper::parse(r#"<component typeCode="COMP" inversionInd="true"/>"#)
343                .unwrap();
344        let relationship = ActRelationship::from_element(&element);
345        assert_eq!(relationship.type_code, "COMP");
346        assert_eq!(relationship.inversion_ind, Some(true));
347    }
348
349    #[test]
350    fn role_link_reads_type() {
351        let element = hl7_2_xml_lite_helper::parse(r#"<roleLink typeCode="REPL"/>"#).unwrap();
352        assert_eq!(RoleLink::from_element(&element).type_code, "REPL");
353    }
354}