use std::collections::HashMap;
use std::sync::LazyLock;
use regex::Regex;
use crate::encoding::HL7Encoding;
use crate::error::Hl7Error;
use crate::field::Field;
use crate::helper;
use crate::segment::Segment;
static SEGMENT_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^([A-Z][A-Z][A-Z1-9])([\(\[]([0-9]+)[\)\]]){0,1}$").unwrap());
static FIELD_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^([0-9]+)([\(\[]([0-9]+)[\)\]]){0,1}$").unwrap());
static OTHER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[1-9]([0-9]{1,2})?$").unwrap());
#[derive(Debug, Clone, Default)]
pub struct Message {
pub hl7_message: String,
pub version: String,
pub message_structure: String,
pub message_control_id: String,
pub processing_id: String,
pub segment_count: usize,
pub encoding: HL7Encoding,
segments: HashMap<String, Vec<Segment>>,
all_segments: Vec<String>,
}
impl Message {
pub fn new() -> Self {
Self::default()
}
pub fn with_message(text: impl Into<String>) -> Self {
Self { hl7_message: text.into(), ..Self::default() }
}
pub fn parse_str(text: impl Into<String>, bypass_validation: bool) -> Result<Self, Hl7Error> {
let mut msg = Self::with_message(text);
msg.parse(bypass_validation)?;
Ok(msg)
}
pub fn parse(&mut self, bypass_validation: bool) -> Result<bool, Hl7Error> {
let is_valid = if bypass_validation { true } else { self.validate_message()? };
if !is_valid {
return Ok(false);
}
if self.all_segments.is_empty() {
self.all_segments = helper::split_message(&self.hl7_message);
}
let enc = self.encoding.clone();
self.segments.clear();
self.segment_count = 0;
let lines = self.all_segments.clone();
for line in lines {
if line.trim().is_empty() {
continue;
}
let segment = Segment::parse(&line, &enc).map_err(|e| {
Hl7Error::with_code(
format!("Failed to parse the message with error - {}", e.message),
Hl7Error::PARSING_ERROR,
)
})?;
self.add_new_segment(segment);
}
let serialized = self.serialize().map_err(|e| {
Hl7Error::with_code(
format!("Failed to serialize parsed message with error - {}", e.message),
Hl7Error::PARSING_ERROR,
)
})?;
if serialized.is_empty() {
return Err(Hl7Error::with_code(
"Unable to serialize to original message - ",
Hl7Error::PARSING_ERROR,
));
}
self.encoding.evaluate_segment_delimiter(&self.hl7_message)?;
Ok(self.equals(&serialized))
}
pub fn serialize(&self) -> Result<String, Hl7Error> {
let mut out = String::new();
for seg in self.segments_in_order() {
seg.serialize(&mut out, &self.encoding);
}
Ok(out)
}
pub fn get_value(&self, path: &str) -> Result<String, Hl7Error> {
let parts: Vec<&str> = path.split('.').collect();
let com_count = parts.len();
if !validate_value_format(&parts) {
return Err(Hl7Error::new(format!("Request format is not valid: {path}")));
}
let caps = SEGMENT_REGEX
.captures(parts[0])
.ok_or_else(|| Hl7Error::new(format!("Request format is not valid: {path}")))?;
let seg_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let mut occurrence = 0usize;
if let Some(m) = caps.get(3)
&& let Ok(v) = m.as_str().parse::<usize>()
{
occurrence = v.saturating_sub(1);
}
let segment = self
.segments
.get(seg_name)
.and_then(|list| list.get(occurrence))
.ok_or_else(|| Hl7Error::new(format!("Segment name not available: {path}")))?;
let enc = &self.encoding;
let str_value: Option<String> = match com_count {
4 => {
let field = get_field(segment, parts[1]).map_err(|e| {
Hl7Error::new(format!("SubComponent not available - {path} Error: {}", e.message))
})?;
let ci = parse_index(parts[2])?;
let sci = parse_index(parts[3])?;
let comp = field.components.get(ci).ok_or_else(|| {
Hl7Error::new(format!("SubComponent not available - {path}"))
})?;
let sub = comp.sub_components.get(sci).ok_or_else(|| {
Hl7Error::new(format!("SubComponent not available - {path}"))
})?;
sub.value(enc)
}
3 => {
let field = get_field(segment, parts[1]).map_err(|e| {
Hl7Error::new(format!("Component not available - {path} Error: {}", e.message))
})?;
let ci = parse_index(parts[2])?;
let comp = field
.components
.get(ci)
.ok_or_else(|| Hl7Error::new(format!("Component not available - {path}")))?;
comp.value(enc)
}
2 => {
let field = get_field(segment, parts[1]).map_err(|e| {
Hl7Error::new(format!("Field not available - {path} Error: {}", e.message))
})?;
field.value(enc)
}
_ => segment.value(enc),
};
Ok(enc.decode(str_value.as_deref().unwrap_or("")))
}
pub fn set_value(&mut self, path: &str, value: &str) -> Result<bool, Hl7Error> {
let parts: Vec<&str> = path.split('.').collect();
let com_count = parts.len();
if !validate_value_format(&parts) {
return Err(Hl7Error::new("Request format is not valid"));
}
let seg_name = parts[0];
let enc = self.encoding.clone();
let list = self
.segments
.get_mut(seg_name)
.ok_or_else(|| Hl7Error::new("Segment name not available"))?;
let mut is_set = false;
for segment in list.iter_mut() {
match com_count {
4 => {
let ci = parse_index(parts[2])?;
let sci = parse_index(parts[3])?;
let field = get_field_mut(segment, parts[1]).map_err(|e| {
Hl7Error::new(format!(
"SubComponent not available - {path} Error: {}",
e.message
))
})?;
let comp = field.components.get_mut(ci).ok_or_else(|| {
Hl7Error::new(format!("SubComponent not available - {path}"))
})?;
let sub = comp.sub_components.get_mut(sci).ok_or_else(|| {
Hl7Error::new(format!("SubComponent not available - {path}"))
})?;
sub.set_value(value);
is_set = true;
}
3 => {
let ci = parse_index(parts[2])?;
let field = get_field_mut(segment, parts[1]).map_err(|e| {
Hl7Error::new(format!("Component not available - {path} Error: {}", e.message))
})?;
let comp = field
.components
.get_mut(ci)
.ok_or_else(|| Hl7Error::new(format!("Component not available - {path}")))?;
comp.set_value(value, &enc);
is_set = true;
}
2 => {
let field = get_field_mut(segment, parts[1]).map_err(|e| {
Hl7Error::new(format!("Field not available - {path} Error: {}", e.message))
})?;
field.set_value(value, &enc);
is_set = true;
}
_ => return Err(Hl7Error::new("Cannot overwrite a segment value")),
}
}
Ok(is_set)
}
pub fn is_componentized(&self, path: &str) -> Result<bool, Hl7Error> {
let parts: Vec<&str> = path.split('.').collect();
if !validate_value_format(&parts) {
return Err(Hl7Error::new("Request format is not valid"));
}
if parts.len() < 2 {
return Err(Hl7Error::new("Field not identified in request"));
}
let segment = self
.segments
.get(parts[0])
.and_then(|list| list.first())
.ok_or_else(|| Hl7Error::new(format!("Field not available - {path}")))?;
let field = get_field(segment, parts[1])
.map_err(|e| Hl7Error::new(format!("Field not available - {path} Error: {}", e.message)))?;
Ok(field.is_componentized)
}
pub fn has_repetitions(&self, path: &str) -> Result<bool, Hl7Error> {
let parts: Vec<&str> = path.split('.').collect();
if !validate_value_format(&parts) {
return Err(Hl7Error::new("Request format is not valid"));
}
if parts.len() < 2 {
return Err(Hl7Error::new("Field not identified in request"));
}
let segment = self
.segments
.get(parts[0])
.and_then(|list| list.first())
.ok_or_else(|| Hl7Error::new(format!("Field not available - {path}")))?;
let count = get_field_repetitions(segment, parts[1])
.map_err(|e| Hl7Error::new(format!("Field not available - {path} Error: {}", e.message)))?;
Ok(count > 1)
}
pub fn is_subcomponentized(&self, path: &str) -> Result<bool, Hl7Error> {
let parts: Vec<&str> = path.split('.').collect();
if !validate_value_format(&parts) {
return Err(Hl7Error::new("Request format is not valid"));
}
if parts.len() < 3 {
return Err(Hl7Error::new("Component not identified in request"));
}
let segment = self
.segments
.get(parts[0])
.and_then(|list| list.first())
.ok_or_else(|| Hl7Error::new(format!("Component not available - {path}")))?;
let field = get_field(segment, parts[1]).map_err(|e| {
Hl7Error::new(format!("Component not available - {path} Error: {}", e.message))
})?;
let ci = parse_index(parts[2])?;
let comp = field
.components
.get(ci)
.ok_or_else(|| Hl7Error::new(format!("Component not available - {path}")))?;
Ok(comp.is_subcomponentized)
}
pub fn add_new_segment(&mut self, mut segment: Segment) -> bool {
segment.sequence_no = self.segment_count;
self.segment_count += 1;
self.segments.entry(segment.name.clone()).or_default().push(segment);
true
}
pub fn remove_segment(&mut self, segment_name: &str, index: usize) -> bool {
if let Some(list) = self.segments.get_mut(segment_name)
&& index < list.len()
{
list.remove(index);
self.segment_count = self.segment_count.saturating_sub(1);
return true;
}
false
}
pub fn segments(&self) -> Vec<&Segment> {
self.segments_in_order()
}
pub fn segments_named(&self, segment_name: &str) -> Vec<&Segment> {
self.segments_in_order()
.into_iter()
.filter(|s| s.name == segment_name)
.collect()
}
pub fn default_segment(&self, segment_name: &str) -> Option<&Segment> {
self.segments_in_order().into_iter().find(|s| s.name == segment_name)
}
pub fn segments_named_mut(&mut self, segment_name: &str) -> Option<&mut Vec<Segment>> {
self.segments.get_mut(segment_name)
}
#[allow(clippy::too_many_arguments)]
pub fn add_segment_msh(
&mut self,
sending_application: &str,
sending_facility: &str,
receiving_application: &str,
receiving_facility: &str,
security: Option<&str>,
message_type: &str,
message_control_id: &str,
processing_id: &str,
version: &str,
) -> Result<(), Hl7Error> {
let date_string = helper::now_long_date();
let delim = self.encoding.field_delimiter;
let all = self.encoding.all_delimiters();
let seg_delim = self.encoding.segment_delimiter.clone();
let mut response = String::new();
response.push_str("MSH");
response.push_str(&all);
response.push(delim);
response.push_str(sending_application);
response.push(delim);
response.push_str(sending_facility);
response.push(delim);
response.push_str(receiving_application);
response.push(delim);
response.push_str(receiving_facility);
response.push(delim);
response.push_str(&self.encoding.encode(&date_string));
response.push(delim);
response.push_str(security.unwrap_or(""));
response.push(delim);
response.push_str(message_type);
response.push(delim);
response.push_str(message_control_id);
response.push(delim);
response.push_str(processing_id);
response.push(delim);
response.push_str(version);
response.push_str(&seg_delim);
let message = Message::parse_str(response, false)?;
let msh = message
.default_segment("MSH")
.ok_or_else(|| Hl7Error::new("MSH segment not found"))?
.clone();
self.add_new_segment(msh);
Ok(())
}
pub fn get_ack(&self, bypass_validation: bool) -> Option<Message> {
self.create_ack_message("AA", false, None, bypass_validation)
}
pub fn get_nack(&self, code: &str, err_msg: &str, bypass_validation: bool) -> Option<Message> {
self.create_ack_message(code, true, Some(err_msg), bypass_validation)
}
pub fn get_mllp(&self) -> Result<Vec<u8>, Hl7Error> {
Ok(helper::get_mllp(&self.serialize()?))
}
fn create_ack_message(
&self,
code: &str,
is_nack: bool,
err_msg: Option<&str>,
bypass_validation: bool,
) -> Option<Message> {
if self.message_structure == "ACK" {
return None;
}
let date_string = helper::now_long_date();
let msh = self.segments.get("MSH")?.first()?;
let delim = self.encoding.field_delimiter;
let all = self.encoding.all_delimiters();
let seg_delim = &self.encoding.segment_delimiter;
let field = |i: usize| -> String {
msh.fields.get(i).and_then(|f| f.value(&self.encoding)).unwrap_or_default()
};
let mut response = String::new();
response.push_str("MSH");
response.push_str(&all);
response.push(delim);
response.push_str(&field(4)); response.push(delim);
response.push_str(&field(5)); response.push(delim);
response.push_str(&field(2)); response.push(delim);
response.push_str(&field(3)); response.push(delim);
response.push_str(&date_string);
response.push(delim);
response.push(delim); response.push_str("ACK");
response.push(delim);
response.push_str(&self.message_control_id);
response.push(delim);
response.push_str(&self.processing_id);
response.push(delim);
response.push_str(&self.version);
response.push_str(seg_delim);
response.push_str("MSA");
response.push(delim);
response.push_str(code);
response.push(delim);
response.push_str(&self.message_control_id);
if is_nack {
response.push(delim);
response.push_str(err_msg.unwrap_or(""));
}
response.push_str(seg_delim);
Message::parse_str(response, bypass_validation).ok()
}
fn segments_in_order(&self) -> Vec<&Segment> {
let mut all: Vec<&Segment> = self.segments.values().flatten().collect();
all.sort_by_key(|s| s.sequence_no);
all
}
fn validate_message(&mut self) -> Result<bool, Hl7Error> {
self.validate_message_inner().map_err(|e| {
Hl7Error::with_code(
format!("Failed to validate the message with error - {}", e.message),
Hl7Error::BAD_MESSAGE,
)
})?;
Ok(true)
}
fn validate_message_inner(&mut self) -> Result<(), Hl7Error> {
if self.hl7_message.is_empty() {
return Err(Hl7Error::with_code("No Message Found", Hl7Error::BAD_MESSAGE));
}
if self.hl7_message.len() < 20 {
return Err(Hl7Error::with_code(
format!("Message Length too short: {} chars.", self.hl7_message.len()),
Hl7Error::BAD_MESSAGE,
));
}
if !self.hl7_message.starts_with("MSH") {
return Err(Hl7Error::with_code(
"MSH segment not found at the beginning of the message",
Hl7Error::BAD_MESSAGE,
));
}
self.encoding.evaluate_segment_delimiter(&self.hl7_message)?;
self.all_segments = helper::split_message(&self.hl7_message);
self.hl7_message =
self.all_segments.join(&self.encoding.segment_delimiter) + &self.encoding.segment_delimiter;
let first = &self.all_segments[0];
let field_delimiters: String = first.chars().skip(3).take(5).collect();
self.encoding.evaluate_delimiters(&field_delimiters)?;
self.decompose_multibyte_hex_sequences();
let fourth_char = self.hl7_message.chars().nth(3);
for segment in &self.all_segments {
if segment.trim().is_empty() {
continue;
}
let name: String = segment.chars().take(3).collect();
if !SEGMENT_REGEX.is_match(&name) {
return Err(Hl7Error::with_code(
format!("Invalid segment name found: {segment}"),
Hl7Error::BAD_MESSAGE,
));
}
if segment.chars().count() > 3 && segment.chars().nth(3) != fourth_char {
return Err(Hl7Error::with_code(
format!("Invalid segment found: {segment}"),
Hl7Error::BAD_MESSAGE,
));
}
}
let msh_line = &self.all_segments[0];
let field_sep_count = msh_line.chars().filter(|c| *c == self.encoding.field_delimiter).count();
if field_sep_count < 11 {
return Err(Hl7Error::with_code(
"MSH segment doesn't contain all the required fields",
Hl7Error::BAD_MESSAGE,
));
}
let msh_fields: Vec<&str> = msh_line.split(self.encoding.field_delimiter).collect();
if msh_fields.len() >= 12 {
let decoded = self.encoding.decode(msh_fields[11]);
self.version =
decoded.split(self.encoding.component_delimiter).next().unwrap_or("").to_string();
} else {
return Err(Hl7Error::with_code(
"HL7 version not found in the MSH segment",
Hl7Error::REQUIRED_FIELD_MISSING,
));
}
let msh_9 = self.encoding.decode(msh_fields[8]);
if msh_9.is_empty() {
return Err(Hl7Error::with_code(
"MSH.9 not available",
Hl7Error::UNSUPPORTED_MESSAGE_TYPE,
));
}
let comps: Vec<&str> = msh_9.split(self.encoding.component_delimiter).collect();
if comps.len() >= 3 {
self.message_structure = comps[2].to_string();
} else if !comps.is_empty() && comps[0] == "ACK" {
self.message_structure = "ACK".to_string();
} else if comps.len() == 2 {
self.message_structure = format!("{}_{}", comps[0], comps[1]);
} else {
return Err(Hl7Error::with_code(
"Message Type & Trigger Event value not found in message",
Hl7Error::UNSUPPORTED_MESSAGE_TYPE,
));
}
self.message_control_id = self.encoding.decode(msh_fields[9]);
if self.message_control_id.is_empty() {
return Err(Hl7Error::with_code(
"MSH.10 - Message Control ID not found",
Hl7Error::REQUIRED_FIELD_MISSING,
));
}
self.processing_id = self.encoding.decode(msh_fields[10]);
if self.processing_id.is_empty() {
return Err(Hl7Error::with_code(
"MSH.11 - Processing ID not found",
Hl7Error::REQUIRED_FIELD_MISSING,
));
}
Ok(())
}
fn equals(&self, other: &str) -> bool {
let seg_chars: Vec<char> = self.encoding.segment_delimiter.chars().collect();
let split = |s: &str| -> Vec<String> {
s.split(|c| seg_chars.contains(&c))
.filter(|p| !p.is_empty())
.map(str::to_string)
.collect()
};
let mut arr1 = split(&self.hl7_message);
let arr2 = split(other);
self.decode_hexa_sequences(&mut arr1, false);
arr1 == arr2
}
fn decompose_multibyte_hex_sequences(&mut self) {
if self.encoding.escape_character == '\0' || self.all_segments.is_empty() {
return;
}
let mut lines = std::mem::take(&mut self.all_segments);
let changed = self.decode_hexa_sequences(&mut lines, true);
self.all_segments = lines;
if changed {
self.hl7_message = self.all_segments.join(&self.encoding.segment_delimiter)
+ &self.encoding.segment_delimiter;
}
}
fn decode_hexa_sequences(&self, lines: &mut [String], decompose: bool) -> bool {
let esc = self.encoding.escape_character;
let pattern = format!(r"\x{{{0:X}}}X([0-9A-Fa-f]*)\x{{{0:X}}}", esc as u32);
let re = match Regex::new(&pattern) {
Ok(r) => r,
Err(_) => return false,
};
let mut changed = false;
for line in lines.iter_mut() {
if !line.contains(esc) {
continue;
}
let replaced = re
.replace_all(line, |caps: ®ex::Captures| {
let whole = &caps[0];
let hex = caps.get(1).map(|m| m.as_str()).unwrap_or("");
if decompose {
decompose_multibyte_hex(whole, hex, esc)
} else if !is_encoded_linebreak_byte(hex) {
HL7Encoding::decode_hex_string(hex)
} else {
whole.to_string()
}
})
.into_owned();
if &replaced != line {
*line = replaced;
changed = true;
}
}
changed
}
}
fn parse_index(s: &str) -> Result<usize, Hl7Error> {
s.parse::<usize>()
.ok()
.and_then(|n| n.checked_sub(1))
.ok_or_else(|| Hl7Error::new(format!("Invalid index: {s}")))
}
fn parse_field_index(index: &str) -> Result<(usize, usize), Hl7Error> {
let caps = FIELD_REGEX
.captures(index)
.ok_or_else(|| Hl7Error::new("Invalid field index"))?;
let field_index = caps[1]
.parse::<usize>()
.ok()
.and_then(|n| n.checked_sub(1))
.ok_or_else(|| Hl7Error::new("Invalid field index"))?;
let repetition = match caps.get(3) {
Some(m) => m
.as_str()
.parse::<usize>()
.ok()
.and_then(|n| n.checked_sub(1))
.ok_or_else(|| Hl7Error::new("Invalid field index"))?,
None => 0,
};
Ok((field_index, repetition))
}
fn get_field<'a>(segment: &'a Segment, index: &str) -> Result<&'a Field, Hl7Error> {
let (field_index, repetition) = parse_field_index(index)?;
let field = segment
.fields
.get(field_index)
.ok_or_else(|| Hl7Error::new("Field not available"))?;
if field.has_repetitions {
field
.repetitions
.get(repetition)
.ok_or_else(|| Hl7Error::new("Field repetition not available"))
} else if repetition == 0 {
Ok(field)
} else {
Err(Hl7Error::new("Field repetition not available"))
}
}
fn get_field_mut<'a>(segment: &'a mut Segment, index: &str) -> Result<&'a mut Field, Hl7Error> {
let (field_index, repetition) = parse_field_index(index)?;
let field = segment
.fields
.get_mut(field_index)
.ok_or_else(|| Hl7Error::new("Field not available"))?;
if field.has_repetitions {
field
.repetitions
.get_mut(repetition)
.ok_or_else(|| Hl7Error::new("Field repetition not available"))
} else if repetition == 0 {
Ok(field)
} else {
Err(Hl7Error::new("Field repetition not available"))
}
}
fn get_field_repetitions(segment: &Segment, index: &str) -> Result<usize, Hl7Error> {
let caps = match FIELD_REGEX.captures(index) {
Some(c) => c,
None => return Ok(0),
};
let field_index = caps[1]
.parse::<usize>()
.ok()
.and_then(|n| n.checked_sub(1))
.ok_or_else(|| Hl7Error::new("Invalid field index"))?;
let field = segment
.fields
.get(field_index)
.ok_or_else(|| Hl7Error::new("Field not available"))?;
if field.has_repetitions {
Ok(field.repetitions.len())
} else {
Ok(1)
}
}
fn validate_value_format(parts: &[&str]) -> bool {
if parts.is_empty() || !SEGMENT_REGEX.is_match(parts[0]) {
return false;
}
let mut is_valid = false;
for (i, part) in parts.iter().enumerate().skip(1) {
let matches = (i == 1 && FIELD_REGEX.is_match(part))
|| (i > 1 && OTHER_REGEX.is_match(part));
if matches {
is_valid = true;
} else {
return false;
}
}
is_valid
}
fn decompose_multibyte_hex(match_value: &str, hex: &str, esc: char) -> String {
if hex.len() <= 2 || !hex.len().is_multiple_of(2) || !contains_encoded_linebreak(hex) {
return match_value.to_string();
}
let mut result = String::with_capacity(match_value.len());
let mut i = 0;
while i + 2 <= hex.len() {
result.push(esc);
result.push('X');
result.push_str(&hex[i..i + 2]);
result.push(esc);
i += 2;
}
result
}
fn contains_encoded_linebreak(hex: &str) -> bool {
(0..hex.len() / 2).any(|i| is_encoded_linebreak_byte(&hex[i * 2..i * 2 + 2]))
}
fn is_encoded_linebreak_byte(hex: &str) -> bool {
matches!(hex, "0D" | "0A" | "0d" | "0a")
}