Skip to main content

asserting/string/
mod.rs

1//! Implementation of assertions for `String` and `str` values.
2
3use crate::assertions::{AssertStringContainsAnyOf, AssertStringPattern};
4use crate::colored::{
5    mark_missing, mark_missing_char, mark_missing_string,
6    mark_selected_chars_in_string_as_unexpected, mark_selected_items_in_collection,
7    mark_unexpected_char_in_string, mark_unexpected_string, mark_unexpected_substring_in_string,
8};
9use crate::expectations::{
10    not, string_contains, string_contains_any_of, string_ends_with, string_starts_with,
11    StringContains, StringContainsAnyOf, StringEndsWith, StringStartWith,
12};
13use crate::properties::{CharCountProperty, DefinedOrderProperty, IsEmptyProperty, LengthProperty};
14use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec};
15use crate::std::fmt::Debug;
16use crate::std::str::Chars;
17use crate::std::{
18    format,
19    string::{String, ToString},
20    vec::Vec,
21};
22use hashbrown::HashSet;
23
24impl IsEmptyProperty for &str {
25    fn is_empty_property(&self) -> bool {
26        self.is_empty()
27    }
28}
29
30impl IsEmptyProperty for String {
31    fn is_empty_property(&self) -> bool {
32        self.is_empty()
33    }
34}
35
36impl LengthProperty for &str {
37    fn length_property(&self) -> usize {
38        self.len()
39    }
40}
41
42impl LengthProperty for String {
43    fn length_property(&self) -> usize {
44        self.len()
45    }
46}
47
48impl CharCountProperty for &str {
49    fn char_count_property(&self) -> usize {
50        self.chars().count()
51    }
52}
53
54impl CharCountProperty for String {
55    fn char_count_property(&self) -> usize {
56        self.chars().count()
57    }
58}
59
60impl DefinedOrderProperty for Chars<'_> {}
61
62// We implement `AssertContains` for different `Pattern` types as the
63// [`core::str::pattern`] API is not stabilized as of February 2025;
64// see issue [#27721](https://github.com/rust-lang/rust/issues/27721).
65// Maybe we keep the implementations for a long time to support an earlier MSRV.
66
67impl<'a, S, R> AssertStringPattern<&'a str> for Spec<'a, S, R>
68where
69    S: 'a + AsRef<str> + Debug,
70    R: FailingStrategy,
71{
72    fn contains(self, pattern: &'a str) -> Self {
73        self.expecting(string_contains(pattern))
74    }
75
76    fn does_not_contain(self, pattern: &'a str) -> Self {
77        self.expecting(not(string_contains(pattern)))
78    }
79
80    fn starts_with(self, pattern: &str) -> Self {
81        self.expecting(string_starts_with(pattern))
82    }
83
84    fn does_not_start_with(self, pattern: &'a str) -> Self {
85        self.expecting(not(string_starts_with(pattern)))
86    }
87
88    fn ends_with(self, pattern: &str) -> Self {
89        self.expecting(string_ends_with(pattern))
90    }
91
92    fn does_not_end_with(self, pattern: &'a str) -> Self {
93        self.expecting(not(string_ends_with(pattern)))
94    }
95}
96
97impl<'a, S, R> AssertStringPattern<String> for Spec<'a, S, R>
98where
99    S: 'a + AsRef<str> + Debug,
100    R: FailingStrategy,
101{
102    fn contains(self, pattern: String) -> Self {
103        self.expecting(string_contains(pattern))
104    }
105
106    fn does_not_contain(self, pattern: String) -> Self {
107        self.expecting(not(string_contains(pattern)))
108    }
109
110    fn starts_with(self, pattern: String) -> Self {
111        self.expecting(string_starts_with(pattern))
112    }
113
114    fn does_not_start_with(self, pattern: String) -> Self {
115        self.expecting(not(string_starts_with(pattern)))
116    }
117
118    fn ends_with(self, pattern: String) -> Self {
119        self.expecting(string_ends_with(pattern))
120    }
121
122    fn does_not_end_with(self, pattern: String) -> Self {
123        self.expecting(not(string_ends_with(pattern)))
124    }
125}
126
127impl<'a, S, R> AssertStringPattern<char> for Spec<'a, S, R>
128where
129    S: 'a + AsRef<str> + Debug,
130    R: FailingStrategy,
131{
132    fn contains(self, expected: char) -> Self {
133        self.expecting(string_contains(expected))
134    }
135
136    fn does_not_contain(self, pattern: char) -> Self {
137        self.expecting(not(string_contains(pattern)))
138    }
139
140    fn starts_with(self, expected: char) -> Self {
141        self.expecting(string_starts_with(expected))
142    }
143
144    fn does_not_start_with(self, pattern: char) -> Self {
145        self.expecting(not(string_starts_with(pattern)))
146    }
147
148    fn ends_with(self, pattern: char) -> Self {
149        self.expecting(string_ends_with(pattern))
150    }
151
152    fn does_not_end_with(self, pattern: char) -> Self {
153        self.expecting(not(string_ends_with(pattern)))
154    }
155}
156
157impl<S> Expectation<S> for StringContains<&str>
158where
159    S: AsRef<str> + Debug,
160{
161    fn test(&mut self, subject: &S) -> bool {
162        subject.as_ref().contains(self.expected)
163    }
164
165    fn message(
166        &self,
167        expression: &Expression<'_>,
168        actual: &S,
169        inverted: bool,
170        format: &DiffFormat,
171    ) -> String {
172        let (not, marked_actual) = if inverted {
173            let marked_actual =
174                mark_unexpected_substring_in_string(actual.as_ref(), self.expected, format);
175            ("not ", marked_actual)
176        } else {
177            let marked_actual = mark_unexpected_string(actual.as_ref(), format);
178            ("", marked_actual)
179        };
180        let marked_expected = mark_missing_string(self.expected, format);
181        format!(
182            "expected {expression} to {not}contain {:?}\n   but was: \"{marked_actual}\"\n  expected: {not}\"{marked_expected}\"",
183            self.expected,
184        )
185    }
186}
187
188impl Invertible for StringContains<&str> {}
189
190impl<S> Expectation<S> for StringContains<String>
191where
192    S: AsRef<str> + Debug,
193{
194    fn test(&mut self, subject: &S) -> bool {
195        subject.as_ref().contains(&self.expected)
196    }
197
198    fn message(
199        &self,
200        expression: &Expression<'_>,
201        actual: &S,
202        inverted: bool,
203        format: &DiffFormat,
204    ) -> String {
205        let (not, marked_actual) = if inverted {
206            let marked_actual =
207                mark_unexpected_substring_in_string(actual.as_ref(), &self.expected, format);
208            ("not ", marked_actual)
209        } else {
210            let marked_actual = mark_unexpected_string(actual.as_ref(), format);
211            ("", marked_actual)
212        };
213        let marked_expected = mark_missing_string(&self.expected, format);
214        format!(
215            "expected {expression} to {not}contain {:?}\n   but was: \"{marked_actual}\"\n  expected: {not}\"{marked_expected}\"",
216            self.expected,
217        )
218    }
219}
220
221impl Invertible for StringContains<String> {}
222
223impl<S> Expectation<S> for StringContains<char>
224where
225    S: AsRef<str> + Debug,
226{
227    fn test(&mut self, subject: &S) -> bool {
228        subject.as_ref().contains(self.expected)
229    }
230
231    fn message(
232        &self,
233        expression: &Expression<'_>,
234        actual: &S,
235        inverted: bool,
236        format: &DiffFormat,
237    ) -> String {
238        let (not, marked_actual) = if inverted {
239            let marked_actual =
240                mark_unexpected_char_in_string(actual.as_ref(), self.expected, format);
241            ("not ", marked_actual)
242        } else {
243            let marked_actual = mark_unexpected_string(actual.as_ref(), format);
244            ("", marked_actual)
245        };
246        let marked_expected = mark_missing_char(self.expected, format);
247        format!(
248            "expected {expression} to {not}contain {:?}\n   but was: \"{marked_actual}\"\n  expected: {not}'{marked_expected}'",
249            self.expected,
250        )
251    }
252}
253
254impl Invertible for StringContains<char> {}
255
256impl<S> Expectation<S> for StringStartWith<&str>
257where
258    S: AsRef<str> + Debug,
259{
260    fn test(&mut self, subject: &S) -> bool {
261        subject.as_ref().starts_with(self.expected)
262    }
263
264    fn message(
265        &self,
266        expression: &Expression<'_>,
267        actual: &S,
268        inverted: bool,
269        format: &DiffFormat,
270    ) -> String {
271        let not = if inverted { "not " } else { "" };
272        let expected_char_len = self.expected.chars().count();
273        let actual_start = actual
274            .as_ref()
275            .chars()
276            .take(expected_char_len)
277            .collect::<String>();
278        let actual_rest = actual
279            .as_ref()
280            .chars()
281            .skip(expected_char_len)
282            .collect::<String>();
283        let marked_actual_start = mark_unexpected_string(&actual_start, format);
284        let marked_expected = mark_missing_string(self.expected, format);
285        format!(
286            "expected {expression} to {not}start with {:?}\n   but was: \"{marked_actual_start}{actual_rest}\"\n  expected: {not}\"{marked_expected}\"",
287            self.expected,
288        )
289    }
290}
291
292impl Invertible for StringStartWith<&str> {}
293
294impl<S> Expectation<S> for StringStartWith<String>
295where
296    S: AsRef<str> + Debug,
297{
298    fn test(&mut self, subject: &S) -> bool {
299        subject.as_ref().starts_with(&self.expected)
300    }
301
302    fn message(
303        &self,
304        expression: &Expression<'_>,
305        actual: &S,
306        inverted: bool,
307        format: &DiffFormat,
308    ) -> String {
309        let not = if inverted { "not " } else { "" };
310        let expected_char_len = self.expected.chars().count();
311        let actual_start = actual
312            .as_ref()
313            .chars()
314            .take(expected_char_len)
315            .collect::<String>();
316        let actual_rest = actual
317            .as_ref()
318            .chars()
319            .skip(expected_char_len)
320            .collect::<String>();
321        let marked_actual_start = mark_unexpected_string(&actual_start, format);
322        let marked_expected = mark_missing_string(&self.expected, format);
323        format!(
324            "expected {expression} to {not}start with {:?}\n   but was: \"{marked_actual_start}{actual_rest}\"\n  expected: {not}\"{marked_expected}\"",
325            self.expected,
326        )
327    }
328}
329
330impl Invertible for StringStartWith<String> {}
331
332impl<S> Expectation<S> for StringStartWith<char>
333where
334    S: AsRef<str> + Debug,
335{
336    fn test(&mut self, subject: &S) -> bool {
337        subject.as_ref().starts_with(self.expected)
338    }
339
340    fn message(
341        &self,
342        expression: &Expression<'_>,
343        actual: &S,
344        inverted: bool,
345        format: &DiffFormat,
346    ) -> String {
347        let not = if inverted { "not " } else { "" };
348        let actual_first_char = actual.as_ref().chars().take(1).collect::<String>();
349        let actual_rest = actual.as_ref().chars().skip(1).collect::<String>();
350        let marked_actual_start = mark_unexpected_string(&actual_first_char, format);
351        let marked_expected = mark_missing_char(self.expected, format);
352        format!(
353            "expected {expression} to {not}start with {:?}\n   but was: \"{marked_actual_start}{actual_rest}\"\n  expected: {not}'{marked_expected}'",
354            self.expected,
355        )
356    }
357}
358
359impl Invertible for StringStartWith<char> {}
360
361impl<S> Expectation<S> for StringEndsWith<&str>
362where
363    S: AsRef<str> + Debug,
364{
365    fn test(&mut self, subject: &S) -> bool {
366        subject.as_ref().ends_with(self.expected)
367    }
368
369    fn message(
370        &self,
371        expression: &Expression<'_>,
372        actual: &S,
373        inverted: bool,
374        format: &DiffFormat,
375    ) -> String {
376        let not = if inverted { "not " } else { "" };
377        let actual_char_len = actual.as_ref().chars().count();
378        let expected_char_len = self.expected.chars().count();
379        let split_point = actual_char_len.saturating_sub(expected_char_len);
380        let actual_start = actual
381            .as_ref()
382            .chars()
383            .take(split_point)
384            .collect::<String>();
385        let actual_end = actual
386            .as_ref()
387            .chars()
388            .skip(split_point)
389            .collect::<String>();
390        let marked_actual_end = mark_unexpected_string(&actual_end, format);
391        let marked_expected = mark_missing_string(self.expected, format);
392        format!(
393            "expected {expression} to {not}end with {:?}\n   but was: \"{actual_start}{marked_actual_end}\"\n  expected: {not}\"{marked_expected}\"",
394            self.expected,
395        )
396    }
397}
398
399impl Invertible for StringEndsWith<&str> {}
400
401impl<S> Expectation<S> for StringEndsWith<String>
402where
403    S: AsRef<str> + Debug,
404{
405    fn test(&mut self, subject: &S) -> bool {
406        subject.as_ref().ends_with(&self.expected)
407    }
408
409    fn message(
410        &self,
411        expression: &Expression<'_>,
412        actual: &S,
413        inverted: bool,
414        format: &DiffFormat,
415    ) -> String {
416        let not = if inverted { "not " } else { "" };
417        let actual_char_len = actual.as_ref().chars().count();
418        let expected_char_len = self.expected.chars().count();
419        let split_point = actual_char_len.saturating_sub(expected_char_len);
420        let actual_start = actual
421            .as_ref()
422            .chars()
423            .take(split_point)
424            .collect::<String>();
425        let actual_end = actual
426            .as_ref()
427            .chars()
428            .skip(split_point)
429            .collect::<String>();
430        let marked_actual_end = mark_unexpected_string(&actual_end, format);
431        let marked_expected = mark_missing_string(&self.expected, format);
432        format!(
433            "expected {expression} to {not}end with {:?}\n   but was: \"{actual_start}{marked_actual_end}\"\n  expected: {not}\"{marked_expected}\"",
434            self.expected,
435        )
436    }
437}
438
439impl Invertible for StringEndsWith<String> {}
440
441impl<S> Expectation<S> for StringEndsWith<char>
442where
443    S: AsRef<str> + Debug,
444{
445    fn test(&mut self, subject: &S) -> bool {
446        subject.as_ref().ends_with(self.expected)
447    }
448
449    fn message(
450        &self,
451        expression: &Expression<'_>,
452        actual: &S,
453        inverted: bool,
454        format: &DiffFormat,
455    ) -> String {
456        let not = if inverted { "not " } else { "" };
457        let actual_last_char = actual
458            .as_ref()
459            .chars()
460            .last()
461            .map(|c| c.to_string())
462            .unwrap_or_default();
463        let mut actual_start = actual.as_ref().to_string();
464        actual_start.pop();
465        let marked_actual_end = mark_unexpected_string(&actual_last_char, format);
466        let marked_expected = mark_missing_char(self.expected, format);
467        format!(
468            "expected {expression} to {not}end with {:?}\n   but was: \"{actual_start}{marked_actual_end}\"\n  expected: {not}'{marked_expected}'",
469            self.expected,
470        )
471    }
472}
473
474impl Invertible for StringEndsWith<char> {}
475
476// When string slices' `contains` function is used with an array of chars or
477// slice of chars, it checks if any of the chars in the array/slice is contained
478// in the string slice. Therefore, we implement the [`AssertContainsAnyOf`]
479// assertion for array/slice of chars as expected value, but not the
480// [`AssertContains`] assertion.
481
482impl<'a, S, R> AssertStringContainsAnyOf<&'a [char]> for Spec<'a, S, R>
483where
484    S: 'a + AsRef<str> + Debug,
485    R: FailingStrategy,
486{
487    fn contains_any_of(self, expected: &'a [char]) -> Self {
488        self.expecting(string_contains_any_of(expected))
489    }
490
491    fn does_not_contain_any_of(self, expected: &'a [char]) -> Self {
492        self.expecting(not(string_contains_any_of(expected)))
493    }
494}
495
496impl<'a, S, R, const N: usize> AssertStringContainsAnyOf<[char; N]> for Spec<'a, S, R>
497where
498    S: 'a + AsRef<str> + Debug,
499    R: FailingStrategy,
500{
501    fn contains_any_of(self, expected: [char; N]) -> Self {
502        self.expecting(string_contains_any_of(expected))
503    }
504
505    fn does_not_contain_any_of(self, expected: [char; N]) -> Self {
506        self.expecting(not(string_contains_any_of(expected)))
507    }
508}
509
510impl<'a, S, R, const N: usize> AssertStringContainsAnyOf<&'a [char; N]> for Spec<'a, S, R>
511where
512    S: 'a + AsRef<str> + Debug,
513    R: FailingStrategy,
514{
515    fn contains_any_of(self, expected: &'a [char; N]) -> Self {
516        self.expecting(string_contains_any_of(expected))
517    }
518
519    fn does_not_contain_any_of(self, expected: &'a [char; N]) -> Self {
520        self.expecting(not(string_contains_any_of(expected)))
521    }
522}
523
524impl<S> Expectation<S> for StringContainsAnyOf<&[char]>
525where
526    S: AsRef<str> + Debug,
527{
528    fn test(&mut self, subject: &S) -> bool {
529        subject.as_ref().contains(self.expected)
530    }
531
532    fn message(
533        &self,
534        expression: &Expression<'_>,
535        actual: &S,
536        inverted: bool,
537        format: &DiffFormat,
538    ) -> String {
539        let (not, marked_actual, marked_expected) = if inverted {
540            let actual = actual.as_ref();
541            let mut found_in_actual = HashSet::new();
542            let mut found_in_expected = HashSet::new();
543            for (exp_idx, expected_char) in self.expected.iter().enumerate() {
544                let found = actual
545                    .chars()
546                    .enumerate()
547                    .filter_map(|(idx, chr)| {
548                        if chr == *expected_char {
549                            Some(idx)
550                        } else {
551                            None
552                        }
553                    })
554                    .collect::<Vec<_>>();
555                if !found.is_empty() {
556                    found_in_actual.extend(found);
557                    found_in_expected.insert(exp_idx);
558                }
559            }
560            let marked_actual =
561                mark_selected_chars_in_string_as_unexpected(actual, &found_in_actual, format);
562            let marked_expected = mark_selected_items_in_collection(
563                self.expected,
564                &found_in_expected,
565                format,
566                mark_missing,
567            );
568            ("not ", marked_actual, marked_expected)
569        } else {
570            let marked_actual = mark_unexpected_string(actual.as_ref(), format);
571            let marked_expected = mark_missing(&self.expected, format);
572            ("", marked_actual, marked_expected)
573        };
574        format!(
575            "expected {expression} to {not}contain any of {:?}\n   but was: \"{marked_actual}\"\n  expected: {not}{marked_expected}",
576            self.expected,
577        )
578    }
579}
580
581impl Invertible for StringContainsAnyOf<&[char]> {}
582
583impl<S, const N: usize> Expectation<S> for StringContainsAnyOf<[char; N]>
584where
585    S: AsRef<str> + Debug,
586{
587    fn test(&mut self, subject: &S) -> bool {
588        subject.as_ref().contains(self.expected)
589    }
590
591    fn message(
592        &self,
593        expression: &Expression<'_>,
594        actual: &S,
595        inverted: bool,
596        format: &DiffFormat,
597    ) -> String {
598        let (not, marked_actual, marked_expected) = if inverted {
599            let actual = actual.as_ref();
600            let mut found_in_actual = HashSet::new();
601            let mut found_in_expected = HashSet::new();
602            for (exp_idx, expected_char) in self.expected.iter().enumerate() {
603                let found = actual
604                    .chars()
605                    .enumerate()
606                    .filter_map(|(idx, chr)| {
607                        if chr == *expected_char {
608                            Some(idx)
609                        } else {
610                            None
611                        }
612                    })
613                    .collect::<Vec<_>>();
614                if !found.is_empty() {
615                    found_in_actual.extend(found);
616                    found_in_expected.insert(exp_idx);
617                }
618            }
619            let marked_actual =
620                mark_selected_chars_in_string_as_unexpected(actual, &found_in_actual, format);
621            let marked_expected = mark_selected_items_in_collection(
622                &self.expected,
623                &found_in_expected,
624                format,
625                mark_missing,
626            );
627            ("not ", marked_actual, marked_expected)
628        } else {
629            let marked_actual = mark_unexpected_string(actual.as_ref(), format);
630            let marked_expected = mark_missing(&self.expected, format);
631            ("", marked_actual, marked_expected)
632        };
633        format!(
634            "expected {expression} to {not}contain any of {:?}\n   but was: \"{marked_actual}\"\n  expected: {not}{marked_expected}",
635            self.expected,
636        )
637    }
638}
639
640impl<const N: usize> Invertible for StringContainsAnyOf<[char; N]> {}
641
642impl<S, const N: usize> Expectation<S> for StringContainsAnyOf<&[char; N]>
643where
644    S: AsRef<str> + Debug,
645{
646    fn test(&mut self, subject: &S) -> bool {
647        subject.as_ref().contains(self.expected)
648    }
649
650    fn message(
651        &self,
652        expression: &Expression<'_>,
653        actual: &S,
654        inverted: bool,
655        format: &DiffFormat,
656    ) -> String {
657        let (not, marked_actual, marked_expected) = if inverted {
658            let actual = actual.as_ref();
659            let mut found_in_actual = HashSet::new();
660            let mut found_in_expected = HashSet::new();
661            for (exp_idx, expected_char) in self.expected.iter().enumerate() {
662                let found = actual
663                    .chars()
664                    .enumerate()
665                    .filter_map(|(idx, chr)| {
666                        if chr == *expected_char {
667                            Some(idx)
668                        } else {
669                            None
670                        }
671                    })
672                    .collect::<Vec<_>>();
673                if !found.is_empty() {
674                    found_in_actual.extend(found);
675                    found_in_expected.insert(exp_idx);
676                }
677            }
678            let marked_actual =
679                mark_selected_chars_in_string_as_unexpected(actual, &found_in_actual, format);
680            let marked_expected = mark_selected_items_in_collection(
681                self.expected,
682                &found_in_expected,
683                format,
684                mark_missing,
685            );
686            ("not ", marked_actual, marked_expected)
687        } else {
688            let marked_actual = mark_unexpected_string(actual.as_ref(), format);
689            let marked_expected = mark_missing(&self.expected, format);
690            ("", marked_actual, marked_expected)
691        };
692        format!(
693            "expected {expression} to {not}contain any of {:?}\n   but was: \"{marked_actual}\"\n  expected: {not}{marked_expected}",
694            self.expected,
695        )
696    }
697}
698
699impl<const N: usize> Invertible for StringContainsAnyOf<&[char; N]> {}
700
701#[cfg(feature = "regex")]
702mod regex {
703    use crate::assertions::AssertStringMatches;
704    use crate::colored::{mark_missing_string, mark_unexpected_string};
705    use crate::expectations::{not, string_matches, StringMatches};
706    use crate::spec::{DiffFormat, Expectation, Expression, FailingStrategy, Invertible, Spec};
707    use crate::std::fmt::Debug;
708
709    impl<S, R> AssertStringMatches for Spec<'_, S, R>
710    where
711        S: AsRef<str> + Debug,
712        R: FailingStrategy,
713    {
714        fn matches(self, regex_pattern: &str) -> Self {
715            self.expecting(string_matches(regex_pattern))
716        }
717
718        fn does_not_match(self, regex_pattern: &str) -> Self {
719            self.expecting(not(string_matches(regex_pattern)))
720        }
721    }
722
723    impl<S> Expectation<S> for StringMatches<'_>
724    where
725        S: AsRef<str> + Debug,
726    {
727        fn test(&mut self, subject: &S) -> bool {
728            self.regex.is_match(subject.as_ref())
729        }
730
731        fn message(
732            &self,
733            expression: &Expression<'_>,
734            actual: &S,
735            inverted: bool,
736            format: &DiffFormat,
737        ) -> String {
738            let (not, does_not_match) = if inverted {
739                ("not ", "    does match")
740            } else {
741                ("", "does not match")
742            };
743            let regex = self.regex.as_str();
744            let marked_actual = mark_unexpected_string(actual.as_ref(), format);
745            let marked_expected = mark_missing_string(regex, format);
746            format!("expected {expression} to {not}match the regex {regex}\n               but was: {marked_actual}\n  {does_not_match} regex: {marked_expected}")
747        }
748    }
749
750    impl Invertible for StringMatches<'_> {}
751}
752
753#[cfg(test)]
754mod tests;