Skip to main content

hl7_2/
builder.rs

1//! Building a message from nothing.
2//!
3//! Reading is the common case, but a system that reads HL7 usually has to
4//! answer in it too — an `ACK` at minimum. A [`Builder`] starts from a
5//! well-formed MSH header for a chosen release and adds segments and
6//! values from there.
7//!
8//! Errors are collected rather than returned at each step, so a chain of
9//! calls stays a chain and [`Builder::build`] reports what went wrong. The
10//! builder takes no timestamp from the clock and invents no control ID:
11//! both are the caller's, because a message that made up its own would be
12//! untraceable and untestable.
13
14use crate::{Error, Message, Options, Version};
15use std::sync::Arc;
16
17/// Assemble a message segment by segment; see the module documentation.
18///
19/// ```
20/// use hl7_2::{Builder, Version};
21///
22/// let message = Builder::new(Version::V2_5)
23///     .message_type("ADT", "A01")
24///     .control_id("MSG00001")
25///     .timestamp("20240101093851")
26///     .sending("HIS", "HOSPITAL")
27///     .receiving("EPIC", "CLINIC")
28///     .segment("EVN")
29///     .set("EVN-1", "A01")
30///     .segment("PID")
31///     .set("PID-3.1", "241900")
32///     .set("PID-5.1.1", "SMITH")
33///     .set("PID-5.2", "JOHN")
34///     .build()?;
35///
36/// assert_eq!(message.structure_id(), "ADT_A01");
37/// assert!(message.to_er7().contains("PID|||241900||SMITH^JOHN"));
38/// # Ok::<(), hl7_2::Error>(())
39/// ```
40#[derive(Debug)]
41pub struct Builder {
42    message: Message,
43    failures: Vec<Error>,
44}
45
46impl Builder {
47    /// Start a message for `version`, with the standard delimiters
48    /// (`|^~\&`), a processing ID of `P` (production), and MSH-12 set.
49    ///
50    /// Everything else — message type, control ID, timestamp, sender,
51    /// receiver — is empty until set, and MSH-9 and MSH-10 being empty is
52    /// exactly what [`Message::validate`] reports, so a half-built message
53    /// says so rather than looking finished.
54    #[must_use]
55    /// # Panics
56    ///
57    /// Never in practice: the bundled dictionaries are embedded at compile
58    /// time and parsed on first use, so a failure here would mean this
59    /// crate shipped a malformed one.
60    pub fn new(version: Version) -> Builder {
61        let header = format!("MSH|^~\\&|||||||||P|{version}");
62        let message = crate::parse_with_options(&header, &Options::new().with_version(version))
63            .expect("a builder's own header is always well formed");
64        Builder {
65            message,
66            failures: Vec::new(),
67        }
68    }
69
70    /// Start a message for `version`, read through `dictionary` — the
71    /// schema-mode counterpart of [`Builder::new`].
72    #[must_use]
73    /// # Panics
74    ///
75    /// Never in practice: the bundled dictionaries are embedded at compile
76    /// time and parsed on first use, so a failure here would mean this
77    /// crate shipped a malformed one.
78    pub fn with_dictionary(version: Version, dictionary: Arc<crate::Dictionary>) -> Builder {
79        let header = format!("MSH|^~\\&|||||||||P|{version}");
80        let options = Options::new()
81            .with_version(version)
82            .with_dictionary(dictionary);
83        let message = crate::parse_with_options(&header, &options)
84            .expect("a builder's own header is always well formed");
85        Builder {
86            message,
87            failures: Vec::new(),
88        }
89    }
90
91    /// Start from an existing message, to add to it or answer it.
92    #[must_use]
93    pub fn from_message(message: Message) -> Builder {
94        Builder {
95            message,
96            failures: Vec::new(),
97        }
98    }
99
100    /// Set MSH-9: the message code and trigger event, e.g. `("ADT",
101    /// "A01")`. The structure ID (MSH-9.3) is filled in from the
102    /// dictionary, so `ADT^A04` correctly declares `ADT_A01`.
103    #[must_use]
104    pub fn message_type(mut self, code: &str, trigger: &str) -> Builder {
105        let structure = self.message.dictionary().structure_id(code, trigger);
106        self = self.set("MSH-9.1", code);
107        self = self.set("MSH-9.2", trigger);
108        self.set("MSH-9.3", &structure)
109    }
110
111    /// Set MSH-10, the message control ID: the sender's own identifier for
112    /// this message, which the receiver echoes in its acknowledgement.
113    #[must_use]
114    pub fn control_id(self, id: &str) -> Builder {
115        self.set("MSH-10", id)
116    }
117
118    /// Set MSH-7, the date and time the message was sent, as HL7 writes it
119    /// (`YYYYMMDDHHMMSS`, optionally with a fraction and an offset).
120    #[must_use]
121    pub fn timestamp(self, timestamp: &str) -> Builder {
122        self.set("MSH-7.1", timestamp)
123    }
124
125    /// Set MSH-3 and MSH-4, the sending application and facility.
126    #[must_use]
127    pub fn sending(self, application: &str, facility: &str) -> Builder {
128        self.set("MSH-3.1", application).set("MSH-4.1", facility)
129    }
130
131    /// Set MSH-5 and MSH-6, the receiving application and facility.
132    #[must_use]
133    pub fn receiving(self, application: &str, facility: &str) -> Builder {
134        self.set("MSH-5.1", application).set("MSH-6.1", facility)
135    }
136
137    /// Set MSH-11, the processing ID: `P` production, `T` training, `D`
138    /// debugging. A builder starts at `P`.
139    #[must_use]
140    pub fn processing_id(self, id: &str) -> Builder {
141        self.set("MSH-11.1", id)
142    }
143
144    /// Append an empty segment. Subsequent [`Builder::set`] calls that name
145    /// this segment address this occurrence, because paths without an
146    /// explicit `[n]` mean the first — so add a segment, fill it, then add
147    /// the next.
148    #[must_use]
149    pub fn segment(mut self, name: &str) -> Builder {
150        self.message.append_segment(name);
151        self
152    }
153
154    /// Set a value, escaping delimiters in it; see [`Message::set`].
155    #[must_use]
156    pub fn set(mut self, path: &str, value: &str) -> Builder {
157        if let Err(error) = self.message.set(path, value) {
158            self.failures.push(error);
159        }
160        self
161    }
162
163    /// Set a value from text that is already ER7-encoded; see
164    /// [`Message::set_er7`].
165    #[must_use]
166    pub fn set_er7(mut self, path: &str, er7_text: &str) -> Builder {
167        if let Err(error) = self.message.set_er7(path, er7_text) {
168            self.failures.push(error);
169        }
170        self
171    }
172
173    /// Write a [`crate::ToHl7`] value's fields into the message being
174    /// built — struct mode's other direction.
175    #[must_use]
176    pub fn encode(mut self, value: &impl crate::ToHl7) -> Builder {
177        if let Err(error) = value.to_hl7(&mut self.message) {
178            self.failures.push(error);
179        }
180        self
181    }
182
183    /// Finish, returning the message, or the first error a step hit.
184    /// # Errors
185    ///
186    /// The first [`Error`] recorded while building — a path that could not
187    /// be written, reported here rather than at the call that caused it, so
188    /// a chain of setters reads as one expression.
189    pub fn build(self) -> Result<Message, Error> {
190        match self.failures.into_iter().next() {
191            Some(error) => Err(error),
192            None => Ok(self.message),
193        }
194    }
195
196    /// Finish, and reject a message that does not pass validation. Use it
197    /// for an outbound message, where sending something malformed costs
198    /// more than noticing here.
199    /// # Errors
200    ///
201    /// As [`Builder::build`], and additionally when the built message does
202    /// not validate against its dictionary.
203    pub fn build_valid(self) -> Result<Message, Error> {
204        let message = self.build()?;
205        let failures: Vec<crate::Diagnostic> = message
206            .validate()
207            .into_iter()
208            .filter(|diagnostic| diagnostic.severity == crate::Severity::Error)
209            .collect();
210        if failures.is_empty() {
211            Ok(message)
212        } else {
213            Err(Error::Invalid(failures))
214        }
215    }
216}
217
218/// Build an `ACK` acknowledging `message`, in the release `message` speaks.
219///
220/// MSA-2 echoes the control ID being acknowledged, which is what lets the
221/// sender match the answer to the question; the acknowledgement's own
222/// control ID and timestamp are the caller's to supply.
223///
224/// ```
225/// let message = hl7_2::parse("MSH|^~\\&|LAB|L|EPIC|E|20240101||ORU^R01|99|P|2.5\rPID|1")?;
226/// let ack = hl7_2::builder::acknowledge(&message, "AA", "ACK00001", "20240101093900").build()?;
227/// assert_eq!(ack.get("MSA-2")?.as_deref(), Some("99"));
228/// // The answer goes back where it came from.
229/// assert_eq!(ack.get("MSH-5.1")?.as_deref(), Some("LAB"));
230/// # Ok::<(), hl7_2::Error>(())
231/// ```
232#[must_use]
233pub fn acknowledge(message: &Message, code: &str, control_id: &str, timestamp: &str) -> Builder {
234    let value = |path: &str| message.get(path).ok().flatten().unwrap_or_default();
235    Builder::new(message.version())
236        .message_type("ACK", &value("MSH-9.2"))
237        .control_id(control_id)
238        .timestamp(timestamp)
239        // Sender and receiver swap: the acknowledgement goes back.
240        .sending(&value("MSH-5.1"), &value("MSH-6.1"))
241        .receiving(&value("MSH-3.1"), &value("MSH-4.1"))
242        .processing_id(&value("MSH-11.1"))
243        .segment("MSA")
244        .set("MSA-1", code)
245        .set("MSA-2", &value("MSH-10"))
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn builds_a_message_from_nothing() {
254        let message = Builder::new(Version::V2_5)
255            .message_type("ADT", "A01")
256            .control_id("1")
257            .timestamp("20240101093851")
258            .segment("EVN")
259            .set("EVN-1", "A01")
260            .segment("PID")
261            .set("PID-3.1", "241900")
262            .segment("PV1")
263            .set("PV1-2", "I")
264            .build()
265            .unwrap();
266        assert_eq!(message.version(), Version::V2_5);
267        assert_eq!(message.structure_id(), "ADT_A01");
268        assert_eq!(message.get("PID-3.1").unwrap().as_deref(), Some("241900"));
269        // Built messages are valid messages.
270        assert_eq!(message.validate(), []);
271        // And they parse back to themselves.
272        let text = message.to_er7();
273        assert_eq!(crate::parse(&text).unwrap().to_er7(), text);
274    }
275
276    #[test]
277    fn resolves_the_structure_id_through_the_dictionary() {
278        let message = Builder::new(Version::V2_5)
279            .message_type("ADT", "A08")
280            .build()
281            .unwrap();
282        assert_eq!(message.get("MSH-9.3").unwrap().as_deref(), Some("ADT_A01"));
283    }
284
285    #[test]
286    fn reports_the_first_failure_at_build_time() {
287        let error = Builder::new(Version::V2_5)
288            .set("PID-3.1", "241900") // no PID segment yet
289            .build()
290            .unwrap_err();
291        assert!(matches!(error, Error::NoSuchSegment { .. }), "{error}");
292    }
293
294    #[test]
295    fn build_valid_rejects_an_incomplete_message() {
296        // No message type and no control ID: not something to send.
297        let error = Builder::new(Version::V2_5).build_valid().unwrap_err();
298        match error {
299            Error::Invalid(diagnostics) => assert_eq!(diagnostics.len(), 2, "{diagnostics:?}"),
300            other => panic!("expected a validation failure, got {other}"),
301        }
302    }
303
304    #[test]
305    fn acknowledges_a_message() {
306        let message = crate::parse(
307            "MSH|^~\\&|LAB|LAB1|EPIC|CLINIC|20240101||ORU^R01|99|P|2.5\rPID|1\rOBR|1\rOBX|1|NM|X||7",
308        )
309        .unwrap();
310        let ack = acknowledge(&message, "AA", "ACK1", "20240101093900")
311            .build_valid()
312            .unwrap();
313        assert_eq!(ack.structure_id(), "ACK");
314        assert_eq!(ack.get("MSA-1").unwrap().as_deref(), Some("AA"));
315        assert_eq!(ack.get("MSA-2").unwrap().as_deref(), Some("99"));
316        assert_eq!(ack.get("MSH-5.1").unwrap().as_deref(), Some("LAB"));
317        assert_eq!(ack.get("MSH-6.1").unwrap().as_deref(), Some("LAB1"));
318        assert_eq!(ack.get("MSH-3.1").unwrap().as_deref(), Some("EPIC"));
319    }
320
321    #[test]
322    fn acknowledges_in_the_senders_release() {
323        let message =
324            crate::parse("MSH|^~\\&|LAB|L|EPIC|E|20240101||ORU^R01|99|P|2.3\rPID|1").unwrap();
325        let ack = acknowledge(&message, "AE", "1", "20240101")
326            .build()
327            .unwrap();
328        assert_eq!(ack.version(), Version::V2_3);
329        assert_eq!(ack.get("MSH-12").unwrap().as_deref(), Some("2.3"));
330    }
331}