manas_http/header/slug/
mod.rs

1//! I define [`Slug`] typed header and related structures.
2//!
3
4use std::{borrow::Cow, fmt::Display, ops::Deref};
5
6use headers::{Header, HeaderName, HeaderValue};
7use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, CONTROLS};
8
9/// `Slug` header is defined in [`rfc5023`](https://datatracker.ietf.org/doc/html/rfc5023#section-9.7)
10///
11/// The syntax of the Slug header is defined using the augmented BNF
12/// syntax defined in Section 2.1 of RFC2616:
13///
14/// ```txt
15///     LWS      = <defined in Section 2.2 of [RFC2616]>
16///     slugtext = %x20-7E | LWS
17///     Slug     = "Slug" ":" *slugtext
18///```
19/// The field value is the percent-encoded value of the UTF-8 encoding of
20/// the character sequence to be included (see Section 2.1 of RFC3986
21/// for the definition of percent encoding, and RFC3629 for the
22/// definition of the UTF-8 encoding).
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Slug {
25    /// Pct decoded slug text.
26    pct_decoded_slugtext: String,
27}
28
29/// Static for `slug` header-name.
30pub static SLUG: HeaderName = HeaderName::from_static("slug");
31
32/// Static for ascii-set to be encoded in slug header.
33pub static SLUG_ENCODE_ASCII_SET: AsciiSet = CONTROLS.add(b'%');
34
35impl Header for Slug {
36    fn name() -> &'static HeaderName {
37        &SLUG
38    }
39
40    fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
41    where
42        Self: Sized,
43        I: Iterator<Item = &'i HeaderValue>,
44    {
45        let mut slugtext = String::new();
46        for value in values {
47            // pct decode bytes first, and then decode utf8 str, as per spec
48            let pct_decoded_value = percent_decode(value.as_bytes()).decode_utf8_lossy();
49
50            if !slugtext.is_empty() {
51                // see <https://stackoverflow.com/a/38406581>
52                slugtext.push(',');
53            }
54            slugtext.push_str(pct_decoded_value.as_ref());
55        }
56        Ok(Self {
57            pct_decoded_slugtext: slugtext,
58        })
59    }
60
61    fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
62        values.extend(std::iter::once(
63            HeaderValue::from_str(
64                Into::<Cow<str>>::into(utf8_percent_encode(
65                    &self.pct_decoded_slugtext,
66                    &SLUG_ENCODE_ASCII_SET,
67                ))
68                .as_ref(),
69            )
70            .expect("Must be valid header"),
71        ))
72    }
73}
74
75impl Display for Slug {
76    #[inline]
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        self.pct_decoded_slugtext.fmt(f)
79    }
80}
81
82impl Deref for Slug {
83    type Target = str;
84
85    #[inline]
86    fn deref(&self) -> &Self::Target {
87        &self.pct_decoded_slugtext
88    }
89}
90
91impl AsRef<str> for Slug {
92    #[inline]
93    fn as_ref(&self) -> &str {
94        &self.pct_decoded_slugtext
95    }
96}
97
98impl From<String> for Slug {
99    #[inline]
100    fn from(s: String) -> Self {
101        Self {
102            pct_decoded_slugtext: s,
103        }
104    }
105}
106
107impl From<&str> for Slug {
108    #[inline]
109    fn from(s: &str) -> Self {
110        Self {
111            pct_decoded_slugtext: s.to_owned(),
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use claims::*;
119    use rstest::*;
120
121    use super::*;
122
123    #[rstest]
124    #[case(&[""], "")]
125    #[case(&["a b"], "a b")]
126    #[case(&["abc", "def"], "abc,def")]
127    #[case(&["%E0%A4%B0%E0%A4%BE%E0%A4%AE %E0%A4%B2%E0%A4%95%E0%A5%8D%E0%A4%B7%E0%A5%8D%E0%A4%AE%E0%A4%A3"], "राम लक्ष्मण")]
128    fn decode_works_correctly(#[case] header_value_strs: &[&str], #[case] expected_slug_str: &str) {
129        let header_values: Vec<HeaderValue> = header_value_strs
130            .iter()
131            .map(|v| assert_ok!(HeaderValue::from_str(v)))
132            .collect();
133        let slug = assert_ok!(Slug::decode(&mut header_values.iter()));
134        assert_eq!(slug.as_ref(), expected_slug_str);
135    }
136
137    #[rstest]
138    #[case("a b", "a b")]
139    #[case("a/b", "a/b")]
140    #[case("a%b", "a%25b")]
141    #[case("राम लक्ष्मण", "%E0%A4%B0%E0%A4%BE%E0%A4%AE %E0%A4%B2%E0%A4%95%E0%A5%8D%E0%A4%B7%E0%A5%8D%E0%A4%AE%E0%A4%A3")]
142    fn encode_works_correctly(#[case] slug_str: &str, #[case] expected_header_str: &str) {
143        let slug: Slug = slug_str.to_string().into();
144        let mut headers = Vec::<HeaderValue>::new();
145        slug.encode(&mut headers);
146
147        let encoded_header = headers.first().expect("Slug value not encoded");
148        let encoded_header_str = assert_ok!(encoded_header.to_str(), "Encoding corruption");
149        assert_eq!(encoded_header_str, expected_header_str, "Invalid encoding");
150    }
151}