icann_rdap_common/check/
string.rs

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