Skip to main content

hl7v2_writer/
lib.rs

1//! HL7 v2 message writer/serializer.
2//!
3//! This crate provides serialization functionality for HL7 v2 messages,
4//! including:
5//! - Converting message structures to HL7 format
6//! - MLLP framing for network transmission
7//! - JSON serialization (re-exported from hl7v2-json)
8//!
9//! # Example
10//!
11//! ```
12//! use hl7v2_model::{Message, Segment, Field, Rep, Comp, Atom, Delims};
13//! use hl7v2_writer::write;
14//!
15//! let message = Message {
16//!     delims: Delims::default(),
17//!     segments: vec![
18//!         Segment {
19//!             id: *b"MSH",
20//!             fields: vec![
21//!                 Field::from_text("^~\\&"),  // MSH-2 encoding chars
22//!                 Field::from_text("SendingApp"),
23//!             ],
24//!         },
25//!     ],
26//!     charsets: vec![],
27//! };
28//!
29//! let bytes = write(&message);
30//! assert!(bytes.starts_with(b"MSH|"));
31//! ```
32
33use hl7v2_escape::escape_text;
34use hl7v2_model::*;
35
36// Re-export JSON functionality from hl7v2-json for backward compatibility
37pub use hl7v2_json::{to_json, to_json_string, to_json_string_pretty};
38
39/// Write HL7 message to bytes.
40///
41/// This function serializes a message structure to its HL7 format.
42///
43/// # Arguments
44///
45/// * `msg` - The message to serialize
46///
47/// # Returns
48///
49/// The serialized HL7 message bytes
50///
51/// # Example
52///
53/// ```
54/// use hl7v2_model::{Message, Delims};
55/// use hl7v2_writer::write;
56///
57/// let message = Message::new();
58/// let bytes = write(&message);
59/// ```
60pub fn write(msg: &Message) -> Vec<u8> {
61    let mut buf = Vec::new();
62
63    // Write segments
64    for segment in &msg.segments {
65        // Write segment ID
66        buf.extend_from_slice(&segment.id);
67
68        // Special handling for MSH segment
69        if &segment.id == b"MSH" {
70            // Write field separator
71            buf.push(msg.delims.field as u8);
72
73            // Write encoding characters as a single field
74            buf.push(msg.delims.comp as u8);
75            buf.push(msg.delims.rep as u8);
76            buf.push(msg.delims.esc as u8);
77            buf.push(msg.delims.sub as u8);
78
79            // Write the rest of the fields
80            for field in &segment.fields[1..] {
81                // Skip the encoding characters field
82                buf.push(msg.delims.field as u8);
83                write_field(&mut buf, field, &msg.delims);
84            }
85        } else {
86            // Write fields
87            for field in &segment.fields {
88                buf.push(msg.delims.field as u8);
89                write_field(&mut buf, field, &msg.delims);
90            }
91        }
92
93        // End segment with carriage return
94        buf.push(b'\r');
95    }
96
97    buf
98}
99
100/// Write HL7 message with MLLP framing.
101///
102/// This function serializes a message and wraps it with MLLP framing.
103///
104/// # Arguments
105///
106/// * `msg` - The message to serialize
107///
108/// # Returns
109///
110/// The MLLP-framed HL7 message bytes
111///
112/// # Example
113///
114/// ```
115/// use hl7v2_model::{Message, Delims};
116/// use hl7v2_writer::write_mllp;
117///
118/// let message = Message::new();
119/// let framed = write_mllp(&message);
120/// assert_eq!(framed[0], 0x0B); // MLLP start byte
121/// ```
122pub fn write_mllp(msg: &Message) -> Vec<u8> {
123    let hl7_bytes = write(msg);
124    hl7v2_mllp::wrap_mllp(&hl7_bytes)
125}
126
127/// Write batch to bytes.
128///
129/// # Arguments
130///
131/// * `batch` - The batch to serialize
132///
133/// # Returns
134///
135/// The serialized HL7 batch bytes
136pub fn write_batch(batch: &Batch) -> Vec<u8> {
137    let mut result = Vec::new();
138
139    // Write BHS if present
140    if let Some(header) = &batch.header {
141        result.extend_from_slice(&header.id);
142        // We need to get delimiters from the first message or use defaults
143        let delims = if let Some(first_msg) = batch.messages.first() {
144            &first_msg.delims
145        } else {
146            &Delims::default()
147        };
148        result.push(delims.field as u8);
149        write_segment_fields(header, &mut result, delims);
150        result.push(b'\r');
151    }
152
153    // Write all messages
154    for message in &batch.messages {
155        result.extend(write(message));
156    }
157
158    // Write BTS if present
159    if let Some(trailer) = &batch.trailer {
160        result.extend_from_slice(&trailer.id);
161        let delims = if let Some(first_msg) = batch.messages.first() {
162            &first_msg.delims
163        } else {
164            &Delims::default()
165        };
166        result.push(delims.field as u8);
167        write_segment_fields(trailer, &mut result, delims);
168        result.push(b'\r');
169    }
170
171    result
172}
173
174/// Write file batch to bytes.
175///
176/// # Arguments
177///
178/// * `file_batch` - The file batch to serialize
179///
180/// # Returns
181///
182/// The serialized HL7 file batch bytes
183pub fn write_file_batch(file_batch: &FileBatch) -> Vec<u8> {
184    let mut result = Vec::new();
185
186    // Write FHS if present
187    if let Some(header) = &file_batch.header {
188        result.extend_from_slice(&header.id);
189        let delims = get_delimiters_from_file_batch(file_batch);
190        result.push(delims.field as u8);
191        write_segment_fields(header, &mut result, &delims);
192        result.push(b'\r');
193    }
194
195    // Write all batches
196    for batch in &file_batch.batches {
197        result.extend(write_batch(batch));
198    }
199
200    // Write FTS if present
201    if let Some(trailer) = &file_batch.trailer {
202        result.extend_from_slice(&trailer.id);
203        let delims = get_delimiters_from_file_batch(file_batch);
204        result.push(delims.field as u8);
205        write_segment_fields(trailer, &mut result, &delims);
206        result.push(b'\r');
207    }
208
209    result
210}
211
212// ============================================================================
213// Internal helper functions
214// ============================================================================
215
216/// Write a field to bytes (with escaping)
217fn write_field(output: &mut Vec<u8>, field: &Field, delims: &Delims) {
218    for (i, rep) in field.reps.iter().enumerate() {
219        if i > 0 {
220            output.push(delims.rep as u8);
221        }
222        write_rep(output, rep, delims);
223    }
224}
225
226/// Write a repetition to bytes (with escaping)
227fn write_rep(output: &mut Vec<u8>, rep: &Rep, delims: &Delims) {
228    for (i, comp) in rep.comps.iter().enumerate() {
229        if i > 0 {
230            output.push(delims.comp as u8);
231        }
232        write_comp(output, comp, delims);
233    }
234}
235
236/// Write a component to bytes (with escaping)
237fn write_comp(output: &mut Vec<u8>, comp: &Comp, delims: &Delims) {
238    for (i, atom) in comp.subs.iter().enumerate() {
239        if i > 0 {
240            output.push(delims.sub as u8);
241        }
242        write_atom(output, atom, delims);
243    }
244}
245
246/// Write an atom to bytes (with escaping)
247fn write_atom(output: &mut Vec<u8>, atom: &Atom, delims: &Delims) {
248    match atom {
249        Atom::Text(text) => {
250            // Escape special characters
251            let escaped = escape_text(text, delims);
252            output.extend_from_slice(escaped.as_bytes());
253        }
254        Atom::Null => {
255            output.extend_from_slice(b"\"\"");
256        }
257    }
258}
259
260/// Helper function to write segment fields (without segment ID)
261fn write_segment_fields(segment: &Segment, output: &mut Vec<u8>, delims: &Delims) {
262    for (i, field) in segment.fields.iter().enumerate() {
263        if i > 0 {
264            output.push(delims.field as u8);
265        }
266        write_field(output, field, delims);
267    }
268}
269
270/// Helper function to get delimiters from a file batch
271fn get_delimiters_from_file_batch(file_batch: &FileBatch) -> Delims {
272    // Try to get delimiters from the first message in the first batch
273    if let Some(first_batch) = file_batch.batches.first()
274        && let Some(first_message) = first_batch.messages.first()
275    {
276        return first_message.delims.clone();
277    }
278    // Fallback to default delimiters
279    Delims::default()
280}
281
282#[cfg(test)]
283mod tests;
284
285#[cfg(test)]
286mod integration_tests {
287    use super::*;
288    use hl7v2_parser::parse;
289
290    #[test]
291    fn test_write_simple_message() {
292        let message = Message {
293            delims: Delims::default(),
294            segments: vec![Segment {
295                id: *b"MSH",
296                fields: vec![
297                    Field::from_text("^~\\&"),
298                    Field::from_text("SendingApp"),
299                    Field::from_text("SendingFac"),
300                ],
301            }],
302            charsets: vec![],
303        };
304
305        let bytes = write(&message);
306        let result = String::from_utf8(bytes).unwrap();
307
308        assert!(result.starts_with("MSH|"));
309        assert!(result.ends_with("\r"));
310    }
311
312    #[test]
313    fn test_write_with_repetitions() {
314        let message = Message {
315            delims: Delims::default(),
316            segments: vec![Segment {
317                id: *b"PID",
318                fields: vec![
319                    Field {
320                        reps: vec![Rep::from_text("1")],
321                    },
322                    Field {
323                        reps: vec![Rep::from_text("12345")],
324                    },
325                    Field {
326                        reps: vec![
327                            Rep {
328                                comps: vec![Comp::from_text("Doe"), Comp::from_text("John")],
329                            },
330                            Rep {
331                                comps: vec![Comp::from_text("Smith"), Comp::from_text("Jane")],
332                            },
333                        ],
334                    },
335                ],
336            }],
337            charsets: vec![],
338        };
339
340        let bytes = write(&message);
341        let result = String::from_utf8(bytes).unwrap();
342
343        // Check for repetition separator
344        assert!(result.contains("Doe^John~Smith^Jane"));
345    }
346
347    #[test]
348    fn test_write_with_escaping() {
349        let message = Message {
350            delims: Delims::default(),
351            segments: vec![Segment {
352                id: *b"PID",
353                fields: vec![
354                    Field::from_text("1"),
355                    Field::from_text("test|value"), // Contains field separator
356                ],
357            }],
358            charsets: vec![],
359        };
360
361        let bytes = write(&message);
362        let result = String::from_utf8(bytes).unwrap();
363
364        // The field separator should be escaped
365        assert!(result.contains("test\\F\\value"));
366    }
367
368    #[test]
369    fn test_write_mllp() {
370        let message = Message {
371            delims: Delims::default(),
372            segments: vec![Segment {
373                id: *b"MSH",
374                fields: vec![Field::from_text("^~\\&")],
375            }],
376            charsets: vec![],
377        };
378
379        let framed = write_mllp(&message);
380
381        assert_eq!(framed[0], hl7v2_mllp::MLLP_START);
382        assert_eq!(framed[framed.len() - 2], hl7v2_mllp::MLLP_END_1);
383        assert_eq!(framed[framed.len() - 1], hl7v2_mllp::MLLP_END_2);
384    }
385
386    #[test]
387    fn test_to_json() {
388        let message = Message {
389            delims: Delims::default(),
390            segments: vec![Segment {
391                id: *b"MSH",
392                fields: vec![Field::from_text("^~\\&"), Field::from_text("SendingApp")],
393            }],
394            charsets: vec![],
395        };
396
397        let json = to_json(&message);
398
399        assert!(json.is_object());
400        assert!(json.get("meta").is_some());
401        assert!(json.get("segments").is_some());
402
403        let meta = json.get("meta").unwrap();
404        assert!(meta.get("delims").is_some());
405    }
406
407    #[test]
408    fn test_roundtrip() {
409        // Create a message
410        let original = Message {
411            delims: Delims::default(),
412            segments: vec![
413                Segment {
414                    id: *b"MSH",
415                    fields: vec![
416                        Field::from_text("^~\\&"),
417                        Field::from_text("SendingApp"),
418                        Field::from_text("SendingFac"),
419                    ],
420                },
421                Segment {
422                    id: *b"PID",
423                    fields: vec![
424                        Field::from_text("1"),
425                        Field::from_text("12345"),
426                        Field {
427                            reps: vec![Rep {
428                                comps: vec![Comp::from_text("Doe"), Comp::from_text("John")],
429                            }],
430                        },
431                    ],
432                },
433            ],
434            charsets: vec![],
435        };
436
437        // Write to bytes
438        let bytes = write(&original);
439
440        // Parse back through the parser crate and compare key structure.
441        let parsed = parse(&bytes).unwrap();
442
443        // Compare
444        assert_eq!(original.segments.len(), parsed.segments.len());
445        assert_eq!(original.segments[0].id, parsed.segments[0].id);
446        assert_eq!(original.segments[1].id, parsed.segments[1].id);
447    }
448}