Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/crdt/doc/
path.rs

1//! Path types for hierarchical document access.
2//!
3//! This module provides type-safe path construction and validation for accessing
4//! nested structures in CRDT documents. The Path/PathBuf types follow the same
5//! borrowed/owned pattern as std::path::Path/PathBuf.
6//!
7//! # Core Types
8//!
9//! - [`Path`] - An unsized borrowed path type (always behind a reference)
10//! - [`PathBuf`] - An owned path type that can be constructed and modified
11//!
12//! # Usage
13//!
14//! ```rust
15//! use eidetica::crdt::doc::{Path, PathBuf};
16//! use std::str::FromStr;
17//! # use eidetica::crdt::Doc;
18//!
19//! // Construct from string (automatically normalized)
20//! let path = PathBuf::from_str("user.profile.name")?;
21//!
22//! // Build incrementally (infallible)
23//! let path = PathBuf::new()
24//!     .push("user")
25//!     .push("profile")
26//!     .push("name");
27//!
28//! // Use in document operations
29//! let mut doc = Doc::new();
30//! doc.set(path, "Alice");
31//! # Ok::<(), Box<dyn std::error::Error>>(())
32//! ```
33
34use std::{borrow::Borrow, fmt, ops::Deref, str::FromStr};
35
36use thiserror::Error;
37
38/// Error type for path validation failures.
39///
40/// Note: Most path operations are now infallible through normalization.
41/// This error type is kept for backward compatibility and component validation.
42#[derive(Debug, Error, PartialEq, Eq)]
43pub enum PathError {
44    /// Invalid component: components cannot contain dots.
45    #[error("Invalid component '{component}': {reason}")]
46    InvalidComponent { component: String, reason: String },
47}
48
49/// Normalizes a path string by cleaning up dots and empty components.
50///
51/// This function implements the core path normalization logic:
52/// - Empty string "" → empty string (refers to current Doc)
53/// - Leading dots ".user" → "user"
54/// - Trailing dots "user." → "user"
55/// - Consecutive dots "user..profile" → "user.profile"
56/// - Pure dots "..." → empty string
57///
58/// # Examples
59///
60/// ```rust
61/// # use eidetica::crdt::doc::path::normalize_path;
62/// assert_eq!(normalize_path(""), "");
63/// assert_eq!(normalize_path(".user"), "user");
64/// assert_eq!(normalize_path("user."), "user");
65/// assert_eq!(normalize_path("user..profile"), "user.profile");
66/// assert_eq!(normalize_path("..."), "");
67/// assert_eq!(normalize_path("user.profile.name"), "user.profile.name");
68/// ```
69pub fn normalize_path(input: &str) -> String {
70    if input.is_empty() {
71        return String::new();
72    }
73
74    input
75        .split('.')
76        .filter(|component| !component.is_empty())
77        .collect::<Vec<_>>()
78        .join(".")
79}
80
81/// A validated component of a path.
82///
83/// Components are individual parts of a path, separated by dots.
84/// They cannot contain dots themselves. Empty components are allowed but
85/// will be filtered during path normalization.
86///
87/// # Examples
88///
89/// ```rust
90/// # use eidetica::crdt::doc::path::Component;
91/// # use std::str::FromStr;
92/// // Valid components
93/// let user = Component::new("user").unwrap();
94/// let profile = Component::new("profile").unwrap();
95/// let empty = Component::new("").unwrap();  // Empty is allowed
96///
97/// // Invalid components (only dots are forbidden)
98/// assert!(Component::new("user.name").is_err()); // Contains dot
99/// ```
100#[derive(Debug, Clone, PartialEq, Eq, Hash)]
101pub struct Component {
102    inner: String,
103}
104
105impl Component {
106    /// Creates a new component from a string.
107    ///
108    /// # Errors
109    /// Returns an error only if the component contains a dot.
110    /// Empty components are now allowed and will be filtered during path normalization.
111    pub fn new(s: impl Into<String>) -> Result<Self, PathError> {
112        let s = s.into();
113
114        if s.contains('.') {
115            return Err(PathError::InvalidComponent {
116                component: s.clone(),
117                reason: "components cannot contain dots".to_string(),
118            });
119        }
120
121        Ok(Component { inner: s })
122    }
123
124    /// Returns the component as a string slice.
125    pub fn as_str(&self) -> &str {
126        &self.inner
127    }
128}
129
130impl AsRef<str> for Component {
131    fn as_ref(&self) -> &str {
132        &self.inner
133    }
134}
135
136impl fmt::Display for Component {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "{}", self.inner)
139    }
140}
141
142impl FromStr for Component {
143    type Err = PathError;
144
145    fn from_str(s: &str) -> Result<Self, Self::Err> {
146        Component::new(s)
147    }
148}
149
150impl TryFrom<String> for Component {
151    type Error = PathError;
152
153    fn try_from(s: String) -> Result<Self, Self::Error> {
154        Component::new(s)
155    }
156}
157
158impl TryFrom<&str> for Component {
159    type Error = PathError;
160
161    fn try_from(s: &str) -> Result<Self, Self::Error> {
162        Component::new(s)
163    }
164}
165
166/// An owned, validated path for hierarchical document access.
167///
168/// `PathBuf` provides a type-safe way to construct and manipulate paths
169/// for accessing nested structures in CRDT documents. It validates that
170/// paths are well-formed and provides efficient access to path component.
171///
172/// # Examples
173///
174/// ```rust
175/// # use eidetica::crdt::doc::PathBuf;
176/// # use std::str::FromStr;
177/// // Create from string (automatically normalized)
178/// let path = PathBuf::from_str("user.profile.name")?;
179///
180/// // Build incrementally (infallible)
181/// let path = PathBuf::new()
182///     .push("user")
183///     .push("profile")
184///     .push("name");
185///
186/// // Get components
187/// let components: Vec<&str> = path.components().collect();
188/// assert_eq!(components, vec!["user", "profile", "name"]);
189/// # Ok::<(), std::convert::Infallible>(())
190/// ```
191#[derive(Debug, Clone, PartialEq, Eq, Hash)]
192pub struct PathBuf {
193    inner: String,
194}
195
196/// A borrowed, validated path for hierarchical document access.
197///
198/// `Path` is the borrowed counterpart to `PathBuf`, similar to how `&str`
199/// relates to `String`. It provides efficient read-only access to path
200/// components without allocation.
201///
202/// This type is unsized and must always be used behind a reference.
203#[derive(Debug, PartialEq, Eq, Hash)]
204pub struct Path {
205    inner: str,
206}
207
208impl PathBuf {
209    /// Creates a new empty path.
210    pub fn new() -> Self {
211        Self {
212            inner: String::new(),
213        }
214    }
215
216    /// Creates a path from a single component.
217    pub fn from_component(component: Component) -> Self {
218        Self {
219            inner: component.inner,
220        }
221    }
222
223    /// Adds a path to the end of this path.
224    ///
225    /// This method accepts both strings and Path types, normalizing the input.
226    /// It's infallible and handles all path joining cases through normalization.
227    ///
228    /// # Examples
229    ///
230    /// ```rust
231    /// # use eidetica::crdt::doc::PathBuf;
232    /// # use std::str::FromStr;
233    /// // Push strings
234    /// let path = PathBuf::new().push("user").push("profile");
235    /// assert_eq!(path.as_str(), "user.profile");
236    ///
237    /// // Push Path types
238    /// let suffix = PathBuf::from_str("name.value").unwrap();
239    /// let path = PathBuf::new().push("user").push(&suffix);
240    /// assert_eq!(path.as_str(), "user.name.value");
241    /// ```
242    pub fn push(mut self, path: impl AsRef<str>) -> Self {
243        let normalized = normalize_path(path.as_ref());
244        if normalized.is_empty() {
245            return self;
246        }
247
248        if self.inner.is_empty() {
249            self.inner = normalized;
250        } else {
251            self.inner.push('.');
252            self.inner.push_str(&normalized);
253        }
254        self
255    }
256
257    /// Adds a validated component to the end of this path.
258    pub fn push_component(mut self, component: Component) -> Self {
259        if self.inner.is_empty() {
260            self.inner = component.inner;
261        } else {
262            self.inner.push('.');
263            self.inner.push_str(&component.inner);
264        }
265        self
266    }
267
268    /// Joins this path with another path.
269    pub fn join(mut self, other: impl AsRef<Path>) -> Self {
270        let other_path = other.as_ref();
271        if self.inner.is_empty() {
272            self.inner = other_path.inner.to_string();
273        } else if !other_path.inner.is_empty() {
274            self.inner.push('.');
275            self.inner.push_str(&other_path.inner);
276        }
277        self
278    }
279
280    /// Returns an iterator over the path components as string slices.
281    pub fn components(&self) -> impl Iterator<Item = &str> {
282        self.inner.split('.').filter(|s| !s.is_empty())
283    }
284
285    /// Returns the number of components in the path.
286    pub fn len(&self) -> usize {
287        if self.inner.is_empty() {
288            0
289        } else {
290            self.inner.split('.').count()
291        }
292    }
293
294    /// Returns `true` if the path has no components.
295    pub fn is_empty(&self) -> bool {
296        self.inner.is_empty()
297    }
298
299    /// Returns the parent path, or `None` if this is the root.
300    pub fn parent(&self) -> Option<PathBuf> {
301        self.inner.rfind('.').map(|last_dot| PathBuf {
302            inner: self.inner[..last_dot].to_string(),
303        })
304    }
305
306    /// Returns the last component of the path, or `None` if empty.
307    pub fn file_name(&self) -> Option<&str> {
308        if self.inner.is_empty() {
309            None
310        } else if let Some(last_dot) = self.inner.rfind('.') {
311            Some(&self.inner[last_dot + 1..])
312        } else {
313            Some(&self.inner)
314        }
315    }
316
317    /// Creates a PathBuf from a normalized string.
318    ///
319    /// This is the internal constructor that assumes the string is already normalized.
320    /// Use `from_str()` or `normalize()` for general string input.
321    fn from_normalized(normalized: String) -> Self {
322        PathBuf { inner: normalized }
323    }
324
325    /// Creates a PathBuf by normalizing the input string.
326    ///
327    /// This method always succeeds by applying path normalization rules.
328    pub fn normalize(path: &str) -> Self {
329        Self::from_normalized(normalize_path(path))
330    }
331}
332
333impl Path {
334    /// Creates a Path from a string slice.
335    ///
336    /// This is a zero-cost operation that wraps the string without validation or normalization.
337    /// Normalization happens during path processing operations when needed.
338    ///
339    /// This follows the same pattern as `std::path::Path::new()`.
340    pub fn new(s: &str) -> &Path {
341        unsafe { Path::from_str_unchecked(s) }
342    }
343
344    /// Creates a Path from a string without validation.
345    ///
346    /// # Safety
347    /// The caller must ensure that the string is a valid path according to our validation rules:
348    /// - No leading or trailing dots
349    /// - No empty components (consecutive dots)
350    /// - Components may not contain dots
351    ///
352    /// This is primarily intended for use with compile-time validated string literals.
353    pub unsafe fn from_str_unchecked(s: &str) -> &Path {
354        // SAFETY: Path has the same memory layout as str
355        unsafe { &*(s as *const str as *const Path) }
356    }
357
358    /// Returns an iterator over the path components as string slices.
359    pub fn components(&self) -> impl Iterator<Item = &str> {
360        self.inner.split('.').filter(|s| !s.is_empty())
361    }
362
363    /// Returns the number of components in the path.
364    pub fn len(&self) -> usize {
365        if self.inner.is_empty() {
366            0
367        } else {
368            self.inner.split('.').count()
369        }
370    }
371
372    /// Returns `true` if the path has no components.
373    pub fn is_empty(&self) -> bool {
374        self.inner.is_empty()
375    }
376
377    /// Returns the last component of the path, or `None` if empty.
378    pub fn file_name(&self) -> Option<&str> {
379        if self.inner.is_empty() {
380            None
381        } else {
382            self.inner.split('.').next_back()
383        }
384    }
385
386    /// Returns the path as a string slice.
387    pub fn as_str(&self) -> &str {
388        &self.inner
389    }
390
391    /// Converts this `Path` to an owned `PathBuf`.
392    pub fn to_path_buf(&self) -> PathBuf {
393        PathBuf {
394            inner: self.inner.to_string(),
395        }
396    }
397}
398
399impl Default for PathBuf {
400    fn default() -> Self {
401        Self::new()
402    }
403}
404
405impl Deref for PathBuf {
406    type Target = Path;
407
408    fn deref(&self) -> &Self::Target {
409        // Safe because Path has the same layout as str
410        unsafe { crate::crdt::doc::path::Path::from_str_unchecked(self.inner.as_str()) }
411    }
412}
413
414impl AsRef<Path> for PathBuf {
415    fn as_ref(&self) -> &Path {
416        self.deref()
417    }
418}
419
420impl AsRef<PathBuf> for PathBuf {
421    fn as_ref(&self) -> &PathBuf {
422        self
423    }
424}
425
426impl AsRef<Path> for Path {
427    fn as_ref(&self) -> &Path {
428        self
429    }
430}
431
432impl AsRef<str> for Path {
433    fn as_ref(&self) -> &str {
434        &self.inner
435    }
436}
437
438impl AsRef<str> for PathBuf {
439    fn as_ref(&self) -> &str {
440        &self.inner
441    }
442}
443
444impl AsRef<Path> for str {
445    fn as_ref(&self) -> &Path {
446        Path::new(self)
447    }
448}
449
450impl AsRef<Path> for String {
451    fn as_ref(&self) -> &Path {
452        self.as_str().as_ref()
453    }
454}
455
456impl Borrow<Path> for PathBuf {
457    fn borrow(&self) -> &Path {
458        self.deref()
459    }
460}
461
462impl FromStr for PathBuf {
463    type Err = std::convert::Infallible;
464
465    fn from_str(s: &str) -> Result<Self, Self::Err> {
466        Ok(Self::normalize(s))
467    }
468}
469
470/// Backward compatibility: FromStr that can return a PathError
471///
472/// This trait implementation allows existing code that expects `Result<PathBuf, PathError>`
473/// to continue working during the migration period.
474pub trait FromStrResult {
475    fn from_str_result(s: &str) -> Result<PathBuf, PathError>;
476}
477
478impl FromStrResult for PathBuf {
479    fn from_str_result(s: &str) -> Result<PathBuf, PathError> {
480        // For backward compatibility, we still normalize but return in Result form
481        Ok(Self::normalize(s))
482    }
483}
484
485impl From<&PathBuf> for PathBuf {
486    fn from(path: &PathBuf) -> Self {
487        path.clone()
488    }
489}
490
491impl fmt::Display for PathBuf {
492    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493        if self.inner.is_empty() {
494            write!(f, "(empty path)")
495        } else {
496            write!(f, "{}", self.inner)
497        }
498    }
499}
500
501impl fmt::Display for Path {
502    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503        if self.inner.is_empty() {
504            write!(f, "(empty path)")
505        } else {
506            write!(f, "{}", &self.inner)
507        }
508    }
509}
510
511/// A builder for constructing paths incrementally.
512#[derive(Debug, Clone)]
513pub struct PathBuilder {
514    inner: String,
515}
516
517impl PathBuilder {
518    /// Creates a new empty path builder.
519    pub fn new() -> Self {
520        Self {
521            inner: String::new(),
522        }
523    }
524
525    /// Adds a component to the path.
526    pub fn component(mut self, component: impl Into<String>) -> Result<Self, PathError> {
527        let component = Component::new(component)?;
528        if self.inner.is_empty() {
529            self.inner = component.inner;
530        } else {
531            self.inner.push('.');
532            self.inner.push_str(&component.inner);
533        }
534        Ok(self)
535    }
536
537    /// Adds a validated component to the path.
538    pub fn push_component(mut self, component: Component) -> Self {
539        if self.inner.is_empty() {
540            self.inner = component.inner;
541        } else {
542            self.inner.push('.');
543            self.inner.push_str(&component.inner);
544        }
545        self
546    }
547
548    /// Builds the final `PathBuf`.
549    pub fn build(self) -> PathBuf {
550        PathBuf { inner: self.inner }
551    }
552}
553
554impl Default for PathBuilder {
555    fn default() -> Self {
556        Self::new()
557    }
558}
559
560/// Constructs a path with compile-time optimization for literals.
561///
562/// This macro provides ergonomic path construction with optimal performance:
563/// - Single string literal returns `&'static Path` (zero allocation!)
564/// - Multiple arguments or runtime values return `PathBuf`
565///
566/// # Syntax
567///
568/// - `path!()` - Empty path (PathBuf)
569/// - `path!("user.profile.name")` - Single literal (&'static Path, zero-cost!)
570/// - `path!("user", "profile", "name")` - Multiple components (PathBuf)
571/// - `path!(base, "profile", "name")` - Mix runtime and literals (PathBuf)
572/// - `path!(existing_path)` - Pass through existing paths (PathBuf)
573///
574/// # Examples
575///
576/// ```rust
577/// # use eidetica::crdt::doc::path;
578/// # use eidetica::crdt::doc::PathBuf;
579/// # use std::str::FromStr;
580/// // Zero-cost literal (returns &'static Path)
581/// let path = path!("user.profile.name");
582///
583/// // Multiple components (returns PathBuf)
584/// let path = path!("user", "profile", "name");
585///
586/// // Mixed runtime/literal (returns PathBuf)
587/// let base = "user";
588/// let path = path!(base, "profile", "name");
589///
590/// // Empty path
591/// let empty = path!();
592/// ```
593#[macro_export]
594macro_rules! path {
595    // Empty path - returns PathBuf
596    () => {
597        $crate::crdt::doc::PathBuf::new()
598    };
599
600    // Single string literal - returns &'static Path (zero allocation!)
601    ($single:literal) => {{
602        // Normalize at compile time (basic)
603        const NORMALIZED: &str = $crate::crdt::doc::path::normalize_const($single);
604        // Safe because we normalized above
605        unsafe { $crate::crdt::doc::path::Path::from_str_unchecked(NORMALIZED) }
606    }};
607
608    // Multiple arguments - returns PathBuf
609    ($first:expr $(, $rest:expr)* $(,)?) => {{
610        let mut path = $crate::crdt::doc::PathBuf::new();
611
612        // Helper function to convert anything to a component
613        fn add_component(path: &mut $crate::crdt::doc::PathBuf, component: impl AsRef<str>) {
614            let component_str = component.as_ref().trim();
615            if !component_str.is_empty() {
616                // Push is now infallible and handles all path strings
617                *path = std::mem::take(path).push(component_str);
618            }
619        }
620
621        // Handle first argument
622        let first_str = $first.to_string();
623        add_component(&mut path, first_str);
624
625        // Handle remaining arguments
626        $(
627            let rest_str = $rest.to_string();
628            add_component(&mut path, rest_str);
629        )*
630
631        path
632    }};
633}
634
635/// Normalizes a path string at compile time for string literals.
636///
637/// This performs basic normalization that can be done in const context.
638/// For string literals, this ensures the path is normalized at compile time.
639///
640/// Note: This is a simplified version that handles most common cases.
641/// For complete normalization, use the runtime `normalize_path()` function.
642pub const fn normalize_const(path: &str) -> &str {
643    // For const context, we can only do basic checks
644    // Complex normalization happens at runtime
645    if path.is_empty() {
646        return "";
647    }
648
649    // In const context, we'll just return the path as-is
650    // The macro will handle basic validation
651    path
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657
658    #[test]
659    fn test_pathbuf_construction() {
660        let path = PathBuf::new();
661        assert!(path.is_empty());
662        assert_eq!(path.len(), 0);
663
664        let component = Component::new("test").unwrap();
665        let path = PathBuf::from_component(component);
666        assert!(!path.is_empty());
667        assert_eq!(path.len(), 1);
668        assert_eq!(path.file_name(), Some("test"));
669    }
670
671    #[test]
672    fn test_pathbuf_push() {
673        // push() accepts strings
674        let path = PathBuf::new().push("user").push("profile").push("name");
675
676        assert_eq!(path.len(), 3);
677        let components: Vec<&str> = path.components().collect();
678        assert_eq!(components, vec!["user", "profile", "name"]);
679        assert_eq!(path.file_name(), Some("name"));
680
681        // push() also accepts Path/PathBuf types
682        let base = PathBuf::new().push("user");
683        let suffix = PathBuf::from_str("profile.name").unwrap();
684        let path = base.push(&suffix);
685        assert_eq!(path.as_str(), "user.profile.name");
686
687        // Can chain push with different types
688        let path = PathBuf::new()
689            .push("user")
690            .push(PathBuf::from_str("profile").unwrap())
691            .push("name");
692        assert_eq!(path.as_str(), "user.profile.name");
693    }
694
695    #[test]
696    fn test_pathbuf_push_normalization() {
697        // push() normalizes path strings with dots
698        let path = PathBuf::new().push("user.name");
699        assert_eq!(path.as_str(), "user.name");
700
701        // Empty strings are ignored
702        let path = PathBuf::new().push("");
703        assert!(path.is_empty());
704
705        // Consecutive dots are normalized
706        let path = PathBuf::new().push("user..name");
707        assert_eq!(path.as_str(), "user.name");
708    }
709
710    #[test]
711    fn test_pathbuf_parent() {
712        let path = PathBuf::from_str("user.profile.name").unwrap();
713        let parent = path.parent().unwrap();
714
715        let parent_components: Vec<&str> = parent.components().collect();
716        assert_eq!(parent_components, vec!["user", "profile"]);
717
718        let root = PathBuf::from_str("user").unwrap();
719        assert!(root.parent().is_none());
720    }
721
722    #[test]
723    fn test_path_validation_success() {
724        let valid_paths = vec!["simple", "user.profile", "user.profile.name", "a.b.c.d.e"];
725
726        for path_str in valid_paths {
727            let path = PathBuf::from_str(path_str);
728            assert!(path.is_ok(), "Path '{path_str}' should be valid");
729        }
730    }
731
732    #[test]
733    fn test_path_normalization_behavior() {
734        let test_cases = vec![
735            // FromStr normalizes all inputs
736            ("", ""),
737            (".user", "user"),
738            ("user.", "user"),
739            ("user..profile", "user.profile"),
740            ("user...profile", "user.profile"),
741            ("...user...profile...", "user.profile"),
742            ("...", ""),
743        ];
744
745        for (input, expected_normalized) in test_cases {
746            let result = PathBuf::from_str(input);
747            assert_eq!(
748                result.unwrap().as_str(),
749                expected_normalized,
750                "Path '{input}' should normalize to '{expected_normalized}'"
751            );
752        }
753    }
754
755    #[test]
756    fn test_path_deref() {
757        let pathbuf = PathBuf::from_str("user.profile.name").unwrap();
758        let path: &Path = &pathbuf;
759
760        assert_eq!(path.as_str(), "user.profile.name");
761        let components: Vec<&str> = path.components().collect();
762        assert_eq!(components, vec!["user", "profile", "name"]);
763    }
764
765    #[test]
766    fn test_path_builder() {
767        let path = PathBuilder::new()
768            .component("user")
769            .unwrap()
770            .component("profile")
771            .unwrap()
772            .component("name")
773            .unwrap()
774            .build();
775
776        let components: Vec<&str> = path.components().collect();
777        assert_eq!(components, vec!["user", "profile", "name"]);
778    }
779
780    #[test]
781    fn test_display() {
782        let path = PathBuf::from_str("user.profile.name").unwrap();
783        assert_eq!(format!("{path}"), "user.profile.name");
784
785        let empty = PathBuf::new();
786        assert_eq!(format!("{empty}"), "(empty path)");
787    }
788
789    #[test]
790    fn test_from_str() {
791        let from_str = PathBuf::from_str("user.profile.name").unwrap();
792
793        let components: Vec<&str> = from_str.components().collect();
794        assert_eq!(components, vec!["user", "profile", "name"]);
795    }
796
797    #[test]
798    fn test_from_str_normalization() {
799        let result = PathBuf::from_str("user..invalid");
800        assert!(result.is_ok()); // Normalizes instead of failing
801        assert_eq!(result.unwrap().as_str(), "user.invalid");
802    }
803
804    #[test]
805    fn test_path_join() {
806        let base = PathBuf::from_str("user").unwrap();
807        let suffix = PathBuf::from_str("profile.name").unwrap();
808
809        let joined = base.join(&suffix);
810        let components: Vec<&str> = joined.components().collect();
811        assert_eq!(components, vec!["user", "profile", "name"]);
812    }
813
814    #[test]
815    fn test_path_macro_from_string() {
816        let path = path!("user.profile.name");
817        let components: Vec<&str> = path.components().collect();
818        assert_eq!(components, vec!["user", "profile", "name"]);
819    }
820
821    #[test]
822    fn test_path_macro_from_components() {
823        let path = path!("user", "profile", "name");
824        let components: Vec<&str> = path.components().collect();
825        assert_eq!(components, vec!["user", "profile", "name"]);
826
827        // Test with trailing comma
828        let path = path!("user", "profile", "name",);
829        let components: Vec<&str> = path.components().collect();
830        assert_eq!(components, vec!["user", "profile", "name"]);
831    }
832
833    #[test]
834    fn test_path_macro_mixed() {
835        let base = "user";
836        let path = path!(base, "profile", "name");
837        let components: Vec<&str> = path.components().collect();
838        assert_eq!(components, vec!["user", "profile", "name"]);
839    }
840
841    #[test]
842    fn test_path_macro_empty_and_edge_cases() {
843        // Empty path is allowed and returns empty PathBuf
844        let empty = path!();
845        assert!(empty.is_empty());
846        assert_eq!(empty.len(), 0);
847
848        // Empty string literal now works and returns empty path
849        let empty_str = path!("");
850        assert!(empty_str.is_empty());
851        assert_eq!(empty_str.len(), 0);
852    }
853
854    #[test]
855    fn test_path_normalization() {
856        // normalize_path() filters empty components
857        assert_eq!(normalize_path(""), "");
858        assert_eq!(normalize_path("user"), "user");
859        assert_eq!(normalize_path(".user"), "user");
860        assert_eq!(normalize_path("user."), "user");
861        assert_eq!(normalize_path("user..profile"), "user.profile");
862        assert_eq!(normalize_path("...user...profile..."), "user.profile");
863        assert_eq!(normalize_path("..."), "");
864        assert_eq!(normalize_path("user.profile.name"), "user.profile.name");
865    }
866
867    #[test]
868    fn test_pathbuf_normalization() {
869        // PathBuf::from_str normalizes input
870        let cases = vec![
871            ("", ""),
872            (".user", "user"),
873            ("user.", "user"),
874            ("user..profile", "user.profile"),
875            ("...user...profile...", "user.profile"),
876            ("...", ""),
877            ("user.profile.name", "user.profile.name"),
878        ];
879
880        for (input, expected) in cases {
881            let path = PathBuf::from_str(input).unwrap();
882            assert_eq!(
883                path.as_str(),
884                expected,
885                "Input '{input}' should normalize to '{expected}'"
886            );
887        }
888    }
889
890    #[test]
891    fn test_unified_macro_behavior() {
892        // Test that different call forms produce equivalent results
893        let literal = path!("user.profile.name");
894        let components = path!("user", "profile", "name");
895        let base = "user";
896        let mixed = path!(base, "profile", "name");
897
898        // All should have the same component structure
899        let literal_vec: Vec<&str> = literal.components().collect();
900        let components_vec: Vec<&str> = components.components().collect();
901        let mixed_vec: Vec<&str> = mixed.components().collect();
902
903        assert_eq!(literal_vec, vec!["user", "profile", "name"]);
904        assert_eq!(components_vec, vec!["user", "profile", "name"]);
905        assert_eq!(mixed_vec, vec!["user", "profile", "name"]);
906
907        // Test different return types work with AsRef<Path>
908        fn accepts_path_ref(p: impl AsRef<Path>) -> String {
909            p.as_ref().as_str().to_string()
910        }
911
912        assert_eq!(accepts_path_ref(literal), "user.profile.name");
913        assert_eq!(accepts_path_ref(&components), "user.profile.name");
914        assert_eq!(accepts_path_ref(&mixed), "user.profile.name");
915    }
916
917    #[test]
918    fn test_component_validation() {
919        // Test valid components
920        assert!(Component::new("user").is_ok());
921        assert!(Component::new("profile123").is_ok());
922        assert!(Component::new("_internal").is_ok());
923        assert!(Component::new("").is_ok()); // Empty components now allowed
924
925        // Test invalid components
926        assert!(Component::new("user.name").is_err()); // Still can't contain dots
927    }
928}