Skip to content

Commit

Permalink
Merge pull request #24 from wokket/feat/15-query-selector
Browse files Browse the repository at this point in the history
Work pending for 0.5 release
  • Loading branch information
wokket authored Aug 13, 2021
2 parents a41b3d5 + ee34aeb commit 37e27d5
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 49 deletions.
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# Changelog

## 0.5.0
- Add `query` functions to replace the string based `Index` impls in the version version. These are functionally identical to the string `Index` implementations, but avoid some lifetime issues (returning `&&str`) and have visible documentation.

## 0.4.0
- Large change (thanks @sempervictus) to allow querying of message content by both numerical indexer and dot-notation string indexers
- Note that the string indexers will be replaced with a normal function call in a future release.

## 0.3.0
- Changes from @sempervictus to expose internal values again
- Extensive work by @sempervictus to expose the segments/fields as collections (which I hadn't got back to after the re-write to slices.)

## 0.2.0
- Re-write to avoid excessive string cloning by operating on slices of the source HL7
- Re-Write to not expose cloned/copied vecs of vecs everywhere. We have all the data in a single string slice to begin with so lets return slices from that.

## 0.1.0
- Initial string.clone() heavy library, nothing to see here...
- Initial string.clone() heavy library, nothing to see here...
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "rust-hl7"
version = "0.4.0"
authors = ["wokket <wokket@gmail.com>"]
version = "0.5.0-pre"
authors = ["wokket <github@wokket.com>"]
edition = "2018"
description = "HL7 Parser and object builder? query'er? - experimental only at any rate"
license = "MIT OR Apache-2.0"
Expand Down
67 changes: 59 additions & 8 deletions benches/simple_parse.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,77 @@
use std::convert::TryFrom;
use criterion::{criterion_group, criterion_main, Criterion};
use rusthl7::{message::*, segments::Segment};
use std::convert::TryFrom;

fn get_sample_message() -> &'static str {
"MSH|^~\\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4\rPID|||555-44-4444||EVERYWOMAN^EVE^E^^^^L|JONES|19620320|F|||153 FERNWOOD DR.^^STATESVILLE^OH^35292||(206)3345232|(206)752-121||||AC555444444||67-A4335^OH^20030520\rOBR|1|845439^GHH OE|1045813^GHH LAB|15545^GLUCOSE|||200202150730|||||||||555-55-5555^PRIMARY^PATRICIA P^^^^MD^^|||||||||F||||||444-44-4444^HIPPOCRATES^HOWARD H^^^^MD\rOBX|1|SN|1554-5^GLUCOSE^POST 12H CFST:MCNC:PT:SER/PLAS:QN||^182|mg/dl|70_105|H|||F"
}

fn message_parse(c: &mut Criterion) {

c.bench_function("oru parse", |b| {

c.bench_function("ORU parse", |b| {
b.iter(|| {
let _ = Message::try_from(get_sample_message()).unwrap();
})
});
}

fn get_segments_by_name(c: &mut Criterion) {
c.bench_function("Get Segment By Name", |b| {
let m = Message::try_from(get_sample_message()).unwrap();

b.iter(|| {
let _segs = m.generic_segments_by_name("OBR").unwrap();
//assert!(segs.len() == 1);
})
});
}

fn get_msh_and_read_field(c: &mut Criterion) {
c.bench_function("Read Field from MSH (variable)", |b| {
let m = Message::try_from(get_sample_message()).unwrap();

b.iter(|| {
let m = Message::try_from(get_sample_message()).unwrap();
let seg = m.segments.first();

if let Some(Segment::MSH(msh)) = seg {
let _app = msh.msh_3_sending_application.as_ref().unwrap();
//println!("{}", _app.value());
let _app = msh.msh_3_sending_application.as_ref().unwrap(); // direct variable access
//println!("{}", _app.value());
}
})
});
}

criterion_group!(benches, message_parse);
fn get_pid_and_read_field_via_vec(c: &mut Criterion) {
c.bench_function("Read Field from PID (lookup)", |b| {
let m = Message::try_from(get_sample_message()).unwrap();

b.iter(|| {
let seg = &m.segments[1];

if let Segment::Generic(pid) = seg {
let _field = pid[3];
assert_eq!(_field, "555-44-4444"); // lookup from vec
}
})
});
}

fn get_pid_and_read_field_via_query(c: &mut Criterion) {
c.bench_function("Read Field from PID (query)", |b| {
let m = Message::try_from(get_sample_message()).unwrap();

b.iter(|| {
let _val = m.query("PID.F3"); // query via Message
assert_eq!(_val, "555-44-4444"); // lookup from vec
})
});
}

criterion_group!(
benches,
message_parse,
get_segments_by_name,
get_msh_and_read_field,
get_pid_and_read_field_via_vec,
get_pid_and_read_field_via_query
);
criterion_main!(benches);
75 changes: 61 additions & 14 deletions src/fields/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use super::separators::Separators;
use super::*;
use std::fmt::Display;
use std::ops::Index;

/// Represents a single field inside the HL7. Note that fields can include repeats, components and sub-components.
/// See [the spec](http://www.hl7.eu/HL7v2x/v251/std251/ch02.html#Heading13) for more info
#[derive(Debug, PartialEq)]

pub struct Field<'a> {
pub source: &'a str,
pub delims: Separators,
Expand All @@ -13,7 +15,11 @@ pub struct Field<'a> {

impl<'a> Field<'a> {
/// Convert the given line of text into a field.
pub fn parse(input: &'a str, delims: &Separators) -> Result<Field<'a>, Hl7ParseError> {
pub fn parse<S: Into<&'a str>>(
input: S,
delims: &Separators,
) -> Result<Field<'a>, Hl7ParseError> {
let input = input.into();
let components = input.split(delims.component).collect::<Vec<&'a str>>();
let subcomponents = components
.iter()
Expand All @@ -26,10 +32,11 @@ impl<'a> Field<'a> {
components,
subcomponents,
};

Ok(field)
}

/// Used to hide the removal of NoneError for #2... If passed `Some()` value it retursn 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 @@ -54,25 +61,63 @@ impl<'a> Field<'a> {
}

/// Compatibility method to get the underlying value of this field.
#[inline]
pub fn value(&self) -> &'a str {
self.source
}

/// Export value to str
#[inline]
pub fn as_str(&self) -> &'a str {
self.source
}

