Skip to main content

structfs_core_store/
path.rs

1//! Path type with validated Unicode identifier components.
2
3use std::fmt;
4
5use bytes::Bytes;
6use structfs_ll_store::LLPath;
7
8/// Errors related to path parsing and validation.
9#[derive(Debug, Clone, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum PathError {
12    /// A path component is not a valid Unicode identifier.
13    InvalidComponent {
14        component: String,
15        position: usize,
16        message: String,
17    },
18    /// The path string is invalid.
19    InvalidPath { message: String },
20}
21
22impl fmt::Display for PathError {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            PathError::InvalidComponent {
26                component,
27                position,
28                message,
29            } => {
30                write!(
31                    f,
32                    "invalid path component '{}' at position {}: {}",
33                    component, position, message
34                )
35            }
36            PathError::InvalidPath { message } => {
37                write!(f, "invalid path: {}", message)
38            }
39        }
40    }
41}
42
43impl std::error::Error for PathError {}
44
45/// A validated path in StructFS.
46///
47/// Path components must be valid Unicode identifiers (per UAX#31) or
48/// numeric strings (for array indexing). This ensures paths can be
49/// used as identifiers in most programming languages.
50///
51/// # Refinement of [`LLPath`]
52///
53/// `Path` is a validated *refinement* of the low-level [`LLPath`]: it wraps an
54/// `LLPath` whose every component is additionally guaranteed to be valid UTF-8
55/// and a valid component grammar (identifier or numeric). Because a `Path`
56/// *is* an `LLPath` that has been validated, widening ([`as_ll`](Self::as_ll) /
57/// [`into_ll`](Self::into_ll)) is free, and narrowing
58/// ([`validate`](Self::validate)) is the single place validation happens.
59/// Components are stored as byte components, so `Path -> LLPath` never copies
60/// and structural ops (`join`/`slice`/`strip_prefix`) clone `Bytes`
61/// (reference-count bumps) rather than deep-copying `String`s.
62#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
63pub struct Path(LLPath);
64
65/// View a validated byte component as `&str`.
66///
67/// Sound because the `Path` invariant guarantees every component is valid
68/// UTF-8; this is the accessor that lets the high-level API keep speaking
69/// `&str` over a byte representation without re-validating.
70#[inline]
71fn component_str(component: &Bytes) -> &str {
72    // SAFETY: every component of a `Path` was validated as a UTF-8 identifier
73    // or numeric string at construction (see `validate_component`).
74    unsafe { std::str::from_utf8_unchecked(component) }
75}
76
77/// Build validated byte components from already-validated strings, moving the
78/// `String` buffers into `Bytes` without copying.
79fn ll_from_strings(components: Vec<String>) -> LLPath {
80    components
81        .into_iter()
82        .map(|s| Bytes::from(s.into_bytes()))
83        .collect()
84}
85
86impl Path {
87    /// Parse a path string, validating components.
88    ///
89    /// # Path Syntax
90    ///
91    /// - Components are separated by `/`
92    /// - Empty components are ignored (normalizes `//` and trailing `/`)
93    /// - Each component must be a valid identifier or numeric string
94    ///
95    /// # Examples
96    ///
97    /// ```rust
98    /// use structfs_core_store::Path;
99    ///
100    /// let path = Path::parse("users/123/name").unwrap();
101    /// assert_eq!(path.len(), 3);
102    ///
103    /// // Trailing slashes are normalized
104    /// assert_eq!(Path::parse("foo/bar/").unwrap(), Path::parse("foo/bar").unwrap());
105    /// ```
106    pub fn parse(s: &str) -> Result<Self, PathError> {
107        if s.is_empty() {
108            return Ok(Path(LLPath::new()));
109        }
110
111        let components: Vec<String> = s
112            .split('/')
113            .filter(|c| !c.is_empty())
114            .map(|c| c.to_string())
115            .collect();
116
117        // Validate each component
118        for (i, component) in components.iter().enumerate() {
119            Self::validate_component(component, i)?;
120        }
121
122        Ok(Path(ll_from_strings(components)))
123    }
124
125    /// Create a path from pre-validated components.
126    ///
127    /// # Panics
128    ///
129    /// Panics if any component is invalid. Use `try_from_components` for
130    /// fallible construction.
131    pub fn from_components(components: Vec<String>) -> Self {
132        for (i, component) in components.iter().enumerate() {
133            Self::validate_component(component, i).expect("invalid component");
134        }
135        Path(ll_from_strings(components))
136    }
137
138    /// Create a path from components that are already known to be valid.
139    ///
140    /// This is the construction path used by the `path!` macro: literals are
141    /// validated at compile time and expressions are `PathComponent` values
142    /// validated at construction, so no runtime re-validation is needed.
143    /// Debug builds re-check as a safety net.
144    ///
145    /// Prefer `from_components`/`try_from_components` for strings whose
146    /// validity is not already guaranteed.
147    #[doc(hidden)]
148    pub fn from_validated_components(components: Vec<String>) -> Self {
149        #[cfg(debug_assertions)]
150        for (i, component) in components.iter().enumerate() {
151            Self::validate_component(component, i).expect("invalid pre-validated component");
152        }
153        Path(ll_from_strings(components))
154    }
155
156    /// Try to create a path from components, validating each.
157    pub fn try_from_components(components: Vec<String>) -> Result<Self, PathError> {
158        for (i, component) in components.iter().enumerate() {
159            Self::validate_component(component, i)?;
160        }
161        Ok(Path(ll_from_strings(components)))
162    }
163
164    /// Validate a single path component against the StructFS grammar.
165    ///
166    /// The grammar is shared with the compile-time `path!` macro via the
167    /// `structfs-path-validation` crate: a component is a UAX#31 identifier
168    /// (an underscore prefix is allowed when followed by more identifier
169    /// characters) or a pure numeric string.
170    ///
171    /// `position` is only used to build the error; pass `0` when validating
172    /// a component in isolation.
173    pub fn validate_component(component: &str, position: usize) -> Result<(), PathError> {
174        structfs_path_validation::validate_component(component).map_err(|message| {
175            PathError::InvalidComponent {
176                component: component.to_string(),
177                position,
178                message,
179            }
180        })
181    }
182
183    /// Check if this path is empty (root path).
184    pub fn is_empty(&self) -> bool {
185        self.0.is_empty()
186    }
187
188    /// Get the number of components.
189    pub fn len(&self) -> usize {
190        self.0.len()
191    }
192
193    /// Iterate over components as validated `&str`s.
194    pub fn iter(&self) -> impl Iterator<Item = &str> {
195        self.0.iter().map(component_str)
196    }
197
198    /// Join this path with another.
199    #[must_use]
200    pub fn join(&self, other: &Path) -> Path {
201        let mut components = self.0.components().to_vec();
202        components.extend(other.0.iter().cloned());
203        Path(LLPath::from_components(components))
204    }
205
206    /// Return a new path with the component appended.
207    #[must_use]
208    pub fn child(&self, component: impl Into<PathComponent>) -> Path {
209        let mut components = self.0.components().to_vec();
210        components.push(Bytes::from(component.into().into_string().into_bytes()));
211        Path(LLPath::from_components(components))
212    }
213
214    /// Append a component in place.
215    pub fn push(&mut self, component: impl Into<PathComponent>) {
216        self.0
217            .push(Bytes::from(component.into().into_string().into_bytes()));
218    }
219
220    /// Check if this path has the given prefix.
221    pub fn has_prefix(&self, prefix: &Path) -> bool {
222        prefix.0.len() <= self.0.len()
223            && prefix.0.components() == &self.0.components()[..prefix.0.len()]
224    }
225
226    /// Strip a prefix from this path.
227    ///
228    /// Returns `None` if the prefix doesn't match.
229    #[must_use]
230    pub fn strip_prefix(&self, prefix: &Path) -> Option<Path> {
231        if self.has_prefix(prefix) {
232            Some(Path(LLPath::from_components(
233                self.0.components()[prefix.0.len()..].to_vec(),
234            )))
235        } else {
236            None
237        }
238    }
239
240    /// Get a slice of components as a new path.
241    pub fn slice(&self, start: usize, end: usize) -> Path {
242        Path(LLPath::from_components(
243            self.0.components()[start..end].to_vec(),
244        ))
245    }
246
247    /// Borrow this path as its underlying [`LLPath`] — the free widening from
248    /// the validated high-level contract to the opaque low-level one.
249    pub fn as_ll(&self) -> &LLPath {
250        &self.0
251    }
252
253    /// Consume this path into its underlying [`LLPath`] — free widening with no
254    /// component copy.
255    pub fn into_ll(self) -> LLPath {
256        self.0
257    }
258
259    /// Validate an [`LLPath`] into a `Path` — the single narrowing point where
260    /// opaque bytes become a validated identifier path. Reuses the `Bytes`
261    /// components (no copy); fails if any component is not valid UTF-8 or not a
262    /// valid component grammar.
263    pub fn validate(ll: LLPath) -> Result<Self, PathError> {
264        for (i, component) in ll.iter().enumerate() {
265            let s = std::str::from_utf8(component.as_ref()).map_err(|_| {
266                PathError::InvalidComponent {
267                    component: format!("{:?}", component.as_ref()),
268                    position: i,
269                    message: "not valid UTF-8".to_string(),
270                }
271            })?;
272            Self::validate_component(s, i)?;
273        }
274        Ok(Path(ll))
275    }
276
277    /// Wrap an [`LLPath`] known to already satisfy the `Path` invariant, without
278    /// re-validating. This is the byte-path analogue of
279    /// [`from_validated_components`](Self::from_validated_components): use it for
280    /// paths that originated host-side from a `Path` (e.g. a write result path
281    /// echoed back), so internal LL->HL hops don't re-pay validation. Debug
282    /// builds re-check as a safety net.
283    pub fn from_ll_unchecked(ll: LLPath) -> Self {
284        #[cfg(debug_assertions)]
285        for (i, component) in ll.iter().enumerate() {
286            let s = std::str::from_utf8(component.as_ref())
287                .expect("pre-validated LL component is not UTF-8");
288            Self::validate_component(s, i).expect("invalid pre-validated LL component");
289        }
290        Path(ll)
291    }
292
293    /// Convert to an owned LL path (byte components).
294    ///
295    /// Now a cheap clone (each component is a reference-counted `Bytes`); prefer
296    /// [`as_ll`](Self::as_ll)/[`into_ll`](Self::into_ll) to avoid even that.
297    pub fn to_ll_path(&self) -> LLPath {
298        self.0.clone()
299    }
300
301    /// Try to create from borrowed LL path components (byte slices).
302    ///
303    /// Copies the components into owned `Bytes` and validates. Fails if any
304    /// component is not valid UTF-8 or not a valid identifier. For an owned
305    /// [`LLPath`], prefer [`validate`](Self::validate) to reuse its `Bytes`.
306    pub fn try_from_ll_path(ll_path: &[impl AsRef<[u8]>]) -> Result<Self, PathError> {
307        let ll: LLPath = ll_path
308            .iter()
309            .map(|b| Bytes::copy_from_slice(b.as_ref()))
310            .collect();
311        Self::validate(ll)
312    }
313}
314
315impl fmt::Display for Path {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        let mut first = true;
318        for component in self.iter() {
319            if !first {
320                f.write_str("/")?;
321            }
322            f.write_str(component)?;
323            first = false;
324        }
325        Ok(())
326    }
327}
328
329impl std::ops::Index<usize> for Path {
330    type Output = str;
331
332    fn index(&self, i: usize) -> &Self::Output {
333        component_str(&self.0[i])
334    }
335}
336
337/// A single validated path component.
338///
339/// Guarantees: the inner string is a valid StructFS path component (UAX#31
340/// identifier or pure numeric). Cannot be constructed from an arbitrary
341/// string without validation, which is what lets the `path!` macro accept
342/// `PathComponent` expressions without a runtime check.
343///
344/// # Arbitrary strings
345///
346/// Real-world identifiers (`my-account`, `hello world`, UUIDs with dashes)
347/// are often not valid components. Use [`PathComponent::encode`] to embed
348/// them losslessly via [Namecode](https://crates.io/crates/namecode) and
349/// [`PathComponent::decode`] to recover the original string.
350#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
351pub struct PathComponent(String);
352
353impl PathComponent {
354    /// Validate and wrap a string as a path component.
355    pub fn try_new(s: impl Into<String>) -> Result<Self, PathError> {
356        let s = s.into();
357        Path::validate_component(&s, 0)?;
358        Ok(Self(s))
359    }
360
361    /// Encode an arbitrary string as a valid path component.
362    ///
363    /// Valid UAX#31 identifiers pass through unchanged; everything else
364    /// (punctuation, spaces, leading digits) is Namecode-encoded into a
365    /// `_N_`-prefixed identifier. Always succeeds and is deterministic.
366    /// Reverse with [`PathComponent::decode`].
367    pub fn encode(s: &str) -> Self {
368        let encoded = namecode::encode(s);
369        debug_assert!(Path::validate_component(&encoded, 0).is_ok());
370        Self(encoded)
371    }
372
373    /// Decode a component produced by [`PathComponent::encode`] back to the
374    /// original string.
375    ///
376    /// Components that are not Namecode-encoded are returned unchanged
377    /// (matching `encode`'s pass-through of valid identifiers). Returns an
378    /// error only for a malformed `_N_`-prefixed component.
379    pub fn decode(&self) -> Result<String, PathError> {
380        match namecode::decode(&self.0) {
381            Ok(decoded) => Ok(decoded),
382            Err(namecode::DecodeError::NotEncoded) => Ok(self.0.clone()),
383            Err(e) => Err(PathError::InvalidComponent {
384                component: self.0.clone(),
385                position: 0,
386                message: format!("malformed namecode encoding: {}", e),
387            }),
388        }
389    }
390
391    /// Get the validated string.
392    pub fn as_str(&self) -> &str {
393        &self.0
394    }
395
396    /// Borrow the validated string.
397    ///
398    /// Used by the `path!` macro to enforce that only `PathComponent` values
399    /// (not bare `String`/`&str`) are accepted as runtime path components.
400    /// Named distinctly so no standard type matches.
401    pub fn validated_str(&self) -> &str {
402        &self.0
403    }
404
405    /// Consume and return the inner string.
406    pub fn into_string(self) -> String {
407        self.0
408    }
409}
410
411impl AsRef<str> for PathComponent {
412    fn as_ref(&self) -> &str {
413        &self.0
414    }
415}
416
417impl fmt::Display for PathComponent {
418    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
419        f.write_str(&self.0)
420    }
421}
422
423// Numeric indices are always valid components.
424impl From<usize> for PathComponent {
425    fn from(i: usize) -> Self {
426        Self(i.to_string())
427    }
428}
429
430impl From<u64> for PathComponent {
431    fn from(i: u64) -> Self {
432        Self(i.to_string())
433    }
434}
435
436impl From<PathComponent> for Path {
437    fn from(c: PathComponent) -> Self {
438        Path(LLPath::from_components(vec![Bytes::from(
439            c.into_string().into_bytes(),
440        )]))
441    }
442}
443
444impl FromIterator<PathComponent> for Path {
445    fn from_iter<I: IntoIterator<Item = PathComponent>>(iter: I) -> Self {
446        Path(
447            iter.into_iter()
448                .map(|c| Bytes::from(c.into_string().into_bytes()))
449                .collect(),
450        )
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457    use crate::path;
458
459    #[test]
460    fn parse_basic_paths() {
461        assert_eq!(Path::parse("").unwrap().len(), 0);
462        assert_eq!(Path::parse("foo").unwrap().len(), 1);
463        assert_eq!(Path::parse("foo/bar").unwrap().len(), 2);
464        assert_eq!(Path::parse("foo/bar/baz").unwrap().len(), 3);
465    }
466
467    #[test]
468    fn normalize_slashes() {
469        assert_eq!(
470            Path::parse("foo/bar/").unwrap(),
471            Path::parse("foo/bar").unwrap()
472        );
473        assert_eq!(
474            Path::parse("foo//bar").unwrap(),
475            Path::parse("foo/bar").unwrap()
476        );
477        assert_eq!(
478            Path::parse("/foo/bar").unwrap(),
479            Path::parse("foo/bar").unwrap()
480        );
481    }
482
483    #[test]
484    fn numeric_components_allowed() {
485        let p = Path::parse("items/0/name").unwrap();
486        assert_eq!(p.len(), 3);
487        assert_eq!(&p[1], "0");
488    }
489
490    #[test]
491    fn unicode_identifiers_allowed() {
492        let p = Path::parse("usuarios/名前").unwrap();
493        assert_eq!(p.len(), 2);
494    }
495
496    #[test]
497    fn invalid_components_rejected() {
498        assert!(Path::parse("foo/bar baz").is_err()); // space
499        assert!(Path::parse("foo/bar-baz").is_err()); // hyphen
500        assert!(Path::parse("foo/.hidden").is_err()); // starts with dot
501        assert!(Path::parse("foo/123abc").is_err()); // starts with digit but not pure numeric
502    }
503
504    #[test]
505    fn has_prefix_works() {
506        let p = path!("foo/bar/baz");
507        assert!(p.has_prefix(&path!("")));
508        assert!(p.has_prefix(&path!("foo")));
509        assert!(p.has_prefix(&path!("foo/bar")));
510        assert!(p.has_prefix(&path!("foo/bar/baz")));
511        assert!(!p.has_prefix(&path!("bar")));
512        assert!(!p.has_prefix(&path!("foo/bar/baz/qux")));
513    }
514
515    #[test]
516    fn strip_prefix_works() {
517        let p = path!("foo/bar/baz");
518        assert_eq!(p.strip_prefix(&path!("foo")), Some(path!("bar/baz")));
519        assert_eq!(p.strip_prefix(&path!("foo/bar")), Some(path!("baz")));
520        assert_eq!(p.strip_prefix(&path!("other")), None);
521    }
522
523    #[test]
524    fn ll_conversion_roundtrips() {
525        let p = path!("users/123/name");
526        let ll = p.to_ll_path();
527        let p2 = Path::try_from_ll_path(&ll.iter().collect::<Vec<_>>()).unwrap();
528        assert_eq!(p, p2);
529    }
530
531    #[test]
532    fn path_error_display_invalid_component() {
533        let err = PathError::InvalidComponent {
534            component: "bad-name".to_string(),
535            position: 2,
536            message: "test message".to_string(),
537        };
538        let display = format!("{}", err);
539        assert!(display.contains("bad-name"));
540        assert!(display.contains("position 2"));
541        assert!(display.contains("test message"));
542    }
543
544    #[test]
545    fn path_error_display_invalid_path() {
546        let err = PathError::InvalidPath {
547            message: "some reason".to_string(),
548        };
549        let display = format!("{}", err);
550        assert!(display.contains("invalid path"));
551        assert!(display.contains("some reason"));
552    }
553
554    #[test]
555    fn path_error_is_error() {
556        let err: Box<dyn std::error::Error> = Box::new(PathError::InvalidPath {
557            message: "test".to_string(),
558        });
559        let _ = err.to_string();
560    }
561
562    #[test]
563    fn from_components_valid() {
564        let p = Path::from_components(vec!["foo".to_string(), "bar".to_string()]);
565        assert_eq!(p.len(), 2);
566    }
567
568    #[test]
569    #[should_panic(expected = "invalid component")]
570    fn from_components_invalid_panics() {
571        Path::from_components(vec!["foo".to_string(), "bad-name".to_string()]);
572    }
573
574    #[test]
575    fn try_from_components_valid() {
576        let p = Path::try_from_components(vec!["foo".to_string(), "bar".to_string()]).unwrap();
577        assert_eq!(p.len(), 2);
578    }
579
580    #[test]
581    fn try_from_components_invalid() {
582        let result = Path::try_from_components(vec!["foo".to_string(), "bad-name".to_string()]);
583        assert!(result.is_err());
584    }
585
586    #[test]
587    fn validate_empty_component_rejected() {
588        let result = Path::try_from_components(vec!["".to_string()]);
589        assert!(result.is_err());
590        let err = result.unwrap_err();
591        assert!(err.to_string().contains("empty component"));
592    }
593
594    #[test]
595    fn validate_underscore_alone_rejected() {
596        // Underscore alone without follow-up character should be rejected
597        let result = Path::parse("_");
598        assert!(result.is_err());
599    }
600
601    #[test]
602    fn validate_underscore_with_continuation_allowed() {
603        // _foo is valid
604        let p = Path::parse("_foo").unwrap();
605        assert_eq!(p.len(), 1);
606    }
607
608    #[test]
609    fn validate_invalid_character_in_middle() {
610        let result = Path::parse("foo$bar");
611        assert!(result.is_err());
612        let err = result.unwrap_err();
613        assert!(err.to_string().contains("invalid character"));
614    }
615
616    #[test]
617    fn index_trait() {
618        let p = path!("foo/bar/baz");
619        assert_eq!(&p[0], "foo");
620        assert_eq!(&p[1], "bar");
621        assert_eq!(&p[2], "baz");
622    }
623
624    #[test]
625    fn slice_method() {
626        let p = path!("a/b/c/d");
627        let sliced = p.slice(1, 3);
628        assert_eq!(sliced.len(), 2);
629        assert_eq!(sliced.to_string(), "b/c");
630    }
631
632    #[test]
633    fn join_method() {
634        let p1 = path!("foo/bar");
635        let p2 = path!("baz/qux");
636        let joined = p1.join(&p2);
637        assert_eq!(joined.to_string(), "foo/bar/baz/qux");
638    }
639
640    #[test]
641    fn join_with_empty() {
642        let p1 = path!("foo");
643        let p2 = path!("");
644        assert_eq!(p1.join(&p2), p1);
645
646        let p3 = path!("");
647        let p4 = path!("bar");
648        assert_eq!(p3.join(&p4), p4);
649    }
650
651    #[test]
652    fn iter_method() {
653        let p = path!("a/b/c");
654        let components: Vec<&str> = p.iter().collect();
655        assert_eq!(components.len(), 3);
656        assert_eq!(components[0], "a");
657        assert_eq!(components[1], "b");
658        assert_eq!(components[2], "c");
659    }
660
661    #[test]
662    fn is_empty() {
663        assert!(path!("").is_empty());
664        assert!(!path!("foo").is_empty());
665    }
666
667    #[test]
668    fn display_impl() {
669        let p = path!("foo/bar/baz");
670        assert_eq!(format!("{}", p), "foo/bar/baz");
671    }
672
673    #[test]
674    fn display_empty() {
675        let p = path!("");
676        assert_eq!(format!("{}", p), "");
677    }
678
679    #[test]
680    fn ll_conversion_invalid_utf8() {
681        let invalid_utf8: Vec<&[u8]> = vec![&[0xff, 0xfe]];
682        let result = Path::try_from_ll_path(&invalid_utf8);
683        assert!(result.is_err());
684        let err = result.unwrap_err();
685        assert!(err.to_string().contains("not valid UTF-8"));
686    }
687
688    #[test]
689    fn path_ord() {
690        let p1 = path!("a/b");
691        let p2 = path!("a/c");
692        let p3 = path!("b/a");
693        assert!(p1 < p2);
694        assert!(p2 < p3);
695    }
696
697    #[test]
698    fn path_hash() {
699        use std::collections::HashSet;
700        let mut set = HashSet::new();
701        set.insert(path!("foo"));
702        set.insert(path!("bar"));
703        set.insert(path!("foo")); // duplicate
704        assert_eq!(set.len(), 2);
705    }
706
707    #[test]
708    fn macro_component_style() {
709        let p = path!("users", 123, "name");
710        assert_eq!(p.to_string(), "users/123/name");
711        assert_eq!(p, path!("users/123/name"));
712    }
713
714    #[test]
715    fn macro_empty() {
716        let p = path!();
717        assert!(p.is_empty());
718    }
719
720    #[test]
721    fn macro_with_runtime_component() {
722        let name = PathComponent::try_new("alice").unwrap();
723        let p = path!("users", name, "profile");
724        assert_eq!(p.to_string(), "users/alice/profile");
725    }
726
727    #[test]
728    fn macro_mixed_literal_forms() {
729        // A literal containing slashes can mix with separate components
730        let p = path!("a/b", "c");
731        assert_eq!(p.to_string(), "a/b/c");
732    }
733
734    #[test]
735    fn path_component_validates() {
736        assert!(PathComponent::try_new("accounts").is_ok());
737        assert!(PathComponent::try_new("42").is_ok());
738        assert!(PathComponent::try_new("café").is_ok());
739        assert!(PathComponent::try_new("_private").is_ok());
740        assert!(PathComponent::try_new("").is_err());
741        assert!(PathComponent::try_new("my-account").is_err());
742        assert!(PathComponent::try_new("my account").is_err());
743        assert!(PathComponent::try_new(".hidden").is_err());
744        assert!(PathComponent::try_new("_").is_err());
745        assert!(PathComponent::try_new("a/b").is_err());
746    }
747
748    #[test]
749    fn path_component_encode_roundtrip() {
750        for original in [
751            "plain",
752            "my-account",
753            "hello world",
754            "slashes/and spaces",
755            "oxide-🦀",
756            "123-456",
757        ] {
758            let component = PathComponent::encode(original);
759            // Encoded form is a valid component usable in paths
760            assert!(PathComponent::try_new(component.as_str()).is_ok());
761            assert_eq!(component.decode().unwrap(), original);
762        }
763    }
764
765    #[test]
766    fn path_component_encode_passthrough() {
767        // Valid identifiers pass through unchanged
768        let component = PathComponent::encode("plain");
769        assert_eq!(component.as_str(), "plain");
770        assert_eq!(component.decode().unwrap(), "plain");
771    }
772
773    #[test]
774    fn path_component_from_index() {
775        let c: PathComponent = 7usize.into();
776        assert_eq!(c.as_str(), "7");
777        let c: PathComponent = 7u64.into();
778        assert_eq!(c.as_str(), "7");
779    }
780
781    #[test]
782    fn child_and_push() {
783        let base = path!("users");
784        let p = base.child(PathComponent::try_new("alice").unwrap());
785        assert_eq!(p.to_string(), "users/alice");
786
787        let mut p2 = path!("items");
788        p2.push(3usize);
789        assert_eq!(p2.to_string(), "items/3");
790    }
791
792    #[test]
793    fn path_from_component_iter() {
794        let p: Path = ["a", "b", "c"]
795            .iter()
796            .map(|s| PathComponent::try_new(*s).unwrap())
797            .collect();
798        assert_eq!(p.to_string(), "a/b/c");
799    }
800
801    #[test]
802    fn validate_component_public() {
803        assert!(Path::validate_component("foo", 0).is_ok());
804        let err = Path::validate_component("bad-name", 2).unwrap_err();
805        assert!(err.to_string().contains("position 2"));
806    }
807
808    #[test]
809    fn path_clone() {
810        let p1 = path!("foo/bar");
811        let p2 = p1.clone();
812        assert_eq!(p1, p2);
813    }
814
815    #[test]
816    fn path_debug() {
817        let p = path!("foo/bar");
818        let debug = format!("{:?}", p);
819        assert!(debug.contains("foo"));
820        assert!(debug.contains("bar"));
821    }
822
823    #[test]
824    fn as_ll_and_into_ll_widen_losslessly() {
825        let p = path!("users/123/name");
826        // Borrowing widening exposes the byte components in order.
827        let ll = p.as_ll();
828        assert_eq!(ll.len(), 3);
829        assert_eq!(ll[0].as_ref(), b"users");
830        assert_eq!(ll[2].as_ref(), b"name");
831        // Owned widening yields the same components.
832        assert_eq!(p.clone().into_ll(), ll.clone());
833    }
834
835    #[test]
836    fn validate_narrows_and_rejects() {
837        // A valid LLPath narrows to the equivalent Path.
838        let ll: LLPath = [Bytes::from_static(b"a"), Bytes::from_static(b"b")]
839            .into_iter()
840            .collect();
841        assert_eq!(Path::validate(ll).unwrap(), path!("a/b"));
842
843        // A non-identifier component is rejected.
844        let bad: LLPath = [Bytes::from_static(b"a-b")].into_iter().collect();
845        assert!(Path::validate(bad).is_err());
846
847        // Non-UTF-8 is rejected.
848        let non_utf8: LLPath = [Bytes::from_static(&[0xff, 0xfe])].into_iter().collect();
849        assert!(Path::validate(non_utf8).is_err());
850    }
851
852    #[test]
853    fn widen_then_narrow_roundtrips() {
854        let p = path!("users/名前/0");
855        // Path -> LLPath (free) -> Path (validated) is the identity.
856        assert_eq!(Path::validate(p.clone().into_ll()).unwrap(), p);
857        // The trusted constructor agrees on already-valid input.
858        assert_eq!(Path::from_ll_unchecked(p.clone().into_ll()), p);
859    }
860}