#![expect(
clippy::arithmetic_side_effects,
clippy::error_impl_error,
clippy::indexing_slicing,
clippy::missing_errors_doc,
reason = "pre-existing model implementation debt moved from staged microcrate into hl7v2; cleanup is split from topology collapse"
)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum Error {
#[error("Invalid segment ID")]
InvalidSegmentId,
#[error("Bad delimiter length")]
BadDelimLength,
#[error("Duplicate delimiters")]
DuplicateDelims,
#[error("Unbalanced escape")]
UnbalancedEscape,
#[error("Invalid escape token")]
InvalidEscapeToken,
#[error("MSH field malformed")]
MshFieldMalformed,
#[error("MSH-10 missing")]
Msh10Missing,
#[error("Invalid processing ID")]
InvalidProcessingId,
#[error("Unrecognized version")]
UnrecognizedVersion,
#[error("Invalid charset")]
InvalidCharset,
#[error("Framing error: {0}")]
Framing(String),
#[error("Write failed")]
WriteFailed,
#[error("Parse error at segment {segment_id} field {field_index}: {source}")]
ParseError {
segment_id: String,
field_index: usize,
#[source]
source: Box<Error>,
},
#[error("Invalid field format: {details}")]
InvalidFieldFormat {
details: String,
},
#[error("Invalid repetition format: {details}")]
InvalidRepFormat {
details: String,
},
#[error("Invalid component format: {details}")]
InvalidCompFormat {
details: String,
},
#[error("Invalid subcomponent format: {details}")]
InvalidSubcompFormat {
details: String,
},
#[error("Batch parsing error: {details}")]
BatchParseError {
details: String,
},
#[error("Invalid batch header: {details}")]
InvalidBatchHeader {
details: String,
},
#[error("Invalid batch trailer: {details}")]
InvalidBatchTrailer {
details: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Delims {
pub field: char,
pub comp: char,
pub rep: char,
pub esc: char,
pub sub: char,
}
impl Default for Delims {
fn default() -> Self {
Self {
field: '|',
comp: '^',
rep: '~',
esc: '\\',
sub: '&',
}
}
}
impl Delims {
pub fn new() -> Self {
Self::default()
}
pub fn parse_from_msh(msh: &str) -> Result<Self, Error> {
if msh.len() < 8 {
return Err(Error::BadDelimLength);
}
let field_sep = msh.chars().nth(3).ok_or(Error::BadDelimLength)?;
let comp_char = msh.chars().nth(4).ok_or(Error::BadDelimLength)?;
let rep_char = msh.chars().nth(5).ok_or(Error::BadDelimLength)?;
let esc_char = msh.chars().nth(6).ok_or(Error::BadDelimLength)?;
let sub_char = msh.chars().nth(7).ok_or(Error::BadDelimLength)?;
let delimiters = [field_sep, comp_char, rep_char, esc_char, sub_char];
for i in 0..delimiters.len() {
for j in (i + 1)..delimiters.len() {
if delimiters[i] == delimiters[j] {
return Err(Error::DuplicateDelims);
}
}
}
Ok(Self {
field: field_sep,
comp: comp_char,
rep: rep_char,
esc: esc_char,
sub: sub_char,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Message {
pub delims: Delims,
pub segments: Vec<Segment>,
#[serde(default)]
pub charsets: Vec<String>,
}
impl Message {
pub fn new() -> Self {
Self {
delims: Delims::default(),
segments: Vec::new(),
charsets: Vec::new(),
}
}
pub fn with_segments(segments: Vec<Segment>) -> Self {
Self {
delims: Delims::default(),
segments,
charsets: Vec::new(),
}
}
}
impl Default for Message {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Batch {
pub header: Option<Segment>, pub messages: Vec<Message>,
pub trailer: Option<Segment>, }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct FileBatch {
pub header: Option<Segment>, pub batches: Vec<Batch>,
pub trailer: Option<Segment>, }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Segment {
pub id: [u8; 3],
pub fields: Vec<Field>,
}
impl Segment {
pub fn new(id: &[u8; 3]) -> Self {
Self {
id: *id,
fields: Vec::new(),
}
}
pub fn id_str(&self) -> &str {
std::str::from_utf8(&self.id).unwrap_or("???")
}
pub fn add_field(&mut self, field: Field) {
self.fields.push(field);
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Field {
pub reps: Vec<Rep>,
}
impl Field {
pub fn new() -> Self {
Self { reps: Vec::new() }
}
pub fn from_text(text: impl Into<String>) -> Self {
Self {
reps: vec![Rep::from_text(text)],
}
}
pub fn add_rep(&mut self, rep: Rep) {
self.reps.push(rep);
}
pub fn first_text(&self) -> Option<&str> {
self.reps
.first()?
.comps
.first()?
.subs
.first()
.and_then(|atom| match atom {
Atom::Text(t) => Some(t.as_str()),
Atom::Null => None,
})
}
}
impl Default for Field {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Rep {
pub comps: Vec<Comp>,
}
impl Rep {
pub fn new() -> Self {
Self { comps: Vec::new() }
}
pub fn from_text(text: impl Into<String>) -> Self {
Self {
comps: vec![Comp::from_text(text)],
}
}
pub fn add_comp(&mut self, comp: Comp) {
self.comps.push(comp);
}
pub fn first_text(&self) -> Option<&str> {
self.comps.first()?.first_text()
}
}
impl Default for Rep {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Comp {
pub subs: Vec<Atom>,
}
impl Comp {
pub fn new() -> Self {
Self { subs: Vec::new() }
}
pub fn from_text(text: impl Into<String>) -> Self {
Self {
subs: vec![Atom::Text(text.into())],
}
}
pub fn add_sub(&mut self, atom: Atom) {
self.subs.push(atom);
}
pub fn first_text(&self) -> Option<&str> {
self.subs.first()?.as_text()
}
}
impl Default for Comp {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Atom {
Text(String),
Null,
}
impl Atom {
pub fn text(s: impl Into<String>) -> Self {
Atom::Text(s.into())
}
pub fn null() -> Self {
Atom::Null
}
pub fn is_null(&self) -> bool {
matches!(self, Atom::Null)
}
pub fn as_text(&self) -> Option<&str> {
match self {
Atom::Text(s) => Some(s.as_str()),
Atom::Null => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Presence {
Missing,
Empty,
Null,
Value(String),
}
impl Presence {
pub fn is_missing(&self) -> bool {
matches!(self, Presence::Missing)
}
pub fn is_present(&self) -> bool {
!self.is_missing()
}
pub fn has_value(&self) -> bool {
matches!(self, Presence::Value(_))
}
pub fn value(&self) -> Option<&str> {
match self {
Presence::Value(v) => Some(v.as_str()),
_ => None,
}
}
}
#[cfg(test)]
mod tests;