1use 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
62impl<'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
476impl<'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;