Skip to main content

web_url/
fragment.rs

1use crate::parse::Error;
2use crate::parse::Error::InvalidFragment;
3use std::fmt::{Display, Formatter};
4
5/// A web-based URL fragment.
6///
7/// # Validation
8/// A fragment will never be empty and will always start with a '#'.
9///
10/// The fragment string can contain any US-ASCII letter, number, or punctuation char.
11#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
12pub struct Fragment<'a> {
13    fragment: &'a str,
14}
15
16impl Default for Fragment<'static> {
17    fn default() -> Self {
18        Self { fragment: "#" }
19    }
20}
21
22impl<'a> Fragment<'a> {
23    //! Construction
24
25    /// Creates a new fragment.
26    ///
27    /// # Safety
28    /// The `fragment` must be valid.
29    pub unsafe fn new(fragment: &'a str) -> Self {
30        debug_assert!(Self::is_valid(fragment));
31
32        Self { fragment }
33    }
34}
35
36impl<'a> TryFrom<&'a str> for Fragment<'a> {
37    type Error = Error;
38
39    fn try_from(fragment: &'a str) -> Result<Self, Self::Error> {
40        if Self::is_valid(fragment) {
41            Ok(Self { fragment })
42        } else {
43            Err(InvalidFragment)
44        }
45    }
46}
47
48impl<'a> Fragment<'a> {
49    //! Validation
50
51    /// Checks if the char `c` is valid.
52    fn is_valid_char(c: u8) -> bool {
53        c.is_ascii_alphanumeric() || c.is_ascii_punctuation()
54    }
55
56    /// Checks if the `fragment` is valid.
57    pub fn is_valid(fragment: &str) -> bool {
58        !fragment.is_empty()
59            && fragment.as_bytes()[0] == b'#'
60            && fragment.as_bytes()[1..]
61                .iter()
62                .all(|c| Self::is_valid_char(*c))
63    }
64}
65
66impl<'a> Fragment<'a> {
67    //! Display
68
69    /// Gets the fragment value. (will not contain the '#' prefix)
70    pub fn value(&self) -> &str {
71        &self.fragment[1..]
72    }
73
74    /// Gets the fragment string. (will contain the '#' prefix)
75    pub const fn as_str(&self) -> &str {
76        self.fragment
77    }
78}
79
80impl<'a> AsRef<str> for Fragment<'a> {
81    fn as_ref(&self) -> &str {
82        self.fragment
83    }
84}
85
86impl<'a> Display for Fragment<'a> {
87    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
88        write!(f, "{}", self.fragment)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use crate::parse::Error::InvalidFragment;
95    use crate::Fragment;
96
97    #[test]
98    fn new() {
99        let fragment: Fragment = unsafe { Fragment::new("#the-fragment") };
100        assert_eq!(fragment.fragment, "#the-fragment");
101    }
102
103    #[test]
104    fn default() {
105        let fragment: Fragment = Fragment::default();
106        assert_eq!(fragment.as_str(), "#");
107        assert_eq!(fragment.value(), "");
108    }
109
110    #[test]
111    fn try_from_str() {
112        assert_eq!(Fragment::try_from("#").unwrap().as_str(), "#");
113        assert_eq!(Fragment::try_from("#section").unwrap().as_str(), "#section");
114        assert_eq!(Fragment::try_from(""), Err(InvalidFragment));
115        assert_eq!(Fragment::try_from("no-hash"), Err(InvalidFragment));
116        assert_eq!(Fragment::try_from("# space"), Err(InvalidFragment));
117    }
118
119    #[test]
120    fn value() {
121        let fragment: Fragment = unsafe { Fragment::new("#the-fragment") };
122        assert_eq!(fragment.value(), "the-fragment");
123
124        let fragment: Fragment = unsafe { Fragment::new("#") };
125        assert_eq!(fragment.value(), "");
126    }
127
128    #[test]
129    fn is_valid() {
130        let test_cases: &[(&str, bool)] = &[
131            ("", false),
132            ("#", true),
133            ("###", true),
134            ("#azAZ09", true),
135            ("#!/&/=/~/", true),
136            ("#?", true),
137            ("#!", true),
138            ("# ", false),
139            ("# x", false),
140        ];
141        for (fragment, expected) in test_cases {
142            let result: bool = Fragment::is_valid(fragment);
143            assert_eq!(result, *expected, "fragment={}", fragment);
144        }
145    }
146
147    #[test]
148    fn display() {
149        let fragment: Fragment = unsafe { Fragment::new("#the-fragment") };
150        assert_eq!(fragment.as_str(), "#the-fragment");
151        assert_eq!(fragment.as_ref(), "#the-fragment");
152        assert_eq!(fragment.to_string(), "#the-fragment");
153    }
154}