use std::fmt::{Debug, Display};
use crate::error::TdmsError;
pub type ObjectPath<'a> = &'a str;
pub type ObjectPathOwned = String;
fn escape_name(name: &str) -> String {
name.replace('\'', "\"")
}
fn parse_path(path: ObjectPath<'_>) -> Result<(Option<&str>, Option<&str>), TdmsError> {
if !path.starts_with('/') {
return Err(TdmsError::InvalidObjectPath(path.to_string()));
}
if path.len() == 1 {
return Ok((None, None));
}
let mut parts = path
.split('/')
.skip(1)
.map(|p| parse_name(p).ok_or_else(|| TdmsError::InvalidObjectPath(path.to_string())));
let group = invert(parts.next())?;
let channel = invert(parts.next())?;
if parts.next().is_some() {
return Err(TdmsError::InvalidObjectPath(path.to_string()));
}
Ok((group, channel))
}
fn parse_name(name: &str) -> Option<&str> {
if !name.starts_with('\'') || !name.ends_with('\'') {
return None;
}
Some(&name[1..name.len() - 1])
}
pub fn path_group_name(path: ObjectPath<'_>) -> Option<&str> {
parse_path(path).ok()?.0
}
fn invert<T, E>(x: Option<Result<T, E>>) -> Result<Option<T>, E> {
x.map_or(Ok(None), |v| v.map(Some))
}
#[derive(Clone, PartialEq, Eq)]
pub struct PropertyPath(String);
impl PropertyPath {
pub fn file() -> Self {
Self(String::from("/"))
}
pub fn group(group: &str) -> Self {
Self(format!("/'{}'", escape_name(group)))
}
pub fn channel(group: &str, channel: &str) -> Self {
Self(format!(
"/'{}'/'{}'",
escape_name(group),
escape_name(channel)
))
}
pub fn path(&self) -> ObjectPath<'_> {
self.0.as_ref()
}
fn path_levels(&self) -> impl Iterator<Item = &str> {
self.0.split('/').skip(1).filter_map(|value| {
if value.starts_with("'") && value.ends_with("'") {
Some(&value[1..value.len() - 1])
} else {
None
}
})
}
pub fn group_name(&self) -> Option<&str> {
self.path_levels().next()
}
pub fn channel_name(&self) -> Option<&str> {
self.path_levels().nth(1)
}
fn path_depth(&self) -> usize {
self.0.chars().filter(|c| *c == '/').count()
}
fn is_channel(&self) -> bool {
self.path_depth() == 2
}
}
impl Debug for PropertyPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Display for PropertyPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'a> TryFrom<ObjectPath<'a>> for PropertyPath {
type Error = TdmsError;
fn try_from(value: ObjectPath) -> Result<Self, Self::Error> {
let parsed = parse_path(value)?;
match parsed {
(None, None) => Ok(Self::file()),
(Some(group), None) => Ok(Self::group(group)),
(Some(group), Some(channel)) => Ok(Self::channel(group, channel)),
_ => unreachable!(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChannelPath(PropertyPath);
impl ChannelPath {
pub fn path(&self) -> ObjectPath<'_> {
self.0.path()
}
pub fn new(group: &str, channel: &str) -> Self {
Self(PropertyPath::channel(group, channel))
}
pub fn group_name(&self) -> &str {
self.0
.group_name()
.expect("ChannelPath must always have a group name")
}
pub fn channel_name(&self) -> &str {
self.0
.channel_name()
.expect("ChannelPath must always have a channel name")
}
}
impl AsRef<ChannelPath> for ChannelPath {
fn as_ref(&self) -> &ChannelPath {
self
}
}
impl AsRef<PropertyPath> for ChannelPath {
fn as_ref(&self) -> &PropertyPath {
&self.0
}
}
impl std::fmt::Display for ChannelPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", PropertyPath::path(&self.0))
}
}
impl<'a> TryFrom<ObjectPath<'a>> for ChannelPath {
type Error = TdmsError;
fn try_from(value: ObjectPath) -> Result<Self, Self::Error> {
let path = PropertyPath::try_from(value)?;
path.try_into()
}
}
impl TryFrom<PropertyPath> for ChannelPath {
type Error = TdmsError;
fn try_from(path: PropertyPath) -> Result<Self, Self::Error> {
if !path.is_channel() {
return Err(TdmsError::InvalidChannelPath(path.path().to_string()));
}
Ok(Self(path))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_root_path() {
let path = PropertyPath::file();
assert_eq!(path.path(), "/");
}
#[test]
fn test_group_path() {
let path = PropertyPath::group("group");
assert_eq!(path.path(), "/'group'");
}
#[test]
fn test_channel_path() {
let path = PropertyPath::channel("group", "channel");
assert_eq!(path.path(), "/'group'/'channel'");
}
#[test]
fn test_channel_path_type() {
let path = ChannelPath::new("group", "channel");
assert_eq!(path.path(), "/'group'/'channel'");
}
#[test]
fn test_group_escapes_chars() {
let path = PropertyPath::group("group'with'quotes");
assert_eq!(path.path(), r#"/'group"with"quotes'"#);
}
#[test]
fn test_channel_escapes_chars() {
let path = PropertyPath::channel("group'with'quotes", "channel'with'quotes");
assert_eq!(path.path(), r#"/'group"with"quotes'/'channel"with"quotes'"#);
}
#[test]
fn test_escapes_run_on_channel_paths() {
let path = ChannelPath::new("group'with'quotes", "channel'with'quotes");
assert_eq!(path.path(), r#"/'group"with"quotes'/'channel"with"quotes'"#);
}
#[test]
fn test_correctly_identifies_group() {
let path = PropertyPath::group("group");
assert!(!path.is_channel());
}
#[test]
fn test_correctly_identifies_channel() {
let path = PropertyPath::channel("group", "channel");
assert!(path.is_channel());
}
#[test]
fn test_property_path_try_from_object_path_invalid() {
let path = PropertyPath::try_from("invalid").unwrap_err();
assert!(matches!(path, TdmsError::InvalidObjectPath(_)));
}
#[test]
fn test_property_path_try_from_object_path_valid() {
let path = PropertyPath::try_from("/'group'/'channel'").unwrap();
assert_eq!(path.path(), "/'group'/'channel'");
}
#[test]
fn test_property_path_try_from_object_path_valid_group() {
let path = PropertyPath::try_from("/'group'").unwrap();
assert_eq!(path.path(), "/'group'");
}
#[test]
fn test_property_path_try_from_object_path_valid_root() {
let path = PropertyPath::try_from("/").unwrap();
assert_eq!(path.path(), "/");
}
#[test]
fn test_channel_path_try_from_object_path_invalid() {
let path = ChannelPath::try_from("invalid").unwrap_err();
assert!(matches!(path, TdmsError::InvalidObjectPath(_)));
}
#[test]
fn test_channel_path_try_from_object_path_valid() {
let path = ChannelPath::try_from("/'group'/'channel'").unwrap();
assert_eq!(path.path(), "/'group'/'channel'");
}
#[test]
fn test_channel_path_try_from_object_path_invalid_group() {
let path = ChannelPath::try_from("/'group'").unwrap_err();
assert!(matches!(path, TdmsError::InvalidChannelPath(_)));
}
#[test]
fn test_channel_path_try_from_object_path_invalid_root() {
let path = ChannelPath::try_from("/").unwrap_err();
assert!(matches!(path, TdmsError::InvalidChannelPath(_)));
}
#[test]
fn test_path_group_name() {
assert_eq!(path_group_name("/'group'/'channel'"), Some("group"));
assert_eq!(path_group_name("/'group'"), Some("group"));
assert_eq!(path_group_name("/"), None);
assert_eq!(path_group_name("invalid"), None);
}
}