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