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 /// # Errors
219 ///
220 /// [`Error::Path`] when `path` is not a valid HL7 path.
221 pub fn get(&self, path: &str) -> Result<Option<String>, Error> {
222 Ok(self.raw.query(path)?)
223 }
224
225 /// Every value matching `path`. A path that omits an occurrence or a
226 /// repetition matches all of them, so `OBX-5` reads every result in the
227 /// message in one call.
228 /// # Errors
229 ///
230 /// [`Error::Path`] when `path` is not a valid HL7 path.
231 pub fn get_all(&self, path: &str) -> Result<Vec<String>, Error> {
232 Ok(self.raw.query_all(path)?)
233 }
234
235 /// Every repetition of the field at `path`, in message order.
236 ///
237 /// This differs from [`Message::get_all`] at exactly one point: a path
238 /// that names a whole field, such as `PID-3`, is one value to `er7` —
239 /// the field's text, repetition separators and all — because that is
240 /// what the field *is*. Here it is the repetitions, because a caller
241 /// asking for a list of them is asking about `241900~99~7` as three
242 /// identifiers rather than one string. Paths that already name a
243 /// repetition or a component behave as [`Message::get_all`].
244 ///
245 /// ```
246 /// let message = hl7_2::parse("MSH|^~\\&|A||||1||ADT^A01|1|P|2.5\rPID|1||241900~99~7")?;
247 /// assert_eq!(message.repetitions("PID-3")?, ["241900", "99", "7"]);
248 /// assert_eq!(message.get("PID-3")?.as_deref(), Some("241900~99~7"));
249 /// # Ok::<(), hl7_2::Error>(())
250 /// ```
251 /// # Errors
252 ///
253 /// [`Error::Path`] when `path` is not a valid HL7 path.
254 pub fn repetitions(&self, path: &str) -> Result<Vec<String>, Error> {
255 let parsed = Path::parse(path)?;
256 let Some(number) = parsed.field else {
257 return self.get_all(path);
258 };
259 if parsed.repetition.is_some() || parsed.component.is_some() {
260 return self.get_all(path);
261 }
262 let mut values = Vec::new();
263 let mut occurrence = 0;
264 for segment in &self.raw.segments {
265 if segment.name != parsed.segment {
266 continue;
267 }
268 occurrence += 1;
269 if parsed
270 .segment_occurrence
271 .is_some_and(|wanted| wanted != occurrence)
272 {
273 continue;
274 }
275 if let Some(field) = segment.field(number) {
276 for repetition in &field.repetitions {
277 values.push(repetition.to_text(&self.raw.separators));
278 }
279 }
280 }
281 Ok(values)
282 }
283
284 /// The data type the dictionary gives the field at `path`, resolving
285 /// OBX-5 through OBX-2. `None` when the segment or field is unknown.
286 /// # Errors
287 ///
288 /// [`Error::Path`] when `path` is not a valid HL7 path.
289 pub fn type_of(&self, path: &str) -> Result<Option<String>, Error> {
290 let path = Path::parse(path)?;
291 let Some(field) = path.field else {
292 return Ok(None);
293 };
294 let occurrence = path.segment_occurrence.unwrap_or(1);
295 let Some(segment) = self.raw.segment_at(&path.segment, occurrence) else {
296 return Ok(None);
297 };
298 Ok(match self.dictionary.field_type(&path.segment, field) {
299 Some(crate::dictionary::VARIABLE) => self.dictionary.variable_type(segment),
300 other => other,
301 }
302 .map(str::to_string))
303 }
304
305 // ---- writing --------------------------------------------------------
306
307 /// Set the value at `path`, creating whatever the path names and the
308 /// message does not yet have.
309 ///
310 /// `value` is data, not wire format: delimiters inside it are escaped,
311 /// so setting `SMITH^JOHN` writes one component containing a literal
312 /// caret. Use [`Message::set_er7`] to write text that is already
313 /// encoded, and note that setting a level replaces everything beneath
314 /// it — `set("PID-5", ...)` discards the components PID-5 had.
315 ///
316 /// The segment must already exist; [`Message::append_segment`] and
317 /// [`crate::Builder`] create segments.
318 ///
319 /// ```
320 /// let mut message = hl7_2::parse("MSH|^~\\&|A||||1||ADT^A01|1|P|2.5\rPID|1")?;
321 /// message.set("PID-5.1", "SMITH")?;
322 /// message.set("PID-5.2", "JOHN")?;
323 /// assert_eq!(message.get("PID-5")?.as_deref(), Some("SMITH^JOHN"));
324 /// # Ok::<(), hl7_2::Error>(())
325 /// ```
326 /// # Errors
327 ///
328 /// [`Error::Path`] when `path` is not a valid HL7 path, or names a
329 /// position that cannot be written.
330 pub fn set(&mut self, path: &str, value: &str) -> Result<(), Error> {
331 let separators = self.raw.separators;
332 let encoded = er7::escape::escape(value, &separators).into_owned();
333 self.write(path, &encoded, Create::Yes)
334 }
335
336 /// Set the value at `path` to text that is already ER7-encoded, so its
337 /// delimiters keep their structural meaning: `set_er7("PID-5",
338 /// "SMITH^JOHN")` writes two components.
339 ///
340 /// The text is parsed only down to the levels the path leaves open —
341 /// writing to `PID-5.1.2` treats the text as a single subcomponent
342 /// value, because there is no level left for a delimiter to divide.
343 /// # Errors
344 ///
345 /// [`Error::Path`] when `path` is not a valid HL7 path, or names a
346 /// position that cannot be written.
347 pub fn set_er7(&mut self, path: &str, er7_text: &str) -> Result<(), Error> {
348 self.write(path, er7_text, Create::Yes)
349 }
350
351 /// Set the value at `path` to the HL7 explicit null `""`, which tells
352 /// the receiver to clear the value rather than leave it alone. This is
353 /// not the same as [`Message::clear`].
354 /// # Errors
355 ///
356 /// [`Error::Path`] when `path` is not a valid HL7 path, or names a
357 /// position that cannot be written.
358 pub fn set_null(&mut self, path: &str) -> Result<(), Error> {
359 self.write(path, er7::message::NULL, Create::Yes)
360 }
361
362 /// Empty the value at `path`, as if the sender had never populated it.
363 /// Compare [`Message::set_null`], which says "clear this" out loud.
364 ///
365 /// Clearing what is already absent does nothing and succeeds — it does
366 /// not create the empty field it would then be emptying, and it does
367 /// not fail on a missing segment. That is what makes writing an
368 /// `Option::None` in struct mode a no-op rather than a message full of
369 /// empty components.
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 clear(&mut self, path: &str) -> Result<(), Error> {
375 self.write(path, "", Create::No)
376 }
377
378 /// Write already-encoded text at `path`, growing the message to fit
379 /// when `create` says to and stopping quietly when it does not.
380 fn write(&mut self, path: &str, encoded: &str, create: Create) -> Result<(), Error> {
381 let separators = self.raw.separators;
382 let path = Path::parse(path)?;
383 let Some(number) = path.field else {
384 return Err(Error::UnwritablePath(
385 "a path must name a field to be written".to_string(),
386 ));
387 };
388 let occurrence = path.segment_occurrence.unwrap_or(1);
389 let segment = match self.raw.segment_at_mut(&path.segment, occurrence) {
390 Some(segment) => segment,
391 None if create == Create::No => return Ok(()),
392 None => {
393 return Err(Error::NoSuchSegment {
394 name: path.segment.clone(),
395 occurrence,
396 });
397 }
398 };
399 if segment.fields.len() < number {
400 if create == Create::No {
401 return Ok(());
402 }
403 segment.fields.resize_with(number, Default::default);
404 }
405 let field = &mut segment.fields[number - 1];
406 let repetition = path.repetition.unwrap_or(1);
407 if field.repetitions.len() < repetition {
408 if create == Create::No {
409 return Ok(());
410 }
411 field.repetitions.resize_with(repetition, Default::default);
412 }
413 let repetition = &mut field.repetitions[repetition - 1];
414 // Each level below the one the path names is filled by splitting
415 // the text on that level's delimiter, so `set_er7("PID-5",
416 // "SMITH^JOHN")` writes two components while `set` — which escaped
417 // the caret first — writes one.
418 match (path.component, path.subcomponent) {
419 (None, _) => {
420 repetition.components = encoded
421 .split(separators.component)
422 .map(|text| component_from(text, &separators))
423 .collect();
424 }
425 (Some(component), None) => {
426 if !grow(&mut repetition.components, component, create) {
427 return Ok(());
428 }
429 repetition.components[component - 1] = component_from(encoded, &separators);
430 }
431 (Some(component), Some(subcomponent)) => {
432 if !grow(&mut repetition.components, component, create) {
433 return Ok(());
434 }
435 let component = &mut repetition.components[component - 1];
436 if !grow(&mut component.subcomponents, subcomponent, create) {
437 return Ok(());
438 }
439 component.subcomponents[subcomponent - 1] = er7::Subcomponent::new(encoded);
440 }
441 }
442 Ok(())
443 }
444
445 /// Append an empty segment named `name` and return it for populating.
446 ///
447 /// ```
448 /// let mut message = hl7_2::parse("MSH|^~\\&|A||||1||ADT^A01|1|P|2.5")?;
449 /// message.append_segment("PID");
450 /// message.set("PID-3.1", "241900")?;
451 /// assert!(message.to_er7().ends_with("\rPID|||241900"));
452 /// # Ok::<(), hl7_2::Error>(())
453 /// ```
454 pub fn append_segment(&mut self, name: &str) -> &mut Segment {
455 self.insert_segment(self.raw.segments.len(), name)
456 }
457
458 /// Insert an empty segment named `name` at `index` in the segment list,
459 /// clamped to the end. Inserting is how a segment lands in the place
460 /// its structure expects rather than after everything else.
461 pub fn insert_segment(&mut self, index: usize, name: &str) -> &mut Segment {
462 let index = index.min(self.raw.segments.len());
463 self.raw.segments.insert(
464 index,
465 Segment {
466 name: name.to_string(),
467 fields: Vec::new(),
468 },
469 );
470 &mut self.raw.segments[index]
471 }
472
473 /// Remove the `occurrence`-th (1-based) segment named `name`, returning
474 /// it. The MSH header cannot be removed.
475 pub fn remove_segment(&mut self, name: &str, occurrence: usize) -> Option<Segment> {
476 let mut seen = 0;
477 let index = self.raw.segments.iter().position(|segment| {
478 if segment.name == name {
479 seen += 1;
480 }
481 seen == occurrence && segment.name == name
482 })?;
483 if index == 0 {
484 return None;
485 }
486 Some(self.raw.segments.remove(index))
487 }
488
489 /// Remove every segment named `name`, returning how many went. The MSH
490 /// header is never removed.
491 pub fn remove_segments(&mut self, name: &str) -> usize {
492 let before = self.raw.segments.len();
493 let mut index = 0;
494 self.raw.segments.retain(|segment| {
495 index += 1;
496 index == 1 || segment.name != name
497 });
498 before - self.raw.segments.len()
499 }
500
501 // ---- validation and struct mode -------------------------------------
502
503 /// Check this message against its dictionary; see [`crate::validate`].
504 ///
505 /// Never fails and never changes the message: it reports. Parsing with
506 /// [`Options::strict`] runs the same check and turns any
507 /// [`Severity::Error`] into a parse failure.
508 #[must_use]
509 pub fn validate(&self) -> Vec<Diagnostic> {
510 crate::validate::validate(self)
511 }
512
513 /// Decode into a type that implements [`crate::FromHl7`] — struct mode.
514 ///
515 /// ```
516 /// # #[cfg(feature = "derive")] fn main() -> Result<(), hl7_2::Error> {
517 /// use hl7_2::FromHl7;
518 ///
519 /// #[derive(FromHl7)]
520 /// struct Patient {
521 /// #[hl7("PID-3.1")]
522 /// id: String,
523 /// #[hl7("PID-5.1.1")]
524 /// family_name: String,
525 /// }
526 ///
527 /// let message = hl7_2::parse("MSH|^~\\&|A||||1||ADT^A01|1|P|2.5\rPID|1||241900||SMITH^JOHN")?;
528 /// let patient: Patient = message.decode()?;
529 /// assert_eq!(patient.id, "241900");
530 /// assert_eq!(patient.family_name, "SMITH");
531 /// # Ok(())
532 /// # }
533 /// # #[cfg(not(feature = "derive"))] fn main() {}
534 /// ```
535 /// # Errors
536 ///
537 /// Whatever the type's [`FromHl7`](crate::FromHl7) implementation
538 /// reports: a path it could not read, or a value it could not convert.
539 pub fn decode<T: crate::FromHl7>(&self) -> Result<T, Error> {
540 T::from_hl7(self)
541 }
542}
543
544impl std::fmt::Display for Message {
545 /// The message as ER7; see [`Message::to_er7`].
546 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547 f.write_str(&self.to_er7())
548 }
549}
550
551/// Whether a write may bring into being what it is writing to.
552#[derive(Debug, Clone, Copy, PartialEq, Eq)]
553enum Create {
554 /// Grow the message so the path exists.
555 Yes,
556 /// Leave the message alone if the path does not already exist.
557 No,
558}
559
560/// Grow `list` so that `position` (1-based) exists, reporting whether the
561/// caller may go on to write there.
562fn grow<T: Default>(list: &mut Vec<T>, position: usize, create: Create) -> bool {
563 if list.len() < position {
564 if create == Create::No {
565 return false;
566 }
567 list.resize_with(position, Default::default);
568 }
569 true
570}
571
572/// Split already-encoded text into one component's subcomponents.
573fn component_from(text: &str, separators: &Separators) -> er7::Component {
574 er7::Component {
575 subcomponents: text
576 .split(separators.subcomponent)
577 .map(er7::Subcomponent::new)
578 .collect(),
579 }
580}
581
582/// Turn a matched layout into tree nodes, cloning each segment's node into
583/// the group it landed in.
584fn build(layout: &[Layout], nodes: &[Node], root_name: &str) -> Vec<Node> {
585 layout
586 .iter()
587 .map(|item| match item {
588 Layout::Segment(index) => nodes[*index].clone(),
589 Layout::Group { name, items } => {
590 generic::group(root_name, name, build(items, nodes, root_name))
591 }
592 })
593 .collect()
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599
600 const HEADER: &str = "MSH|^~\\&|hphis||EPIC||20131011093851||ADT^A01|14AAACVDD|P|2.5";
601
602 fn message() -> Message {
603 crate::parse(&format!("{HEADER}\rPID|1||241900||TEST^FOUAZ\rNK1|1")).unwrap()
604 }
605
606 #[test]
607 fn reads_version_and_structure_off_the_header() {
608 let message = message();
609 assert_eq!(message.version(), Version::V2_5);
610 assert_eq!(message.structure_id(), "ADT_A01");
611 // A trigger event that shares a structure resolves through the
612 // dictionary's aliases.
613 let other =
614 crate::parse("MSH|^~\\&|A||||1||ADT^A08|1|P|2.5\rEVN|A08\rPID|1\rPV1|1").unwrap();
615 assert_eq!(other.structure_id(), "ADT_A01");
616 // MSH-9.3 wins when the sender supplies one.
617 let explicit = crate::parse("MSH|^~\\&|A||||1||ADT^A08^ADT_A08|1|P|2.5").unwrap();
618 assert_eq!(explicit.structure_id(), "ADT_A08");
619 }
620
621 #[test]
622 fn defaults_the_version_when_msh_12_is_missing_or_odd() {
623 let message = crate::parse("MSH|^~\\&|A||||1||ACK|1|P|\rMSA|AA|1").unwrap();
624 assert_eq!(message.version(), Version::V2_5);
625 let message = crate::parse("MSH|^~\\&|A||||1||ACK|1|P|2.3.1\rMSA|AA|1").unwrap();
626 assert_eq!(message.version(), Version::V2_3_1);
627 // An unmodelled point release reads as the nearest older one.
628 let message = crate::parse("MSH|^~\\&|A||||1||ACK|1|P|2.5.2\rMSA|AA|1").unwrap();
629 assert_eq!(message.version(), Version::V2_5_1);
630 }
631
632 #[test]
633 fn reads_values_by_path() {
634 let message = message();
635 assert_eq!(message.get("PID-5.1").unwrap().as_deref(), Some("TEST"));
636 assert_eq!(message.get("PID-5").unwrap().as_deref(), Some("TEST^FOUAZ"));
637 assert_eq!(message.get("PID-99").unwrap(), None);
638 assert_eq!(message.get("ZZZ-1").unwrap(), None);
639 assert!(matches!(message.get("PID-0"), Err(Error::Path(_))));
640 assert_eq!(message.type_of("PID-5").unwrap().as_deref(), Some("XPN"));
641 assert_eq!(message.type_of("ZZZ-1").unwrap(), None);
642 }
643
644 #[test]
645 fn writes_values_creating_what_is_missing() {
646 let mut message = message();
647 message.set("PID-8", "F").unwrap();
648 assert_eq!(message.get("PID-8").unwrap().as_deref(), Some("F"));
649 // Beyond the current end of the segment.
650 message.set("PID-11.3", "SEATTLE").unwrap();
651 assert_eq!(message.get("PID-11.3").unwrap().as_deref(), Some("SEATTLE"));
652 // Into a repetition that does not exist yet.
653 message.set("PID[1]-3[2].1", "OTHER").unwrap();
654 assert_eq!(
655 message.get_all("PID-3.1").unwrap(),
656 ["241900".to_string(), "OTHER".to_string()]
657 );
658 // A missing segment is an error, not a silent no-op.
659 assert!(matches!(
660 message.set("OBX-5", "x"),
661 Err(Error::NoSuchSegment { .. })
662 ));
663 assert!(matches!(
664 message.set("PID", "x"),
665 Err(Error::UnwritablePath(_))
666 ));
667 }
668
669 #[test]
670 fn set_escapes_and_set_er7_does_not() {
671 let mut message = message();
672 message.set("PID-5.1", "SMITH^JOHN").unwrap();
673 // One component holding a literal caret ...
674 assert!(message.to_er7().contains("\\S\\"), "{}", message.to_er7());
675 assert_eq!(
676 message.get("PID-5.1").unwrap().as_deref(),
677 Some("SMITH^JOHN")
678 );
679 // ... versus two components.
680 let mut message = message2();
681 message.set_er7("PID-5", "SMITH^JOHN").unwrap();
682 assert_eq!(message.get("PID-5.2").unwrap().as_deref(), Some("JOHN"));
683 }
684
685 fn message2() -> Message {
686 crate::parse(&format!("{HEADER}\rPID|1")).unwrap()
687 }
688
689 #[test]
690 fn distinguishes_clearing_from_nulling() {
691 let mut message = message();
692 message.set_null("PID-5").unwrap();
693 assert!(message.to_er7().contains("|\"\""));
694 message.clear("PID-5").unwrap();
695 assert!(!message.to_er7().contains("\"\""));
696 // Clearing what was never there leaves no trace of having tried:
697 // no empty components, no error about the missing segment.
698 let before = message.to_er7();
699 message.clear("PID-5.3").unwrap();
700 message.clear("PID-40.2").unwrap();
701 message.clear("OBX-5").unwrap();
702 assert_eq!(message.to_er7(), before);
703 }
704
705 #[test]
706 fn adds_and_removes_segments() {
707 let mut message = message();
708 message.append_segment("OBX");
709 message.set("OBX-3.1", "GLU").unwrap();
710 assert!(message.to_er7().ends_with("OBX|||GLU"));
711 message.insert_segment(1, "EVN");
712 assert_eq!(message.raw().segments[1].name, "EVN");
713 assert_eq!(message.remove_segments("NK1"), 1);
714 assert!(message.segment("NK1").is_none());
715 // The header stays put whatever is asked.
716 assert_eq!(message.remove_segments("MSH"), 0);
717 assert!(message.remove_segment("MSH", 1).is_none());
718 }
719
720 #[test]
721 fn round_trips_an_unmodified_message() {
722 let text = format!("{HEADER}\rPID|1||241900||TEST^FOUAZ\rNK1|1");
723 assert_eq!(crate::parse(&text).unwrap().to_er7(), text);
724 }
725
726 #[test]
727 fn falls_back_to_a_flat_tree_when_the_structure_does_not_fit() {
728 let message = message(); // ADT_A01 requires EVN and PV1
729 assert!(message.layout().is_none());
730 let tree = message.tree();
731 assert_eq!(tree.name(), "ADT_A01");
732 assert!(tree.child("PID").is_some(), "segments must stay reachable");
733 }
734}