dev_bestia_url_utf8/
url_utf8_mod.rs

1// url_utf8_mod
2
3// Cargo.toml:
4// percent-encoding = "2.1.0"
5// unwrap = "1.2.1"
6
7// region: use statements
8
9use core::str::FromStr;
10use percent_encoding::{percent_decode_str, AsciiSet, CONTROLS};
11use std::string::ToString;
12
13use dev_bestia_string_utils::*;
14
15// endregion: use statements
16
17// region: url encoding
18
19/// Constructor macro for UrlUtf8EncodedString  
20#[macro_export]
21macro_rules! url_u {
22    // TODO: the macro could use repetition to avoid having 4 fn with different number of parameters.
23    // 1 arguments, 0 fragment to encode
24    ($literal:literal) => {
25        // The macro will expand into the contents of this block.
26        UrlUtf8EncodedString::new_0($literal)
27    };
28    // 2 arguments, 1 fragment to encode
29    ($literal:expr,$part_1:expr) => {
30        // The macro will expand into the contents of this block.
31        UrlUtf8EncodedString::new_1($literal, $part_1)
32    };
33    // 3 arguments, 2 fragments to encode
34    ($literal:expr,$part_1:expr,$part_2:expr) => {
35        // The macro will expand into the contents of this block.
36        UrlUtf8EncodedString::new_2($literal, $part_1, $part_2)
37    };
38    // 4 arguments, 3 fragments to encode
39    ($literal:expr,$part_1:expr,$part_2:expr,$part_3:expr) => {
40        // The macro will expand into the contents of this block.
41        UrlUtf8EncodedString::new_3($literal, $part_1, $part_2, $part_3)
42    };
43    // 5 arguments, 4 fragments to encode
44    ($literal:expr,$part_1:expr,$part_2:expr,$part_3:expr,$part_4:expr) => {
45        // The macro will expand into the contents of this block.
46        UrlUtf8EncodedString::new_4($literal, $part_1, $part_2, $part_3, $part_4)
47    };
48}
49
50/// Type UrlUtf8EncodedString explicitly informs that the content has been url encoded.  
51/// It contains a string with the whole url.  
52/// The url is constructed with a special macro, where the dynamic parts are always encoded.  
53/// It is impossible to force the developer to properly encode the static part of the url.  
54/// But this special type is making this kind of errors difficult, obvious and traceable.  
55#[derive(Clone, Debug)]
56pub struct UrlUtf8EncodedString {
57    /// private inaccessible field with encoded url  
58    s: String,
59}
60
61impl UrlUtf8EncodedString {
62    /// constructor with 0 dynamic fragment  
63    pub fn new_0(literal: &str) -> UrlUtf8EncodedString {
64        UrlUtf8EncodedString { s: s!(literal) }
65    }
66    /// constructor with 1 dynamic fragment  
67    pub fn new_1(literal: &str, part_1: &str) -> UrlUtf8EncodedString {
68        UrlUtf8EncodedString {
69            s: literal.replacen("{}", &Self::encode_fragment(part_1), 1),
70        }
71    }
72    /// constructor with 2 dynamic fragment  
73    pub fn new_2(literal: &str, part_1: &str, part_2: &str) -> UrlUtf8EncodedString {
74        UrlUtf8EncodedString {
75            s: literal.replacen("{}", &Self::encode_fragment(part_1), 1).replacen("{}", &Self::encode_fragment(part_2), 1),
76        }
77    }
78    /// constructor with 3 dynamic fragment  
79    pub fn new_3(literal: &str, part_1: &str, part_2: &str, part_3: &str) -> UrlUtf8EncodedString {
80        UrlUtf8EncodedString {
81            s: literal
82                .replacen("{}", &Self::encode_fragment(part_1), 1)
83                .replacen("{}", &Self::encode_fragment(part_2), 1)
84                .replacen("{}", &Self::encode_fragment(part_3), 1),
85        }
86    }
87    /// constructor with 4 dynamic fragment  
88    pub fn new_4(literal: &str, part_1: &str, part_2: &str, part_3: &str, part_4: &str) -> UrlUtf8EncodedString {
89        UrlUtf8EncodedString {
90            s: literal
91                .replacen("{}", &Self::encode_fragment(part_1), 1)
92                .replacen("{}", &Self::encode_fragment(part_2), 1)
93                .replacen("{}", &Self::encode_fragment(part_3), 1)
94                .replacen("{}", &Self::encode_fragment(part_4), 1),
95        }
96    }
97    /// encode fragment / part - associated fn  
98    pub fn encode_fragment(s: &str) -> String {
99        // return
100        s!(percent_encoding::utf8_percent_encode(s, FRAGMENT))
101    }
102}
103impl ToString for UrlUtf8EncodedString {
104    #[inline]
105    /// returns encoded string (for use in html attributes)  
106    fn to_string(&self) -> String {
107        // return
108        self.s.clone()
109    }
110}
111
112// end region: url encoding
113
114// region: url part decoding
115/// https://url.spec.whatwg.org/#fragment-percent-encode-set  
116const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
117
118/// the url must be utf 8. Only the 5 control characters are encoded.  
119/// url has parts or fragments or segments delimited mostly by slash /  
120/// every part must be encoded/decoded separately,  
121/// to maintain the control character slash /  
122#[derive(Clone, Debug)]
123pub struct UrlPartUtf8Decoded {
124    /// private inaccessible field - normal string - decoded  
125    s: String,
126}
127
128impl UrlPartUtf8Decoded {
129    /// Constructor from encoded str  
130    /// Decodes the string. It can error.  
131    fn new(s: &str) -> Result<Self, crate::UrlUtf8Error> {
132        let s = s!(percent_decode_str(s).decode_utf8()?);
133        Ok(UrlPartUtf8Decoded { s })
134    }
135    #[allow(unused)]
136    /// rarely needed constructor from decoded (normal) string  
137    pub fn new_from_decoded_string(s: &str) -> Self {
138        UrlPartUtf8Decoded { s: s!(s) }
139    }
140    #[allow(unused)]
141    /// rarely needed get encoded string  
142    pub fn get_encoded_string(&self) -> String {
143        UrlUtf8EncodedString::encode_fragment(&self.s)
144    }
145}
146/// implementing FromStr because of path! in warp web server router  
147/// it assumes that the original string is encoded  
148impl FromStr for UrlPartUtf8Decoded {
149    type Err = crate::UrlUtf8Error;
150    #[inline]
151    /// constructor, decodes the string from encoded str.  
152    /// It can error.  
153    /// It is used for path! in warp web server router.  
154    fn from_str(s: &str) -> Result<Self, crate::UrlUtf8Error> {
155        UrlPartUtf8Decoded::new(s)
156    }
157}
158impl ToString for UrlPartUtf8Decoded {
159    #[inline]
160    /// returns decoded string (normal string)  
161    fn to_string(&self) -> String {
162        // return
163        self.s.clone()
164    }
165}
166
167// region: url part decoding
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use unwrap::unwrap;
173
174    #[test]
175    fn test_decode_01() {
176        let s = s!(unwrap!(UrlPartUtf8Decoded::new("a%20b%3Cc")));
177        assert_eq!(&s, "a b<c");
178    }
179
180    #[test]
181    fn test_encode_02() {
182        let s = url_u!("/one/two/{}/", "a b<c>d'e\"f");
183        let norm_str = s!(s);
184        assert_eq!(&norm_str, "/one/two/a%20b%3Cc%3Ed\'e%22f/");
185    }
186
187    #[test]
188    fn test_03() {
189        let s = url_u!("/one/two/{}/{}/", "a b<ccc", ">ddd'e\"f");
190        let norm_str = s!(s);
191        assert_eq!(norm_str, "/one/two/a%20b%3Cccc/%3Eddd\'e%22f/");
192    }
193
194    #[test]
195    fn test_04() {
196        let s = url_u!("/one{}one/two/{}/{}/", "1 1 ", "a b<ccc", ">ddd'e\"f");
197        let norm_str = s!(s);
198
199        assert_eq!(norm_str, "/one1%201%20one/two/a%20b%3Cccc/%3Eddd\'e%22f/");
200    }
201    #[test]
202    fn test_05() {
203        let s = url_u!("/one{}one/two{}two/{}/{}/", "1 1 ", " 2 2", "a b<ccc", ">ddd'e\"f");
204        let norm_str = s!(s);
205        assert_eq!(norm_str, "/one1%201%20one/two%202%202two/a%20b%3Cccc/%3Eddd\'e%22f/");
206    }
207}