diff --git a/benches/simple_parse.rs b/benches/simple_parse.rs index 9f430b3..f7ab924 100644 --- a/benches/simple_parse.rs +++ b/benches/simple_parse.rs @@ -19,7 +19,7 @@ fn get_segments_by_name(c: &mut Criterion) { let m = Message::try_from(get_sample_message()).unwrap(); b.iter(|| { - let _segs = m.segments_by_name("OBR").unwrap(); + let _segs = m.segments_by_identifier("OBR").unwrap(); //assert!(segs.len() == 1); }) }); diff --git a/examples/typed_segment.rs b/examples/typed_segment.rs index b9ea946..fc40889 100644 --- a/examples/typed_segment.rs +++ b/examples/typed_segment.rs @@ -96,7 +96,7 @@ impl<'a> Clone for MshSegment<'a> { /// Extracts header element for external use pub fn msh<'a>(msg: &Message<'a>) -> Result, Hl7ParseError> { - let seg = msg.segments_by_name("MSH").unwrap()[0]; + let seg = msg.segments_by_identifier("MSH").unwrap()[0]; let segment = MshSegment::parse(seg.source, &msg.get_separators()).expect("Failed to parse MSH segment"); Ok(segment) diff --git a/src/fields.rs b/src/fields.rs index 5364356..2b66d05 100644 --- a/src/fields.rs +++ b/src/fields.rs @@ -8,7 +8,7 @@ use std::ops::Index; #[derive(Debug, PartialEq)] pub struct Field<'a> { pub source: &'a str, - pub delims: Separators, + delims: Separators, pub repeats: Vec<&'a str>, pub components: Vec>, pub subcomponents: Vec>>, @@ -44,7 +44,9 @@ impl<'a> Field<'a> { Ok(field) } - /// Used to hide the removal of NoneError for #2... If passed `Some()` value it returns a field with that value. If passed `None() it returns an `Err(Hl7ParseError::MissingRequiredValue{})` + /// Used to hide the removal of NoneError for #2... + /// If passed `Some()` value it returns a field with that value. + /// If passed `None` it returns an `Err(Hl7ParseError::MissingRequiredValue{})` pub fn parse_mandatory( input: Option<&'a str>, delims: &Separators, @@ -57,6 +59,10 @@ impl<'a> Field<'a> { /// Converts a possibly blank string into a possibly blank field! /// Note this handles optional fields, not the nul (`""`) value. + /// Specfically: + /// - If passed `None` it returns `Ok(None)` + /// - If passed `Some("")` it returns `Ok(None)` + /// - If Passed `Some(real_value)` it returns `Ok(Some(Field))` pub fn parse_optional( input: Option<&'a str>, delims: &Separators, @@ -69,14 +75,22 @@ impl<'a> Field<'a> { } /// Compatibility method to get the underlying value of this field. + /// NOTE that this is deprecated as a duplicate of [`Field::as_str()`]. + /// + /// This function was chosen as the deprecation victim as a future version of the library may include strongly typed Field's (eg DateTime) + /// at which point a generically typed 'value()' function will need to be implemented. #[inline] + #[deprecated( + since = "0.6.0", + note = "This function is a duplicate of the `as_str()` function which should be used instead." + )] pub fn value(&self) -> &'a str { self.source } - /// Export value to str + /// Gets the raw string value that was used to create this field. This method does not allocate. #[inline] - pub fn as_str(&self) -> &'a str { + pub fn as_str(&'a self) -> &'a str { self.source } @@ -279,7 +293,7 @@ mod tests { //an empty string (as seen when `split()`ing) should be none match Field::parse_optional(Some("xxx"), &d) { - Ok(Some(field)) => assert_eq!(field.value(), "xxx"), + Ok(Some(field)) => assert_eq!(field.as_str(), "xxx"), _ => assert!(false), } } @@ -289,7 +303,7 @@ mod tests { let d = Separators::default(); match Field::parse_mandatory(Some("xxx"), &d) { - Ok(field) => assert_eq!(field.value(), "xxx"), + Ok(field) => assert_eq!(field.as_str(), "xxx"), _ => assert!(false), } } diff --git a/src/message.rs b/src/message.rs index d988122..5f96f59 100644 --- a/src/message.rs +++ b/src/message.rs @@ -7,47 +7,76 @@ use std::ops::Index; /// A Message is an entire HL7 message parsed into it's constituent segments, fields, repeats and subcomponents, /// and it consists of (1 or more) Segments. -/// Message parses the source string into &str slices (minimising copying) +/// Message parses the source string into `&str` slices (minimising copying) and can be created using either the [`Message::new()`] function or `TryFrom::try_from()` impl. +/// ## Example: +/// ``` +/// # use rusthl7::Hl7ParseError; +/// # use rusthl7::Message; +/// use std::convert::TryFrom; +/// # fn main() -> Result<(), Hl7ParseError> { +/// let source = "MSH|^~\\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4\rOBR|1|Foo\rOBR|2|Bar"; +/// let m = Message::new(source); // Note that this method can panic +/// let result = Message::try_from(source); // while try_from() returns a `Result` +/// assert!(result.is_ok()); +/// # Ok(()) +/// # } +/// ``` #[derive(Debug, PartialEq)] pub struct Message<'a> { - pub source: &'a str, + source: &'a str, pub segments: Vec>, separators: Separators, } impl<'a> Message<'a> { + /// Takes the source HL7 string and parses it into a message. Segments + /// and other data are slices (`&str`) into the source HL7 for minimal (preferably 0) copying. + /// ⚠ If an error occurs this method will panic (for back-compat reasons)! For the preferred non-panicing alternative import the `std::convert::TryFrom` trait and use the `try_from()` function. ⚠ + /// ## Example: + /// ``` + /// # use rusthl7::Hl7ParseError; + /// # use rusthl7::Message; + /// # use std::convert::TryFrom; + /// # fn main() -> Result<(), Hl7ParseError> { + /// let m = Message::try_from("MSH|^~\\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4")?; + /// # Ok(()) + /// # } + /// ``` pub fn new(source: &'a str) -> Message<'a> { - let separators = str::parse::(source).unwrap(); - let segments: Vec> = source - .split(separators.segment) - .map(|line| Segment::parse(line, &separators).unwrap()) - .collect(); - - Message { - source, - segments, - separators, - } + Message::try_from(source).unwrap() } - /// Extracts generic elements for external use by matching first field to name - pub fn segments_by_name(&self, name: &str) -> Result>, Hl7ParseError> { + /// Queries for segments of the given type (i.e. matches by identifier, or name), returning a set of 0 or more segments. + /// ## Example: + /// ``` + /// # use rusthl7::Hl7ParseError; + /// # use rusthl7::Message; + /// # fn main() -> Result<(), Hl7ParseError> { + /// let source = "MSH|^~\\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4\rOBR|1|Foo\rOBR|2|Bar"; + /// let m = Message::new(source); + /// let obr_segments = m.segments_by_identifier("OBR")?; + /// assert_eq!(obr_segments.len(), 2); + /// # Ok(()) + /// # } + /// ``` + pub fn segments_by_identifier(&self, name: &str) -> Result>, Hl7ParseError> { let found: Vec<&Segment<'a>> = self .segments .iter() - .filter(|s| s.fields[0].source == name) + .filter(|s| s.identifier() == name) .collect(); Ok(found) } /// Present input vectors of &generics to vectors of &str pub fn segments_to_str_vecs( - segments: Vec<&Segment<'a>>, + segments: Vec<&'a Segment<'a>>, ) -> Result>, Hl7ParseError> { let vecs = segments .iter() - .map(|s| s.fields.iter().map(|f| f.value()).collect()) + .map(|s| s.fields.iter().map(|f| f.as_str()).collect()) .collect(); + Ok(vecs) } @@ -69,7 +98,9 @@ impl<'a> Message<'a> { self.source } - /// Gets the delimiter information for this Message + /// Gets the delimiter information for this Message. + /// Remember that in HL7 _each individual message_ can have unique characters as separators between fields, repeats, components and sub-components, and so this is a per-message value. + /// This method does not allocate pub fn get_separators(&self) -> Separators { self.separators } @@ -102,7 +133,7 @@ impl<'a> Message<'a> { /// Parse query/index string to fill-in missing values. /// Required when conumer requests "PID.F3.C1" to pass integers down /// to the usize indexers at the appropriate positions - pub fn parse_query_string(query: &str) -> Vec<&str> { + fn parse_query_string(query: &str) -> Vec<&str> { fn query_idx_pos(indices: &[&str], idx: &str) -> Option { indices[1..] .iter() @@ -160,23 +191,43 @@ impl<'a> Message<'a> { impl<'a> TryFrom<&'a str> for Message<'a> { type Error = Hl7ParseError; - /// Takes the source HL7 string and parses it into this message. Segments - /// and other data are slices (`&str`) into the source HL7 + /// Takes the source HL7 string and parses it into a message. Segments + /// and other data are slices (`&str`) into the source HL7 for minimal (preferably 0) copying. + /// ## Example: + /// ``` + /// # use rusthl7::Hl7ParseError; + /// # use rusthl7::Message; + /// # use std::convert::TryFrom; + /// # fn main() -> Result<(), Hl7ParseError> { + /// let m = Message::try_from("MSH|^~\\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4")?; + /// # Ok(()) + /// # } + /// ``` fn try_from(source: &'a str) -> Result { - let delimiters = str::parse::(source)?; + let separators = str::parse::(source)?; - let segments: Result>, Hl7ParseError> = source - .split(delimiters.segment) - .map(|line| Segment::parse(line, &delimiters)) + let possible: Vec> = source + .split(separators.segment) + .map(|line| Segment::parse(line, &separators)) .collect(); - let msg = Message { + // TODO: This `foreach s { s? }` equivalent seems unrusty.... find a better way + + let mut segments = Vec::::new(); + for s in possible { + match s { + Ok(s) => segments.push(s), + Err(e) => return Err(e), + } + } + + let m = Message { source, - segments: segments?, - separators: delimiters, + segments, + separators, }; - Ok(msg) + Ok(m) } } @@ -264,11 +315,10 @@ mod tests { } #[test] - fn ensure_segments_are_found() -> Result<(), Hl7ParseError> { + fn ensure_missing_segments_are_not_found() -> Result<(), Hl7ParseError> { let hl7 = "MSH|^~\\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4\rOBR|segment"; let msg = Message::try_from(hl7)?; - - assert_eq!(msg.segments_by_name("OBR").unwrap().len(), 1); + assert_eq!(msg.segments_by_identifier("EVN").unwrap().len(), 0); Ok(()) } @@ -276,8 +326,8 @@ mod tests { fn ensure_segments_convert_to_vectors() -> Result<(), Hl7ParseError> { let hl7 = "MSH|^~\\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4\rOBR|segment"; let msg = Message::try_from(hl7)?; - let segs = msg.segments_by_name("OBR")?; - let sval = segs.first().unwrap().fields.first().unwrap().value(); + let segs = msg.segments_by_identifier("OBR")?; + let sval = segs.first().unwrap().fields.first().unwrap().as_str(); let vecs = Message::segments_to_str_vecs(segs).unwrap(); let vval = vecs.first().unwrap().first().unwrap(); diff --git a/src/segments.rs b/src/segments.rs index 33c5ed7..61cb3e2 100644 --- a/src/segments.rs +++ b/src/segments.rs @@ -1,4 +1,4 @@ -use super::{fields::Field, separators::Separators, Hl7ParseError}; +use crate::{Field, Hl7ParseError, Separators}; use std::fmt::Display; use std::ops::Index; @@ -6,35 +6,69 @@ use std::ops::Index; #[derive(Debug, PartialEq, Clone)] pub struct Segment<'a> { pub source: &'a str, - pub delim: char, + delim: char, pub fields: Vec>, } impl<'a> Segment<'a> { - /// Convert the given line of text into a Segment. + /// Convert the given line of text into a Segment. NOTE: This is not normally needed to be called directly by + /// consumers but is used indirectly via [`crate::Message::new()`]. pub fn parse>( input: S, delims: &Separators, ) -> Result, Hl7ParseError> { - let input = input.into(); + // non-generic inner to reduce compile times/code bloat + fn inner<'a>(input: &'a str, delims: &Separators) -> Result, Hl7ParseError> { + let fields: Result>, Hl7ParseError> = input + .split(delims.field) + .map(|line| Field::parse(line, delims)) + .collect(); + + let fields = fields?; + let seg = Segment { + source: input, + delim: delims.segment, + fields, + }; + Ok(seg) + } - let fields: Result>, Hl7ParseError> = input - .split(delims.field) - .map(|line| Field::parse(line, delims)) - .collect(); + let input = input.into(); + inner(input, delims) + } - let fields = fields?; - let seg = Segment { - source: input, - delim: delims.segment, - fields, - }; - Ok(seg) + /// Get the identifier (ie type, or name) for this segment. + /// ## Example: + /// ``` + /// # use rusthl7::Hl7ParseError; + /// # use rusthl7::{Segment, Separators}; + /// # fn main() -> Result<(), Hl7ParseError> { + /// let segment = Segment::parse("OBR|field1|field2", &Separators::default())?; + /// assert_eq!("OBR", segment.identifier()); + /// # Ok(()) + /// # } + /// ``` + /// eg a segment `EVN||200708181123||` has an identifer of `EVN`. + pub fn identifier(&self) -> &'a str { + self.fields[0].source } - /// Export source to str + /// Returns the original `&str` used to initialise this Segment. This method does not allocate. + /// ## Example: + /// ``` + /// # use rusthl7::Hl7ParseError; + /// # use rusthl7::Message; + /// # use std::convert::TryFrom; + /// # fn main() -> Result<(), Hl7ParseError> { + /// let source = "MSH|^~\\&|GHH LAB|ELAB-3\rOBR|field1|field2"; + /// let m = Message::try_from(source)?; + /// let obr = &m.segments[1]; + /// assert_eq!("OBR|field1|field2", obr.as_str()); + /// # Ok(()) + /// # } + /// ``` #[inline] - pub fn as_str(&self) -> &'a str { + pub fn as_str(&'a self) -> &'a str { self.source } @@ -74,7 +108,7 @@ impl<'a> Segment<'a> { } impl<'a> Display for Segment<'a> { - /// Required for to_string() and other formatter consumers + /// Required for to_string() and other formatter consumers. This returns the source string that represents the segment. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.source) } @@ -120,7 +154,6 @@ impl<'a> Index<(usize, usize, usize)> for Segment<'a> { impl<'a> Index<&str> for Segment<'a> { type Output = &'a str; /// Access Field as string reference - #[cfg(feature = "string_index")] fn index(&self, fidx: &str) -> &Self::Output { let sections = fidx.split('.').collect::>(); let stringnum = sections[0] @@ -157,7 +190,6 @@ impl<'a> Index for Segment<'a> { type Output = &'a str; /// Access Segment, Field, or sub-field string references by string index - #[cfg(feature = "string_index")] fn index(&self, idx: String) -> &Self::Output { &self[idx.as_str()] } diff --git a/src/separators.rs b/src/separators.rs index a81e8a4..2e2a609 100644 --- a/src/separators.rs +++ b/src/separators.rs @@ -4,16 +4,20 @@ use std::str::FromStr; /// A helper struct to store the separator (delimiter) characters used to parse this message. /// Note that HL7 allows each _message_ to define it's own separators, although most messages -/// use a default set (available from `Separators::default()`) +/// use a default set (available from [`Separators::default()`]) #[derive(Debug, PartialEq, Clone, Copy)] pub struct Separators { /// constant value, spec fixed to '\r' (ASCII 13, 0x0D) pub segment: char, + /// Field separator char, defaults to `|` pub field: char, + /// Field repeat separator char, defaults to `~` pub repeat: char, + /// Component separator char, defaults to `^` pub component: char, + /// Sub-Component separator char, defaults to `&` pub subcomponent: char, - + /// Character used to wrap an [`EscapeSequence`], defaults to `\` (a single back slash) pub escape_char: char, }