Skip to content

Commit

Permalink
feat #16 - Lots of work on the docs, and some general cleanup (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
wokket authored Sep 16, 2021
1 parent d287a51 commit e1b8456
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 65 deletions.
2 changes: 1 addition & 1 deletion benches/simple_parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
});
Expand Down
2 changes: 1 addition & 1 deletion examples/typed_segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl<'a> Clone for MshSegment<'a> {

/// Extracts header element for external use
pub fn msh<'a>(msg: &Message<'a>) -> Result<MshSegment<'a>, 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)
Expand Down
26 changes: 20 additions & 6 deletions src/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<&'a str>>,
pub subcomponents: Vec<Vec<Vec<&'a str>>>,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
}

Expand Down Expand Up @@ -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),
}
}
Expand All @@ -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),
}
}
Expand Down
120 changes: 85 additions & 35 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Segment<'a>>,
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::<Separators>(source).unwrap();
let segments: Vec<Segment<'a>> = 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<Vec<&Segment<'a>>, 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<Vec<&Segment<'a>>, 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<Vec<Vec<&'a str>>, 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)
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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<usize> {
indices[1..]
.iter()
Expand Down Expand Up @@ -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<Self, Self::Error> {
let delimiters = str::parse::<Separators>(source)?;
let separators = str::parse::<Separators>(source)?;

let segments: Result<Vec<Segment<'a>>, Hl7ParseError> = source
.split(delimiters.segment)
.map(|line| Segment::parse(line, &delimiters))
let possible: Vec<Result<Segment, Hl7ParseError>> = 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::<Segment>::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)
}
}

Expand Down Expand Up @@ -264,20 +315,19 @@ 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(())
}

#[test]
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();

Expand Down
Loading

0 comments on commit e1b8456

Please sign in to comment.