moq_lite/
path.rs

1use std::fmt::{self, Display};
2
3use crate::coding::{Decode, DecodeError, Encode};
4
5/// A borrowed reference to a path.
6///
7/// This type is to Path as &str is to String. It provides a way to work with
8/// path strings without requiring ownership.
9#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
10pub struct PathRef<'a>(&'a str);
11
12impl<'a> PathRef<'a> {
13	/// Create a new PathRef from a string slice.
14	///
15	/// Leading and trailing slashes are automatically trimmed.
16	pub fn new(s: &'a str) -> Self {
17		Self(s.trim_start_matches('/').trim_end_matches('/'))
18	}
19
20	/// Get the path as a string slice.
21	pub fn as_str(&self) -> &str {
22		self.0
23	}
24
25	/// Check if the path is empty.
26	pub fn is_empty(&self) -> bool {
27		self.0.is_empty()
28	}
29
30	/// Get the length of the path in bytes.
31	pub fn len(&self) -> usize {
32		self.0.len()
33	}
34
35	/// Convert to an owned Path.
36	pub fn to_path(&self) -> Path {
37		Path::new(self.0)
38	}
39}
40
41impl<'a> From<&'a str> for PathRef<'a> {
42	fn from(s: &'a str) -> Self {
43		Self::new(s)
44	}
45}
46
47impl<'a> From<&'a String> for PathRef<'a> {
48	fn from(s: &'a String) -> Self {
49		Self::new(s.as_str())
50	}
51}
52
53impl<'a> From<&'a Path> for PathRef<'a> {
54	fn from(p: &'a Path) -> Self {
55		// Path is already trimmed, so we can use it directly
56		Self(p.0.as_str())
57	}
58}
59
60impl<'a, 'b> From<&'a PathRef<'b>> for PathRef<'a>
61where
62	'b: 'a,
63{
64	fn from(p: &'a PathRef<'b>) -> Self {
65		Self(p.0)
66	}
67}
68
69impl<'a> AsRef<str> for PathRef<'a> {
70	fn as_ref(&self) -> &str {
71		self.0
72	}
73}
74
75impl<'a> Display for PathRef<'a> {
76	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77		write!(f, "{}", self.0)
78	}
79}
80
81/// A broadcast path that provides safe prefix matching operations.
82///
83/// This type wraps a String but provides path-aware operations that respect
84/// delimiter boundaries, preventing issues like "foo" matching "foobar".
85///
86/// Paths are automatically trimmed of leading and trailing slashes on creation,
87/// making all slashes implicit at boundaries.
88///
89/// # Examples
90/// ```
91/// use moq_lite::{Path, PathRef};
92///
93/// // Creation automatically trims slashes
94/// let path1 = Path::new("/foo/bar/");
95/// let path2 = Path::new("foo/bar");
96/// assert_eq!(path1, path2);
97///
98/// // Methods accept both &str and &Path via PathRef
99/// let base = Path::new("api/v1");
100/// assert!(base.has_prefix("api"));
101/// assert!(base.has_prefix(&Path::new("api/v1")));
102///
103/// let joined = base.join("users");
104/// assert_eq!(joined.as_str(), "api/v1/users");
105/// ```
106#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize))]
108pub struct Path(String);
109
110impl Path {
111	/// Create a new Path from a string.
112	///
113	/// Leading and trailing slashes are automatically trimmed.
114	pub fn new<S: Into<String>>(s: S) -> Self {
115		let s = s.into();
116		Self(s.trim_start_matches('/').trim_end_matches('/').to_string())
117	}
118
119	/// Check if this path has the given prefix, respecting path boundaries.
120	///
121	/// Unlike String::starts_with, this ensures that "foo" does not match "foobar".
122	/// The prefix must either:
123	/// - Be exactly equal to this path
124	/// - Be followed by a '/' delimiter in the original path
125	/// - Be empty (matches everything)
126	///
127	/// # Examples
128	/// ```
129	/// use moq_lite::Path;
130	///
131	/// let path = Path::new("foo/bar");
132	/// assert!(path.has_prefix("foo"));
133	/// assert!(path.has_prefix(&Path::new("foo")));
134	/// assert!(path.has_prefix("foo/"));
135	/// assert!(!path.has_prefix("fo"));
136	///
137	/// let path = Path::new("foobar");
138	/// assert!(!path.has_prefix("foo"));
139	/// ```
140	pub fn has_prefix<'a>(&self, prefix: impl Into<PathRef<'a>>) -> bool {
141		let prefix = prefix.into();
142		if prefix.is_empty() {
143			return true;
144		}
145
146		if !self.0.starts_with(prefix.as_str()) {
147			return false;
148		}
149
150		// Check if the prefix is the exact match
151		if self.0.len() == prefix.len() {
152			return true;
153		}
154
155		// Otherwise, ensure the character after the prefix is a delimiter
156		self.0.chars().nth(prefix.len()) == Some('/')
157	}
158
159	/// Strip the given prefix from this path, returning the suffix.
160	///
161	/// Returns None if the prefix doesn't match according to has_prefix rules.
162	///
163	/// # Examples
164	/// ```
165	/// use moq_lite::Path;
166	///
167	/// let path = Path::new("foo/bar/baz");
168	/// let suffix = path.strip_prefix("foo").unwrap();
169	/// assert_eq!(suffix.as_str(), "bar/baz");
170	///
171	/// let suffix = path.strip_prefix(&Path::new("foo/")).unwrap();
172	/// assert_eq!(suffix.as_str(), "bar/baz");
173	/// ```
174	pub fn strip_prefix<'a>(&'a self, prefix: impl Into<PathRef<'a>>) -> Option<PathRef<'a>> {
175		let prefix = prefix.into();
176		if !self.has_prefix(prefix) {
177			return None;
178		}
179
180		let suffix = &self.0[prefix.len()..];
181		// Trim leading slash since paths should not start with /
182		let suffix = suffix.trim_start_matches('/');
183		Some(PathRef(suffix))
184	}
185
186	/// Get the path as a string slice.
187	pub fn as_str(&self) -> &str {
188		&self.0
189	}
190
191	/// Check if the path is empty.
192	pub fn is_empty(&self) -> bool {
193		self.0.is_empty()
194	}
195
196	/// Get the length of the path in bytes.
197	pub fn len(&self) -> usize {
198		self.0.len()
199	}
200
201	/// Join this path with another path component.
202	///
203	/// # Examples
204	/// ```
205	/// use moq_lite::Path;
206	///
207	/// let base = Path::new("foo");
208	/// let joined = base.join("bar");
209	/// assert_eq!(joined.as_str(), "foo/bar");
210	///
211	/// let joined = base.join(&Path::new("bar"));
212	/// assert_eq!(joined.as_str(), "foo/bar");
213	/// ```
214	pub fn join<'a>(&self, other: impl Into<PathRef<'a>>) -> Path {
215		let other = other.into();
216		if self.0.is_empty() {
217			other.to_path()
218		} else if other.is_empty() {
219			self.clone()
220		} else {
221			// Since paths are trimmed, we always need to add a slash
222			Path::new(format!("{}/{}", self.0, other.as_str()))
223		}
224	}
225}
226
227impl Display for Path {
228	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229		write!(f, "{}", self.0)
230	}
231}
232
233impl AsRef<str> for Path {
234	fn as_ref(&self) -> &str {
235		&self.0
236	}
237}
238
239impl From<String> for Path {
240	fn from(s: String) -> Self {
241		Self::new(s)
242	}
243}
244
245impl From<&str> for Path {
246	fn from(s: &str) -> Self {
247		Self::new(s)
248	}
249}
250
251impl From<&String> for Path {
252	fn from(s: &String) -> Self {
253		Self::new(s)
254	}
255}
256
257impl From<&Path> for Path {
258	fn from(p: &Path) -> Self {
259		p.clone()
260	}
261}
262
263impl From<PathRef<'_>> for Path {
264	fn from(p: PathRef<'_>) -> Self {
265		Path::new(p.0)
266	}
267}
268
269impl Decode for Path {
270	fn decode<R: bytes::Buf>(r: &mut R) -> Result<Self, DecodeError> {
271		let path = String::decode(r)?;
272		Ok(Self::new(path))
273	}
274}
275
276impl Encode for Path {
277	fn encode<W: bytes::BufMut>(&self, w: &mut W) {
278		self.0.encode(w)
279	}
280}
281
282#[cfg(feature = "serde")]
283impl<'de> serde::Deserialize<'de> for Path {
284	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
285	where
286		D: serde::Deserializer<'de>,
287	{
288		let s = String::deserialize(deserializer)?;
289		Ok(Path::new(s))
290	}
291}
292
293#[cfg(test)]
294mod tests {
295	use super::*;
296
297	#[test]
298	fn test_has_prefix() {
299		let path = Path::new("foo/bar/baz");
300
301		// Valid prefixes - test with both &str and &Path
302		assert!(path.has_prefix(""));
303		assert!(path.has_prefix("foo"));
304		assert!(path.has_prefix(&Path::new("foo")));
305		assert!(path.has_prefix("foo/"));
306		assert!(path.has_prefix("foo/bar"));
307		assert!(path.has_prefix(&Path::new("foo/bar/")));
308		assert!(path.has_prefix("foo/bar/baz"));
309
310		// Invalid prefixes - should not match partial components
311		assert!(!path.has_prefix("f"));
312		assert!(!path.has_prefix(&Path::new("fo")));
313		assert!(!path.has_prefix("foo/b"));
314		assert!(!path.has_prefix("foo/ba"));
315		assert!(!path.has_prefix(&Path::new("foo/bar/ba")));
316
317		// Edge case: "foobar" should not match "foo"
318		let path = Path::new("foobar");
319		assert!(!path.has_prefix("foo"));
320		assert!(path.has_prefix(&Path::new("foobar")));
321	}
322
323	#[test]
324	fn test_strip_prefix() {
325		let path = Path::new("foo/bar/baz");
326
327		// Test with both &str and &Path
328		assert_eq!(path.strip_prefix("").unwrap().as_str(), "foo/bar/baz");
329		assert_eq!(path.strip_prefix("foo").unwrap().as_str(), "bar/baz");
330		assert_eq!(path.strip_prefix(&Path::new("foo/")).unwrap().as_str(), "bar/baz");
331		assert_eq!(path.strip_prefix("foo/bar").unwrap().as_str(), "baz");
332		assert_eq!(path.strip_prefix(&Path::new("foo/bar/")).unwrap().as_str(), "baz");
333		assert_eq!(path.strip_prefix("foo/bar/baz").unwrap().as_str(), "");
334
335		// Should fail for invalid prefixes
336		assert!(path.strip_prefix("fo").is_none());
337		assert!(path.strip_prefix(&Path::new("bar")).is_none());
338	}
339
340	#[test]
341	fn test_join() {
342		// Test with both &str and &Path
343		assert_eq!(Path::new("foo").join("bar").as_str(), "foo/bar");
344		assert_eq!(Path::new("foo/").join(&Path::new("bar")).as_str(), "foo/bar");
345		assert_eq!(Path::new("").join("bar").as_str(), "bar");
346		assert_eq!(Path::new("foo/bar").join(&Path::new("baz")).as_str(), "foo/bar/baz");
347	}
348
349	#[test]
350	fn test_empty() {
351		let empty = Path::new("");
352		assert!(empty.is_empty());
353		assert_eq!(empty.len(), 0);
354
355		let non_empty = Path::new("foo");
356		assert!(!non_empty.is_empty());
357		assert_eq!(non_empty.len(), 3);
358	}
359
360	#[test]
361	fn test_from_conversions() {
362		let path1 = Path::from("foo/bar");
363		let path2 = Path::from(String::from("foo/bar"));
364		let s = String::from("foo/bar");
365		let path3 = Path::from(&s);
366
367		assert_eq!(path1.as_str(), "foo/bar");
368		assert_eq!(path2.as_str(), "foo/bar");
369		assert_eq!(path3.as_str(), "foo/bar");
370	}
371
372	#[test]
373	fn test_path_prefix_join() {
374		let prefix = Path::new("foo");
375		let suffix = Path::new("bar/baz");
376		let path = prefix.join(&suffix);
377		assert_eq!(path.as_str(), "foo/bar/baz");
378
379		let prefix = Path::new("foo/");
380		let suffix = Path::new("bar/baz");
381		let path = prefix.join(&suffix);
382		assert_eq!(path.as_str(), "foo/bar/baz");
383
384		let prefix = Path::new("foo");
385		let suffix = Path::new("/bar/baz");
386		let path = prefix.join(&suffix);
387		assert_eq!(path.as_str(), "foo/bar/baz");
388
389		let prefix = Path::new("");
390		let suffix = Path::new("bar/baz");
391		let path = prefix.join(&suffix);
392		assert_eq!(path.as_str(), "bar/baz");
393	}
394
395	#[test]
396	fn test_path_prefix_conversions() {
397		let prefix1 = Path::from("foo/bar");
398		let prefix2 = Path::from(String::from("foo/bar"));
399		let s = String::from("foo/bar");
400		let prefix3 = Path::from(&s);
401
402		assert_eq!(prefix1.as_str(), "foo/bar");
403		assert_eq!(prefix2.as_str(), "foo/bar");
404		assert_eq!(prefix3.as_str(), "foo/bar");
405	}
406
407	#[test]
408	fn test_path_suffix_conversions() {
409		let suffix1 = Path::from("foo/bar");
410		let suffix2 = Path::from(String::from("foo/bar"));
411		let s = String::from("foo/bar");
412		let suffix3 = Path::from(&s);
413
414		assert_eq!(suffix1.as_str(), "foo/bar");
415		assert_eq!(suffix2.as_str(), "foo/bar");
416		assert_eq!(suffix3.as_str(), "foo/bar");
417	}
418
419	#[test]
420	fn test_path_types_basic_operations() {
421		let prefix = Path::new("foo/bar");
422		assert_eq!(prefix.as_str(), "foo/bar");
423		assert!(!prefix.is_empty());
424		assert_eq!(prefix.len(), 7);
425
426		let suffix = Path::new("baz/qux");
427		assert_eq!(suffix.as_str(), "baz/qux");
428		assert!(!suffix.is_empty());
429		assert_eq!(suffix.len(), 7);
430
431		let empty_prefix = Path::new("");
432		assert!(empty_prefix.is_empty());
433		assert_eq!(empty_prefix.len(), 0);
434
435		let empty_suffix = Path::new("");
436		assert!(empty_suffix.is_empty());
437		assert_eq!(empty_suffix.len(), 0);
438	}
439
440	#[test]
441	fn test_prefix_has_prefix() {
442		// Test empty prefix (should match everything)
443		let prefix = Path::new("foo/bar");
444		assert!(prefix.has_prefix(&Path::new("")));
445
446		// Test exact matches
447		let prefix = Path::new("foo/bar");
448		assert!(prefix.has_prefix(&Path::new("foo/bar")));
449
450		// Test valid prefixes
451		assert!(prefix.has_prefix(&Path::new("foo")));
452		assert!(prefix.has_prefix(&Path::new("foo/")));
453
454		// Test invalid prefixes - partial matches should fail
455		assert!(!prefix.has_prefix(&Path::new("f")));
456		assert!(!prefix.has_prefix(&Path::new("fo")));
457		assert!(!prefix.has_prefix(&Path::new("foo/b")));
458		assert!(!prefix.has_prefix(&Path::new("foo/ba")));
459
460		// Test edge cases
461		let prefix = Path::new("foobar");
462		assert!(!prefix.has_prefix(&Path::new("foo")));
463		assert!(prefix.has_prefix(&Path::new("foobar")));
464
465		// Test trailing slash handling
466		let prefix = Path::new("foo/bar/");
467		assert!(prefix.has_prefix(&Path::new("foo")));
468		assert!(prefix.has_prefix(&Path::new("foo/")));
469		assert!(prefix.has_prefix(&Path::new("foo/bar")));
470		assert!(prefix.has_prefix(&Path::new("foo/bar/")));
471
472		// Test single component
473		let prefix = Path::new("foo");
474		assert!(prefix.has_prefix(&Path::new("")));
475		assert!(prefix.has_prefix(&Path::new("foo")));
476		assert!(prefix.has_prefix(&Path::new("foo/"))); // "foo/" becomes "foo" after trimming
477		assert!(!prefix.has_prefix(&Path::new("f")));
478
479		// Test empty prefix
480		let prefix = Path::new("");
481		assert!(prefix.has_prefix(&Path::new("")));
482		assert!(!prefix.has_prefix(&Path::new("foo")));
483	}
484
485	#[test]
486	fn test_prefix_join() {
487		// Basic joining
488		let prefix = Path::new("foo");
489		let suffix = Path::new("bar");
490		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar");
491
492		// Trailing slash on prefix
493		let prefix = Path::new("foo/");
494		let suffix = Path::new("bar");
495		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar");
496
497		// Leading slash on suffix
498		let prefix = Path::new("foo");
499		let suffix = Path::new("/bar");
500		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar");
501
502		// Trailing slash on suffix
503		let prefix = Path::new("foo");
504		let suffix = Path::new("bar/");
505		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar"); // trailing slash is trimmed
506
507		// Both have slashes
508		let prefix = Path::new("foo/");
509		let suffix = Path::new("/bar");
510		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar");
511
512		// Empty suffix
513		let prefix = Path::new("foo");
514		let suffix = Path::new("");
515		assert_eq!(prefix.join(&suffix).as_str(), "foo");
516
517		// Empty prefix
518		let prefix = Path::new("");
519		let suffix = Path::new("bar");
520		assert_eq!(prefix.join(&suffix).as_str(), "bar");
521
522		// Both empty
523		let prefix = Path::new("");
524		let suffix = Path::new("");
525		assert_eq!(prefix.join(&suffix).as_str(), "");
526
527		// Complex paths
528		let prefix = Path::new("foo/bar");
529		let suffix = Path::new("baz/qux");
530		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar/baz/qux");
531
532		// Complex paths with slashes
533		let prefix = Path::new("foo/bar/");
534		let suffix = Path::new("/baz/qux/");
535		assert_eq!(prefix.join(&suffix).as_str(), "foo/bar/baz/qux"); // all slashes are trimmed
536	}
537
538	#[test]
539	fn test_path_ref() {
540		// Test PathRef creation and trimming
541		let ref1 = PathRef::new("/foo/bar/");
542		assert_eq!(ref1.as_str(), "foo/bar");
543
544		let ref2 = PathRef::from("///foo///");
545		assert_eq!(ref2.as_str(), "foo");
546
547		// Test conversions
548		let path = Path::new("foo/bar");
549		let path_ref = PathRef::from(&path);
550		assert_eq!(path_ref.as_str(), "foo/bar");
551
552		// Test that Path methods work with PathRef
553		let path2 = Path::new("foo/bar/baz");
554		assert!(path2.has_prefix(path_ref));
555		assert_eq!(path2.strip_prefix(path_ref).unwrap().as_str(), "baz");
556
557		// Test empty PathRef
558		let empty = PathRef::new("");
559		assert!(empty.is_empty());
560		assert_eq!(empty.len(), 0);
561	}
562
563	#[test]
564	fn test_prefix_strip_prefix() {
565		// Test basic stripping
566		let prefix = Path::new("foo/bar/baz");
567		assert_eq!(prefix.strip_prefix(&Path::new("")).unwrap().as_str(), "foo/bar/baz");
568		assert_eq!(prefix.strip_prefix(&Path::new("foo")).unwrap().as_str(), "bar/baz");
569		assert_eq!(prefix.strip_prefix(&Path::new("foo/")).unwrap().as_str(), "bar/baz");
570		assert_eq!(prefix.strip_prefix(&Path::new("foo/bar")).unwrap().as_str(), "baz");
571		assert_eq!(prefix.strip_prefix(&Path::new("foo/bar/")).unwrap().as_str(), "baz");
572		assert_eq!(prefix.strip_prefix(&Path::new("foo/bar/baz")).unwrap().as_str(), "");
573
574		// Test invalid prefixes
575		assert!(prefix.strip_prefix(&Path::new("fo")).is_none());
576		assert!(prefix.strip_prefix(&Path::new("bar")).is_none());
577		assert!(prefix.strip_prefix(&Path::new("foo/ba")).is_none());
578
579		// Test edge cases
580		let prefix = Path::new("foobar");
581		assert!(prefix.strip_prefix(&Path::new("foo")).is_none());
582		assert_eq!(prefix.strip_prefix(&Path::new("foobar")).unwrap().as_str(), "");
583
584		// Test empty prefix
585		let prefix = Path::new("");
586		assert_eq!(prefix.strip_prefix(&Path::new("")).unwrap().as_str(), "");
587		assert!(prefix.strip_prefix(&Path::new("foo")).is_none());
588
589		// Test single component
590		let prefix = Path::new("foo");
591		assert_eq!(prefix.strip_prefix(&Path::new("foo")).unwrap().as_str(), "");
592		assert_eq!(prefix.strip_prefix(&Path::new("foo/")).unwrap().as_str(), ""); // "foo/" becomes "foo" after trimming
593
594		// Test trailing slash handling
595		let prefix = Path::new("foo/bar/");
596		assert_eq!(prefix.strip_prefix(&Path::new("foo")).unwrap().as_str(), "bar");
597		assert_eq!(prefix.strip_prefix(&Path::new("foo/")).unwrap().as_str(), "bar");
598		assert_eq!(prefix.strip_prefix(&Path::new("foo/bar")).unwrap().as_str(), "");
599		assert_eq!(prefix.strip_prefix(&Path::new("foo/bar/")).unwrap().as_str(), "");
600	}
601}