/// Access string reference of a Field component by String index
/// Adjust the index by one as medical people do not count from zero
pub fn query<'b, S>(&self, sidx: S) -> &'a str
where
S: Into<&'b str>,
{
let sidx = sidx.into();
let parts = sidx.split('.').collect::<Vec<&str>>();

if parts.len() == 1 {
let stringnums = parts[0]
.chars()
.filter(|c| c.is_digit(10))
.collect::<String>();
let idx: usize = stringnums.parse().unwrap();

self[idx - 1]
} else if parts.len() == 2 {
let stringnums = parts[0]
.chars()
.filter(|c| c.is_digit(10))
.collect::<String>();

let idx0: usize = stringnums.parse().unwrap();

let stringnums = parts[1]
.chars()
.filter(|c| c.is_digit(10))
.collect::<String>();

let idx1: usize = stringnums.parse().unwrap();

self[(idx0 - 1, idx1 - 1)]
} else {
""
}
}
}

use std::fmt::Display;
impl<'a> Display for Field<'a> {
/// Required for to_string() and other formatter consumers
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.source)
}
}

use std::ops::Index;
impl<'a> Clone for Field<'a> {
/// Creates a new Message object using a clone of the original's source
fn clone(&self) -> Self {
Expand All @@ -85,8 +130,9 @@ impl<'a> Index<usize> for Field<'a> {
type Output = &'a str;
fn index(&self, idx: usize) -> &Self::Output {
if idx > self.components.len() - 1 {
return &"";
return &""; //TODO: We're returning &&str here which doesn't seem right?!?
}

&self.components[idx]
}
}
Expand All @@ -96,16 +142,17 @@ impl<'a> Index<(usize, usize)> for Field<'a> {
type Output = &'a str;
fn index(&self, idx: (usize, usize)) -> &Self::Output {
if idx.0 > self.components.len() - 1 || idx.1 > self.subcomponents[idx.0].len() - 1 {
return &"";
return &""; //TODO: We're returning &&str here which doesn't seem right?!?
}

&self.subcomponents[idx.0][idx.1]
}
}

/// DEPRECATED. Access string reference of a Field component by String index
/// Adjust the index by one as medical people do not count from zero
#[allow(useless_deprecated)]
#[deprecated(note="This will be removed in a future version")]
#[deprecated(note = "This will be removed in a future version")]
impl<'a> Index<String> for Field<'a> {
type Output = &'a str;
fn index(&self, sidx: String) -> &Self::Output {
Expand Down Expand Up @@ -146,7 +193,7 @@ impl<'a> Index<&str> for Field<'a> {

/// DEPRECATED. Access Segment, Field, or sub-field string references by string index
#[allow(useless_deprecated)]
#[deprecated(note="This will be removed in a future version")]
#[deprecated(note = "This will be removed in a future version")]
fn index(&self, idx: &str) -> &Self::Output {
&self[String::from(idx)]
}
Expand Down Expand Up @@ -213,6 +260,7 @@ mod tests {
fn test_parse_components() {
let d = Separators::default();
let f = Field::parse_mandatory(Some("xxx^yyy"), &d).unwrap();

assert_eq!(f.components.len(), 2)
}

Expand Down Expand Up @@ -250,10 +298,9 @@ mod tests {
let d = Separators::default();
let f = Field::parse_mandatory(Some("xxx^yyy&zzz"), &d).unwrap();
let idx0 = String::from("R2");
let idx1 = String::from("R2.C2");
let oob = String::from("R2.C3");
assert_eq!(f[idx0.clone()], "yyy&zzz");
assert_eq!(f[idx1], "zzz");
assert_eq!(f[oob], "");
let oob = "R2.C3";
assert_eq!(f.query(&*idx0), "yyy&zzz");
assert_eq!(f.query("R2.C2"), "zzz");
assert_eq!(f.query(oob), "");
}
}
73 changes: 70 additions & 3 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,64 @@ impl<'a> Message<'a> {
Ok(vecs)
}

/// Export source to str
/// Returns the source string slice used to create this Message initially. This method does not allocate.
/// ## Example:
/// ```
/// # use rusthl7::Hl7ParseError;
/// # use rusthl7::message::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";
/// let m = Message::try_from(source)?;
/// assert_eq!(source, m.as_str());
/// # Ok(())
/// # }
/// ```
#[inline]
pub fn as_str(&self) -> &'a str {
self.source
}

/// Access Segment, Field, or sub-field string references by string index
pub fn query<'b, S>(&self, idx: S) -> &'a str
where
S: Into<&'b str>,
{
let idx = idx.into();

// Parse index elements
let indices: Vec<&str> = idx.split('.').collect();
let seg_name = indices[0];
// Find our first segment without offending the borow checker

let seg_index = self
.segments
.iter()
.position(|r| &r.as_str()[..seg_name.len()] == seg_name);

match seg_index {
//TODO: What is this doing...
Some(_) => {}
None => return "",
}

let seg = &self.segments[seg_index.unwrap()];

// Return the appropriate source reference
match seg {
// Short circuit for now
Segment::MSH(m) => m.source,
// Parse out slice depth
Segment::Generic(g) => {
if indices.len() < 2 {
g.source
} else {
let query = indices[1..].join(".");
g.query(&*query)
}
}
}
}
}

impl<'a> TryFrom<&'a str> for Message<'a> {
Expand Down Expand Up @@ -110,6 +164,17 @@ impl<'a> Display for Message<'a> {

impl<'a> Clone for Message<'a> {
/// Creates a new cloned Message object referencing the same source slice as the original.
/// ## Example:
/// ```
/// # use rusthl7::Hl7ParseError;
/// # use rusthl7::message::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")?;
/// let cloned = m.clone(); // this object is looking at the same string slice as m
/// # Ok(())
/// # }
/// ```
fn clone(&self) -> Self {
Message::try_from(self.source).unwrap()
}
Expand Down Expand Up @@ -139,7 +204,7 @@ impl<'a> Index<String> for Message<'a> {

/// DEPRECATED. Access Segment, Field, or sub-field string references by string index
#[allow(useless_deprecated)]
#[deprecated(note="This will be removed in a future version")]
#[deprecated(note = "This will be removed in a future version")]
fn index(&self, idx: String) -> &Self::Output {
// Parse index elements
let indices: Vec<&str> = idx.split('.').collect();
Expand Down Expand Up @@ -175,7 +240,7 @@ impl<'a> Index<&str> for Message<'a> {

/// DEPRECATED. Access Segment, Field, or sub-field string references by string index
#[allow(useless_deprecated)]
#[deprecated(note="This will be removed in a future version")]
#[deprecated(note = "This will be removed in a future version")]
fn index(&self, idx: &str) -> &Self::Output {
&self[String::from(idx)]
}
Expand Down Expand Up @@ -258,6 +323,8 @@ mod tests {
fn ensure_index() -> Result<(), Hl7ParseError> {
let hl7 = "MSH|^~\\&|GHH LAB|ELAB-3|GHH OE|BLDG4|200202150930||ORU^R01|CNTRL-3456|P|2.4\rOBR|segment^sub&segment";
let msg = Message::try_from(hl7)?;
assert_eq!(msg.query("OBR.F1.R2.C1"), "sub");
assert_eq!(msg.query(&*"OBR.F1.R2.C1".to_string()), "sub"); // Test the Into param with a String
assert_eq!(msg[String::from("OBR.F1.R2.C1")], "sub");
Ok(())
}
Expand Down
Loading

0 comments on commit 37e27d5

Please sign in to comment.