Skip to main content

hl7v2_model/
lib.rs

1//! Core data model for HL7 v2 messages.
2//!
3//! This crate provides the foundational data structures for HL7 v2 messages,
4//! including:
5//! - Message, Segment, Field, Repetition, Component, Atom types
6//! - Delimiter configuration
7//! - Error types
8//! - Presence semantics
9//!
10//! This crate has minimal dependencies and focuses solely on data representation.
11
12use serde::{Deserialize, Serialize};
13
14/// Error type for HL7 v2 operations
15#[derive(Debug, Clone, PartialEq, thiserror::Error)]
16pub enum Error {
17    #[error("Invalid segment ID")]
18    InvalidSegmentId,
19
20    #[error("Bad delimiter length")]
21    BadDelimLength,
22
23    #[error("Duplicate delimiters")]
24    DuplicateDelims,
25
26    #[error("Unbalanced escape")]
27    UnbalancedEscape,
28
29    #[error("Invalid escape token")]
30    InvalidEscapeToken,
31
32    #[error("MSH field malformed")]
33    MshFieldMalformed,
34
35    #[error("MSH-10 missing")]
36    Msh10Missing,
37
38    #[error("Invalid processing ID")]
39    InvalidProcessingId,
40
41    #[error("Unrecognized version")]
42    UnrecognizedVersion,
43
44    #[error("Invalid charset")]
45    InvalidCharset,
46
47    #[error("Framing error: {0}")]
48    Framing(String),
49
50    #[error("Write failed")]
51    WriteFailed,
52
53    #[error("Parse error at segment {segment_id} field {field_index}: {source}")]
54    ParseError {
55        segment_id: String,
56        field_index: usize,
57        #[source]
58        source: Box<Error>,
59    },
60
61    #[error("Invalid field format: {details}")]
62    InvalidFieldFormat { details: String },
63
64    #[error("Invalid repetition format: {details}")]
65    InvalidRepFormat { details: String },
66
67    #[error("Invalid component format: {details}")]
68    InvalidCompFormat { details: String },
69
70    #[error("Invalid subcomponent format: {details}")]
71    InvalidSubcompFormat { details: String },
72
73    #[error("Batch parsing error: {details}")]
74    BatchParseError { details: String },
75
76    #[error("Invalid batch header: {details}")]
77    InvalidBatchHeader { details: String },
78
79    #[error("Invalid batch trailer: {details}")]
80    InvalidBatchTrailer { details: String },
81}
82
83/// Delimiters used in HL7 v2 messages
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct Delims {
86    pub field: char,
87    pub comp: char,
88    pub rep: char,
89    pub esc: char,
90    pub sub: char,
91}
92
93impl Default for Delims {
94    fn default() -> Self {
95        Self {
96            field: '|',
97            comp: '^',
98            rep: '~',
99            esc: '\\',
100            sub: '&',
101        }
102    }
103}
104
105impl Delims {
106    /// Create default delimiters (|^~\&)
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Parse delimiters from an MSH segment
112    pub fn parse_from_msh(msh: &str) -> Result<Self, Error> {
113        if msh.len() < 8 {
114            return Err(Error::BadDelimLength);
115        }
116
117        let field_sep = msh.chars().nth(3).ok_or(Error::BadDelimLength)?;
118        let comp_char = msh.chars().nth(4).ok_or(Error::BadDelimLength)?;
119        let rep_char = msh.chars().nth(5).ok_or(Error::BadDelimLength)?;
120        let esc_char = msh.chars().nth(6).ok_or(Error::BadDelimLength)?;
121        let sub_char = msh.chars().nth(7).ok_or(Error::BadDelimLength)?;
122
123        // Check that all delimiters are distinct
124        let delimiters = [field_sep, comp_char, rep_char, esc_char, sub_char];
125        for i in 0..delimiters.len() {
126            for j in (i + 1)..delimiters.len() {
127                if delimiters[i] == delimiters[j] {
128                    return Err(Error::DuplicateDelims);
129                }
130            }
131        }
132
133        Ok(Self {
134            field: field_sep,
135            comp: comp_char,
136            rep: rep_char,
137            esc: esc_char,
138            sub: sub_char,
139        })
140    }
141}
142
143/// Main message structure
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct Message {
146    pub delims: Delims,
147    pub segments: Vec<Segment>,
148    /// Character sets used in the message (from MSH-18)
149    #[serde(default)]
150    pub charsets: Vec<String>,
151}
152
153impl Message {
154    /// Create a new empty message with default delimiters
155    pub fn new() -> Self {
156        Self {
157            delims: Delims::default(),
158            segments: Vec::new(),
159            charsets: Vec::new(),
160        }
161    }
162
163    /// Create a message with the given segments
164    pub fn with_segments(segments: Vec<Segment>) -> Self {
165        Self {
166            delims: Delims::default(),
167            segments,
168            charsets: Vec::new(),
169        }
170    }
171}
172
173impl Default for Message {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179/// A batch of HL7 messages
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
181pub struct Batch {
182    pub header: Option<Segment>, // BHS segment
183    pub messages: Vec<Message>,
184    pub trailer: Option<Segment>, // BTS segment
185}
186
187/// A file containing batches of HL7 messages
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
189pub struct FileBatch {
190    pub header: Option<Segment>, // FHS segment
191    pub batches: Vec<Batch>,
192    pub trailer: Option<Segment>, // FTS segment
193}
194
195/// A segment in an HL7 message
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct Segment {
198    pub id: [u8; 3],
199    pub fields: Vec<Field>,
200}
201
202impl Segment {
203    /// Create a new segment with the given ID
204    pub fn new(id: &[u8; 3]) -> Self {
205        Self {
206            id: *id,
207            fields: Vec::new(),
208        }
209    }
210
211    /// Get the segment ID as a string
212    pub fn id_str(&self) -> &str {
213        std::str::from_utf8(&self.id).unwrap_or("???")
214    }
215
216    /// Add a field to the segment
217    pub fn add_field(&mut self, field: Field) {
218        self.fields.push(field);
219    }
220}
221
222/// A field in a segment
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
224pub struct Field {
225    pub reps: Vec<Rep>,
226}
227
228impl Field {
229    /// Create a new empty field
230    pub fn new() -> Self {
231        Self { reps: Vec::new() }
232    }
233
234    /// Create a field with a single text value
235    pub fn from_text(text: impl Into<String>) -> Self {
236        Self {
237            reps: vec![Rep::from_text(text)],
238        }
239    }
240
241    /// Add a repetition to the field
242    pub fn add_rep(&mut self, rep: Rep) {
243        self.reps.push(rep);
244    }
245
246    /// Get the first value as text (convenience method)
247    pub fn first_text(&self) -> Option<&str> {
248        self.reps
249            .first()?
250            .comps
251            .first()?
252            .subs
253            .first()
254            .and_then(|atom| match atom {
255                Atom::Text(t) => Some(t.as_str()),
256                Atom::Null => None,
257            })
258    }
259}
260
261impl Default for Field {
262    fn default() -> Self {
263        Self::new()
264    }
265}
266
267/// A repetition of a field
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269pub struct Rep {
270    pub comps: Vec<Comp>,
271}
272
273impl Rep {
274    /// Create a new empty repetition
275    pub fn new() -> Self {
276        Self { comps: Vec::new() }
277    }
278
279    /// Create a repetition with a single text value
280    pub fn from_text(text: impl Into<String>) -> Self {
281        Self {
282            comps: vec![Comp::from_text(text)],
283        }
284    }
285
286    /// Add a component to the repetition
287    pub fn add_comp(&mut self, comp: Comp) {
288        self.comps.push(comp);
289    }
290}
291
292impl Default for Rep {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298/// A component of a field
299#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
300pub struct Comp {
301    pub subs: Vec<Atom>,
302}
303
304impl Comp {
305    /// Create a new empty component
306    pub fn new() -> Self {
307        Self { subs: Vec::new() }
308    }
309
310    /// Create a component with a single text value
311    pub fn from_text(text: impl Into<String>) -> Self {
312        Self {
313            subs: vec![Atom::Text(text.into())],
314        }
315    }
316
317    /// Add a subcomponent to the component
318    pub fn add_sub(&mut self, atom: Atom) {
319        self.subs.push(atom);
320    }
321}
322
323impl Default for Comp {
324    fn default() -> Self {
325        Self::new()
326    }
327}
328
329/// An atomic value in the message
330#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
331pub enum Atom {
332    Text(String),
333    Null,
334}
335
336impl Atom {
337    /// Create a text atom
338    pub fn text(s: impl Into<String>) -> Self {
339        Atom::Text(s.into())
340    }
341
342    /// Create a null atom
343    pub fn null() -> Self {
344        Atom::Null
345    }
346
347    /// Check if this is a null atom
348    pub fn is_null(&self) -> bool {
349        matches!(self, Atom::Null)
350    }
351
352    /// Get the text value if this is a text atom
353    pub fn as_text(&self) -> Option<&str> {
354        match self {
355            Atom::Text(s) => Some(s.as_str()),
356            Atom::Null => None,
357        }
358    }
359}
360
361/// Presence semantics for HL7 v2 fields
362#[derive(Debug, Clone, PartialEq)]
363pub enum Presence {
364    /// Field is not present in the message (index out of range)
365    Missing,
366    /// Field is present but empty (zero-length)
367    Empty,
368    /// Field contains a literal NULL value ("")
369    Null,
370    /// Field contains a value
371    Value(String),
372}
373
374impl Presence {
375    /// Check if the field is missing
376    pub fn is_missing(&self) -> bool {
377        matches!(self, Presence::Missing)
378    }
379
380    /// Check if the field is present (may be empty or have a value)
381    pub fn is_present(&self) -> bool {
382        !self.is_missing()
383    }
384
385    /// Check if the field has an actual value
386    pub fn has_value(&self) -> bool {
387        matches!(self, Presence::Value(_))
388    }
389
390    /// Get the value if present
391    pub fn value(&self) -> Option<&str> {
392        match self {
393            Presence::Value(v) => Some(v.as_str()),
394            _ => None,
395        }
396    }
397}
398
399#[cfg(test)]
400mod tests;