use crate::Error;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Terminator {
#[default]
Cr,
Lf,
CrLf,
}
impl Terminator {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Terminator::Cr => "\r",
Terminator::Lf => "\n",
Terminator::CrLf => "\r\n",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Separators {
pub field: char,
pub component: char,
pub repetition: char,
pub escape: char,
pub subcomponent: char,
pub truncation: Option<char>,
}
impl Default for Separators {
fn default() -> Self {
Separators {
field: '|',
component: '^',
repetition: '~',
escape: '\\',
subcomponent: '&',
truncation: None,
}
}
}
impl Separators {
pub fn from_header(line: &str) -> Result<Separators, Error> {
let mut chars = line.chars().skip(3);
let field = chars
.next()
.ok_or_else(|| Error::BadHeader("no field separator after the segment name".into()))?;
let encoding: Vec<char> = chars.take_while(|&c| c != field).take(5).collect();
let default = Separators::default();
let separators = Separators {
field,
component: encoding.first().copied().unwrap_or(default.component),
repetition: encoding.get(1).copied().unwrap_or(default.repetition),
escape: encoding.get(2).copied().unwrap_or(default.escape),
subcomponent: encoding.get(3).copied().unwrap_or(default.subcomponent),
truncation: encoding.get(4).copied(),
};
separators.validate()?;
Ok(separators)
}
pub fn validate(&self) -> Result<(), Error> {
for (role, c) in self.roles() {
if c.is_alphanumeric() {
return Err(Error::BadHeader(format!(
"{role} separator {c:?} is alphanumeric"
)));
}
if c == '\r' || c == '\n' {
return Err(Error::BadHeader(format!(
"{role} separator {c:?} would end a segment"
)));
}
}
let roles = self.roles();
for (i, (role, c)) in roles.iter().enumerate() {
if let Some((other, _)) = roles[..i].iter().find(|(_, d)| d == c) {
return Err(Error::BadHeader(format!(
"{other} and {role} separators are both {c:?}"
)));
}
}
Ok(())
}
#[must_use]
pub fn is_delimiter(&self, c: char) -> bool {
self.roles().iter().any(|&(_, d)| d == c)
}
#[must_use]
pub fn encoding_characters(&self) -> String {
let mut s = String::with_capacity(5);
s.push(self.component);
s.push(self.repetition);
s.push(self.escape);
s.push(self.subcomponent);
if let Some(truncation) = self.truncation {
s.push(truncation);
}
s
}
fn roles(&self) -> Vec<(&'static str, char)> {
let mut roles = vec![
("field", self.field),
("component", self.component),
("repetition", self.repetition),
("escape", self.escape),
("subcomponent", self.subcomponent),
];
if let Some(truncation) = self.truncation {
roles.push(("truncation", truncation));
}
roles
}
}
impl fmt::Display for Separators {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.field, self.encoding_characters())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_recommended_delimiters() {
let seps = Separators::from_header(r"MSH|^~\&|LAB|||20260815||ADT^A08").unwrap();
assert_eq!(seps, Separators::default());
assert_eq!(seps.encoding_characters(), r"^~\&");
assert_eq!(seps.to_string(), r"|^~\&");
}
#[test]
fn reads_custom_delimiters() {
let seps = Separators::from_header("MSH#*!?@#LAB").unwrap();
assert_eq!(
seps,
Separators {
field: '#',
component: '*',
repetition: '!',
escape: '?',
subcomponent: '@',
truncation: None,
}
);
}
#[test]
fn reads_the_truncation_character() {
let seps = Separators::from_header(r"MSH|^~\&#|LAB").unwrap();
assert_eq!(seps.truncation, Some('#'));
assert_eq!(seps.encoding_characters(), r"^~\&#");
}
#[test]
fn fills_in_omitted_encoding_characters() {
let seps = Separators::from_header("MSH|^~|LAB").unwrap();
assert_eq!(seps.escape, '\\');
assert_eq!(seps.subcomponent, '&');
}
#[test]
fn rejects_unusable_delimiters() {
assert!(Separators::from_header("MSH").is_err());
assert!(Separators::from_header("MSHX^~\\&").is_err());
assert!(Separators::from_header(r"MSH|^^\&|LAB").is_err());
assert!(Separators::from_header("MSH|^~&&|LAB").is_err());
assert!(
Separators {
component: '\r',
..Separators::default()
}
.validate()
.is_err()
);
}
#[test]
fn stops_reading_encoding_characters_at_the_field_separator() {
let seps = Separators::from_header(r"MSH|^~\|LAB").unwrap();
assert_eq!(seps.escape, '\\');
assert_eq!(seps.subcomponent, '&');
assert_eq!(seps.truncation, None);
}
#[test]
fn recognizes_its_own_delimiters() {
let seps = Separators::default();
assert!(seps.is_delimiter('|'));
assert!(seps.is_delimiter('&'));
assert!(!seps.is_delimiter('#'));
assert!(!seps.is_delimiter('A'));
}
}