web_url/
fragment.rs

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