web_url/
fragment.rs

1use crate::parse::Error;
2use crate::parse::Error::*;
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. (will not contain the '#' prefix)
70    pub fn fragment(&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::Fragment;
95
96    #[test]
97    fn new() {
98        let fragment: Fragment = unsafe { Fragment::new("#the-fragment") };
99        assert_eq!(fragment.fragment, "#the-fragment");
100    }
101
102    #[test]
103    fn is_valid() {
104        let test_cases: &[(&str, bool)] = &[
105            ("", false),
106            ("#", true),
107            ("###", true),
108            ("#azAZ09", true),
109            ("#!/&/=/~/", true),
110            ("#?", true),
111            ("#!", true),
112            ("# ", false),
113            ("# x", false),
114        ];
115        for (fragment, expected) in test_cases {
116            let result: bool = Fragment::is_valid(fragment);
117            assert_eq!(result, *expected, "fragment={}", fragment);
118        }
119    }
120
121    #[test]
122    fn display() {
123        let fragment: Fragment = unsafe { Fragment::new("#the-fragment") };
124        assert_eq!(fragment.as_str(), "#the-fragment");
125        assert_eq!(fragment.as_ref(), "#the-fragment");
126        assert_eq!(fragment.to_string(), "#the-fragment");
127    }
128}