icann_rdap_common/check/
string.rs

1/// Functions for types that can be turned into strings.
2///
3/// Example:
4/// ```rust
5/// use icann_rdap_common::check::*;
6///
7/// let s = "  ";
8/// assert!(s.is_whitespace_or_empty());
9/// ```
10pub trait StringCheck {
11    /// Tests if the string is empty, including for if the string only has whitespace.
12    fn is_whitespace_or_empty(&self) -> bool;
13
14    /// Tests if the string contains only letters, digits, or hyphens and is not empty.
15    fn is_ldh_string(&self) -> bool;
16
17    /// Tests if a string is an LDH doamin name. This is not to be confused with [StringCheck::is_ldh_string],
18    /// which checks individual domain labels.
19    fn is_ldh_domain_name(&self) -> bool;
20
21    /// Tests if a string is a Unicode domain name.
22    fn is_unicode_domain_name(&self) -> bool;
23
24    /// Tests if a string is begins with a period and only has one label.
25    fn is_tld(&self) -> bool;
26}
27
28impl<T: ToString> StringCheck for T {
29    fn is_whitespace_or_empty(&self) -> bool {
30        let s = self.to_string();
31        s.is_empty() || s.chars().all(char::is_whitespace)
32    }
33
34    fn is_ldh_string(&self) -> bool {
35        let s = self.to_string();
36        !s.is_empty() && s.chars().all(char::is_ldh)
37    }
38
39    fn is_ldh_domain_name(&self) -> bool {
40        let s = self.to_string();
41        s == "." || (!s.is_empty() && s.split_terminator('.').all(|s| s.is_ldh_string()))
42    }
43
44    fn is_unicode_domain_name(&self) -> bool {
45        let s = self.to_string();
46        s == "."
47            || (!s.is_empty()
48                && s.split_terminator('.').all(|s| {
49                    s.chars()
50                        .all(|c| c == '-' || (!c.is_ascii_punctuation() && !c.is_whitespace()))
51                }))
52    }
53
54    fn is_tld(&self) -> bool {
55        let s = self.to_string();
56        s.starts_with('.')
57            && s.len() > 2
58            && s.matches('.').count() == 1
59            && s.split_terminator('.').all(|s| {
60                s.chars()
61                    .all(|c| !c.is_ascii_punctuation() && !c.is_whitespace())
62            })
63    }
64}
65
66/// Functions for types that can be turned into arrays of strings.
67///
68/// Example:
69/// ```rust
70/// use icann_rdap_common::check::*;
71///
72/// let a: &[&str] = &["foo",""];
73/// assert!(a.is_empty_or_any_empty_or_whitespace());
74/// ```
75pub trait StringListCheck {
76    /// Tests if a list of strings is empty, or if any of the
77    /// elemeents of the list are empty or whitespace.
78    fn is_empty_or_any_empty_or_whitespace(&self) -> bool;
79
80    /// Tests if a list of strings ard LDH strings. See [CharCheck::is_ldh].
81    fn is_ldh_string_list(&self) -> bool;
82}
83
84impl<T: ToString> StringListCheck for &[T] {
85    fn is_empty_or_any_empty_or_whitespace(&self) -> bool {
86        self.is_empty() || self.iter().any(|s| s.to_string().is_whitespace_or_empty())
87    }
88
89    fn is_ldh_string_list(&self) -> bool {
90        !self.is_empty() && self.iter().all(|s| s.to_string().is_ldh_string())
91    }
92}
93
94impl<T: ToString> StringListCheck for Vec<T> {
95    fn is_empty_or_any_empty_or_whitespace(&self) -> bool {
96        self.is_empty() || self.iter().any(|s| s.to_string().is_whitespace_or_empty())
97    }
98
99    fn is_ldh_string_list(&self) -> bool {
100        !self.is_empty() && self.iter().all(|s| s.to_string().is_ldh_string())
101    }
102}
103
104/// Functions for chars.
105///
106/// Example:
107/// ```rust
108/// use icann_rdap_common::check::*;
109///
110/// let c = 'a';
111/// assert!(c.is_ldh());
112/// ```
113pub trait CharCheck {
114    /// Checks if the character is a letter, digit or a hyphen
115    #[allow(clippy::wrong_self_convention)]
116    fn is_ldh(self) -> bool;
117}
118
119impl CharCheck for char {
120    fn is_ldh(self) -> bool {
121        matches!(self, 'A'..='Z' | 'a'..='z' | '0'..='9' | '-')
122    }
123}
124
125#[cfg(test)]
126#[allow(non_snake_case)]
127mod tests {
128    use rstest::rstest;
129
130    use crate::check::string::{CharCheck, StringListCheck};
131
132    use super::StringCheck;
133
134    #[rstest]
135    #[case("foo", false)]
136    #[case("", true)]
137    #[case(" ", true)]
138    #[case("foo bar", false)]
139    fn GIVEN_string_WHEN_is_whitespace_or_empty_THEN_correct_result(
140        #[case] test_string: &str,
141        #[case] expected: bool,
142    ) {
143        // GIVEN in parameters
144
145        // WHEN
146        let actual = test_string.is_whitespace_or_empty();
147
148        // THEN
149        assert_eq!(actual, expected);
150    }
151
152    #[rstest]
153    #[case(&[], true)]
154    #[case(&["foo"], false)]
155    #[case(&["foo",""], true)]
156    #[case(&["foo","bar"], false)]
157    #[case(&["foo","bar baz"], false)]
158    #[case(&[""], true)]
159    #[case(&[" "], true)]
160    fn GIVEN_string_list_WHEN_is_whitespace_or_empty_THEN_correct_result(
161        #[case] test_list: &[&str],
162        #[case] expected: bool,
163    ) {
164        // GIVEN in parameters
165
166        // WHEN
167        let actual = test_list.is_empty_or_any_empty_or_whitespace();
168
169        // THEN
170        assert_eq!(actual, expected);
171    }
172
173    #[rstest]
174    #[case('a', true)]
175    #[case('l', true)]
176    #[case('z', true)]
177    #[case('A', true)]
178    #[case('L', true)]
179    #[case('Z', true)]
180    #[case('0', true)]
181    #[case('3', true)]
182    #[case('9', true)]
183    #[case('-', true)]
184    #[case('_', false)]
185    #[case('.', false)]
186    fn GIVEN_char_WHEN_is_ldh_THEN_correct_result(#[case] test_char: char, #[case] expected: bool) {
187        // GIVEN in parameters
188
189        // WHEN
190        let actual = test_char.is_ldh();
191
192        // THEN
193        assert_eq!(actual, expected);
194    }
195
196    #[rstest]
197    #[case("foo", true)]
198    #[case("", false)]
199    #[case("foo-bar", true)]
200    #[case("foo bar", false)]
201    fn GIVEN_string_WHEN_is_ldh_string_THEN_correct_result(
202        #[case] test_string: &str,
203        #[case] expected: bool,
204    ) {
205        // GIVEN in parameters
206
207        // WHEN
208        let actual = test_string.is_ldh_string();
209
210        // THEN
211        assert_eq!(actual, expected);
212    }
213
214    #[rstest]
215    #[case("foo", false)]
216    #[case("", false)]
217    #[case("foo-bar", false)]
218    #[case("foo bar", false)]
219    #[case(".", false)]
220    #[case(".foo.bar", false)]
221    #[case(".foo", true)]
222    fn GIVEN_string_WHEN_is_tld_THEN_correct_result(
223        #[case] test_string: &str,
224        #[case] expected: bool,
225    ) {
226        // GIVEN in parameters
227
228        // WHEN
229        let actual = test_string.is_tld();
230
231        // THEN
232        assert_eq!(actual, expected);
233    }
234
235    #[rstest]
236    #[case(&[], false)]
237    #[case(&["foo"], true)]
238    #[case(&["foo",""], false)]
239    #[case(&["foo","bar"], true)]
240    #[case(&["foo","bar baz"], false)]
241    #[case(&[""], false)]
242    #[case(&[" "], false)]
243    fn GIVEN_string_list_WHEN_is_ldh_string_list_THEN_correct_result(
244        #[case] test_list: &[&str],
245        #[case] expected: bool,
246    ) {
247        // GIVEN in parameters
248
249        // WHEN
250        let actual = test_list.is_ldh_string_list();
251
252        // THEN
253        assert_eq!(actual, expected);
254    }
255
256    #[rstest]
257    #[case("foo", true)]
258    #[case("", false)]
259    #[case(".", true)]
260    #[case("foo.bar", true)]
261    #[case("foo.bar.", true)]
262    fn GIVEN_string_WHEN_is_ldh_domain_name_THEN_correct_result(
263        #[case] test_string: &str,
264        #[case] expected: bool,
265    ) {
266        // GIVEN in parameters
267
268        // WHEN
269        let actual = test_string.is_ldh_domain_name();
270
271        // THEN
272        assert_eq!(actual, expected);
273    }
274
275    #[rstest]
276    #[case("foo", true)]
277    #[case("", false)]
278    #[case(".", true)]
279    #[case("foo.bar", true)]
280    #[case("foo.bar.", true)]
281    #[case("fo_o.bar.", false)]
282    #[case("fo o.bar.", false)]
283    fn GIVEN_string_WHEN_is_unicode_domain_name_THEN_correct_result(
284        #[case] test_string: &str,
285        #[case] expected: bool,
286    ) {
287        // GIVEN in parameters
288
289        // WHEN
290        let actual = test_string.is_unicode_domain_name();
291
292        // THEN
293        assert_eq!(actual, expected);
294    }
295}