Skip to main content

hl7_2/
message.rs

1//! The parsed message: what release it speaks, what structure it is, and
2//! how to read, change, and write it back.
3//!
4//! A [`Message`] is an `er7::Message` plus the two things `er7` cannot
5//! know — which HL7 release the sender used, and which dictionary that
6//! selects — and every mode reads it through those. It is also the
7//! multi-modal escape hatch the design calls for: the raw message is never
8//! consumed or discarded, so a caller who has decoded into a struct can
9//! still reach [`Message::raw`] for the one vendor field the struct does
10//! not model, without re-parsing.
11
12use crate::dictionary::Dictionary;
13use crate::generic::Node;
14use crate::structure::{self, Layout};
15use crate::validate::{Diagnostic, Severity};
16use crate::{Error, Options, Version, generic};
17use er7::{Path, Segment, Separators};
18use std::sync::Arc;
19
20/// One parsed HL7 v2 message.
21///
22/// Built by [`crate::parse`] or [`crate::parse_with_options`]; see the
23/// crate documentation for the three modes that read it.
24#[derive(Debug, Clone)]
25pub struct Message {
26    raw: er7::Message,
27    version: Version,
28    dictionary: Arc<Dictionary>,
29}
30
31impl Message {
32    /// Parse `text` under `options`. See [`crate::parse_with_options`],
33    /// which is the same call under the name callers reach for.
34    pub(crate) fn parse(text: &str, options: &Options) -> Result<Message, Error> {
35        let raw = er7::parse(&crate::normalize(text))?;
36        let version = options
37            .version
38            .or_else(|| Version::from_message(&raw))
39            .unwrap_or_default();
40        let dictionary = match &options.dictionary {
41            Some(dictionary) => Arc::clone(dictionary),
42            None => version.dictionary(),
43        };
44        let message = Message {
45            raw,
46            version,
47            dictionary,
48        };
49        if options.strict {
50            let failures: Vec<Diagnostic> = message
51                .validate()
52                .into_iter()
53                .filter(|diagnostic| diagnostic.severity == Severity::Error)
54                .collect();
55            if !failures.is_empty() {
56                return Err(Error::Invalid(failures));
57            }
58        }
59        Ok(message)
60    }
61
62    /// The HL7 release this message is read as: MSH-12 resolved through
63    /// [`Version::nearest`], or whatever [`Options::version`] forced.
64    #[must_use]
65    pub fn version(&self) -> Version {
66        self.version
67    }
68
69    /// The dictionary this message is read through.
70    #[must_use]
71    pub fn dictionary(&self) -> &Dictionary {
72        &self.dictionary
73    }
74
75    /// The message structure ID: MSH-9.3 when the sender supplied one,
76    /// otherwise derived from MSH-9.1 and MSH-9.2 through the dictionary —
77    /// `ORU_R01`, `ADT_A01`, `ACK`.
78    ///
79    /// Read from the message each time rather than cached at parse, so a
80    /// message whose header was changed — by [`Message::set`] or by
81    /// [`crate::Builder`] — reports what it now says it is.
82    #[must_use]
83    pub fn structure_id(&self) -> String {
84        match self.raw.message_structure().filter(|id| !id.is_empty()) {
85            Some(id) => id,
86            None => self.dictionary.structure_id(
87                &self.raw.message_code().unwrap_or_default(),
88                &self.raw.trigger_event().unwrap_or_default(),
89            ),
90        }
91    }
92
93    /// The delimiters this message declared in MSH-1 and MSH-2.
94    #[must_use]
95    pub fn separators(&self) -> &Separators {
96        &self.raw.separators
97    }
98
99    /// The underlying `er7` message — the escape hatch.
100    ///
101    /// Everything this crate knows is derived from here, and nothing is
102    /// lost on the way in, so a caller who needs a field no mode models
103    /// reads it here rather than parsing the text a second time.
104    #[must_use]
105    pub fn raw(&self) -> &er7::Message {
106        &self.raw
107    }
108
109    /// The underlying `er7` message, mutably. Changes are visible to every
110    /// other method immediately, including [`Message::to_er7`].
111    pub fn raw_mut(&mut self) -> &mut er7::Message {
112        &mut self.raw
113    }
114
115    /// Take the underlying `er7` message, dropping the dictionary.
116    #[must_use]
117    pub fn into_raw(self) -> er7::Message {
118        self.raw
119    }
120
121    /// Every segment, in message order.
122    pub fn segments(&self) -> impl Iterator<Item = &Segment> {
123        self.raw.segments.iter()
124    }
125
126    /// The first segment named `name`.
127    #[must_use]
128    pub fn segment(&self, name: &str) -> Option<&Segment> {
129        self.raw.segment(name)
130    }
131
132    /// The `occurrence`-th (1-based) segment named `name`.
133    #[must_use]
134    pub fn segment_at(&self, name: &str, occurrence: usize) -> Option<&Segment> {
135        self.raw.segment_at(name, occurrence)
136    }
137
138    /// Write the message back as ER7.
139    ///
140    /// For a message parsed and not modified this reproduces the input,
141    /// differing only where the input was not canonical (other segment
142    /// terminators, blank lines, leading whitespace) — that guarantee is
143    /// `er7`'s, and this crate does not weaken it.
144    #[must_use]
145    pub fn to_er7(&self) -> String {
146        self.raw.to_er7()
147    }
148
149    /// Write the message back as ER7, choosing the segment terminator; see
150    /// [`er7::RenderOptions`].
151    #[must_use]
152    pub fn to_er7_with(&self, options: er7::RenderOptions) -> String {
153        self.raw.to_er7_with(options)
154    }
155
156    // ---- generic mode ---------------------------------------------------
157
158    /// The whole message as a navigable tree, with segments grouped into
159    /// the message structure when they fit it and left flat when they do
160    /// not. See [`crate::generic`] for the naming rules.
161    #[must_use]
162    pub fn tree(&self) -> Node {
163        self.tree_with_options(true)
164    }
165
166    /// The message as a tree, with message-structure grouping optionally
167    /// suppressed. A flat tree is one node per segment under the root, and
168    /// is what a caller who navigates by segment name wants.
169    #[must_use]
170    pub fn tree_with_options(&self, grouped: bool) -> Node {
171        let separators = &self.raw.separators;
172        let mut occurrences: Vec<usize> = Vec::with_capacity(self.raw.segments.len());
173        let mut counts: std::collections::BTreeMap<&str, usize> =
174            std::collections::BTreeMap::default();
175        for segment in &self.raw.segments {
176            let count = counts.entry(segment.name.as_str()).or_default();
177            *count += 1;
178            occurrences.push(*count);
179        }
180        let nodes: Vec<Node> = self
181            .raw
182            .segments
183            .iter()
184            .zip(&occurrences)
185            .map(|(segment, occurrence)| {
186                generic::segment(segment, *occurrence, &self.dictionary, separators)
187            })
188            .collect();
189        let structure_id = self.structure_id();
190        let children = match grouped.then(|| self.layout()).flatten() {
191            Some(layout) => build(&layout, &nodes, &structure_id),
192            None => nodes,
193        };
194        generic::root(&structure_id, children)
195    }
196
197    /// How this message's segments fit its structure, or `None` when the
198    /// dictionary has no grammar for it or the segments do not fit.
199    #[must_use]
200    pub fn layout(&self) -> Option<Vec<Layout>> {
201        let items = self.dictionary.structure(&self.structure_id())?;
202        let names: Vec<&str> = self
203            .raw
204            .segments
205            .iter()
206            .map(|segment| segment.name.as_str())
207            .collect();
208        structure::group(items, &names)
209    }
210
211    // ---- reading --------------------------------------------------------
212
213    /// The value at `path`, e.g. `PID-5.1`, `OBX[2]-5`, `MSH-9.3`.
214    ///
215    /// Returns the decoded text of the first match, or `None` when the path
216    /// names nothing in this message. Path syntax is `er7`'s; see
217    /// [`er7::Path`].
218    ///
219    /// # The explicit null reads as empty here
220    ///
221    /// Decoded, `""` *is* the empty string, so a field the sender nulled
222    /// and a field they left empty both come back as `Some("")`. The two
223    /// are opposite instructions — "clear what you have" against "I am
224    /// saying nothing about this" — and acting on the wrong one leaves a
225    /// withdrawn allergy on a record, so where the difference matters, ask
226    /// a reader that keeps it:
227    ///
228    /// ```
229    /// let message = hl7_2::parse("MSH|^~\\&|A||||1||ADT^A01|1|P|2.5\rPID|1|\"\"||X")?;
230    ///
231    /// // Indistinguishable through `get`: PID-2 is null, PID-3 is empty.
232    /// assert_eq!(message.get("PID-2")?.as_deref(), Some(""));
233    /// assert_eq!(message.get("PID-3")?.as_deref(), Some(""));
234    ///
235    /// // Distinct in the tree: the null is a node, the empty field is not.
236    /// let tree = message.tree();
237    /// let null = tree.descendants().find(|n| n.path() == "PID[1]-2[1]").unwrap();
238    /// assert!(null.is_null());
239    /// assert!(tree.descendants().all(|n| n.path() != "PID[1]-3[1]"));
240    /// # Ok::<(), hl7_2::Error>(())
241    /// ```
242    ///
243    /// [`Message::set_null`] is the writing half of the same distinction
244    /// (§7.2).
245    /// # Errors
246    ///
247    /// [`Error::Path`] when `path` is not a valid HL7 path.
248    pub fn get(&self, path: &str) -> Result<Option<String>, Error> {
249        Ok(self.raw.query(path)?)
250    }
251
252    /// Every value matching `path`. A path that omits an occurrence or a
253    /// repetition matches all of them, so `OBX-5` reads every result in the
254    /// message in one call.
255    /// # Errors
256    ///
257    /// [`Error::Path`] when `path` is not a valid HL7 path.
258    pub fn get_all(&self, path: &str) -> Result<Vec<String>, Error> {
259        Ok(self.raw.query_all(path)?)
260    }
261
262    /// Every repetition of the field at `path`, in message order.
263    ///
264    /// This differs from [`Message::get_all`] at exactly one point: a path
265    /// that names a whole field, such as `PID-3`, is one value to `er7` —
266    /// the field's text, repetition separators and all — because that is
267    /// what the field *is*. Here it is the repetitions, because a caller
268    /// asking for a list of them is asking about `241900~99~7` as three
269    /// identifiers rather than one string. Paths that already name a
270    /// repetition or a component behave as [`Message::get_all`].
271    ///
272    /// ```
273    /// let message = hl7_2::parse("MSH|^~\\&|A||||1||ADT^A01|1|P|2.5\rPID|1||241900~99~7")?;
274    /// assert_eq!(message.repetitions("PID-3")?, ["241900", "99", "7"]);
275    /// assert_eq!(message.get("PID-3")?.as_deref(), Some("241900~99~7"));
276    /// # Ok::<(), hl7_2::Error>(())
277    /// ```
278    /// # Errors
279    ///
280    /// [`Error::Path`] when `path` is not a valid HL7 path.
281    pub fn repetitions(&self, path: &str) -> Result<Vec<String>, Error> {
282        let parsed = Path::parse(path)?;
283        let Some(number) = parsed.field else {
284            return self.get_all(path);
285        };
286        if parsed.repetition.is_some() || parsed.component.is_some() {
287            return self.get_all(path);
288        }
289        let mut values = Vec::new();
290        let mut occurrence = 0;
291        for segment in &self.raw.segments {
292            if segment.name != parsed.segment {
293                continue;
294            }
295            occurrence += 1;
296            if parsed
297                .segment_occurrence
298                .is_some_and(|wanted| wanted != occurrence)
299            {
300                continue;
301            }
302            if let Some(field) = segment.field(number) {
303                for repetition in &field.repetitions {
304                    values.push(repetition.to_text(&self.raw.separators));
305                }
306            }
307        }
308        Ok(values)
309    }
310
311    /// The data type the dictionary gives the field at `path`, resolving
312    /// OBX-5 through OBX-2. `None` when the segment or field is unknown.
313    /// # Errors
314    ///
315    /// [`Error::Path`] when `path` is not a valid HL7 path.
316    pub fn type_of(&self, path: &str) -> Result<Option<String>, Error> {
317        let path = Path::parse(path)?;
318        let Some(field) = path.field else {
319            return Ok(None);
320        };
321        let occurrence = path.segment_occurrence.unwrap_or(1);
322        let Some(segment) = self.raw.segment_at(&path.segment, occurrence) else {
323            return Ok(None);
324        };
325        Ok(match self.dictionary.field_type(&path.segment, field) {
326            Some(crate::dictionary::VARIABLE) => self.dictionary.variable_type(segment),
327            other => other,
328        }
329        .map(str::to_string))
330    }
331
332    // ---- writing --------------------------------------------------------
333
334    /// Set the value at `path`, creating whatever the path names and the
335    /// message does not yet have.
336    ///
337    /// `value` is data, not wire format: delimiters inside it are escaped,
338    /// so setting `SMITH^JOHN` writes one component containing a literal
339    /// caret. Use [`Message::set_er7`] to write text that is already
340    /// encoded, and note that setting a level replaces everything beneath
341    /// it — `set("PID-5", ...)` discards the components PID-5 had.
342    ///
343    /// The segment must already exist; [`Message::append_segment`] and
344    /// [`crate::Builder`] create segments.
345    ///
346    /// ```
347    /// let mut message = hl7_2::parse("MSH|^~\\&|A||||1||ADT^A01|1|P|2.5\rPID|1")?;
348    /// message.set("PID-5.1", "SMITH")?;
349    /// message.set("PID-5.2", "JOHN")?;
350    /// assert_eq!(message.get("PID-5")?.as_deref(), Some("SMITH^JOHN"));
351    /// # Ok::<(), hl7_2::Error>(())
352    /// ```
353    /// # Errors
354    ///
355    /// [`Error::Path`] when `path` is not a valid HL7 path, or names a
356    /// position that cannot be written.
357    pub fn set(&mut self, path: &str, value: &str) -> Result<(), Error> {
358        let separators = self.raw.separators;
359        let encoded = er7::escape::escape(value, &separators).into_owned();
360        self.write(path, &encoded, Create::Yes)
361    }
362
363    /// Set the value at `path` to text that is already ER7-encoded, so its
364    /// delimiters keep their structural meaning: `set_er7("PID-5",
365    /// "SMITH^JOHN")` writes two components.
366    ///
367    /// The text is parsed only down to the levels the path leaves open —
368    /// writing to `PID-5.1.2` treats the text as a single subcomponent
369    /// value, because there is no level left for a delimiter to divide.
370    /// # Errors
371    ///
372    /// [`Error::Path`] when `path` is not a valid HL7 path, or names a
373    /// position that cannot be written.
374    pub fn set_er7(&mut self, path: &str, er7_text: &str) -> Result<(), Error> {
375        self.write(path, er7_text, Create::Yes)
376    }
377
378    /// Set the value at `path` to the HL7 explicit null `""`, which tells
379    /// the receiver to clear the value rather than leave it alone. This is
380    /// not the same as [`Message::clear`].
381    /// # Errors
382    ///
383    /// [`Error::Path`] when `path` is not a valid HL7 path, or names a
384    /// position that cannot be written.
385    pub fn set_null(&mut self, path: &str) -> Result<(), Error> {
386        self.write(path, er7::message::NULL, Create::Yes)
387    }
388
389    /// Empty the value at `path`, as if the sender had never populated it.
390    /// Compare [`Message::set_null`], which says "clear this" out loud.
391    ///
392    /// Clearing what is already absent does nothing and succeeds — it does
393    /// not create the empty field it would then be emptying, and it does
394    /// not fail on a missing segment. That is what makes writing an
395    /// `Option::None` in struct mode a no-op rather than a message full of
396    /// empty components.
397    /// # Errors
398    ///
399    /// [`Error::Path`] when `path` is not a valid HL7 path, or names a
400    /// position that cannot be written.
401    pub fn clear(&mut self, path: &str) -> Result<(), Error> {
402        self.write(path, "", Create::No)
403    }
404
405    /// Write already-encoded text at `path`, growing the message to fit
406    /// when `create` says to and stopping quietly when it does not.
407    fn write(&mut self, path: &str, encoded: &str, create: Create) -> Result<(), Error> {
408        let separators = self.raw.separators;
409        let path = Path::parse(path)?;
410        let Some(number) = path.field else {
411            return Err(Error::UnwritablePath(
412                "a path must name a field to be written".to_string(),
413            ));
414        };
415        let occurrence = path.segment_occurrence.unwrap_or(1);
416        let segment = match self.raw.segment_at_mut(&path.segment, occurrence) {
417            Some(segment) => segment,
418            None if create == Create::No => return Ok(()),
419            None => {
420                return Err(Error::NoSuchSegment {
421                    name: path.segment.clone(),
422                    occurrence,
423                });
424            }
425        };
426        if segment.fields.len() < number {
427            if create == Create::No {
428                return Ok(());
429            }
430            segment.fields.resize_with(number, Default::default);
431        }
432        let field = &mut segment.fields[number - 1];
433        let repetition = path.repetition.unwrap_or(1);
434        if field.repetitions.len() < repetition {
435            if create == Create::No {
436                return Ok(());
437            }
438            field.repetitions.resize_with(repetition, Default::default);
439        }
440        let repetition = &mut field.repetitions[repetition - 1];
441        // Each level below the one the path names is filled by splitting
442        // the text on that level's delimiter, so `set_er7("PID-5",
443        // "SMITH^JOHN")` writes two components while `set` — which escaped
444        // the caret first — writes one.
445        match (path.component, path.subcomponent) {
446            (None, _) => {
447                repetition.components = encoded
448                    .split(separators.component)
449                    .map(|text| component_from(text, &separators))
450                    .collect();
451            }
452            (Some(component), None) => {
453                if !grow(&mut repetition.components, component, create) {
454                    return Ok(());
455                }
456                repetition.components[component - 1] = component_from(encoded, &separators);
457            }
458            (Some(component), Some(subcomponent)) => {
459                if !grow(&mut repetition.components, component, create) {
460                    return Ok(());
461                }
462                let component = &mut repetition.components[component - 1];
463                if !grow(&mut component.subcomponents, subcomponent, create) {
464                    return Ok(());
465                }
466                component.subcomponents[subcomponent - 1] = er7::Subcomponent::new(encoded);
467            }
468        }
469        Ok(())
470    }
471
472    /// Append an empty segment named `name` and return it for populating.
473    ///
474    /// ```
475    /// let mut message = hl7_2::parse("MSH|^~\\&|A||||1||ADT^A01|1|P|2.5")?;
476    /// message.append_segment("PID");
477    /// message.set("PID-3.1", "241900")?;
478    /// assert!(message.to_er7().ends_with("\rPID|||241900"));
479    /// # Ok::<(), hl7_2::Error>(())
480    /// ```
481    pub fn append_segment(&mut self, name: &str) -> &mut Segment {
482        self.insert_segment(self.raw.segments.len(), name)
483    }
484
485    /// Insert an empty segment named `name` at `index` in the segment list,
486    /// clamped to the end. Inserting is how a segment lands in the place
487    /// its structure expects rather than after everything else.
488    pub fn insert_segment(&mut self, index: usize, name: &str) -> &mut Segment {
489        let index = index.min(self.raw.segments.len());
490        self.raw.segments.insert(
491            index,
492            Segment {
493                name: name.to_string(),
494                fields: Vec::new(),
495            },
496        );
497        &mut self.raw.segments[index]
498    }
499
500    /// Remove the `occurrence`-th (1-based) segment named `name`, returning
501    /// it. The MSH header cannot be removed.
502    pub fn remove_segment(&mut self, name: &str, occurrence: usize) -> Option<Segment> {
503        let mut seen = 0;
504        let index = self.raw.segments.iter().position(|segment| {
505            if segment.name == name {
506                seen += 1;
507            }
508            seen == occurrence && segment.name == name
509        })?;
510        if index == 0 {
511            return None;
512        }
513        Some(self.raw.segments.remove(index))
514    }
515
516    /// Remove every segment named `name`, returning how many went. The MSH
517    /// header is never removed.
518    pub fn remove_segments(&mut self, name: &str) -> usize {
519        let before = self.raw.segments.len();
520        let mut index = 0;
521        self.raw.segments.retain(|segment| {
522            index += 1;
523            index == 1 || segment.name != name
524        });
525        before - self.raw.segments.len()
526    }
527
528    // ---- validation and struct mode -------------------------------------
529
530    /// Check this message against its dictionary; see [`crate::validate`].
531    ///
532    /// Never fails and never changes the message: it reports. Parsing with
533    /// [`Options::strict`] runs the same check and turns any
534    /// [`Severity::Error`] into a parse failure.
535    #[must_use]
536    pub fn validate(&self) -> Vec<Diagnostic> {
537        crate::validate::validate(self)
538    }
539
540    /// Decode into a type that implements [`crate::FromHl7`] — struct mode.
541    ///
542    /// ```
543    /// # #[cfg(feature = "derive")] fn main() -> Result<(), hl7_2::Error> {
544    /// use hl7_2::FromHl7;
545    ///
546    /// #[derive(FromHl7)]
547    /// struct Patient {
548    ///     #[hl7("PID-3.1")]
549    ///     id: String,
550    ///     #[hl7("PID-5.1.1")]
551    ///     family_name: String,
552    /// }
553    ///
554    /// let message = hl7_2::parse("MSH|^~\\&|A||||1||ADT^A01|1|P|2.5\rPID|1||241900||SMITH^JOHN")?;
555    /// let patient: Patient = message.decode()?;
556    /// assert_eq!(patient.id, "241900");
557    /// assert_eq!(patient.family_name, "SMITH");
558    /// # Ok(())
559    /// # }
560    /// # #[cfg(not(feature = "derive"))] fn main() {}
561    /// ```
562    /// # Errors
563    ///
564    /// Whatever the type's [`FromHl7`](crate::FromHl7) implementation
565    /// reports: a path it could not read, or a value it could not convert.
566    pub fn decode<T: crate::FromHl7>(&self) -> Result<T, Error> {
567        T::from_hl7(self)
568    }
569}
570
571impl std::fmt::Display for Message {
572    /// The message as ER7; see [`Message::to_er7`].
573    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
574        f.write_str(&self.to_er7())
575    }
576}
577
578/// Whether a write may bring into being what it is writing to.
579#[derive(Debug, Clone, Copy, PartialEq, Eq)]
580enum Create {
581    /// Grow the message so the path exists.
582    Yes,
583    /// Leave the message alone if the path does not already exist.
584    No,
585}
586
587/// Grow `list` so that `position` (1-based) exists, reporting whether the
588/// caller may go on to write there.
589fn grow<T: Default>(list: &mut Vec<T>, position: usize, create: Create) -> bool {
590    if list.len() < position {
591        if create == Create::No {
592            return false;
593        }
594        list.resize_with(position, Default::default);
595    }
596    true
597}
598
599/// Split already-encoded text into one component's subcomponents.
600fn component_from(text: &str, separators: &Separators) -> er7::Component {
601    er7::Component {
602        subcomponents: text
603            .split(separators.subcomponent)
604            .map(er7::Subcomponent::new)
605            .collect(),
606    }
607}
608
609/// Turn a matched layout into tree nodes, cloning each segment's node into
610/// the group it landed in.
611fn build(layout: &[Layout], nodes: &[Node], root_name: &str) -> Vec<Node> {
612    layout
613        .iter()
614        .map(|item| match item {
615            Layout::Segment(index) => nodes[*index].clone(),
616            Layout::Group { name, items } => {
617                generic::group(root_name, name, build(items, nodes, root_name))
618            }
619        })
620        .collect()
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626
627    const HEADER: &str = "MSH|^~\\&|hphis||EPIC||20131011093851||ADT^A01|14AAACVDD|P|2.5";
628
629    fn message() -> Message {
630        crate::parse(&format!("{HEADER}\rPID|1||241900||TEST^FOUAZ\rNK1|1")).unwrap()
631    }
632
633    #[test]
634    fn reads_version_and_structure_off_the_header() {
635        let message = message();
636        assert_eq!(message.version(), Version::V2_5);
637        assert_eq!(message.structure_id(), "ADT_A01");
638        // A trigger event that shares a structure resolves through the
639        // dictionary's aliases.
640        let other =
641            crate::parse("MSH|^~\\&|A||||1||ADT^A08|1|P|2.5\rEVN|A08\rPID|1\rPV1|1").unwrap();
642        assert_eq!(other.structure_id(), "ADT_A01");
643        // MSH-9.3 wins when the sender supplies one.
644        let explicit = crate::parse("MSH|^~\\&|A||||1||ADT^A08^ADT_A08|1|P|2.5").unwrap();
645        assert_eq!(explicit.structure_id(), "ADT_A08");
646    }
647
648    #[test]
649    fn defaults_the_version_when_msh_12_is_missing_or_odd() {
650        let message = crate::parse("MSH|^~\\&|A||||1||ACK|1|P|\rMSA|AA|1").unwrap();
651        assert_eq!(message.version(), Version::V2_5);
652        let message = crate::parse("MSH|^~\\&|A||||1||ACK|1|P|2.3.1\rMSA|AA|1").unwrap();
653        assert_eq!(message.version(), Version::V2_3_1);
654        // An unmodelled point release reads as the nearest older one.
655        let message = crate::parse("MSH|^~\\&|A||||1||ACK|1|P|2.5.2\rMSA|AA|1").unwrap();
656        assert_eq!(message.version(), Version::V2_5_1);
657    }
658
659    #[test]
660    fn reads_values_by_path() {
661        let message = message();
662        assert_eq!(message.get("PID-5.1").unwrap().as_deref(), Some("TEST"));
663        assert_eq!(message.get("PID-5").unwrap().as_deref(), Some("TEST^FOUAZ"));
664        assert_eq!(message.get("PID-99").unwrap(), None);
665        assert_eq!(message.get("ZZZ-1").unwrap(), None);
666        assert!(matches!(message.get("PID-0"), Err(Error::Path(_))));
667        assert_eq!(message.type_of("PID-5").unwrap().as_deref(), Some("XPN"));
668        assert_eq!(message.type_of("ZZZ-1").unwrap(), None);
669    }
670
671    #[test]
672    fn writes_values_creating_what_is_missing() {
673        let mut message = message();
674        message.set("PID-8", "F").unwrap();
675        assert_eq!(message.get("PID-8").unwrap().as_deref(), Some("F"));
676        // Beyond the current end of the segment.
677        message.set("PID-11.3", "SEATTLE").unwrap();
678        assert_eq!(message.get("PID-11.3").unwrap().as_deref(), Some("SEATTLE"));
679        // Into a repetition that does not exist yet.
680        message.set("PID[1]-3[2].1", "OTHER").unwrap();
681        assert_eq!(
682            message.get_all("PID-3.1").unwrap(),
683            ["241900".to_string(), "OTHER".to_string()]
684        );
685        // A missing segment is an error, not a silent no-op.
686        assert!(matches!(
687            message.set("OBX-5", "x"),
688            Err(Error::NoSuchSegment { .. })
689        ));
690        assert!(matches!(
691            message.set("PID", "x"),
692            Err(Error::UnwritablePath(_))
693        ));
694    }
695
696    #[test]
697    fn set_escapes_and_set_er7_does_not() {
698        let mut message = message();
699        message.set("PID-5.1", "SMITH^JOHN").unwrap();
700        // One component holding a literal caret ...
701        assert!(message.to_er7().contains("\\S\\"), "{}", message.to_er7());
702        assert_eq!(
703            message.get("PID-5.1").unwrap().as_deref(),
704            Some("SMITH^JOHN")
705        );
706        // ... versus two components.
707        let mut message = message2();
708        message.set_er7("PID-5", "SMITH^JOHN").unwrap();
709        assert_eq!(message.get("PID-5.2").unwrap().as_deref(), Some("JOHN"));
710    }
711
712    fn message2() -> Message {
713        crate::parse(&format!("{HEADER}\rPID|1")).unwrap()
714    }
715
716    #[test]
717    fn distinguishes_clearing_from_nulling() {
718        let mut message = message();
719        message.set_null("PID-5").unwrap();
720        assert!(message.to_er7().contains("|\"\""));
721        message.clear("PID-5").unwrap();
722        assert!(!message.to_er7().contains("\"\""));
723        // Clearing what was never there leaves no trace of having tried:
724        // no empty components, no error about the missing segment.
725        let before = message.to_er7();
726        message.clear("PID-5.3").unwrap();
727        message.clear("PID-40.2").unwrap();
728        message.clear("OBX-5").unwrap();
729        assert_eq!(message.to_er7(), before);
730    }
731
732    #[test]
733    fn adds_and_removes_segments() {
734        let mut message = message();
735        message.append_segment("OBX");
736        message.set("OBX-3.1", "GLU").unwrap();
737        assert!(message.to_er7().ends_with("OBX|||GLU"));
738        message.insert_segment(1, "EVN");
739        assert_eq!(message.raw().segments[1].name, "EVN");
740        assert_eq!(message.remove_segments("NK1"), 1);
741        assert!(message.segment("NK1").is_none());
742        // The header stays put whatever is asked.
743        assert_eq!(message.remove_segments("MSH"), 0);
744        assert!(message.remove_segment("MSH", 1).is_none());
745    }
746
747    #[test]
748    fn round_trips_an_unmodified_message() {
749        let text = format!("{HEADER}\rPID|1||241900||TEST^FOUAZ\rNK1|1");
750        assert_eq!(crate::parse(&text).unwrap().to_er7(), text);
751    }
752
753    #[test]
754    fn falls_back_to_a_flat_tree_when_the_structure_does_not_fit() {
755        let message = message(); // ADT_A01 requires EVN and PV1
756        assert!(message.layout().is_none());
757        let tree = message.tree();
758        assert_eq!(tree.name(), "ADT_A01");
759        assert!(tree.child("PID").is_some(), "segments must stay reachable");
760    }
761}