use crate::Error;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Path {
pub segment: String,
pub segment_occurrence: Option<usize>,
pub field: Option<usize>,
pub repetition: Option<usize>,
pub component: Option<usize>,
pub subcomponent: Option<usize>,
}
impl Path {
pub fn parse(text: &str) -> Result<Path, Error> {
let text = text.trim();
let mut rest = text;
let bad = |detail: &str| Error::BadPath(format!("{text:?}: {detail}"));
let end = rest
.find(|c: char| !c.is_ascii_alphanumeric())
.unwrap_or(rest.len());
let segment = rest[..end].to_string();
if segment.is_empty() {
return Err(bad("expected a segment name"));
}
rest = &rest[end..];
let segment_occurrence = take_occurrence(&mut rest, text)?;
if rest.is_empty() {
return Ok(Path {
segment,
segment_occurrence,
field: None,
repetition: None,
component: None,
subcomponent: None,
});
}
if !rest.starts_with(['-', '.']) {
return Err(bad("expected '-' or '.' after the segment name"));
}
rest = &rest[1..];
let field = Some(take_index(&mut rest, text)?);
let repetition = take_occurrence(&mut rest, text)?;
let component = take_step(&mut rest, text)?;
let subcomponent = if component.is_some() {
take_step(&mut rest, text)?
} else {
None
};
if !rest.is_empty() {
return Err(bad(&format!("unexpected trailing {rest:?}")));
}
Ok(Path {
segment,
segment_occurrence,
field,
repetition,
component,
subcomponent,
})
}
}
fn take_step(rest: &mut &str, text: &str) -> Result<Option<usize>, Error> {
if !rest.starts_with('.') {
return Ok(None);
}
*rest = &rest[1..];
take_index(rest, text).map(Some)
}
fn take_occurrence(rest: &mut &str, text: &str) -> Result<Option<usize>, Error> {
if !rest.starts_with('[') {
return Ok(None);
}
let close = rest
.find(']')
.ok_or_else(|| Error::BadPath(format!("{text:?}: unclosed '['")))?;
let mut inner = &rest[1..close];
let index = take_index(&mut inner, text)?;
if !inner.is_empty() {
return Err(Error::BadPath(format!(
"{text:?}: unexpected {inner:?} inside brackets"
)));
}
*rest = &rest[close + 1..];
Ok(Some(index))
}
fn take_index(rest: &mut &str, text: &str) -> Result<usize, Error> {
let end = rest
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(rest.len());
let digits = &rest[..end];
let index: usize = digits
.parse()
.map_err(|_| Error::BadPath(format!("{text:?}: expected a number")))?;
if index == 0 {
return Err(Error::BadPath(format!(
"{text:?}: indices are 1-based, so 0 is not a position"
)));
}
*rest = &rest[end..];
Ok(index)
}
impl FromStr for Path {
type Err = Error;
fn from_str(text: &str) -> Result<Path, Error> {
Path::parse(text)
}
}
impl fmt::Display for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.segment)?;
if let Some(occurrence) = self.segment_occurrence {
write!(f, "[{occurrence}]")?;
}
if let Some(field) = self.field {
write!(f, "-{field}")?;
}
if let Some(repetition) = self.repetition {
write!(f, "[{repetition}]")?;
}
if let Some(component) = self.component {
write!(f, ".{component}")?;
}
if let Some(subcomponent) = self.subcomponent {
write!(f, ".{subcomponent}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn path(text: &str) -> Path {
Path::parse(text).unwrap()
}
#[test]
fn parses_each_depth() {
assert_eq!(path("MSH").field, None);
assert_eq!(path("PID-5").field, Some(5));
assert_eq!(path("PID-5.1").component, Some(1));
assert_eq!(path("PID-5.1.2").subcomponent, Some(2));
}
#[test]
fn parses_occurrences() {
assert_eq!(path("OBX[3]-5").segment_occurrence, Some(3));
assert_eq!(path("PID-13[2].4").repetition, Some(2));
assert_eq!(path("PID-13").repetition, None);
}
#[test]
fn accepts_both_spellings() {
assert_eq!(path("PID.5.1"), path("PID-5.1"));
assert_eq!(path(" PID-5 "), path("PID-5"));
}
#[test]
fn round_trips_through_display() {
for text in ["MSH", "PID-5", "PID-5.1", "PID-5.1.2", "OBX[2]-5[1].1.2"] {
assert_eq!(path(text).to_string(), text);
}
}
#[test]
fn rejects_malformed_paths() {
for text in [
"", "-5", "PID-", "PID-5.", "PID-0", "PID[0]-5", "PID[2-5", "PID-5x", "PID/5",
] {
assert!(
Path::parse(text).is_err(),
"expected {text:?} to be rejected"
);
}
}
}