hl7-net 0.1.0

Lightweight HL7 V2 parser/writer, ported from the Efferent HL7-V2 .NET library
Documentation
use crate::encoding::HL7Encoding;
use crate::error::Hl7Error;
use crate::field::Field;

/// An HL7 segment: a name (e.g. `MSH`, `PID`) followed by a list of [`Field`]s.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Segment {
    raw: String,
    /// The segment name (first three characters).
    pub name: String,
    /// The fields of the segment. For `MSH`, index 0 is the field-delimiter field
    /// and index 1 is the encoding-characters field.
    pub fields: Vec<Field>,
    /// Order in which the segment appeared in the original message.
    pub(crate) sequence_no: usize,
}

impl Segment {
    /// Creates an empty, named segment.
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            raw: String::new(),
            name: name.into(),
            fields: Vec::new(),
            sequence_no: 0,
        }
    }

    /// Parses a segment from a raw line, applying the MSH special-casing for the
    /// delimiter fields.
    pub fn parse(raw: &str, enc: &HL7Encoding) -> Result<Self, Hl7Error> {
        if raw.chars().count() < 3 {
            return Err(Hl7Error::with_code(
                format!("Invalid segment (too short): {raw}"),
                Hl7Error::BAD_MESSAGE,
            ));
        }

        let name: String = raw.chars().take(3).collect();
        let is_msh = name == "MSH";

        let parts: Vec<&str> = raw.split(enc.field_delimiter).collect();
        let mut fields = Vec::with_capacity(parts.len());

        for (i, part) in parts.iter().enumerate().skip(1) {
            let field = if is_msh && i == 1 {
                Field::new_delimiters(part)
            } else {
                Field::parse(part, enc)
            };
            fields.push(field);
        }

        if is_msh {
            // MSH-1 is the field delimiter itself.
            fields.insert(0, Field::new_delimiters(&enc.field_delimiter.to_string()));
        }

        Ok(Self { raw: raw.to_string(), name, fields, sequence_no: 0 })
    }

    /// A deep copy of the segment.
    pub fn deep_copy(&self) -> Segment {
        self.clone()
    }

    /// The raw value, or `None` when "present but null".
    pub fn raw(&self, enc: &HL7Encoding) -> Option<&str> {
        if self.raw == enc.present_but_null {
            None
        } else {
            Some(&self.raw)
        }
    }

    /// The decoded value of the whole segment line.
    pub fn value(&self, enc: &HL7Encoding) -> Option<String> {
        if self.raw == enc.present_but_null {
            None
        } else {
            Some(enc.decode(&self.raw))
        }
    }

    /// The 0-based sequence number of the segment within its message.
    pub fn sequence_no(&self) -> usize {
        self.sequence_no
    }

    /// Appends an empty field.
    pub fn add_empty_field(&mut self, enc: &HL7Encoding) {
        self.add_new_field(Field::parse("", enc));
    }

    /// Appends a field parsed from `content`.
    pub fn add_new_field_value(&mut self, content: &str, enc: &HL7Encoding) {
        self.add_new_field(Field::parse(content, enc));
    }

    /// Appends a field.
    pub fn add_new_field(&mut self, field: Field) {
        self.fields.push(field);
    }

    /// Inserts a field at a 1-based position, padding with blank fields when the
    /// position is beyond the current length.
    pub fn add_new_field_at(&mut self, field: Field, position: usize, enc: &HL7Encoding) {
        let index = position.saturating_sub(1);

        if index < self.fields.len() {
            self.fields[index] = field;
        } else {
            while self.fields.len() < index {
                self.fields.push(Field::parse("", enc));
            }
            self.fields.push(field);
        }
    }

    /// Returns the field at the given 1-based position.
    pub fn field(&self, position: usize) -> Result<&Field, Hl7Error> {
        position
            .checked_sub(1)
            .and_then(|i| self.fields.get(i))
            .ok_or_else(|| Hl7Error::new("Field not available"))
    }

    /// Serializes the segment into `out`.
    pub fn serialize(&self, out: &mut String, enc: &HL7Encoding) {
        out.push_str(&self.name);

        if !self.fields.is_empty() {
            out.push(enc.field_delimiter);
        }

        // For MSH, field 0 is the field delimiter (already written above).
        let start = if self.name == "MSH" { 1 } else { 0 };

        for i in start..self.fields.len() {
            if i > start {
                out.push(enc.field_delimiter);
            }
            self.fields[i].serialize(out, enc);
        }

        out.push_str(&enc.segment_delimiter);
    }
}