From eda2e86aa2aae0d51eb39f87213250d81d63c8ae Mon Sep 17 00:00:00 2001 From: "Aode (Lion)" Date: Tue, 5 Oct 2021 18:34:01 -0500 Subject: [PATCH] Bro we are pathing --- .gitignore | 2 + Cargo.toml | 12 + src/lib.rs | 681 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 695 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5c6f6b0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "path-gen" +version = "0.1.0" +edition = "2018" + +[features] +default = [] +test = [] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..93eeff8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,681 @@ +use std::{borrow::Cow, fmt::Display, marker::PhantomData, str::FromStr}; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PathGen<'a> { + root: Cow<'a, str>, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Prefix { + inner: Inner, + segment: PhantomData, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Field { + inner: Inner, + segment: Segment, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Node { + inner: Inner, + segment: Segment, +} + +#[derive(Debug)] +pub struct DeconstructError; + +impl std::fmt::Display for DeconstructError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Invalid path format") + } +} + +impl std::error::Error for DeconstructError {} + +pub struct PathParser { + phantom: PhantomData, +} + +pub trait PathNode: Display + FromStr { + const NAME: &'static str; +} + +pub trait PathField: Display + FromStr {} + +impl PathGen<'static> { + pub fn new_owned(root: String) -> Self { + PathGen { + root: Cow::Owned(root), + } + } +} + +impl<'a> PathGen<'a> { + pub const fn new(root: &'a str) -> Self { + PathGen { + root: Cow::Borrowed(root), + } + } + + pub const fn parser() -> PathParser { + PathParser { + phantom: PhantomData, + } + } + + pub fn push(self, node: N) -> Node + where + N: PathNode, + { + Node { + inner: self, + segment: node, + } + } + + pub fn prefix(self) -> Prefix + where + N: PathNode, + { + Prefix { + inner: self, + segment: PhantomData, + } + } +} + +impl Node { + pub fn push(self, node: N) -> Node + where + N: PathNode, + { + Node { + inner: self, + segment: node, + } + } + + pub fn pop(self) -> Inner { + self.inner + } + + pub fn field(self, field: F) -> Field + where + F: PathField, + { + Field { + inner: self, + segment: field, + } + } + + pub fn prefix(self) -> Prefix + where + N: PathNode, + { + Prefix { + inner: self, + segment: PhantomData, + } + } +} + +impl Field +where + Self: Construct, +{ + pub fn construct(&self) -> String { + Construct::construct(self) + } +} + +impl Prefix +where + Self: Construct, +{ + pub fn construct(&self) -> String { + Construct::construct(self) + } +} + +impl Display for Field +where + Self: Construct, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.construct()) + } +} + +impl Display for Prefix +where + Self: Construct, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.construct()) + } +} + +impl PathParser { + pub fn push(self) -> PathParser> + where + N: PathNode, + { + PathParser { + phantom: PhantomData, + } + } + + pub fn prefix(self) -> PathParser> + where + N: PathNode, + { + PathParser { + phantom: PhantomData, + } + } + + pub fn field(self) -> PathParser> + where + F: PathField, + { + PathParser { + phantom: PhantomData, + } + } +} + +impl PathParser> { + pub fn parse<'a, E>(&self, s: &'a str) -> Result, E> + where + Field: Deconstruct<'a, E> + 'a, + { + Field::::deconstruct(s) + } +} + +impl PathParser> { + pub fn parse<'a, E>(&self, s: &'a str) -> Result, E> + where + Prefix: Deconstruct<'a, E> + 'a, + { + Prefix::::deconstruct(s) + } +} + +mod hidden { + pub trait Construct { + fn construct(&self) -> String; + } + + pub trait Deconstruct<'a, E> { + fn deconstruct(s: &'a str) -> Result + where + Self: Sized + 'a; + } +} +use hidden::{Construct, Deconstruct}; + +impl<'a> Construct for PathGen<'a> { + fn construct(&self) -> String { + self.root.to_string() + } +} + +impl<'a, E> Deconstruct<'a, E> for PathGen<'a> { + fn deconstruct(s: &'a str) -> Result { + Ok(PathGen { + root: Cow::Borrowed(s), + }) + } +} + +impl<'a, E, Inner, N> Deconstruct<'a, E> for Node +where + Inner: Deconstruct<'a, E>, + N: PathNode, + E: From<::Err>, + E: From, +{ + fn deconstruct(s: &'a str) -> Result + where + Self: Sized + 'a, + { + if let Some((rest, value)) = rsplit_once_escaped(s) { + if let Some((rest, name)) = rsplit_once_escaped(rest) { + if unescape(name) == N::NAME { + let inner = Inner::deconstruct(rest)?; + let segment = N::from_str(&unescape(value))?; + return Ok(Node { inner, segment }); + } + } + } + + Err(DeconstructError.into()) + } +} + +impl<'a, E, Inner, F> Deconstruct<'a, E> for Field +where + Inner: Deconstruct<'a, E>, + F: PathField, + E: From<::Err>, + E: From, +{ + fn deconstruct(s: &'a str) -> Result + where + Self: Sized + 'a, + { + if let Some((rest, value)) = rsplit_once_escaped(s) { + let inner = Inner::deconstruct(rest)?; + let segment = F::from_str(&unescape(value))?; + return Ok(Field { inner, segment }); + } else { + Err(DeconstructError.into()) + } + } +} + +impl<'a, E, Inner, N> Deconstruct<'a, E> for Prefix +where + Inner: Deconstruct<'a, E>, + N: PathNode, + E: From<::Err>, + E: From, +{ + fn deconstruct(s: &'a str) -> Result + where + Self: Sized + 'a, + { + if let Some((rest, name)) = rsplit_once_escaped(s) { + if unescape(name) == N::NAME { + let inner = Inner::deconstruct(rest)?; + return Ok(Prefix { + inner, + segment: PhantomData, + }); + } + } + + Err(DeconstructError.into()) + } +} + +impl Construct for Node +where + Inner: Construct, + N: PathNode, +{ + fn construct(&self) -> String { + self.inner.construct() + "." + &escape(N::NAME) + "." + &escape(&self.segment.to_string()) + } +} + +impl Construct for Field +where + Inner: Construct, + F: PathField, +{ + fn construct(&self) -> String { + self.inner.construct() + "." + &escape(&self.segment.to_string()) + } +} + +impl Construct for Prefix +where + Inner: Construct, + N: PathNode, +{ + fn construct(&self) -> String { + self.inner.construct() + "." + &escape(N::NAME) + } +} + +const ESCAPE_CHARS: &'static [char] = &['.', '\\']; + +fn rsplit_once_escaped<'a>(s: &'a str) -> Option<(&'a str, &'a str)> { + let mut position: usize = s.len(); + + while let Some(index) = s[..position].rfind('.') { + let mut escaped = false; + for prev in (0..index).rev() { + if s.get(prev..prev + 1) != Some("\\") { + break; + } + + escaped = !escaped; + } + + if !escaped { + let (left, right) = s.split_at(index); + return Some((left, &right[1..])); + } + + position = index; + } + + None +} + +fn escape(s: &str) -> Cow<'_, str> { + if s.find(ESCAPE_CHARS).is_some() { + let v = s + .split('.') + .map(|part| part.split('\\').collect::>().join("\\\\")) + .collect::>(); + + return Cow::Owned(v.join("\\.")); + } + + Cow::Borrowed(s) +} + +fn unescape(s: &str) -> Cow<'_, str> { + if s.find(ESCAPE_CHARS).is_some() { + let v = s + .split("\\\\") + .map(|part| part.split("\\.").collect::>().join(".")) + .collect::>(); + + return Cow::Owned(v.join("\\")); + } + + Cow::Borrowed(s) +} + +#[cfg(feature = "test")] +pub mod test { + use super::{PathField, PathNode}; + use std::{ + fmt::{Debug, Display}, + str::FromStr, + }; + + pub fn test_pathnode(path_node: N) + where + N: PathNode + Debug + Eq, + ::Err: Debug, + { + test_roundtrip(path_node); + } + + pub fn test_field(path_field: F) + where + F: PathField + Debug + Eq, + ::Err: Debug, + { + test_roundtrip(path_field) + } + + fn test_roundtrip(item: T) + where + T: Display + FromStr + Debug + Eq, + ::Err: Debug, + { + let string = item.to_string(); + let round_trip = string.parse::().unwrap(); + let plus_one = round_trip.to_string(); + + assert_eq!(round_trip, item); + assert_eq!(string, plus_one); + } +} + +#[cfg(test)] +mod tests { + use std::{fmt::Display, str::FromStr}; + + use super::{escape, rsplit_once_escaped, unescape, PathField, PathGen, PathNode}; + + const ROOT: PathGen<'static> = PathGen::new("test-root"); + + #[test] + fn splits_on_dot() { + let input = "left of dot . right of dot"; + let (left, right) = rsplit_once_escaped(input).unwrap(); + assert_eq!(left, "left of dot "); + assert_eq!(right, " right of dot"); + } + + #[test] + fn splits_on_ending_dot() { + let input = "left of dot ."; + let (left, right) = rsplit_once_escaped(input).unwrap(); + assert_eq!(left, "left of dot "); + assert_eq!(right, ""); + } + + #[test] + fn splits_on_starting_dot() { + let input = ". right of dot"; + let (left, right) = rsplit_once_escaped(input).unwrap(); + assert_eq!(left, ""); + assert_eq!(right, " right of dot"); + } + + #[test] + fn splits_last_dot() { + let input = "left of dots . between dots . right of dots"; + let (left, right) = rsplit_once_escaped(input).unwrap(); + assert_eq!(left, "left of dots . between dots "); + assert_eq!(right, " right of dots"); + } + + #[test] + fn doesnt_split_escaped_dot() { + let input = "left of dot \\. right of dot"; + let opt = rsplit_once_escaped(input); + assert!(opt.is_none()); + } + + #[test] + fn splits_unescaped_dot_with_preceding_escaped_dots() { + let input = "left of dots . between dots \\. right of dots"; + let (left, right) = rsplit_once_escaped(input).unwrap(); + assert_eq!(left, "left of dots "); + assert_eq!(right, " between dots \\. right of dots"); + } + + #[test] + fn splits_unescaped_dot_with_preceding_backslashes() { + let input = "left of dots . between dots \\\\. right of dots"; + let (left, right) = rsplit_once_escaped(input).unwrap(); + assert_eq!(left, "left of dots . between dots \\\\"); + assert_eq!(right, " right of dots"); + } + + #[test] + fn splits_unescaped_dot_with_preceding_escaped_dots_with_preceding_backslashes() { + let input = "left of dots . between dots \\\\\\. right of dots"; + let (left, right) = rsplit_once_escaped(input).unwrap(); + assert_eq!(left, "left of dots "); + assert_eq!(right, " between dots \\\\\\. right of dots"); + } + + #[test] + fn escape_output() { + let inputs = [ + ( + "..\\.\\.\\..\\\\\\...", + "\\.\\.\\\\\\.\\\\\\.\\\\\\.\\.\\\\\\\\\\\\\\.\\.\\.", + ), + (".", "\\."), + ("\\", "\\\\"), + ("\\.", "\\\\\\."), + (".\\", "\\.\\\\"), + (".\\\\\\", "\\.\\\\\\\\\\\\"), + ("\\\\\\.", "\\\\\\\\\\\\\\."), + ("...\\", "\\.\\.\\.\\\\"), + ("\\...", "\\\\\\.\\.\\."), + ("Some text with a . in it", "Some text with a \\. in it"), + ("Some text with a \\ in it", "Some text with a \\\\ in it"), + ( + "Some text with a \\ and a . in it", + "Some text with a \\\\ and a \\. in it", + ), + ( + "Some text with a . and a \\ in it", + "Some text with a \\. and a \\\\ in it", + ), + ]; + + for (start, escaped) in inputs { + assert_eq!(&escape(start), escaped); + assert_eq!(&unescape(escaped), start); + } + } + + #[test] + fn construct_field() { + let s = ROOT + .push(Deck("deck-id".to_owned())) + .push(Key(5)) + .field(CommandField) + .construct(); + + assert_eq!(&s, "test-root.deck.deck-id.key.5.command") + } + + #[test] + fn construct_prefix() { + let s = ROOT + .push(Deck("deck-id".to_owned())) + .prefix::() + .construct(); + + assert_eq!(&s, "test-root.deck.deck-id.key") + } + + #[test] + fn construct_escaped_prefix() { + let s = ROOT + .push(Deck("deck.id".to_owned())) + .prefix::() + .construct(); + + assert_eq!(&s, "test-root.deck.deck\\.id.key") + } + + #[test] + fn deconstruct_field() { + let parser = PathGen::parser() + .push::() + .push::() + .field::(); + + let s = "test-root.deck.deck-id.key.5.command"; + + let path = parser.parse::>(s).unwrap(); + + assert_eq!( + path, + ROOT.push(Deck("deck-id".to_owned())) + .push(Key(5)) + .field(CommandField) + ); + } + + #[test] + fn deconstruct_prefix() { + let parser = PathGen::parser().push::().prefix::(); + + let s = "test-root.deck.deck-id.key"; + + let path = parser.parse::>(s).unwrap(); + + assert_eq!(path, ROOT.push(Deck("deck-id".to_owned())).prefix::()); + } + + #[test] + fn deconstruct_escaped_prefix() { + let parser = PathGen::parser().push::().prefix::(); + + let s = "test-root.deck.deck\\.id.key"; + + let path = parser.parse::>(s).unwrap(); + + assert_eq!(path, ROOT.push(Deck("deck.id".to_owned())).prefix::()); + } + + #[test] + fn test_parts() { + super::test::test_pathnode(Deck("hello".to_string())); + super::test::test_pathnode(Key(5)); + super::test::test_field(CommandField); + } + + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + struct Deck(String); + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + struct Key(u8); + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + struct CommandField; + #[derive(Debug)] + struct CommandFieldError; + + impl std::fmt::Display for CommandFieldError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "string did not match 'command'") + } + } + + impl std::error::Error for CommandFieldError {} + + impl PathNode for Deck { + const NAME: &'static str = "deck"; + } + + impl PathNode for Key { + const NAME: &'static str = "key"; + } + + impl PathField for CommandField {} + + impl FromStr for Deck { + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> Result { + Ok(Deck(s.to_owned())) + } + } + + impl FromStr for Key { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + Ok(Key(s.parse()?)) + } + } + + impl FromStr for CommandField { + type Err = CommandFieldError; + + fn from_str(s: &str) -> Result { + if s == "command" { + Ok(CommandField) + } else { + Err(CommandFieldError) + } + } + } + + impl Display for Deck { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } + } + + impl Display for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } + } + + impl Display for CommandField { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "command") + } + } +}