Skip to main content

hl7_3/
typed.rs

1//! Struct mode: compile-time types read off an XML element, instead of
2//! walking [`crate::rim`] types by hand.
3//!
4//! ```
5//! # #[cfg(feature = "derive")] fn main() {
6//! use hl7_3::rim::Act;
7//! use hl7_3::FromElement;
8//!
9//! #[derive(FromElement, Default)]
10//! struct Observation {
11//!     #[element("classCode")] class_code: String,
12//!     #[element(nested = "component")] component: Act, // Act's own FromElement
13//!     #[element(raw)] raw: hl7_3::xml::Element,
14//! }
15//!
16//! let element = hl7_3::xml::parse(
17//!     r#"<observation classCode="OBS"><component classCode="ACT" moodCode="RQO"/></observation>"#,
18//! )
19//! .unwrap();
20//! let observation = Observation::from_element(&element);
21//! assert_eq!(observation.class_code, "OBS");
22//! assert_eq!(observation.component.mood_code, "RQO");
23//! # }
24//! # #[cfg(not(feature = "derive"))] fn main() {}
25//! ```
26//!
27//! ## Total, on purpose — unlike `hl7-2`'s struct mode
28//!
29//! `hl7-2`'s [`FromHl7`](https://docs.rs/hl7-2/latest/hl7_2/trait.FromHl7.html)
30//! returns a `Result`: a required field absent from an HL7 v2 message is an
31//! error, because v2's dictionary says what a message *should* carry.
32//! [`FromElement`] returns `Self` directly — no `Result`, because that
33//! matches how [`crate::rim`] and [`crate::message`] already read: a
34//! missing attribute or child degrades to a default, never a failure. A
35//! struct mapped onto the wrong element just reads defaults everywhere,
36//! the same way `Act::from_element` on an unrelated element reads empty
37//! `class_code`/`mood_code` rather than panicking.
38
39use crate::xml::Element;
40
41/// A type that can be read out of an XML element — struct mode's entry
42/// point. Implement it by hand, or derive it with `#[derive(FromElement)]`
43/// (`hl7-3-derive`, behind this crate's `derive` feature) and one
44/// `#[element(...)]` attribute per field:
45///
46/// | attribute | reads |
47/// |---|---|
48/// | `#[element("classCode")]` | the `classCode` attribute, via [`FromElementValue::from_attribute`] |
49/// | `#[element(child = "id")]` | the `id` child's text, via [`FromElementValue::from_child_text`] |
50/// | `#[element(nested = "code")]` | the `code` child, via the field type's own `FromElement` |
51/// | `#[element(raw)]` | the whole element (field type must be [`Element`]) |
52/// | none | `Default::default()` |
53pub trait FromElement: Sized {
54    /// Read `element` into `Self`. Never fails — see the module
55    /// documentation for why there is no `Result` here.
56    fn from_element(element: &Element) -> Self;
57}
58
59/// A single field's value, read from an attribute or a child element's
60/// text — the two atoms [`FromElement`] fields are built from.
61///
62/// Implemented for [`String`], [`bool`], and the integer and
63/// floating-point types. A value that is absent, or present but not this
64/// type's shape, reads as the type's `Default` — silently, the same
65/// "degrade, don't reject" choice [`crate::rim`] makes for `classCode` and
66/// friends. Prefer `String` and parse deliberately (with your own error
67/// handling) where silent defaulting on bad input would hide something
68/// you need to know about.
69pub trait FromElementValue: Sized {
70    /// Read from an attribute's value, or `None` when the attribute is
71    /// absent.
72    fn from_attribute(value: Option<&str>) -> Self;
73    /// Read from a child element's text, or `None` when the child is
74    /// absent or has no text.
75    fn from_child_text(text: Option<&str>) -> Self;
76}
77
78impl FromElementValue for String {
79    fn from_attribute(value: Option<&str>) -> String {
80        value.unwrap_or_default().to_string()
81    }
82
83    fn from_child_text(text: Option<&str>) -> String {
84        text.unwrap_or_default().to_string()
85    }
86}
87
88impl FromElementValue for Option<String> {
89    fn from_attribute(value: Option<&str>) -> Option<String> {
90        value.map(str::to_string)
91    }
92
93    fn from_child_text(text: Option<&str>) -> Option<String> {
94        text.map(str::to_string)
95    }
96}
97
98/// Accepts the same spellings [`crate::rim::ActRelationship::inversion_ind`]
99/// does implicitly (`"true"`); anything else, including absent, is `false`.
100impl FromElementValue for bool {
101    fn from_attribute(value: Option<&str>) -> bool {
102        value == Some("true")
103    }
104
105    fn from_child_text(text: Option<&str>) -> bool {
106        text == Some("true")
107    }
108}
109
110/// Numbers parse with their own `FromStr`; absent or unparseable is `0`,
111/// per this module's "total" rule.
112macro_rules! numbers {
113    ($($type:ty),*) => {$(
114        impl FromElementValue for $type {
115            fn from_attribute(value: Option<&str>) -> $type {
116                value.and_then(|v| v.trim().parse().ok()).unwrap_or_default()
117            }
118
119            fn from_child_text(text: Option<&str>) -> $type {
120                text.and_then(|v| v.trim().parse().ok()).unwrap_or_default()
121            }
122        }
123    )*};
124}
125
126numbers!(
127    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64
128);
129
130/// Absent, or a code this crate doesn't recognize as one of its
131/// [`crate::vocabulary::NullFlavor`] variants, both read as `None` — the
132/// same "not every code needs a named variant" choice
133/// [`crate::vocabulary::NullFlavor::Unrecognized`] makes, one level up.
134/// Prefer `element.attribute("nullFlavor")` directly, or
135/// [`crate::vocabulary::NullFlavor::of`], when you need the code even for
136/// values this type doesn't name.
137impl FromElementValue for Option<crate::vocabulary::NullFlavor> {
138    fn from_attribute(value: Option<&str>) -> Option<crate::vocabulary::NullFlavor> {
139        value.map(crate::vocabulary::NullFlavor::parse)
140    }
141
142    fn from_child_text(text: Option<&str>) -> Option<crate::vocabulary::NullFlavor> {
143        text.map(crate::vocabulary::NullFlavor::parse)
144    }
145}
146
147impl FromElement for crate::vocabulary::Ivl {
148    fn from_element(element: &Element) -> Self {
149        crate::vocabulary::Ivl::from_element(element)
150    }
151}
152
153impl FromElement for crate::vocabulary::Pq {
154    fn from_element(element: &Element) -> Self {
155        crate::vocabulary::Pq::from_element(element)
156    }
157}
158
159impl FromElement for crate::vocabulary::Ed {
160    fn from_element(element: &Element) -> Self {
161        crate::vocabulary::Ed::from_element(element)
162    }
163}
164
165impl FromElement for crate::rim::Act {
166    fn from_element(element: &Element) -> Self {
167        crate::rim::Act::from_element(element)
168    }
169}
170
171impl FromElement for crate::rim::Entity {
172    fn from_element(element: &Element) -> Self {
173        crate::rim::Entity::from_element(element)
174    }
175}
176
177impl FromElement for crate::rim::Role {
178    fn from_element(element: &Element) -> Self {
179        crate::rim::Role::from_element(element)
180    }
181}
182
183impl FromElement for crate::rim::Participation {
184    fn from_element(element: &Element) -> Self {
185        crate::rim::Participation::from_element(element)
186    }
187}
188
189impl FromElement for crate::rim::ActRelationship {
190    fn from_element(element: &Element) -> Self {
191        crate::rim::ActRelationship::from_element(element)
192    }
193}
194
195impl FromElement for crate::rim::RoleLink {
196    fn from_element(element: &Element) -> Self {
197        crate::rim::RoleLink::from_element(element)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::rim::Act;
205
206    #[derive(Debug, Default)]
207    struct Observation {
208        class_code: String,
209        mood_code: String,
210        note: Option<String>,
211        component: Act,
212        raw: Element,
213    }
214
215    // The shape `#[derive(FromElement)]` generates, written out so the
216    // trait is tested without the derive feature.
217    impl FromElement for Observation {
218        fn from_element(element: &Element) -> Observation {
219            Observation {
220                class_code: FromElementValue::from_attribute(element.attribute("classCode")),
221                mood_code: FromElementValue::from_attribute(element.attribute("moodCode")),
222                note: FromElementValue::from_child_text(
223                    element.child("note").and_then(Element::text_opt),
224                ),
225                component: element
226                    .child("component")
227                    .map(Act::from_element)
228                    .unwrap_or_default(),
229                raw: element.clone(),
230            }
231        }
232    }
233
234    #[test]
235    fn reads_attributes_child_text_and_nested_types() {
236        let element = crate::xml::parse(
237            r#"<observation classCode="OBS" moodCode="EVN">
238                 <note>elevated</note>
239                 <component classCode="ACT" moodCode="EVN"/>
240               </observation>"#,
241        )
242        .unwrap();
243        let observation = Observation::from_element(&element);
244        assert_eq!(observation.class_code, "OBS");
245        assert_eq!(observation.mood_code, "EVN");
246        assert_eq!(observation.note.as_deref(), Some("elevated"));
247        assert_eq!(observation.component.class_code, "ACT");
248        assert_eq!(observation.raw.local_name(), "observation");
249    }
250
251    #[test]
252    fn missing_attributes_and_children_degrade_to_defaults() {
253        let element = crate::xml::parse(r"<observation/>").unwrap();
254        let observation = Observation::from_element(&element);
255        assert_eq!(observation.class_code, "");
256        assert_eq!(observation.note, None);
257        assert_eq!(observation.component, Act::default());
258    }
259
260    #[test]
261    fn numbers_and_bool_default_silently_on_bad_input() {
262        assert_eq!(u32::from_attribute(Some("7")), 7);
263        assert_eq!(u32::from_attribute(Some("not a number")), 0);
264        assert_eq!(u32::from_attribute(None), 0);
265        assert!(bool::from_attribute(Some("true")));
266        assert!(!bool::from_attribute(Some("yes")));
267        assert!(!bool::from_attribute(None));
268    }
269}