use crate::escape::escape_text;
use crate::model::*;
pub mod json;
pub use json::{to_json, to_json_string, to_json_string_pretty};
pub fn write(msg: &Message) -> Vec<u8> {
let mut buf = Vec::new();
for segment in &msg.segments {
buf.extend_from_slice(&segment.id);
if &segment.id == b"MSH" {
buf.push(msg.delims.field as u8);
buf.push(msg.delims.comp as u8);
buf.push(msg.delims.rep as u8);
buf.push(msg.delims.esc as u8);
buf.push(msg.delims.sub as u8);
for field in segment.fields.iter().skip(1) {
buf.push(msg.delims.field as u8);
write_field(&mut buf, field, &msg.delims);
}
} else {
for field in &segment.fields {
buf.push(msg.delims.field as u8);
write_field(&mut buf, field, &msg.delims);
}
}
buf.push(b'\r');
}
buf
}
pub fn write_mllp(msg: &Message) -> Vec<u8> {
let hl7_bytes = write(msg);
crate::transport::mllp::wrap_mllp(&hl7_bytes)
}
pub fn write_batch(batch: &Batch) -> Vec<u8> {
let mut result = Vec::new();
if let Some(header) = &batch.header {
result.extend_from_slice(&header.id);
let delims = if let Some(first_msg) = batch.messages.first() {
&first_msg.delims
} else {
&Delims::default()
};
result.push(delims.field as u8);
write_segment_fields(header, &mut result, delims);
result.push(b'\r');
}
for message in &batch.messages {
result.extend(write(message));
}
if let Some(trailer) = &batch.trailer {
result.extend_from_slice(&trailer.id);
let delims = if let Some(first_msg) = batch.messages.first() {
&first_msg.delims
} else {
&Delims::default()
};
result.push(delims.field as u8);
write_segment_fields(trailer, &mut result, delims);
result.push(b'\r');
}
result
}
pub fn write_file_batch(file_batch: &FileBatch) -> Vec<u8> {
let mut result = Vec::new();
if let Some(header) = &file_batch.header {
result.extend_from_slice(&header.id);
let delims = get_delimiters_from_file_batch(file_batch);
result.push(delims.field as u8);
write_segment_fields(header, &mut result, &delims);
result.push(b'\r');
}
for batch in &file_batch.batches {
result.extend(write_batch(batch));
}
if let Some(trailer) = &file_batch.trailer {
result.extend_from_slice(&trailer.id);
let delims = get_delimiters_from_file_batch(file_batch);
result.push(delims.field as u8);
write_segment_fields(trailer, &mut result, &delims);
result.push(b'\r');
}
result
}
fn write_field(output: &mut Vec<u8>, field: &Field, delims: &Delims) {
for (i, rep) in field.reps.iter().enumerate() {
if i > 0 {
output.push(delims.rep as u8);
}
write_rep(output, rep, delims);
}
}
fn write_rep(output: &mut Vec<u8>, rep: &Rep, delims: &Delims) {
for (i, comp) in rep.comps.iter().enumerate() {
if i > 0 {
output.push(delims.comp as u8);
}
write_comp(output, comp, delims);
}
}
fn write_comp(output: &mut Vec<u8>, comp: &Comp, delims: &Delims) {
for (i, atom) in comp.subs.iter().enumerate() {
if i > 0 {
output.push(delims.sub as u8);
}
write_atom(output, atom, delims);
}
}
fn write_atom(output: &mut Vec<u8>, atom: &Atom, delims: &Delims) {
match atom {
Atom::Text(text) => {
let escaped = escape_text(text, delims);
output.extend_from_slice(escaped.as_bytes());
}
Atom::Null => {
output.extend_from_slice(b"\"\"");
}
}
}
fn write_segment_fields(segment: &Segment, output: &mut Vec<u8>, delims: &Delims) {
for (i, field) in segment.fields.iter().enumerate() {
if i > 0 {
output.push(delims.field as u8);
}
write_field(output, field, delims);
}
}
fn get_delimiters_from_file_batch(file_batch: &FileBatch) -> Delims {
if let Some(first_batch) = file_batch.batches.first()
&& let Some(first_message) = first_batch.messages.first()
{
return first_message.delims.clone();
}
Delims::default()
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod integration_tests {
#![expect(
clippy::indexing_slicing,
clippy::unwrap_used,
reason = "pre-existing writer inline test debt moved into hl7v2; cleanup is split from topology collapse"
)]
use super::*;
use crate::parser::parse;
#[test]
fn test_write_simple_message() {
let message = Message {
delims: Delims::default(),
segments: vec![Segment {
id: *b"MSH",
fields: vec![
Field::from_text("^~\\&"),
Field::from_text("SendingApp"),
Field::from_text("SendingFac"),
],
}],
charsets: vec![],
};
let bytes = write(&message);
let result = String::from_utf8(bytes).unwrap();
assert!(result.starts_with("MSH|"));
assert!(result.ends_with('\r'));
}
#[test]
fn test_write_with_repetitions() {
let message = Message {
delims: Delims::default(),
segments: vec![Segment {
id: *b"PID",
fields: vec![
Field {
reps: vec![Rep::from_text("1")],
},
Field {
reps: vec![Rep::from_text("12345")],
},
Field {
reps: vec![
Rep {
comps: vec![Comp::from_text("Doe"), Comp::from_text("John")],
},
Rep {
comps: vec![Comp::from_text("Smith"), Comp::from_text("Jane")],
},
],
},
],
}],
charsets: vec![],
};
let bytes = write(&message);
let result = String::from_utf8(bytes).unwrap();
assert!(result.contains("Doe^John~Smith^Jane"));
}
#[test]
fn test_write_with_escaping() {
let message = Message {
delims: Delims::default(),
segments: vec![Segment {
id: *b"PID",
fields: vec![
Field::from_text("1"),
Field::from_text("test|value"), ],
}],
charsets: vec![],
};
let bytes = write(&message);
let result = String::from_utf8(bytes).unwrap();
assert!(result.contains("test\\F\\value"));
}
#[test]
fn test_write_mllp() {
let message = Message {
delims: Delims::default(),
segments: vec![Segment {
id: *b"MSH",
fields: vec![Field::from_text("^~\\&")],
}],
charsets: vec![],
};
let framed = write_mllp(&message);
assert_eq!(framed[0], crate::transport::mllp::MLLP_START);
assert_eq!(framed[framed.len() - 2], crate::transport::mllp::MLLP_END_1);
assert_eq!(framed[framed.len() - 1], crate::transport::mllp::MLLP_END_2);
}
#[test]
fn test_to_json() {
let message = Message {
delims: Delims::default(),
segments: vec![Segment {
id: *b"MSH",
fields: vec![Field::from_text("^~\\&"), Field::from_text("SendingApp")],
}],
charsets: vec![],
};
let json = to_json(&message);
assert!(json.is_object());
assert!(json.get("meta").is_some());
assert!(json.get("segments").is_some());
let meta = json.get("meta").unwrap();
assert!(meta.get("delims").is_some());
}
#[test]
fn test_roundtrip() {
let original = Message {
delims: Delims::default(),
segments: vec![
Segment {
id: *b"MSH",
fields: vec![
Field::from_text("^~\\&"),
Field::from_text("SendingApp"),
Field::from_text("SendingFac"),
],
},
Segment {
id: *b"PID",
fields: vec![
Field::from_text("1"),
Field::from_text("12345"),
Field {
reps: vec![Rep {
comps: vec![Comp::from_text("Doe"), Comp::from_text("John")],
}],
},
],
},
],
charsets: vec![],
};
let bytes = write(&original);
let parsed = parse(&bytes).unwrap();
assert_eq!(original.segments.len(), parsed.segments.len());
assert_eq!(original.segments[0].id, parsed.segments[0].id);
assert_eq!(original.segments[1].id, parsed.segments[1].id);
}
}