use std::{borrow::Cow, fmt, num::ParseIntError, str::FromStr};
const INVALID_FIELD_NAME_CHARS: [char; 3] = ['.', '[', ']'];
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(
"failed to parse '{0}' as it contains at least one invalid character: {INVALID_FIELD_NAME_CHARS:?}"
)]
InvalidCharInName(String),
#[error("array segment '{0}' does not contain proper brackets")]
IncompleteArraySegment(String),
#[error("invalid index")]
InvalidIdx(#[from] ParseIntError),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SourcePath {
segments: Vec<PathSegment>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSegment {
Field(FieldName),
Array {
name: FieldName,
index: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldName(Cow<'static, str>);
impl SourcePath {
pub fn new() -> Self {
Default::default()
}
pub fn join(&self, segment: PathSegment) -> Self {
let mut new = self.clone();
new.segments.push(segment);
new
}
pub fn is_matching_base(&self, base: &Self) -> bool {
base.segments
.iter()
.zip(&self.segments)
.all(|(base, to_match)| base == to_match)
}
}
impl fmt::Display for SourcePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.segments.is_empty() {
f.write_str("root")
} else {
let mut segments = self.segments.iter();
let start = segments.next().expect("segments is not empty");
write!(f, "{start}")?;
for segment in segments {
write!(f, ".{segment}")?;
}
Ok(())
}
}
}
impl FromStr for SourcePath {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let segments = s
.split('.')
.map(|segment| segment.parse())
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { segments })
}
}
impl PathSegment {
pub fn field(name: FieldName) -> Self {
Self::Field(name)
}
pub fn array(name: FieldName, index: usize) -> Self {
Self::Array { name, index }
}
}
impl fmt::Display for PathSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PathSegment::Field(field) => write!(f, "{field}"),
PathSegment::Array { name, index } => write!(f, "{name}[{index}]"),
}
}
}
impl FromStr for PathSegment {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.ends_with(']') {
if let Some(idx) = s.find('[') {
let idx_str = &s[idx + 1..s.len() - 1];
let field_idx = idx_str.parse()?;
return Ok(Self::Array {
name: s[..idx].parse()?,
index: field_idx,
});
} else {
return Err(Error::IncompleteArraySegment(s.to_string()));
}
}
Ok(Self::Field(s.parse()?))
}
}
impl fmt::Display for FieldName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FieldName {
#[doc(hidden)]
pub const fn new_unchecked(name: &'static str) -> Self {
Self(Cow::Borrowed(name))
}
pub fn as_str(&self) -> &str {
self.as_ref()
}
}
impl AsRef<str> for FieldName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl FromStr for FieldName {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
validate_str_as_field_name(s)?;
Ok(Self(Cow::Owned(s.to_string())))
}
}
impl TryFrom<String> for FieldName {
type Error = Error;
fn try_from(name: String) -> Result<Self, Self::Error> {
validate_str_as_field_name(&name)?;
Ok(Self(Cow::Owned(name)))
}
}
impl<'a> TryFrom<&'a str> for FieldName {
type Error = <Self as FromStr>::Err;
fn try_from(name: &'a str) -> Result<Self, Self::Error> {
name.parse()
}
}
fn validate_str_as_field_name(name: &str) -> Result<(), Error> {
if name.contains(INVALID_FIELD_NAME_CHARS) {
Err(Error::InvalidCharInName(name.to_string()))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::n;
#[test]
fn should_serialize_root() {
let path = SourcePath::new();
let string = path.to_string();
assert_eq!(string.as_str(), "root");
}
#[test]
fn should_display_multi_segment_path() {
let path = SourcePath::new()
.join(PathSegment::field(n("foo")))
.join(PathSegment::array(n("bar"), 42))
.join(PathSegment::field(n("baz")));
let string = path.to_string();
assert_eq!(string.as_str(), "foo.bar[42].baz");
}
#[test]
fn should_parse_path() {
let expect = SourcePath::new()
.join(PathSegment::array(n("foo"), 21))
.join(PathSegment::field(n("bar")))
.join(PathSegment::field(n("xyz")));
let path = "foo[21].bar.xyz";
let parsed = path.parse::<SourcePath>().unwrap();
assert_eq!(parsed, expect);
}
}