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}