moq_lite/
path.rs

1use std::borrow::Cow;
2use std::fmt::{self, Display};
3
4use crate::coding::{Decode, DecodeError, Encode};
5
6/// A trait alias for types that can be converted to a PathRef.
7/// This provides better error messages and documentation.
8pub trait IntoPathRef<'a>: Into<PathRef<'a>> {}
9
10impl<'a, T: Into<PathRef<'a>>> IntoPathRef<'a> for T {}
11
12/// A borrowed reference to a path.
13///
14/// This type is to Path as &str is to String. It provides a way to work with
15/// path strings without requiring ownership. Uses Cow to avoid allocations
16/// when no normalization is needed, but can normalize internal multiple slashes
17/// when required.
18#[derive(Debug, PartialEq, Eq, Hash, Clone)]
19pub struct PathRef<'a>(Cow<'a, str>);
20
21impl<'a> PathRef<'a> {
22	/// Create a new PathRef from a string slice.
23	///
24	/// Leading and trailing slashes are automatically trimmed.
25	/// Multiple consecutive internal slashes are collapsed to single slashes.
26	pub fn new(s: &'a str) -> Self {
27		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
28
29		// Check if we need to normalize (has multiple consecutive slashes)
30		if trimmed.contains("//") {
31			// Only allocate if we actually need to normalize
32			let normalized = trimmed
33				.split('/')
34				.filter(|s| !s.is_empty())
35				.collect::<Vec<_>>()
36				.join("/");
37			Self(Cow::Owned(normalized))
38		} else {
39			// No normalization needed - use borrowed string
40			Self(Cow::Borrowed(trimmed))
41		}
42	}
43
44	/// Check if this path has the given prefix, respecting path boundaries.
45	///
46	/// Unlike String::starts_with, this ensures that "foo" does not match "foobar".
47	/// The prefix must either:
48	/// - Be exactly equal to this path
49	/// - Be followed by a '/' delimiter in the original path
50	/// - Be empty (matches everything)
51	///
52	/// # Examples
53	/// ```
54	/// use moq_lite::Path;
55	///
56	/// let path = Path::new("foo/bar");
57	/// assert!(path.has_prefix("foo"));
58	/// assert!(path.has_prefix(&Path::new("foo")));
59	/// assert!(path.has_prefix("foo/"));
60	/// assert!(!path.has_prefix("fo"));
61	///
62	/// let path = Path::new("foobar");
63	/// assert!(!path.has_prefix("foo"));
64	/// ```
65	pub fn has_prefix<'b>(&self, prefix: impl Into<PathRef<'b>>) -> bool {
66		let prefix = prefix.into();
67		if prefix.is_empty() {
68			return true;
69		}
70
71		if !self.0.starts_with(prefix.as_str()) {
72			return false;
73		}
74
75		// Check if the prefix is the exact match
76		if self.0.len() == prefix.len() {
77			return true;
78		}
79
80		// Otherwise, ensure the character after the prefix is a delimiter
81		self.0.chars().nth(prefix.len()) == Some('/')
82	}
83
84	/// Get the path as a string slice.
85	pub fn as_str(&self) -> &str {
86		&self.0
87	}
88
89	/// Check if the path is empty.
90	pub fn is_empty(&self) -> bool {
91		self.0.is_empty()
92	}
93
94	/// Get the length of the path in bytes.
95	pub fn len(&self) -> usize {
96		self.0.len()
97	}
98
99	/// Convert to an owned Path.
100	pub fn to_owned(&self) -> Path {
101		Path(self.0.clone().into_owned())
102	}
103}
104
105impl<'a> From<&'a str> for PathRef<'a> {
106	fn from(s: &'a str) -> Self {
107		Self::new(s)
108	}
109}
110
111impl<'a> From<&'a String> for PathRef<'a> {
112	fn from(s: &'a String) -> Self {
113		Self::new(s.as_str())
114	}
115}
116
117impl From<String> for PathRef<'static> {
118	fn from(s: String) -> Self {
119		// It's annoying that this logic is duplicated, but I couldn't figure out how to reuse PathRef::new.
120		let trimmed = s.trim_start_matches('/').trim_end_matches('/');
121
122		// Check if we need to normalize (has multiple consecutive slashes)
123		if trimmed.contains("//") {
124			// Only allocate if we actually need to normalize
125			let normalized = trimmed
126				.split('/')
127				.filter(|s| !s.is_empty())
128				.collect::<Vec<_>>()
129				.join("/");
130			Self(Cow::Owned(normalized))
131		} else if trimmed == s {
132			// String is already trimmed and normalized, use it directly
133			Self(Cow::Owned(s))
134		} else {
135			// Need to trim but don't need to normalize internal slashes
136			Self(Cow::Owned(trimmed.to_string()))
137		}
138	}
139}
140
141impl<'a> From<&'a Path> for PathRef<'a> {
142	fn from(p: &'a Path) -> Self {
143		// Path is already normalized, so we can use it directly as borrowed
144		Self(Cow::Borrowed(p.0.as_str()))
145	}
146}
147
148impl<'a, 'b> From<&'a PathRef<'b>> for PathRef<'a>
149where
150	'b: 'a,
151{
152	fn from(p: &'a PathRef<'b>) -> Self {
153		Self(p.0.clone())
154	}
155}
156
157impl<'a> AsRef<str> for PathRef<'a> {
158	fn as_ref(&self) -> &str {
159		&self.0
160	}
161}
162
163impl<'a> Display for PathRef<'a> {
164	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165		write!(f, "{}", self.0)
166	}
167}
168
169/// A broadcast path that provides safe prefix matching operations.
170///
171/// This type wraps a String but provides path-aware operations that respect
172/// delimiter boundaries, preventing issues like "foo" matching "foobar".
173///
174/// Paths are automatically trimmed of leading and trailing slashes on creation,
175/// making all slashes implicit at boundaries.
176/// All paths are RELATIVE; you cannot join with a leading slash to make an absolute path.
177///
178/// # Examples
179/// ```
180/// use moq_lite::{Path, PathRef};
181///
182/// // Creation automatically trims slashes
183/// let path1 = Path::new("/foo/bar/");
184/// let path2 = Path::new("foo/bar");
185/// assert_eq!(path1, path2);
186///
187/// // Methods accept both &str and &Path via PathRef
188/// let base = Path::new("api/v1");
189/// assert!(base.has_prefix("api"));
190/// assert!(base.has_prefix(&Path::new("api/v1")));
191///
192/// let joined = base.join("users");
193/// assert_eq!(joined.as_str(), "api/v1/users");
194/// ```
195#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize))]
197pub struct Path(String);
198
199impl Path {
200	/// Create a new Path from a string or PathRef.
201	///
202	/// Leading and trailing slashes are automatically trimmed.
203	/// Multiple consecutive internal slashes are collapsed to single slashes.
204	/// If a PathRef is provided, sanitization is skipped since PathRef is already normalized.
205	pub fn new<'a>(path: impl Into<PathRef<'a>>) -> Self {
206		// PathRef has already done all the sanitization work
207		Self(path.into().0.to_string())
208	}
209
210	/// Check if this path has the given prefix, respecting path boundaries.
211	///
212	/// Unlike String::starts_with, this ensures that "foo" does not match "foobar".
213	/// The prefix must either:
214	/// - Be exactly equal to this path
215	/// - Be followed by a '/' delimiter in the original path
216	/// - Be empty (matches everything)
217	///
218	/// # Examples
219	/// ```
220	/// use moq_lite::Path;
221	///
222	/// let path = Path::new("foo/bar");
223	/// assert!(path.has_prefix("foo"));
224	/// assert!(path.has_prefix(&Path::new("foo")));
225	/// assert!(path.has_prefix("foo/"));
226	/// assert!(!path.has_prefix("fo"));
227	///
228	/// let path = Path::new("foobar");
229	/// assert!(!path.has_prefix("foo"));
230	/// ```
231	pub fn has_prefix<'a>(&self, prefix: impl Into<PathRef<'a>>) -> bool {
232		let inner: PathRef = self.into();
233		inner.has_prefix(prefix)
234	}
235
236	/// Strip the given prefix from this path, returning the suffix.
237	///
238	/// Returns None if the prefix doesn't match according to has_prefix rules.
239	///
240	/// # Examples
241	/// ```
242	/// use moq_lite::Path;
243	///
244	/// let path = Path::new("foo/bar/baz");
245	/// let suffix = path.strip_prefix("foo").unwrap();
246	/// assert_eq!(suffix.as_str(), "bar/baz");
247	///
248	/// let prefix = Path::new("foo/");
249	/// let suffix = path.strip_prefix(&prefix).unwrap();
250	/// assert_eq!(suffix.as_str(), "bar/baz");
251	/// ```
252	pub fn strip_prefix<'a>(&self, prefix: impl Into<PathRef<'a>>) -> Option<PathRef<'_>> {
253		let prefix = prefix.into();
254		if !self.has_prefix(&prefix) {
255			return None;
256		}
257
258		let suffix = &self.0[prefix.len()..];
259		// Trim leading slash since paths should not start with /
260		let suffix = suffix.trim_start_matches('/');
261		Some(PathRef(Cow::Borrowed(suffix)))
262	}
263
264	/// Get the path as a string slice.
265	pub fn as_str(&self) -> &str {
266		&self.0
267	}
268
269	/// Check if the path is empty.
270	pub fn is_empty(&self) -> bool {
271		self.0.is_empty()
272	}
273
274	/// Get the length of the path in bytes.
275	pub fn len(&self) -> usize {
276		self.0.len()
277	}
278
279	/// Join this path with another path component.
280	///
281	/// # Examples
282	/// ```
283	/// use moq_lite::Path;
284	///
285	/// let base = Path::new("foo");
286	/// let joined = base.join("bar");
287	/// assert_eq!(joined.as_str(), "foo/bar");
288	///
289	/// let joined = base.join(&Path::new("bar"));
290	/// assert_eq!(joined.as_str(), "foo/bar");
291	/// ```
292	pub fn join<'a>(&self, other: impl Into<PathRef<'a>>) -> Path {
293		let other = other.into();
294		if self.0.is_empty() {
295			other.to_owned()
296		} else if other.is_empty() {
297			self.clone()
298		} else {
299			// Since paths are trimmed, we always need to add a slash
300			Path::new(format!("{}/{}", self.0, other.as_str()))
301		}
302	}
303}
304
305impl Display for Path {
306	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307		write!(f, "{}", self.0)
308	}
309}
310
311impl AsRef<str> for Path {
312	fn as_ref(&self) -> &str {
313		&self.0
314	}
315}
316
317impl From<String> for Path {
318	fn from(s: String) -> Self {
319		Self::new(&s)
320	}
321}
322
323impl From<&str> for Path {
324	fn from(s: &str) -> Self {
325		Self::new(s)
326	}
327}
328
329impl From<&String> for Path {
330	fn from(s: &String) -> Self {
331		Self::new(s)
332	}
333}
334
335impl From<&Path> for Path {
336	fn from(p: &Path) -> Self {
337		p.clone()
338	}
339}
340
341impl From<PathRef<'_>> for Path {
342	fn from(p: PathRef<'_>) -> Self {
343		Path(p.0.into_owned())
344	}
345}
346
347impl Decode for Path {
348	fn decode<R: bytes::Buf>(r: &mut R) -> Result<Self, DecodeError> {
349		let path = String::decode(r)?;
350		Ok(Self::new(&path))
351	}
352}
353
354impl Encode for Path {
355	fn encode<W: bytes::BufMut>(&self, w: &mut W) {
356		self.0.encode(w)
357	}
358}
359
360#[cfg(feature = "serde")]
361impl<'de> serde::Deserialize<'de> for Path {
362	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
363	where
364		D: serde::Deserializer<'de>,
365	{
366		let s = String::deserialize(deserializer)?;
367		Ok(Path::new(&s))
368	}
369}
370
371#[cfg(test)]
372mod tests {
373	use super::*;
374
375	#[test]
376	fn test_has_prefix() {
377		let path = Path::new("foo/bar/baz");
378
379		// Valid prefixes - test with both &str and &Path
380		assert!(path.has_prefix(""));
381		assert!(path.has_prefix("foo"));
382		assert!(path.has_prefix(&Path::new("foo")));
383		assert!(path.has_prefix("foo/"));
384		assert!(path.has_prefix("foo/bar"));
385		assert!(path.has_prefix(&Path::new("foo/bar/")));
386		assert!(path.has_prefix("foo/bar/baz"));
387
388		// Invalid prefixes - should not match partial components
389		assert!(!path.has_prefix("f"));
390		assert!(!path.has_prefix(&Path::new("fo")));
391		assert!(!path.has_prefix("foo/b"));
392		assert!(!path.has_prefix("foo/ba"));
393		assert!(!path.has_prefix(&Path::new("foo/bar/ba")));
394
395		// Edge case: "foobar" should not match "foo"
396		let path = Path::new("foobar");
397		assert!(!path.has_prefix("foo"));
398		assert!(path.has_prefix(&Path::new("foobar")));
399	}
400
401	#[test]
402	fn test_strip_prefix() {
403		let path = Path::new("foo/bar/baz");
404
405		// Test with both &str and &Path
406		assert_eq!(path.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
407		assert_eq!(path.strip_prefix("foo").unwrap().as_str(), "bar/baz");
408		assert_eq!(path.strip_prefix(&Path::new("foo/")).unwrap().as_str(), "bar/baz");
409		assert_eq!(path.strip_prefix("foo/bar").unwrap().as_str(), "baz");
410		assert_eq!(path.strip_prefix(&Path::new("foo/bar/")).unwrap().as_str(), "baz");
411		assert_eq!(path.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
412
413		// Should fail for invalid prefixes
414		assert!(path.strip_prefix("fo").is_none());
415		assert!(path.strip_prefix(&Path::new("bar")).is_none());
416	}
417
418	#[test]
419	fn test_join() {
420		// Test with both &str and &Path
421		assert_eq!(Path::new("foo").join("bar").as_str(), "foo/bar");
422		assert_eq!(Path::new("foo/").join(&Path::new("bar")).as_str(), "foo/bar");
423		assert_eq!(Path::new("").join("bar").as_str(), "bar");
424		assert_eq!(Path::new("foo/bar").join(&Path::new("baz")).as_str(), "foo/bar/baz");
425	}
426
427	#[test]
428	fn test_empty() {
429		let empty = Path::new("");
430		assert!(empty.is_empty());
431		assert_eq!(empty.len(), 0);
432
433		let non_empty = Path::new("foo");
434		assert!(!non_empty.is_empty());
435		assert_eq!(non_empty.len(), 3);
436	}
437
438	#[test]
439	fn test_from_conversions() {
440		let path1 = Path::from("foo/bar");
441		let path2 = Path::from(String::from("foo/bar"));
442		let s = String::from("foo/bar");
443		let path3 = Path::from(&s);
444
445		assert_eq!(path1.as_str(), "foo/bar");
446		assert_eq!(path2.as_str(), "foo/bar");
447		assert_eq!(path3.as_str(), "foo/bar");
448	}
449
450	#[test]
451	fn test_path_prefix_join() {
452		let prefix = Path::new("foo");
453		let suffix = Path::new("bar/baz");
454		let path = prefix.join(&suffix);
455		assert_eq!(path.as_str(), "foo/bar/baz");
456
457		let prefix = Path::new("foo/");
458		let suffix = Path::new("bar/baz");
459		let path = prefix.join(&suffix);
460		assert_eq!(path.as_str(), "foo/bar/baz");
461
462		let prefix = Path::new("foo");
463		let suffix = Path::new("/bar/baz");
464		let path = prefix.join(&suffix);
465		assert_eq!(path.as_str(), "foo/bar/baz");
466
467		let prefix = Path::new("");
468		let suffix = Path::new("bar/baz");
469		let path = prefix.join(&suffix);
470		assert_eq!(path.as_str(), "bar/baz");
471	}
472
473	#[test]
474	fn test_path_prefix_conversions() {
475		let prefix1 = Path::from("foo/bar");
476		let prefix2 = Path::from(String::from("foo/bar"));
477		let s = String::from("foo/bar");
478		let prefix3 = Path::from(&s);
479
480		assert_eq!(prefix1.as_str(), "foo/bar");
481		assert_eq!(prefix2.as_str(), "foo/bar");
482		assert_eq!(prefix3.as_str(), "foo/bar");
483	}
484
485	#[test]
486	fn test_path_suffix_conversions() {
487		let suffix1 = Path::from("foo/bar");
488		let suffix2 = Path::from(String::from("foo/bar"));
489		let s = String::from("foo/bar");
490		let suffix3 = Path::from(&s);
491
492		assert_eq!(suffix1.as_str(), "foo/bar");
493		assert_eq!(suffix2.as_str(), "foo/bar");
494		assert_eq!(suffix3.as_str(), "foo/bar");
495	}
496
497	#[test]
498	fn test_path_types_basic_operations() {
499		let prefix = Path::new("foo/bar");
500		assert_eq!(prefix.as_str(), "foo/bar");
501		assert!(!prefix.is_empty());
502		assert_eq!(prefix.len(), 7);
503
504		let suffix = Path::new("baz/qux");
505		assert_eq!(suffix.as_str(), "baz/qux");
506		assert!(!suffix.is_empty());
507		assert_eq!(suffix.len(), 7);
508
509		let empty_prefix = Path::new("");
510		assert!(empty_prefix.is_empty());
511		assert_eq!(empty_prefix.len(), 0);
512
513		let empty_suffix = Path::new("");
514		assert!(empty_suffix.is_empty());
515		assert_eq!(empty_suffix.len(), 0);
516	}
517
518	#[test]
519	fn test_prefix_has_prefix() {
520		// Test empty prefix (should match everything)
521		let prefix = Path::new("foo/bar");
522		assert!(prefix.has_prefix(&Path::new("")));
523
524		// Test exact matches
525		let prefix = Path::new("foo/bar");
526		assert!(prefix.has_prefix(&Path::new("foo/bar")));
527
528		// Test valid prefixes
529		assert!(prefix.has_prefix(&Path::new("foo")));
530		assert!(prefix.has_prefix(&Path::new("foo/")));
531
532		// Test invalid prefixes - partial matches should fail
533		assert!(!prefix.has_prefix(&Path::new("f")));
534		assert!(!prefix.has_prefix(&Path::new("fo")));
535		assert!(!prefix.has_prefix(&Path::new("foo/b")));
536		assert!(!prefix.has_prefix(&Path::new("foo/ba")));
537
538		// Test edge cases
539		let prefix = Path::new("foobar");
540		assert!(!prefix.has_prefix(&Path::new("foo")));
541		assert!(prefix.has_prefix(&Path::new("foobar")));
542
543		// Test trailing slash handling
544		let prefix = Path::new("foo/bar/");
545		assert!(prefix.has_prefix(&Path::new("foo")));
546		assert!(prefix.has_prefix(&Path::new("foo/")));
547		assert!(prefix.has_prefix(&Path::new("foo/bar")));
548		assert!(prefix.has_prefix(&Path::new("foo/bar/")));
549
550		// Test single component
551		let prefix = Path::new("foo");
552		assert!(prefix.has_prefix(&Path::new("")));
553		assert!(prefix.has_prefix(&Path::new("foo")));
554		assert!(prefix.has_prefix(&Path::new("foo/"))); // "foo/" becomes "foo" after trimming
555		assert!(!prefix.has_prefix(&Path::new("f")));
556
557		// Test empty prefix
558		let prefix = Path::new("");
559		assert!(prefix.has_prefix(&Path::new("")));
560		assert!(!prefix.has_prefix(&Path::new("foo")));
561	}
562
563	#[test]
564	fn test_prefix_join() {
565		// Basic joining
566		let prefix = Path::new("foo");
567		let suffix = Path::new("bar");
568		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar");
569
570		// Trailing slash on prefix
571		let prefix = Path::new("foo/");
572		let suffix = Path::new("bar");
573		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar");
574
575		// Leading slash on suffix
576		let prefix = Path::new("foo");
577		let suffix = Path::new("/bar");
578		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar");
579
580		// Trailing slash on suffix
581		let prefix = Path::new("foo");
582		let suffix = Path::new("bar/");
583		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar"); // trailing slash is trimmed
584
585		// Both have slashes
586		let prefix = Path::new("foo/");
587		let suffix = Path::new("/bar");
588		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar");
589
590		// Empty suffix
591		let prefix = Path::new("foo");
592		let suffix = Path::new("");
593		assert_eq!(prefix.join(&suffix).as_str(), "foo");
594
595		// Empty prefix
596		let prefix = Path::new("");
597		let suffix = Path::new("bar");
598		assert_eq!(prefix.join(&suffix).as_str(), "bar");
599
600		// Both empty
601		let prefix = Path::new("");
602		let suffix = Path::new("");
603		assert_eq!(prefix.join(&suffix).as_str(), "");
604
605		// Complex paths
606		let prefix = Path::new("foo/bar");
607		let suffix = Path::new("baz/qux");
608		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar/baz/qux");
609
610		// Complex paths with slashes
611		let prefix = Path::new("foo/bar/");
612		let suffix = Path::new("/baz/qux/");
613		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar/baz/qux"); // all slashes are trimmed
614	}
615
616	#[test]
617	fn test_path_ref() {
618		// Test PathRef creation and normalization
619		let ref1 = PathRef::new("/foo/bar/");
620		assert_eq!(ref1.as_str(), "foo/bar");
621
622		let ref2 = PathRef::from("///foo///");
623		assert_eq!(ref2.as_str(), "foo");
624
625		// Test PathRef normalizes multiple slashes
626		let ref3 = PathRef::new("foo//bar///baz");
627		assert_eq!(ref3.as_str(), "foo/bar/baz");
628
629		// Test conversions
630		let path = Path::new("foo/bar");
631		let path_ref = PathRef::from(&path);
632		assert_eq!(path_ref.as_str(), "foo/bar");
633
634		// Test that Path methods work with PathRef
635		let path2 = Path::new("foo/bar/baz");
636		assert!(path2.has_prefix(&path_ref));
637		assert_eq!(path2.strip_prefix(&path_ref).unwrap().as_str(), "baz");
638
639		// Test empty PathRef
640		let empty = PathRef::new("");
641		assert!(empty.is_empty());
642		assert_eq!(empty.len(), 0);
643	}
644
645	#[test]
646	fn test_multiple_consecutive_slashes() {
647		let path = Path::new("foo//bar///baz");
648		// Multiple consecutive slashes are collapsed to single slashes
649		assert_eq!(path.as_str(), "foo/bar/baz");
650
651		// Test with leading and trailing slashes too
652		let path2 = Path::new("//foo//bar///baz//");
653		assert_eq!(path2.as_str(), "foo/bar/baz");
654
655		// Test empty segments are handled correctly
656		let path3 = Path::new("foo///bar");
657		assert_eq!(path3.as_str(), "foo/bar");
658	}
659
660	#[test]
661	fn test_removes_multiple_slashes_comprehensively() {
662		// Test various multiple slash scenarios
663		assert_eq!(Path::new("foo//bar").as_str(), "foo/bar");
664		assert_eq!(Path::new("foo///bar").as_str(), "foo/bar");
665		assert_eq!(Path::new("foo////bar").as_str(), "foo/bar");
666
667		// Multiple occurrences of double slashes
668		assert_eq!(Path::new("foo//bar//baz").as_str(), "foo/bar/baz");
669		assert_eq!(Path::new("a//b//c//d").as_str(), "a/b/c/d");
670
671		// Mixed slash counts
672		assert_eq!(Path::new("foo//bar///baz////qux").as_str(), "foo/bar/baz/qux");
673
674		// With leading and trailing slashes
675		assert_eq!(Path::new("//foo//bar//").as_str(), "foo/bar");
676		assert_eq!(Path::new("///foo///bar///").as_str(), "foo/bar");
677
678		// Edge case: only slashes
679		assert_eq!(Path::new("//").as_str(), "");
680		assert_eq!(Path::new("////").as_str(), "");
681
682		// Test that operations work correctly with normalized paths
683		let path_with_slashes = Path::new("foo//bar///baz");
684		assert!(path_with_slashes.has_prefix("foo/bar"));
685		assert_eq!(path_with_slashes.strip_prefix("foo").unwrap().as_str(), "bar/baz");
686		assert_eq!(path_with_slashes.join("qux").as_str(), "foo/bar/baz/qux");
687
688		// Test PathRef to Path conversion
689		let path_ref = PathRef::new("foo//bar///baz");
690		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // PathRef now normalizes too
691		let path_from_ref = path_ref.to_owned();
692		assert_eq!(path_from_ref.as_str(), "foo/bar/baz"); // Both are normalized
693	}
694
695	#[test]
696	fn test_path_ref_multiple_slashes() {
697		// PathRef now normalizes multiple slashes using Cow
698		let path_ref = PathRef::new("//foo//bar///baz//");
699		assert_eq!(path_ref.as_str(), "foo/bar/baz"); // Fully normalized
700
701		// Various multiple slash scenarios are normalized in PathRef
702		assert_eq!(PathRef::new("foo//bar").as_str(), "foo/bar");
703		assert_eq!(PathRef::new("foo///bar").as_str(), "foo/bar");
704		assert_eq!(PathRef::new("a//b//c//d").as_str(), "a/b/c/d");
705
706		// Conversion to Path maintains normalized form
707		assert_eq!(PathRef::new("foo//bar").to_owned().as_str(), "foo/bar");
708		assert_eq!(PathRef::new("foo///bar").to_owned().as_str(), "foo/bar");
709		assert_eq!(PathRef::new("a//b//c//d").to_owned().as_str(), "a/b/c/d");
710
711		// Edge cases
712		assert_eq!(PathRef::new("//").as_str(), "");
713		assert_eq!(PathRef::new("////").as_str(), "");
714		assert_eq!(PathRef::new("//").to_owned().as_str(), "");
715		assert_eq!(PathRef::new("////").to_owned().as_str(), "");
716
717		// Test that PathRef avoids allocation when no normalization needed
718		let normal_path = PathRef::new("foo/bar/baz");
719		assert_eq!(normal_path.as_str(), "foo/bar/baz");
720		// This should use Cow::Borrowed internally (no allocation)
721
722		let needs_norm = PathRef::new("foo//bar");
723		assert_eq!(needs_norm.as_str(), "foo/bar");
724		// This should use Cow::Owned internally (allocation only when needed)
725	}
726
727	#[test]
728	fn test_ergonomic_conversions() {
729		// Test that all these work ergonomically in function calls
730		fn takes_path_ref<'a>(p: impl Into<PathRef<'a>>) -> String {
731			p.into().as_str().to_string()
732		}
733
734		// Alternative API using the trait alias for better error messages
735		fn takes_path_ref_with_trait<'a>(p: impl IntoPathRef<'a>) -> String {
736			p.into().as_str().to_string()
737		}
738
739		// String literal
740		assert_eq!(takes_path_ref("foo//bar"), "foo/bar");
741
742		// String (owned) - this should now work without &
743		let owned_string = String::from("foo//bar///baz");
744		assert_eq!(takes_path_ref(owned_string), "foo/bar/baz");
745
746		// &String
747		let string_ref = String::from("foo//bar");
748		assert_eq!(takes_path_ref(&string_ref), "foo/bar");
749
750		// PathRef
751		let path_ref = PathRef::new("foo//bar");
752		assert_eq!(takes_path_ref(&path_ref), "foo/bar");
753
754		// Path
755		let path = Path::new("foo//bar");
756		assert_eq!(takes_path_ref(&path), "foo/bar");
757
758		// Test that Path::new works with all these types
759		let _path1 = Path::new("foo/bar"); // &str
760		let _path2 = Path::new(String::from("foo/bar")); // String - should now work
761		let _path3 = Path::new(String::from("foo/bar")); // &String
762		let _path4 = Path::new(PathRef::new("foo/bar")); // PathRef
763
764		// Test the trait alias version works the same
765		assert_eq!(takes_path_ref_with_trait("foo//bar"), "foo/bar");
766		assert_eq!(takes_path_ref_with_trait(String::from("foo//bar")), "foo/bar");
767	}
768
769	#[test]
770	fn test_prefix_strip_prefix() {
771		// Test basic stripping
772		let prefix = Path::new("foo/bar/baz");
773		assert_eq!(prefix.strip_prefix(&Path::new("")).unwrap().as_str(), "foo/bar/baz");
774		assert_eq!(prefix.strip_prefix(&Path::new("foo")).unwrap().as_str(), "bar/baz");
775		assert_eq!(prefix.strip_prefix(&Path::new("foo/")).unwrap().as_str(), "bar/baz");
776		assert_eq!(prefix.strip_prefix(&Path::new("foo/bar")).unwrap().as_str(), "baz");
777		assert_eq!(prefix.strip_prefix(&Path::new("foo/bar/")).unwrap().as_str(), "baz");
778		assert_eq!(prefix.strip_prefix(&Path::new("foo/bar/baz")).unwrap().as_str(), "");
779
780		// Test invalid prefixes
781		assert!(prefix.strip_prefix(&Path::new("fo")).is_none());
782		assert!(prefix.strip_prefix(&Path::new("bar")).is_none());
783		assert!(prefix.strip_prefix(&Path::new("foo/ba")).is_none());
784
785		// Test edge cases
786		let prefix = Path::new("foobar");
787		assert!(prefix.strip_prefix(&Path::new("foo")).is_none());
788		assert_eq!(prefix.strip_prefix(&Path::new("foobar")).unwrap().as_str(), "");
789
790		// Test empty prefix
791		let prefix = Path::new("");
792		assert_eq!(prefix.strip_prefix(&Path::new("")).unwrap().as_str(), "");
793		assert!(prefix.strip_prefix(&Path::new("foo")).is_none());
794
795		// Test single component
796		let prefix = Path::new("foo");
797		assert_eq!(prefix.strip_prefix(&Path::new("foo")).unwrap().as_str(), "");
798		assert_eq!(prefix.strip_prefix(&Path::new("foo/")).unwrap().as_str(), ""); // "foo/" becomes "foo" after trimming
799
800		// Test trailing slash handling
801		let prefix = Path::new("foo/bar/");
802		assert_eq!(prefix.strip_prefix(&Path::new("foo")).unwrap().as_str(), "bar");
803		assert_eq!(prefix.strip_prefix(&Path::new("foo/")).unwrap().as_str(), "bar");
804		assert_eq!(prefix.strip_prefix(&Path::new("foo/bar")).unwrap().as_str(), "");
805		assert_eq!(prefix.strip_prefix(&Path::new("foo/bar/")).unwrap().as_str(), "");
806	}
807}