Skip to main content

yew_nav_link/utils/
path.rs

1//! # Path Utilities
2//!
3//! Helper functions for working with URL paths: normalization,
4//! absolute path detection, and path joining.
5//!
6//! # Example
7//!
8//! ```rust
9//! use yew_nav_link::{is_absolute, join_paths, normalize_path};
10//!
11//! assert_eq!(normalize_path("/docs//api/"), "/docs/api");
12//! assert!(is_absolute("/docs"));
13//! assert_eq!(join_paths("/base", "child"), "/base/child");
14//! ```
15//!
16//! # Functions
17//!
18//! | Function | Signature | Description |
19//! |----------|-----------|-------------|
20//! | `normalize_path` | `(path: &str) -> String` | Collapse double slashes, trim trailing `/` |
21//! | `is_absolute` | `(path: &str) -> bool` | Check if path starts with `/` |
22//! | `join_paths` | `(base: &str, path: &str) -> String` | Join or replace base path |
23
24/// Normalizes a path by collapsing duplicate slashes and removing trailing
25/// slashes.
26///
27/// Preserves the root `/` path.
28#[must_use]
29pub fn normalize_path(path: &str) -> String {
30    let mut result = String::with_capacity(path.len());
31    let mut prev_was_slash = false;
32
33    for ch in path.chars() {
34        if ch == '/' {
35            if !prev_was_slash {
36                result.push(ch);
37                prev_was_slash = true;
38            }
39        } else {
40            result.push(ch);
41            prev_was_slash = false;
42        }
43    }
44
45    if result.len() > 1 && result.ends_with('/') {
46        result.pop();
47    }
48
49    result
50}
51
52/// Returns `true` if the path starts with `/` (i.e. is absolute).
53#[must_use]
54pub fn is_absolute(path: &str) -> bool {
55    path.starts_with('/')
56}
57
58/// Joins a base path with a child path.
59///
60/// If `path` is absolute, it is returned (normalized) directly.
61/// Otherwise the two paths are concatenated and normalized.
62#[must_use]
63pub fn join_paths(base: &str, path: &str) -> String {
64    if path.starts_with('/') {
65        normalize_path(path)
66    } else {
67        let base = base.trim_end_matches('/');
68        normalize_path(&format!("{base}/{path}"))
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn normalize_removes_double_slashes() {
78        assert_eq!(normalize_path("/docs//api"), "/docs/api");
79        assert_eq!(normalize_path("a//b///c"), "a/b/c");
80    }
81
82    #[test]
83    fn normalize_removes_trailing_slash() {
84        assert_eq!(normalize_path("/docs/"), "/docs");
85        assert_eq!(normalize_path("/"), "/");
86    }
87
88    #[test]
89    fn is_absolute_returns_true_for_rooted_paths() {
90        assert!(is_absolute("/"));
91        assert!(is_absolute("/docs"));
92        assert!(is_absolute("/docs/api"));
93    }
94
95    #[test]
96    fn is_absolute_returns_false_for_relative_paths() {
97        assert!(!is_absolute(""));
98        assert!(!is_absolute("docs"));
99        assert!(!is_absolute("./docs"));
100        assert!(!is_absolute("../docs"));
101    }
102
103    #[test]
104    fn join_paths_with_absolute() {
105        assert_eq!(join_paths("/base", "/docs"), "/docs");
106        assert_eq!(join_paths("/base", "/"), "/");
107    }
108
109    #[test]
110    fn join_paths_with_relative() {
111        assert_eq!(join_paths("/base", "docs"), "/base/docs");
112        assert_eq!(join_paths("/base/", "docs"), "/base/docs");
113    }
114}