Skip to main content

rxpect/expectations/
string.rs

1use super::predicate::PredicateExpectation;
2use crate::{ExpectProjection, ExpectationBuilder};
3use std::fmt::Debug;
4
5/// Expectations for strings
6pub trait StringExpectations<'e, T>
7where
8    T: Debug + 'e,
9{
10    /// Expect that a string contains a substring
11    /// ```
12    /// # use rxpect::expect;
13    /// # use rxpect::expectations::StringExpectations;
14    ///
15    /// let text = "Hello, world!";
16    /// expect(text).to_contain("world");
17    /// ```
18    /// asserts that `text` contains the substring "world"
19    fn to_contain(self, substring: &'e str) -> Self;
20
21    /// Expect that a string does not contain a substring
22    /// ```
23    /// # use rxpect::expect;
24    /// # use rxpect::expectations::StringExpectations;
25    ///
26    /// let text = "Hello, world!";
27    /// expect(text).to_not_contain("foo");
28    /// ```
29    /// asserts that `text` does not contain the substring "foo"
30    fn to_not_contain(self, substring: &'e str) -> Self;
31
32    /// Expect that a string has a specific length
33    /// ```
34    /// # use rxpect::expect;
35    /// # use rxpect::expectations::StringExpectations;
36    ///
37    /// let text = "Hello";
38    /// expect(text).to_have_length(5);
39    /// ```
40    /// asserts that `text` has a length of 5 characters
41    fn to_have_length(self, length: usize) -> Self;
42
43    /// Expect that a string starts with a specific prefix
44    /// ```
45    /// # use rxpect::expect;
46    /// # use rxpect::expectations::StringExpectations;
47    ///
48    /// let text = "Hello, world!";
49    /// expect(text).to_start_with("Hello");
50    /// ```
51    /// asserts that `text` starts with the prefix "Hello"
52    fn to_start_with(self, prefix: &'e str) -> Self;
53
54    /// Expect that a string ends with a specific suffix
55    /// ```
56    /// # use rxpect::expect;
57    /// # use rxpect::expectations::StringExpectations;
58    ///
59    /// let text = "Hello, world!";
60    /// expect(text).to_end_with("world!");
61    /// ```
62    /// asserts that `text` ends with the suffix "world!"
63    fn to_end_with(self, suffix: &'e str) -> Self;
64
65    /// Expect that a string is empty
66    /// ```
67    /// # use rxpect::expect;
68    /// # use rxpect::expectations::StringExpectations;
69    ///
70    /// let text = "";
71    /// expect(text).to_be_empty();
72    /// ```
73    /// asserts that `text` is an empty string
74    fn to_be_empty(self) -> Self;
75
76    /// Expect that a string consists entirely of whitespace characters
77    /// ```
78    /// # use rxpect::expect;
79    /// # use rxpect::expectations::StringExpectations;
80    ///
81    /// let text = "   \t\n";
82    /// expect(text).to_be_all_whitespace();
83    /// ```
84    /// asserts that `text` consists entirely of whitespace characters
85    fn to_be_all_whitespace(self) -> Self;
86
87    /// Expect that a string consists entirely of alphabetic characters
88    /// ```
89    /// # use rxpect::expect;
90    /// # use rxpect::expectations::StringExpectations;
91    ///
92    /// let text = "Hello";
93    /// expect(text).to_be_alphabetic();
94    /// ```
95    /// asserts that `text` consists entirely of alphabetic characters
96    fn to_be_alphabetic(self) -> Self;
97
98    /// Expect that a string consists entirely of numeric characters
99    /// ```
100    /// # use rxpect::expect;
101    /// # use rxpect::expectations::StringExpectations;
102    ///
103    /// let text = "12345";
104    /// expect(text).to_be_numeric();
105    /// ```
106    /// asserts that `text` consists entirely of numeric characters
107    fn to_be_numeric(self) -> Self;
108
109    /// Expect that a string consists entirely of alphanumeric characters
110    /// ```
111    /// # use rxpect::expect;
112    /// # use rxpect::expectations::StringExpectations;
113    ///
114    /// let text = "Hello123";
115    /// expect(text).to_be_alphanumeric();
116    /// ```
117    /// asserts that `text` consists entirely of alphanumeric characters
118    fn to_be_alphanumeric(self) -> Self;
119
120    /// Make expectations on the number of characters of the string.
121    ///
122    /// ```
123    /// # use rxpect::expect;
124    /// # use rxpect::expectations::StringExpectations;
125    /// # use rxpect::expectations::OrderExpectations;
126    ///
127    /// let text = "Hello123";
128    /// expect(text).length().to_be_greater_than_or_equal(2);
129    /// ```
130    /// asserts that `text` has a length of at least 2
131    fn length(self) -> impl ExpectationBuilder<'e, Value = usize>
132    where
133        Self: Sized;
134}
135
136impl<'e, T, B> StringExpectations<'e, T> for B
137where
138    T: AsRef<str> + Debug + 'e,
139    B: ExpectationBuilder<'e, Value = T>,
140{
141    fn to_contain(self, substring: &'e str) -> Self {
142        self.to_pass(PredicateExpectation::new(
143            substring,
144            |a: &T, b| a.as_ref().contains(b),
145            |a: &T, b| format!("Expected \"{}\" to contain \"{b}\"", a.as_ref()),
146        ))
147    }
148
149    fn to_not_contain(self, substring: &'e str) -> Self {
150        self.to_pass(PredicateExpectation::new(
151            substring,
152            |a: &T, b| !a.as_ref().contains(b),
153            |a: &T, b| format!("Expected \"{}\" to not contain \"{b}\"", a.as_ref()),
154        ))
155    }
156
157    fn to_have_length(self, length: usize) -> Self {
158        self.to_pass(PredicateExpectation::new(
159            length,
160            |a: &T, &b| a.as_ref().chars().count() == b,
161            |a: &T, &b| {
162                format!(
163                    "Expected \"{}\" to have length {b}, but it has length {}",
164                    a.as_ref(),
165                    a.as_ref().chars().count()
166                )
167            },
168        ))
169    }
170
171    fn to_start_with(self, prefix: &'e str) -> Self {
172        self.to_pass(PredicateExpectation::new(
173            prefix,
174            |a: &T, b| a.as_ref().starts_with(b),
175            |a: &T, b| format!("Expected \"{}\" to start with \"{b}\"", a.as_ref()),
176        ))
177    }
178
179    fn to_end_with(self, suffix: &'e str) -> Self {
180        self.to_pass(PredicateExpectation::new(
181            suffix,
182            |a: &T, b| a.as_ref().ends_with(b),
183            |a: &T, b| format!("Expected \"{}\" to end with \"{b}\"", a.as_ref()),
184        ))
185    }
186
187    fn to_be_empty(self) -> Self {
188        self.to_pass(PredicateExpectation::new(
189            (),
190            |a: &T, _| a.as_ref().is_empty(),
191            |a: &T, _| format!("Expected \"{}\" to be empty", a.as_ref()),
192        ))
193    }
194
195    fn to_be_all_whitespace(self) -> Self {
196        self.to_pass(PredicateExpectation::new(
197            (),
198            |a: &T, _| a.as_ref().chars().all(|c| c.is_whitespace()),
199            |a: &T, _| format!("Expected \"{}\" to be all whitespace", a.as_ref()),
200        ))
201    }
202
203    fn to_be_alphabetic(self) -> Self {
204        self.to_pass(PredicateExpectation::new(
205            (),
206            |a: &T, _| !a.as_ref().is_empty() && a.as_ref().chars().all(|c| c.is_alphabetic()),
207            |a: &T, _| format!("Expected \"{}\" to be alphabetic", a.as_ref()),
208        ))
209    }
210
211    fn to_be_numeric(self) -> Self {
212        self.to_pass(PredicateExpectation::new(
213            (),
214            |a: &T, _| !a.as_ref().is_empty() && a.as_ref().chars().all(|c| c.is_numeric()),
215            |a: &T, _| format!("Expected \"{}\" to be numeric", a.as_ref()),
216        ))
217    }
218
219    fn to_be_alphanumeric(self) -> Self {
220        self.to_pass(PredicateExpectation::new(
221            (),
222            |a: &T, _| !a.as_ref().is_empty() && a.as_ref().chars().all(|c| c.is_alphanumeric()),
223            |a: &T, _| format!("Expected \"{}\" to be alphanumeric", a.as_ref()),
224        ))
225    }
226
227    fn length(self) -> impl ExpectationBuilder<'e, Value = usize>
228    where
229        Self: Sized,
230    {
231        self.projected_by(|it: &T| it.as_ref().chars().count())
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use crate::expect;
238    use crate::expectations::string::StringExpectations;
239    use rstest::rstest;
240
241    #[rstest]
242    #[case("", "")]
243    #[case("foobar", "foo")]
244    #[case("foobar", "bar")]
245    #[case("foobar", "oob")]
246    #[case("foobar", "")]
247    fn that_to_contain_passes_when_string_contains_the_substring(
248        #[case] actual: &str,
249        #[case] substring: &str,
250    ) {
251        expect(actual).to_contain(substring);
252    }
253
254    #[rstest]
255    #[case("", "foo")]
256    #[case("foobar", "rab")]
257    #[case("foobar", "oof")]
258    #[case("foobar", "boo")]
259    #[should_panic]
260    fn that_to_contain_does_not_pass_when_string_does_not_contain_the_substring(
261        #[case] actual: &str,
262        #[case] substring: &str,
263    ) {
264        expect(actual).to_contain(substring);
265    }
266
267    #[rstest]
268    #[case("", "foo")]
269    #[case("foobar", "rab")]
270    #[case("foobar", "oof")]
271    #[case("foobar", "boo")]
272    fn that_to_not_contain_passes_when_string_does_not_contain_the_substring(
273        #[case] actual: &str,
274        #[case] substring: &str,
275    ) {
276        expect(actual).to_not_contain(substring);
277    }
278
279    #[rstest]
280    #[case("", "")]
281    #[case("foobar", "foo")]
282    #[case("foobar", "bar")]
283    #[case("foobar", "oob")]
284    #[case("foobar", "")]
285    #[should_panic]
286    fn that_to_not_contain_does_not_pass_when_string_contains_the_substring(
287        #[case] actual: &str,
288        #[case] substring: &str,
289    ) {
290        expect(actual).to_not_contain(substring);
291    }
292
293    #[rstest]
294    #[case("", 0)]
295    #[case("a", 1)]
296    #[case("ab", 2)]
297    #[case("abc", 3)]
298    #[case("abcd", 4)]
299    fn that_to_have_length_passes_when_string_has_expected_length(
300        #[case] actual: &str,
301        #[case] length: usize,
302    ) {
303        expect(actual).to_have_length(length);
304    }
305
306    #[rstest]
307    #[case("", 1)]
308    #[case("a", 0)]
309    #[case("ab", 3)]
310    #[case("abc", 2)]
311    #[case("abcd", 5)]
312    #[should_panic]
313    fn that_to_have_length_does_not_pass_when_string_does_not_have_expected_length(
314        #[case] actual: &str,
315        #[case] length: usize,
316    ) {
317        expect(actual).to_have_length(length);
318    }
319
320    #[rstest]
321    #[case("Ä", 1)] // 2 UTF-8 bytes, 1 character
322    #[case("中文", 2)] // 6 UTF-8 bytes, 2 characters
323    #[case("héllo", 5)] // 6 UTF-8 bytes, 5 characters
324    fn that_to_have_length_counts_characters_not_bytes(
325        #[case] actual: &str,
326        #[case] length: usize,
327    ) {
328        expect(actual).to_have_length(length);
329    }
330
331    #[rstest]
332    #[case("Ä", 1)] // 2 UTF-8 bytes, 1 character
333    #[case("中文", 2)] // 6 UTF-8 bytes, 2 characters
334    #[case("héllo", 5)] // 6 UTF-8 bytes, 5 characters
335    fn that_length_projection_counts_characters_not_bytes(
336        #[case] actual: &str,
337        #[case] length: usize,
338    ) {
339        use crate::expectations::EqualityExpectations;
340        expect(actual).length().to_equal(length);
341    }
342
343    #[rstest]
344    #[case("", "")]
345    #[case("foobar", "foo")]
346    #[case("foobar", "f")]
347    #[case("foobar", "")]
348    fn that_to_start_with_passes_when_string_starts_with_prefix(
349        #[case] actual: &str,
350        #[case] prefix: &str,
351    ) {
352        expect(actual).to_start_with(prefix);
353    }
354
355    #[rstest]
356    #[case("", "foo")]
357    #[case("foobar", "bar")]
358    #[case("foobar", "oo")]
359    #[case("foobar", "foobar1")]
360    #[should_panic]
361    fn that_to_start_with_does_not_pass_when_string_does_not_start_with_prefix(
362        #[case] actual: &str,
363        #[case] prefix: &str,
364    ) {
365        expect(actual).to_start_with(prefix);
366    }
367
368    #[rstest]
369    #[case("", "")]
370    #[case("foobar", "bar")]
371    #[case("foobar", "r")]
372    #[case("foobar", "")]
373    fn that_to_end_with_passes_when_string_ends_with_suffix(
374        #[case] actual: &str,
375        #[case] suffix: &str,
376    ) {
377        expect(actual).to_end_with(suffix);
378    }
379
380    #[rstest]
381    #[case("", "foo")]
382    #[case("foobar", "foo")]
383    #[case("foobar", "ba")]
384    #[case("foobar", "1foobar")]
385    #[should_panic]
386    fn that_to_end_with_does_not_pass_when_string_does_not_end_with_suffix(
387        #[case] actual: &str,
388        #[case] suffix: &str,
389    ) {
390        expect(actual).to_end_with(suffix);
391    }
392
393    #[rstest]
394    #[case("")]
395    fn that_to_be_empty_passes_when_string_is_empty(#[case] actual: &str) {
396        expect(actual).to_be_empty();
397    }
398
399    #[rstest]
400    #[case("a")]
401    #[case("foo")]
402    #[case(" ")]
403    #[should_panic]
404    fn that_to_be_empty_does_not_pass_when_string_is_not_empty(#[case] actual: &str) {
405        expect(actual).to_be_empty();
406    }
407
408    #[rstest]
409    #[case("")]
410    #[case(" ")]
411    #[case("  ")]
412    #[case("\t")]
413    #[case("\n")]
414    #[case(" \t\n\r")]
415    fn that_to_be_all_whitespace_passes_when_string_is_all_whitespace(#[case] actual: &str) {
416        expect(actual).to_be_all_whitespace();
417    }
418
419    #[rstest]
420    #[case("a")]
421    #[case("foo")]
422    #[case(" a")]
423    #[case("a ")]
424    #[case(" a ")]
425    #[should_panic]
426    fn that_to_be_all_whitespace_does_not_pass_when_string_is_not_all_whitespace(
427        #[case] actual: &str,
428    ) {
429        expect(actual).to_be_all_whitespace();
430    }
431
432    #[rstest]
433    #[case("a")]
434    #[case("abc")]
435    #[case("Abc")]
436    #[case("ABC")]
437    #[case("абв")]
438    fn that_to_be_alphabetic_passes_when_string_is_alphabetic(#[case] actual: &str) {
439        expect(actual).to_be_alphabetic();
440    }
441
442    #[rstest]
443    #[case("")]
444    #[case("123")]
445    #[case("a1")]
446    #[case("1a")]
447    #[case("a b")]
448    #[case("a-b")]
449    #[should_panic]
450    fn that_to_be_alphabetic_does_not_pass_when_string_is_not_alphabetic(#[case] actual: &str) {
451        expect(actual).to_be_alphabetic();
452    }
453
454    #[rstest]
455    #[case("0")]
456    #[case("123")]
457    #[case("١٢٣")] // Arabic numerals
458    fn that_to_be_numeric_passes_when_string_is_numeric(#[case] actual: &str) {
459        expect(actual).to_be_numeric();
460    }
461
462    #[rstest]
463    #[case("")]
464    #[case("abc")]
465    #[case("1a")]
466    #[case("a1")]
467    #[case("1 2")]
468    #[case("1-2")]
469    #[should_panic]
470    fn that_to_be_numeric_does_not_pass_when_string_is_not_numeric(#[case] actual: &str) {
471        expect(actual).to_be_numeric();
472    }
473}