use crate::message::is_header_name;
use crate::{Component, Field, Message, Repetition, Segment, Separators, Subcomponent};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
Er7,
Text,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RenderOptions {
pub terminator: crate::Terminator,
pub trailing_terminator: bool,
}
impl Message {
#[must_use]
pub fn to_er7(&self) -> String {
self.to_er7_with(RenderOptions::default())
}
#[must_use]
pub fn to_er7_with(&self, options: RenderOptions) -> String {
let mut out = String::new();
for (index, segment) in self.segments.iter().enumerate() {
if index > 0 {
out.push_str(options.terminator.as_str());
}
write_segment(&mut out, segment, &self.separators, Mode::Er7);
}
if options.trailing_terminator && !self.segments.is_empty() {
out.push_str(options.terminator.as_str());
}
out
}
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_er7())
}
}
macro_rules! writers {
($type:ty, $write:ident, $what:literal) => {
impl $type {
#[doc = concat!("Write this ", $what, " as ER7, exactly as a receiver would read it.")]
pub fn to_er7(&self, separators: &Separators) -> String {
let mut out = String::new();
$write(&mut out, self, separators, Mode::Er7);
out
}
#[doc = concat!("Write this ", $what, " with its leaf text escape-decoded.")]
pub fn to_text(&self, separators: &Separators) -> String {
let mut out = String::new();
$write(&mut out, self, separators, Mode::Text);
out
}
}
};
}
writers!(Segment, write_segment, "segment");
writers!(Field, write_field, "field");
writers!(Repetition, write_repetition, "repetition");
writers!(Component, write_component, "component");
writers!(Subcomponent, write_subcomponent, "subcomponent");
fn write_segment(out: &mut String, segment: &Segment, separators: &Separators, mode: Mode) {
out.push_str(&segment.name);
if is_header_name(&segment.name) {
for field in segment.fields.iter().take(2) {
write_field(out, field, separators, Mode::Er7);
}
for field in segment.fields.iter().skip(2) {
out.push(separators.field);
write_field(out, field, separators, mode);
}
return;
}
for field in &segment.fields {
out.push(separators.field);
write_field(out, field, separators, mode);
}
}
fn write_field(out: &mut String, field: &Field, separators: &Separators, mode: Mode) {
join(
out,
&field.repetitions,
separators.repetition,
|out, item| write_repetition(out, item, separators, mode),
);
}
fn write_repetition(
out: &mut String,
repetition: &Repetition,
separators: &Separators,
mode: Mode,
) {
join(
out,
&repetition.components,
separators.component,
|out, item| write_component(out, item, separators, mode),
);
}
fn write_component(out: &mut String, component: &Component, separators: &Separators, mode: Mode) {
join(
out,
&component.subcomponents,
separators.subcomponent,
|out, item| write_subcomponent(out, item, separators, mode),
);
}
fn write_subcomponent(
out: &mut String,
subcomponent: &Subcomponent,
separators: &Separators,
mode: Mode,
) {
match mode {
Mode::Er7 => out.push_str(&subcomponent.raw),
Mode::Text => out.push_str(&subcomponent.value(separators)),
}
}
fn join<T>(out: &mut String, items: &[T], separator: char, mut write: impl FnMut(&mut String, &T)) {
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push(separator);
}
write(out, item);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Terminator, parse};
#[test]
fn round_trips_a_canonical_message() {
let text = "MSH|^~\\&|LAB|ACME|EHR||20260815||ORU^R01|9|P|2.5\r\
PID|1||12345^^^ACME&1.2.3&ISO^MR||SMITH^JOHN\r\
OBX|1|NM|2093-3^Cholesterol^LN||187|mg/dL|||||F";
assert_eq!(parse(text).unwrap().to_er7(), text);
}
#[test]
fn round_trips_custom_delimiters() {
let text = "MSH#*!?@#LAB#*A*B#C!D";
assert_eq!(parse(text).unwrap().to_er7(), text);
}
#[test]
fn normalizes_terminators_and_blank_lines() {
let text = "MSH|^~\\&|LAB\r\n\r\nPID|1\n";
assert_eq!(parse(text).unwrap().to_er7(), "MSH|^~\\&|LAB\rPID|1");
}
#[test]
fn writes_the_chosen_terminator() {
let message = parse("MSH|^~\\&|LAB\rPID|1").unwrap();
let options = RenderOptions {
terminator: Terminator::CrLf,
trailing_terminator: true,
};
assert_eq!(message.to_er7_with(options), "MSH|^~\\&|LAB\r\nPID|1\r\n");
}
#[test]
fn decodes_only_the_leaves() {
let message = parse(r"MSH|^~\&|LAB|Smith \T\ Jones").unwrap();
let separators = &message.separators;
let segment = message.segment("MSH").unwrap();
assert_eq!(
segment.field(4).unwrap().to_er7(separators),
r"Smith \T\ Jones"
);
assert_eq!(
segment.field(4).unwrap().to_text(separators),
"Smith & Jones"
);
assert!(segment.to_text(separators).starts_with(r"MSH|^~\&|"));
}
#[test]
fn keeps_empty_positions() {
let text = "MSH|^~\\&|LAB\rPID|A~~B|^^C|||D";
assert_eq!(parse(text).unwrap().to_er7(), text);
}
}