use std::{borrow::Borrow, fmt, ops::Deref, str::FromStr};
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PathError {
#[error("Invalid component '{component}': {reason}")]
InvalidComponent { component: String, reason: String },
}
pub fn normalize_path(input: &str) -> String {
if input.is_empty() {
return String::new();
}
input
.split('.')
.filter(|component| !component.is_empty())
.collect::<Vec<_>>()
.join(".")
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Component {
inner: String,
}
impl Component {
pub fn new(s: impl Into<String>) -> Result<Self, PathError> {
let s = s.into();
if s.contains('.') {
return Err(PathError::InvalidComponent {
component: s.clone(),
reason: "components cannot contain dots".to_string(),
});
}
Ok(Component { inner: s })
}
pub fn as_str(&self) -> &str {
&self.inner
}
}
impl AsRef<str> for Component {
fn as_ref(&self) -> &str {
&self.inner
}
}
impl fmt::Display for Component {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.inner)
}
}
impl FromStr for Component {
type Err = PathError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Component::new(s)
}
}
impl TryFrom<String> for Component {
type Error = PathError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Component::new(s)
}
}
impl TryFrom<&str> for Component {
type Error = PathError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Component::new(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PathBuf {
inner: String,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Path {
inner: str,
}
impl PathBuf {
pub fn new() -> Self {
Self {
inner: String::new(),
}
}
pub fn from_component(component: Component) -> Self {
Self {
inner: component.inner,
}
}
pub fn push(mut self, path: impl AsRef<str>) -> Self {
let normalized = normalize_path(path.as_ref());
if normalized.is_empty() {
return self;
}
if self.inner.is_empty() {
self.inner = normalized;
} else {
self.inner.push('.');
self.inner.push_str(&normalized);
}
self
}
pub fn push_component(mut self, component: Component) -> Self {
if self.inner.is_empty() {
self.inner = component.inner;
} else {
self.inner.push('.');
self.inner.push_str(&component.inner);
}
self
}
pub fn join(mut self, other: impl AsRef<Path>) -> Self {
let other_path = other.as_ref();
if self.inner.is_empty() {
self.inner = other_path.inner.to_string();
} else if !other_path.inner.is_empty() {
self.inner.push('.');
self.inner.push_str(&other_path.inner);
}
self
}
pub fn components(&self) -> impl Iterator<Item = &str> {
self.inner.split('.').filter(|s| !s.is_empty())
}
pub fn len(&self) -> usize {
if self.inner.is_empty() {
0
} else {
self.inner.split('.').count()
}
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn parent(&self) -> Option<PathBuf> {
self.inner.rfind('.').map(|last_dot| PathBuf {
inner: self.inner[..last_dot].to_string(),
})
}
pub fn file_name(&self) -> Option<&str> {
if self.inner.is_empty() {
None
} else if let Some(last_dot) = self.inner.rfind('.') {
Some(&self.inner[last_dot + 1..])
} else {
Some(&self.inner)
}
}
fn from_normalized(normalized: String) -> Self {
PathBuf { inner: normalized }
}
pub fn normalize(path: &str) -> Self {
Self::from_normalized(normalize_path(path))
}
}
impl Path {
pub fn new(s: &str) -> &Path {
unsafe { Path::from_str_unchecked(s) }
}
pub unsafe fn from_str_unchecked(s: &str) -> &Path {
unsafe { &*(s as *const str as *const Path) }
}
pub fn components(&self) -> impl Iterator<Item = &str> {
self.inner.split('.').filter(|s| !s.is_empty())
}
pub fn len(&self) -> usize {
if self.inner.is_empty() {
0
} else {
self.inner.split('.').count()
}
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn file_name(&self) -> Option<&str> {
if self.inner.is_empty() {
None
} else {
self.inner.split('.').next_back()
}
}
pub fn as_str(&self) -> &str {
&self.inner
}
pub fn to_path_buf(&self) -> PathBuf {
PathBuf {
inner: self.inner.to_string(),
}
}
}
impl Default for PathBuf {
fn default() -> Self {
Self::new()
}
}
impl Deref for PathBuf {
type Target = Path;
fn deref(&self) -> &Self::Target {
unsafe { crate::crdt::doc::path::Path::from_str_unchecked(self.inner.as_str()) }
}
}
impl AsRef<Path> for PathBuf {
fn as_ref(&self) -> &Path {
self.deref()
}
}
impl AsRef<PathBuf> for PathBuf {
fn as_ref(&self) -> &PathBuf {
self
}
}
impl AsRef<Path> for Path {
fn as_ref(&self) -> &Path {
self
}
}
impl AsRef<str> for Path {
fn as_ref(&self) -> &str {
&self.inner
}
}
impl AsRef<str> for PathBuf {
fn as_ref(&self) -> &str {
&self.inner
}
}
impl AsRef<Path> for str {
fn as_ref(&self) -> &Path {
Path::new(self)
}
}
impl AsRef<Path> for String {
fn as_ref(&self) -> &Path {
self.as_str().as_ref()
}
}
impl Borrow<Path> for PathBuf {
fn borrow(&self) -> &Path {
self.deref()
}
}
impl FromStr for PathBuf {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::normalize(s))
}
}
pub trait FromStrResult {
fn from_str_result(s: &str) -> Result<PathBuf, PathError>;
}
impl FromStrResult for PathBuf {
fn from_str_result(s: &str) -> Result<PathBuf, PathError> {
Ok(Self::normalize(s))
}
}
impl From<&PathBuf> for PathBuf {
fn from(path: &PathBuf) -> Self {
path.clone()
}
}
impl fmt::Display for PathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.inner.is_empty() {
write!(f, "(empty path)")
} else {
write!(f, "{}", self.inner)
}
}
}
impl fmt::Display for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.inner.is_empty() {
write!(f, "(empty path)")
} else {
write!(f, "{}", &self.inner)
}
}
}
#[derive(Debug, Clone)]
pub struct PathBuilder {
inner: String,
}
impl PathBuilder {
pub fn new() -> Self {
Self {
inner: String::new(),
}
}
pub fn component(mut self, component: impl Into<String>) -> Result<Self, PathError> {
let component = Component::new(component)?;
if self.inner.is_empty() {
self.inner = component.inner;
} else {
self.inner.push('.');
self.inner.push_str(&component.inner);
}
Ok(self)
}
pub fn push_component(mut self, component: Component) -> Self {
if self.inner.is_empty() {
self.inner = component.inner;
} else {
self.inner.push('.');
self.inner.push_str(&component.inner);
}
self
}
pub fn build(self) -> PathBuf {
PathBuf { inner: self.inner }
}
}
impl Default for PathBuilder {
fn default() -> Self {
Self::new()
}
}
#[macro_export]
macro_rules! path {
() => {
$crate::crdt::doc::PathBuf::new()
};
($single:literal) => {{
const NORMALIZED: &str = $crate::crdt::doc::path::normalize_const($single);
unsafe { $crate::crdt::doc::path::Path::from_str_unchecked(NORMALIZED) }
}};
($first:expr $(, $rest:expr)* $(,)?) => {{
let mut path = $crate::crdt::doc::PathBuf::new();
fn add_component(path: &mut $crate::crdt::doc::PathBuf, component: impl AsRef<str>) {
let component_str = component.as_ref().trim();
if !component_str.is_empty() {
*path = std::mem::take(path).push(component_str);
}
}
let first_str = $first.to_string();
add_component(&mut path, first_str);
$(
let rest_str = $rest.to_string();
add_component(&mut path, rest_str);
)*
path
}};
}
pub const fn normalize_const(path: &str) -> &str {
if path.is_empty() {
return "";
}
path
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pathbuf_construction() {
let path = PathBuf::new();
assert!(path.is_empty());
assert_eq!(path.len(), 0);
let component = Component::new("test").unwrap();
let path = PathBuf::from_component(component);
assert!(!path.is_empty());
assert_eq!(path.len(), 1);
assert_eq!(path.file_name(), Some("test"));
}
#[test]
fn test_pathbuf_push() {
let path = PathBuf::new().push("user").push("profile").push("name");
assert_eq!(path.len(), 3);
let components: Vec<&str> = path.components().collect();
assert_eq!(components, vec!["user", "profile", "name"]);
assert_eq!(path.file_name(), Some("name"));
let base = PathBuf::new().push("user");
let suffix = PathBuf::from_str("profile.name").unwrap();
let path = base.push(&suffix);
assert_eq!(path.as_str(), "user.profile.name");
let path = PathBuf::new()
.push("user")
.push(PathBuf::from_str("profile").unwrap())
.push("name");
assert_eq!(path.as_str(), "user.profile.name");
}
#[test]
fn test_pathbuf_push_normalization() {
let path = PathBuf::new().push("user.name");
assert_eq!(path.as_str(), "user.name");
let path = PathBuf::new().push("");
assert!(path.is_empty());
let path = PathBuf::new().push("user..name");
assert_eq!(path.as_str(), "user.name");
}
#[test]
fn test_pathbuf_parent() {
let path = PathBuf::from_str("user.profile.name").unwrap();
let parent = path.parent().unwrap();
let parent_components: Vec<&str> = parent.components().collect();
assert_eq!(parent_components, vec!["user", "profile"]);
let root = PathBuf::from_str("user").unwrap();
assert!(root.parent().is_none());
}
#[test]
fn test_path_validation_success() {
let valid_paths = vec!["simple", "user.profile", "user.profile.name", "a.b.c.d.e"];
for path_str in valid_paths {
let path = PathBuf::from_str(path_str);
assert!(path.is_ok(), "Path '{path_str}' should be valid");
}
}
#[test]
fn test_path_normalization_behavior() {
let test_cases = vec![
("", ""),
(".user", "user"),
("user.", "user"),
("user..profile", "user.profile"),
("user...profile", "user.profile"),
("...user...profile...", "user.profile"),
("...", ""),
];
for (input, expected_normalized) in test_cases {
let result = PathBuf::from_str(input);
assert_eq!(
result.unwrap().as_str(),
expected_normalized,
"Path '{input}' should normalize to '{expected_normalized}'"
);
}
}
#[test]
fn test_path_deref() {
let pathbuf = PathBuf::from_str("user.profile.name").unwrap();
let path: &Path = &pathbuf;
assert_eq!(path.as_str(), "user.profile.name");
let components: Vec<&str> = path.components().collect();
assert_eq!(components, vec!["user", "profile", "name"]);
}
#[test]
fn test_path_builder() {
let path = PathBuilder::new()
.component("user")
.unwrap()
.component("profile")
.unwrap()
.component("name")
.unwrap()
.build();
let components: Vec<&str> = path.components().collect();
assert_eq!(components, vec!["user", "profile", "name"]);
}
#[test]
fn test_display() {
let path = PathBuf::from_str("user.profile.name").unwrap();
assert_eq!(format!("{path}"), "user.profile.name");
let empty = PathBuf::new();
assert_eq!(format!("{empty}"), "(empty path)");
}
#[test]
fn test_from_str() {
let from_str = PathBuf::from_str("user.profile.name").unwrap();
let components: Vec<&str> = from_str.components().collect();
assert_eq!(components, vec!["user", "profile", "name"]);
}
#[test]
fn test_from_str_normalization() {
let result = PathBuf::from_str("user..invalid");
assert!(result.is_ok()); assert_eq!(result.unwrap().as_str(), "user.invalid");
}
#[test]
fn test_path_join() {
let base = PathBuf::from_str("user").unwrap();
let suffix = PathBuf::from_str("profile.name").unwrap();
let joined = base.join(&suffix);
let components: Vec<&str> = joined.components().collect();
assert_eq!(components, vec!["user", "profile", "name"]);
}
#[test]
fn test_path_macro_from_string() {
let path = path!("user.profile.name");
let components: Vec<&str> = path.components().collect();
assert_eq!(components, vec!["user", "profile", "name"]);
}
#[test]
fn test_path_macro_from_components() {
let path = path!("user", "profile", "name");
let components: Vec<&str> = path.components().collect();
assert_eq!(components, vec!["user", "profile", "name"]);
let path = path!("user", "profile", "name",);
let components: Vec<&str> = path.components().collect();
assert_eq!(components, vec!["user", "profile", "name"]);
}
#[test]
fn test_path_macro_mixed() {
let base = "user";
let path = path!(base, "profile", "name");
let components: Vec<&str> = path.components().collect();
assert_eq!(components, vec!["user", "profile", "name"]);
}
#[test]
fn test_path_macro_empty_and_edge_cases() {
let empty = path!();
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
let empty_str = path!("");
assert!(empty_str.is_empty());
assert_eq!(empty_str.len(), 0);
}
#[test]
fn test_path_normalization() {
assert_eq!(normalize_path(""), "");
assert_eq!(normalize_path("user"), "user");
assert_eq!(normalize_path(".user"), "user");
assert_eq!(normalize_path("user."), "user");
assert_eq!(normalize_path("user..profile"), "user.profile");
assert_eq!(normalize_path("...user...profile..."), "user.profile");
assert_eq!(normalize_path("..."), "");
assert_eq!(normalize_path("user.profile.name"), "user.profile.name");
}
#[test]
fn test_pathbuf_normalization() {
let cases = vec![
("", ""),
(".user", "user"),
("user.", "user"),
("user..profile", "user.profile"),
("...user...profile...", "user.profile"),
("...", ""),
("user.profile.name", "user.profile.name"),
];
for (input, expected) in cases {
let path = PathBuf::from_str(input).unwrap();
assert_eq!(
path.as_str(),
expected,
"Input '{input}' should normalize to '{expected}'"
);
}
}
#[test]
fn test_unified_macro_behavior() {
let literal = path!("user.profile.name");
let components = path!("user", "profile", "name");
let base = "user";
let mixed = path!(base, "profile", "name");
let literal_vec: Vec<&str> = literal.components().collect();
let components_vec: Vec<&str> = components.components().collect();
let mixed_vec: Vec<&str> = mixed.components().collect();
assert_eq!(literal_vec, vec!["user", "profile", "name"]);
assert_eq!(components_vec, vec!["user", "profile", "name"]);
assert_eq!(mixed_vec, vec!["user", "profile", "name"]);
fn accepts_path_ref(p: impl AsRef<Path>) -> String {
p.as_ref().as_str().to_string()
}
assert_eq!(accepts_path_ref(literal), "user.profile.name");
assert_eq!(accepts_path_ref(&components), "user.profile.name");
assert_eq!(accepts_path_ref(&mixed), "user.profile.name");
}
#[test]
fn test_component_validation() {
assert!(Component::new("user").is_ok());
assert!(Component::new("profile123").is_ok());
assert!(Component::new("_internal").is_ok());
assert!(Component::new("").is_ok());
assert!(Component::new("user.name").is_err()); }
}