1use std::fmt::{Debug, Display};
27
28use rust_decimal::Decimal;
29use thiserror::Error;
30
31use crate::collections::{MapLike, SetLike};
32
33pub const FAILED: &str = "Condition failed";
39
40#[derive(Clone, Debug, Error, Eq, PartialEq)]
42pub enum CorrectnessError {
43 #[error("{message}")]
45 PredicateViolation {
46 message: String,
48 },
49 #[error("invalid string for '{param}', was empty")]
51 EmptyString {
52 param: String,
54 },
55 #[error("invalid string for '{param}', was all whitespace")]
57 WhitespaceString {
58 param: String,
60 },
61 #[error("invalid string for '{param}' contained a non-ASCII char, was '{value}'")]
63 NonAsciiString {
64 param: String,
66 value: String,
68 },
69 #[error("invalid string for '{param}' did not contain '{pattern}', was '{value}'")]
71 MissingSubstring {
72 param: String,
74 pattern: String,
76 value: String,
78 },
79 #[error(
81 "'{lhs_param}' {type_name} of {lhs} was not equal to '{rhs_param}' {type_name} of {rhs}"
82 )]
83 EqualityMismatch {
84 lhs_param: String,
86 rhs_param: String,
88 lhs: String,
90 rhs: String,
92 type_name: &'static str,
94 },
95 #[error("invalid {type_name} for '{param}' not positive, was {value}")]
97 NotPositive {
98 param: String,
100 value: String,
102 type_name: &'static str,
104 },
105 #[error("invalid {type_name} for '{param}' negative, was {value}")]
107 NegativeValue {
108 param: String,
110 value: String,
112 type_name: &'static str,
114 },
115 #[error("invalid {type_name} for '{param}', was {value}")]
117 InvalidValue {
118 param: String,
120 value: String,
122 type_name: &'static str,
124 },
125 #[error("invalid {type_name} for '{param}' not in range [{min}, {max}], was {value}")]
127 OutOfRange {
128 param: String,
130 min: String,
132 max: String,
134 value: String,
136 type_name: &'static str,
138 },
139 #[error("the '{param}' {collection_kind} `{type_repr}` was not empty")]
141 CollectionNotEmpty {
142 param: String,
144 collection_kind: &'static str,
146 type_repr: String,
148 },
149 #[error("the '{param}' {collection_kind} `{type_repr}` was empty")]
151 CollectionEmpty {
152 param: String,
154 collection_kind: &'static str,
156 type_repr: String,
158 },
159 #[error("the '{key_name}' key {key} was already in the '{map_name}' map `{map_type_repr}`")]
161 KeyPresent {
162 key_name: String,
164 map_name: String,
166 key: String,
168 map_type_repr: String,
170 },
171 #[error("the '{key_name}' key {key} was not in the '{map_name}' map `{map_type_repr}`")]
173 KeyMissing {
174 key_name: String,
176 map_name: String,
178 key: String,
180 map_type_repr: String,
182 },
183 #[error("the '{member_name}' member was already in the '{set_name}' set `{set_type_repr}`")]
185 MemberPresent {
186 member_name: String,
188 set_name: String,
190 set_type_repr: String,
192 },
193 #[error("the '{member_name}' member was not in the '{set_name}' set `{set_type_repr}`")]
195 MemberMissing {
196 member_name: String,
198 set_name: String,
200 set_type_repr: String,
202 },
203}
204
205pub type Result<T> = std::result::Result<T, CorrectnessError>;
207
208pub type CorrectnessResult<T> = Result<T>;
210
211pub trait CorrectnessResultExt<T> {
220 fn expect_display(self, msg: &str) -> T;
223}
224
225impl<T> CorrectnessResultExt<T> for CorrectnessResult<T> {
226 #[inline]
227 #[track_caller]
228 fn expect_display(self, msg: &str) -> T {
229 match self {
230 Ok(value) => value,
231 Err(e) => panic!("{msg}: {e}"),
232 }
233 }
234}
235
236#[inline(always)]
242pub fn check_predicate_true(predicate: bool, fail_msg: &str) -> Result<()> {
243 if !predicate {
244 return Err(CorrectnessError::PredicateViolation {
245 message: fail_msg.to_string(),
246 });
247 }
248 Ok(())
249}
250
251#[inline(always)]
257pub fn check_predicate_false(predicate: bool, fail_msg: &str) -> Result<()> {
258 check_predicate_true(!predicate, fail_msg)
259}
260
261#[inline(always)]
270pub fn check_nonempty_string<T: AsRef<str>>(s: T, param: &str) -> Result<()> {
271 if s.as_ref().is_empty() {
272 return Err(CorrectnessError::EmptyString {
273 param: param.to_string(),
274 });
275 }
276 Ok(())
277}
278
279#[inline(always)]
288pub fn check_valid_string_ascii<T: AsRef<str>>(s: T, param: &str) -> Result<()> {
289 let s = s.as_ref();
290
291 if s.is_empty() {
292 return Err(CorrectnessError::EmptyString {
293 param: param.to_string(),
294 });
295 }
296
297 let mut has_non_whitespace = false;
299
300 for c in s.chars() {
301 if !c.is_whitespace() {
302 has_non_whitespace = true;
303 }
304
305 if !c.is_ascii() {
306 return Err(CorrectnessError::NonAsciiString {
307 param: param.to_string(),
308 value: s.to_string(),
309 });
310 }
311 }
312
313 if !has_non_whitespace {
314 return Err(CorrectnessError::WhitespaceString {
315 param: param.to_string(),
316 });
317 }
318
319 Ok(())
320}
321
322#[inline(always)]
333pub fn check_valid_string_utf8<T: AsRef<str>>(s: T, param: &str) -> Result<()> {
334 let s = s.as_ref();
335
336 if s.is_empty() {
337 return Err(CorrectnessError::EmptyString {
338 param: param.to_string(),
339 });
340 }
341
342 let has_non_whitespace = s.chars().any(|c| !c.is_whitespace());
343
344 if !has_non_whitespace {
345 return Err(CorrectnessError::WhitespaceString {
346 param: param.to_string(),
347 });
348 }
349
350 Ok(())
351}
352
353#[inline(always)]
362pub fn check_valid_string_ascii_optional<T: AsRef<str>>(s: Option<T>, param: &str) -> Result<()> {
363 if let Some(s) = s {
364 check_valid_string_ascii(s, param)?;
365 }
366 Ok(())
367}
368
369#[inline(always)]
375pub fn check_string_contains<T: AsRef<str>>(s: T, pat: &str, param: &str) -> Result<()> {
376 let s = s.as_ref();
377 if !s.contains(pat) {
378 return Err(CorrectnessError::MissingSubstring {
379 param: param.to_string(),
380 pattern: pat.to_string(),
381 value: s.to_string(),
382 });
383 }
384 Ok(())
385}
386
387#[inline(always)]
393pub fn check_equal<T: PartialEq + Debug + Display>(
394 lhs: &T,
395 rhs: &T,
396 lhs_param: &str,
397 rhs_param: &str,
398) -> Result<()> {
399 if lhs != rhs {
400 return Err(CorrectnessError::EqualityMismatch {
401 lhs_param: lhs_param.to_string(),
402 rhs_param: rhs_param.to_string(),
403 lhs: lhs.to_string(),
404 rhs: rhs.to_string(),
405 type_name: "value",
406 });
407 }
408 Ok(())
409}
410
411#[inline(always)]
417pub fn check_equal_u8(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> Result<()> {
418 if lhs != rhs {
419 return Err(CorrectnessError::EqualityMismatch {
420 lhs_param: lhs_param.to_string(),
421 rhs_param: rhs_param.to_string(),
422 lhs: lhs.to_string(),
423 rhs: rhs.to_string(),
424 type_name: "u8",
425 });
426 }
427 Ok(())
428}
429
430#[inline(always)]
436pub fn check_equal_usize(lhs: usize, rhs: usize, lhs_param: &str, rhs_param: &str) -> Result<()> {
437 if lhs != rhs {
438 return Err(CorrectnessError::EqualityMismatch {
439 lhs_param: lhs_param.to_string(),
440 rhs_param: rhs_param.to_string(),
441 lhs: lhs.to_string(),
442 rhs: rhs.to_string(),
443 type_name: "usize",
444 });
445 }
446 Ok(())
447}
448
449#[inline(always)]
455pub fn check_positive_usize(value: usize, param: &str) -> Result<()> {
456 if value == 0 {
457 return Err(CorrectnessError::NotPositive {
458 param: param.to_string(),
459 value: value.to_string(),
460 type_name: "usize",
461 });
462 }
463 Ok(())
464}
465
466#[inline(always)]
472pub fn check_positive_u64(value: u64, param: &str) -> Result<()> {
473 if value == 0 {
474 return Err(CorrectnessError::NotPositive {
475 param: param.to_string(),
476 value: value.to_string(),
477 type_name: "u64",
478 });
479 }
480 Ok(())
481}
482
483#[inline(always)]
489pub fn check_positive_u128(value: u128, param: &str) -> Result<()> {
490 if value == 0 {
491 return Err(CorrectnessError::NotPositive {
492 param: param.to_string(),
493 value: value.to_string(),
494 type_name: "u128",
495 });
496 }
497 Ok(())
498}
499
500#[inline(always)]
506pub fn check_positive_i64(value: i64, param: &str) -> Result<()> {
507 if value <= 0 {
508 return Err(CorrectnessError::NotPositive {
509 param: param.to_string(),
510 value: value.to_string(),
511 type_name: "i64",
512 });
513 }
514 Ok(())
515}
516
517#[inline(always)]
523pub fn check_positive_i128(value: i128, param: &str) -> Result<()> {
524 if value <= 0 {
525 return Err(CorrectnessError::NotPositive {
526 param: param.to_string(),
527 value: value.to_string(),
528 type_name: "i128",
529 });
530 }
531 Ok(())
532}
533
534#[inline(always)]
540pub fn check_non_negative_f64(value: f64, param: &str) -> Result<()> {
541 if value.is_nan() || value.is_infinite() {
542 return Err(CorrectnessError::InvalidValue {
543 param: param.to_string(),
544 value: value.to_string(),
545 type_name: "f64",
546 });
547 }
548
549 if value < 0.0 {
550 return Err(CorrectnessError::NegativeValue {
551 param: param.to_string(),
552 value: value.to_string(),
553 type_name: "f64",
554 });
555 }
556 Ok(())
557}
558
559#[inline(always)]
565pub fn check_in_range_inclusive_u8(value: u8, l: u8, r: u8, param: &str) -> Result<()> {
566 if value < l || value > r {
567 return Err(CorrectnessError::OutOfRange {
568 param: param.to_string(),
569 min: l.to_string(),
570 max: r.to_string(),
571 value: value.to_string(),
572 type_name: "u8",
573 });
574 }
575 Ok(())
576}
577
578#[inline(always)]
584pub fn check_in_range_inclusive_u64(value: u64, l: u64, r: u64, param: &str) -> Result<()> {
585 if value < l || value > r {
586 return Err(CorrectnessError::OutOfRange {
587 param: param.to_string(),
588 min: l.to_string(),
589 max: r.to_string(),
590 value: value.to_string(),
591 type_name: "u64",
592 });
593 }
594 Ok(())
595}
596
597#[inline(always)]
603pub fn check_in_range_inclusive_i64(value: i64, l: i64, r: i64, param: &str) -> Result<()> {
604 if value < l || value > r {
605 return Err(CorrectnessError::OutOfRange {
606 param: param.to_string(),
607 min: l.to_string(),
608 max: r.to_string(),
609 value: value.to_string(),
610 type_name: "i64",
611 });
612 }
613 Ok(())
614}
615
616#[inline(always)]
622pub fn check_in_range_inclusive_f64(value: f64, l: f64, r: f64, param: &str) -> Result<()> {
623 const EPSILON: f64 = 1e-15;
629
630 if value.is_nan() || value.is_infinite() {
631 return Err(CorrectnessError::InvalidValue {
632 param: param.to_string(),
633 value: value.to_string(),
634 type_name: "f64",
635 });
636 }
637
638 if value < l - EPSILON || value > r + EPSILON {
639 return Err(CorrectnessError::OutOfRange {
640 param: param.to_string(),
641 min: l.to_string(),
642 max: r.to_string(),
643 value: value.to_string(),
644 type_name: "f64",
645 });
646 }
647 Ok(())
648}
649
650#[inline(always)]
656pub fn check_in_range_inclusive_usize(value: usize, l: usize, r: usize, param: &str) -> Result<()> {
657 if value < l || value > r {
658 return Err(CorrectnessError::OutOfRange {
659 param: param.to_string(),
660 min: l.to_string(),
661 max: r.to_string(),
662 value: value.to_string(),
663 type_name: "usize",
664 });
665 }
666 Ok(())
667}
668
669#[inline(always)]
675pub fn check_slice_empty<T>(slice: &[T], param: &str) -> Result<()> {
676 if !slice.is_empty() {
677 return Err(CorrectnessError::CollectionNotEmpty {
678 param: param.to_string(),
679 collection_kind: "slice",
680 type_repr: slice_type_repr::<T>(),
681 });
682 }
683 Ok(())
684}
685
686#[inline(always)]
692pub fn check_slice_not_empty<T>(slice: &[T], param: &str) -> Result<()> {
693 if slice.is_empty() {
694 return Err(CorrectnessError::CollectionEmpty {
695 param: param.to_string(),
696 collection_kind: "slice",
697 type_repr: slice_type_repr::<T>(),
698 });
699 }
700 Ok(())
701}
702
703#[inline(always)]
709pub fn check_map_empty<M>(map: &M, param: &str) -> Result<()>
710where
711 M: MapLike,
712{
713 if !map.is_empty() {
714 return Err(CorrectnessError::CollectionNotEmpty {
715 param: param.to_string(),
716 collection_kind: "map",
717 type_repr: map_type_repr::<M>(),
718 });
719 }
720 Ok(())
721}
722
723#[inline(always)]
729pub fn check_map_not_empty<M>(map: &M, param: &str) -> Result<()>
730where
731 M: MapLike,
732{
733 if map.is_empty() {
734 return Err(CorrectnessError::CollectionEmpty {
735 param: param.to_string(),
736 collection_kind: "map",
737 type_repr: map_type_repr::<M>(),
738 });
739 }
740 Ok(())
741}
742
743#[inline(always)]
749pub fn check_key_not_in_map<M>(key: &M::Key, map: &M, key_name: &str, map_name: &str) -> Result<()>
750where
751 M: MapLike,
752{
753 if map.contains_key(key) {
754 return Err(CorrectnessError::KeyPresent {
755 key_name: key_name.to_string(),
756 map_name: map_name.to_string(),
757 key: key.to_string(),
758 map_type_repr: map_type_repr::<M>(),
759 });
760 }
761 Ok(())
762}
763
764#[inline(always)]
770pub fn check_key_in_map<M>(key: &M::Key, map: &M, key_name: &str, map_name: &str) -> Result<()>
771where
772 M: MapLike,
773{
774 if !map.contains_key(key) {
775 return Err(CorrectnessError::KeyMissing {
776 key_name: key_name.to_string(),
777 map_name: map_name.to_string(),
778 key: key.to_string(),
779 map_type_repr: map_type_repr::<M>(),
780 });
781 }
782 Ok(())
783}
784
785#[inline(always)]
791pub fn check_member_not_in_set<S>(
792 member: &S::Item,
793 set: &S,
794 member_name: &str,
795 set_name: &str,
796) -> Result<()>
797where
798 S: SetLike,
799{
800 if set.contains(member) {
801 return Err(CorrectnessError::MemberPresent {
802 member_name: member_name.to_string(),
803 set_name: set_name.to_string(),
804 set_type_repr: set_type_repr::<S>(),
805 });
806 }
807 Ok(())
808}
809
810#[inline(always)]
816pub fn check_member_in_set<S>(
817 member: &S::Item,
818 set: &S,
819 member_name: &str,
820 set_name: &str,
821) -> Result<()>
822where
823 S: SetLike,
824{
825 if !set.contains(member) {
826 return Err(CorrectnessError::MemberMissing {
827 member_name: member_name.to_string(),
828 set_name: set_name.to_string(),
829 set_type_repr: set_type_repr::<S>(),
830 });
831 }
832 Ok(())
833}
834
835#[inline(always)]
841pub fn check_positive_decimal(value: Decimal, param: &str) -> Result<()> {
842 if value <= Decimal::ZERO {
843 return Err(CorrectnessError::NotPositive {
844 param: param.to_string(),
845 value: value.to_string(),
846 type_name: "Decimal",
847 });
848 }
849 Ok(())
850}
851
852fn slice_type_repr<T>() -> String {
853 format!("&[{}]", std::any::type_name::<T>())
854}
855
856fn map_type_repr<M>() -> String
857where
858 M: MapLike,
859{
860 format!(
861 "&<{}, {}>",
862 std::any::type_name::<M::Key>(),
863 std::any::type_name::<M::Value>(),
864 )
865}
866
867fn set_type_repr<S>() -> String
868where
869 S: SetLike,
870{
871 format!("&<{}>", std::any::type_name::<S::Item>())
872}
873
874#[cfg(test)]
875mod tests {
876 use std::{
877 collections::{HashMap, HashSet},
878 fmt::Display,
879 str::FromStr,
880 };
881
882 use rstest::rstest;
883 use rust_decimal::Decimal;
884
885 use super::*;
886
887 #[rstest]
888 fn test_check_predicate_true_returns_typed_error_with_stable_display() {
889 let error = check_predicate_true(false, "the predicate was false").unwrap_err();
890
891 assert_eq!(
892 error,
893 CorrectnessError::PredicateViolation {
894 message: "the predicate was false".to_string(),
895 }
896 );
897 assert_eq!(error.to_string(), "the predicate was false");
898 }
899
900 #[rstest]
901 fn test_expect_display_returns_ok_value() {
902 let result: CorrectnessResult<i32> = Ok(42);
903 assert_eq!(result.expect_display(FAILED), 42);
904 }
905
906 #[rstest]
907 #[should_panic(expected = "Condition failed: invalid string for 'value', was empty")]
908 fn test_expect_display_panics_with_display_form_on_err() {
909 let result: CorrectnessResult<()> = Err(CorrectnessError::EmptyString {
910 param: "value".to_string(),
911 });
912 result.expect_display(FAILED);
913 }
914
915 #[rstest]
916 #[should_panic(expected = "custom prefix: the predicate was false")]
917 fn test_expect_display_uses_provided_prefix() {
918 let result: CorrectnessResult<()> = Err(CorrectnessError::PredicateViolation {
919 message: "the predicate was false".to_string(),
920 });
921 result.expect_display("custom prefix");
922 }
923
924 #[rstest]
925 #[case(false, false)]
926 #[case(true, true)]
927 fn test_check_predicate_true(#[case] predicate: bool, #[case] expected: bool) {
928 let result = check_predicate_true(predicate, "the predicate was false").is_ok();
929 assert_eq!(result, expected);
930 }
931
932 #[rstest]
933 #[case(false, true)]
934 #[case(true, false)]
935 fn test_check_predicate_false(#[case] predicate: bool, #[case] expected: bool) {
936 let result = check_predicate_false(predicate, "the predicate was true").is_ok();
937 assert_eq!(result, expected);
938 }
939
940 #[rstest]
941 #[case("a")]
942 #[case(" ")] #[case(" ")] #[case("🦀")] #[case(" a")]
946 #[case("a ")]
947 #[case("abc")]
948 fn test_check_nonempty_string_with_valid_values(#[case] s: &str) {
949 assert!(check_nonempty_string(s, "value").is_ok());
950 }
951
952 #[rstest]
953 #[case("")] fn test_check_nonempty_string_with_invalid_values(#[case] s: &str) {
955 assert!(check_nonempty_string(s, "value").is_err());
956 }
957
958 #[rstest]
959 #[case(" a")]
960 #[case("a ")]
961 #[case("a a")]
962 #[case(" a ")]
963 #[case("abc")]
964 fn test_check_valid_string_ascii_with_valid_value(#[case] s: &str) {
965 assert!(check_valid_string_ascii(s, "value").is_ok());
966 }
967
968 #[rstest]
969 #[case("")] #[case(" ")] #[case(" ")] #[case("🦀")] fn test_check_valid_string_ascii_with_invalid_values(#[case] s: &str) {
974 assert!(check_valid_string_ascii(s, "value").is_err());
975 }
976
977 #[rstest]
978 fn test_check_valid_string_ascii_returns_empty_string_error_with_stable_display() {
979 let error = check_valid_string_ascii("", "value").unwrap_err();
980
981 assert_eq!(
982 error,
983 CorrectnessError::EmptyString {
984 param: "value".to_string(),
985 }
986 );
987 assert_eq!(error.to_string(), "invalid string for 'value', was empty");
988 }
989
990 #[rstest]
991 fn test_check_valid_string_ascii_returns_non_ascii_error_with_stable_display() {
992 let error = check_valid_string_ascii("🦀", "value").unwrap_err();
993
994 assert_eq!(
995 error,
996 CorrectnessError::NonAsciiString {
997 param: "value".to_string(),
998 value: "🦀".to_string(),
999 }
1000 );
1001 assert_eq!(
1002 error.to_string(),
1003 "invalid string for 'value' contained a non-ASCII char, was '🦀'"
1004 );
1005 }
1006
1007 #[rstest]
1008 fn test_check_valid_string_ascii_returns_whitespace_string_error_with_stable_display() {
1009 let error = check_valid_string_ascii(" ", "value").unwrap_err();
1010
1011 assert_eq!(
1012 error,
1013 CorrectnessError::WhitespaceString {
1014 param: "value".to_string(),
1015 }
1016 );
1017 assert_eq!(
1018 error.to_string(),
1019 "invalid string for 'value', was all whitespace"
1020 );
1021 }
1022
1023 #[rstest]
1024 #[case(" a")]
1025 #[case("a ")]
1026 #[case("abc")]
1027 #[case("ETHUSDT")]
1028 fn test_check_valid_string_utf8_with_valid_values(#[case] s: &str) {
1029 assert!(check_valid_string_utf8(s, "value").is_ok());
1030 }
1031
1032 #[rstest]
1033 #[case("")] #[case(" ")] #[case(" ")] fn test_check_valid_string_utf8_with_invalid_values(#[case] s: &str) {
1037 assert!(check_valid_string_utf8(s, "value").is_err());
1038 }
1039
1040 #[rstest]
1041 #[case(None)]
1042 #[case(Some(" a"))]
1043 #[case(Some("a "))]
1044 #[case(Some("a a"))]
1045 #[case(Some(" a "))]
1046 #[case(Some("abc"))]
1047 fn test_check_valid_string_ascii_optional_with_valid_value(#[case] s: Option<&str>) {
1048 assert!(check_valid_string_ascii_optional(s, "value").is_ok());
1049 }
1050
1051 #[rstest]
1052 #[case("a", "a")]
1053 fn test_check_string_contains_when_does_contain(#[case] s: &str, #[case] pat: &str) {
1054 assert!(check_string_contains(s, pat, "value").is_ok());
1055 }
1056
1057 #[rstest]
1058 #[case("a", "b")]
1059 fn test_check_string_contains_when_does_not_contain(#[case] s: &str, #[case] pat: &str) {
1060 assert!(check_string_contains(s, pat, "value").is_err());
1061 }
1062
1063 #[rstest]
1064 #[case(0u8, 0u8, "left", "right", true)]
1065 #[case(1u8, 1u8, "left", "right", true)]
1066 #[case(0u8, 1u8, "left", "right", false)]
1067 #[case(1u8, 0u8, "left", "right", false)]
1068 #[case(10i32, 10i32, "left", "right", true)]
1069 #[case(10i32, 20i32, "left", "right", false)]
1070 #[case("hello", "hello", "left", "right", true)]
1071 #[case("hello", "world", "left", "right", false)]
1072 fn test_check_equal<T: PartialEq + Debug + Display>(
1073 #[case] lhs: T,
1074 #[case] rhs: T,
1075 #[case] lhs_param: &str,
1076 #[case] rhs_param: &str,
1077 #[case] expected: bool,
1078 ) {
1079 let result = check_equal(&lhs, &rhs, lhs_param, rhs_param).is_ok();
1080 assert_eq!(result, expected);
1081 }
1082
1083 #[rstest]
1084 #[case(0, 0, "left", "right", true)]
1085 #[case(1, 1, "left", "right", true)]
1086 #[case(0, 1, "left", "right", false)]
1087 #[case(1, 0, "left", "right", false)]
1088 fn test_check_equal_u8_when_equal(
1089 #[case] lhs: u8,
1090 #[case] rhs: u8,
1091 #[case] lhs_param: &str,
1092 #[case] rhs_param: &str,
1093 #[case] expected: bool,
1094 ) {
1095 let result = check_equal_u8(lhs, rhs, lhs_param, rhs_param).is_ok();
1096 assert_eq!(result, expected);
1097 }
1098
1099 #[rstest]
1100 fn test_check_equal_u8_returns_equality_mismatch_with_stable_display() {
1101 let error = check_equal_u8(1, 2, "left", "right").unwrap_err();
1102
1103 assert_eq!(
1104 error,
1105 CorrectnessError::EqualityMismatch {
1106 lhs_param: "left".to_string(),
1107 rhs_param: "right".to_string(),
1108 lhs: "1".to_string(),
1109 rhs: "2".to_string(),
1110 type_name: "u8",
1111 }
1112 );
1113 assert_eq!(
1114 error.to_string(),
1115 "'left' u8 of 1 was not equal to 'right' u8 of 2"
1116 );
1117 }
1118
1119 #[rstest]
1120 #[case(0, 0, "left", "right", true)]
1121 #[case(1, 1, "left", "right", true)]
1122 #[case(0, 1, "left", "right", false)]
1123 #[case(1, 0, "left", "right", false)]
1124 fn test_check_equal_usize_when_equal(
1125 #[case] lhs: usize,
1126 #[case] rhs: usize,
1127 #[case] lhs_param: &str,
1128 #[case] rhs_param: &str,
1129 #[case] expected: bool,
1130 ) {
1131 let result = check_equal_usize(lhs, rhs, lhs_param, rhs_param).is_ok();
1132 assert_eq!(result, expected);
1133 }
1134
1135 #[rstest]
1136 #[case(1, true)]
1137 #[case(usize::MAX, true)]
1138 #[case(0, false)]
1139 fn test_check_positive_usize(#[case] value: usize, #[case] expected: bool) {
1140 assert_eq!(check_positive_usize(value, "value").is_ok(), expected);
1141 }
1142
1143 #[rstest]
1144 fn test_check_positive_usize_returns_not_positive_error_with_stable_display() {
1145 let error = check_positive_usize(0, "param").unwrap_err();
1146
1147 assert_eq!(
1148 error,
1149 CorrectnessError::NotPositive {
1150 param: "param".to_string(),
1151 value: "0".to_string(),
1152 type_name: "usize",
1153 }
1154 );
1155 assert_eq!(
1156 error.to_string(),
1157 "invalid usize for 'param' not positive, was 0"
1158 );
1159 }
1160
1161 #[rstest]
1162 #[case(1, "value")]
1163 fn test_check_positive_u64_when_positive(#[case] value: u64, #[case] param: &str) {
1164 assert!(check_positive_u64(value, param).is_ok());
1165 }
1166
1167 #[rstest]
1168 #[case(0, "value")]
1169 fn test_check_positive_u64_when_not_positive(#[case] value: u64, #[case] param: &str) {
1170 assert!(check_positive_u64(value, param).is_err());
1171 }
1172
1173 #[rstest]
1174 #[case(1, "value")]
1175 fn test_check_positive_i64_when_positive(#[case] value: i64, #[case] param: &str) {
1176 assert!(check_positive_i64(value, param).is_ok());
1177 }
1178
1179 #[rstest]
1180 #[case(0, "value")]
1181 #[case(-1, "value")]
1182 fn test_check_positive_i64_when_not_positive(#[case] value: i64, #[case] param: &str) {
1183 assert!(check_positive_i64(value, param).is_err());
1184 }
1185
1186 #[rstest]
1187 #[case(0.0, "value")]
1188 #[case(1.0, "value")]
1189 fn test_check_non_negative_f64_when_not_negative(#[case] value: f64, #[case] param: &str) {
1190 assert!(check_non_negative_f64(value, param).is_ok());
1191 }
1192
1193 #[rstest]
1194 #[case(f64::NAN, "value")]
1195 #[case(f64::INFINITY, "value")]
1196 #[case(f64::NEG_INFINITY, "value")]
1197 #[case(-0.1, "value")]
1198 fn test_check_non_negative_f64_when_negative(#[case] value: f64, #[case] param: &str) {
1199 assert!(check_non_negative_f64(value, param).is_err());
1200 }
1201
1202 #[rstest]
1203 #[case(0, 0, 0, "value")]
1204 #[case(0, 0, 1, "value")]
1205 #[case(1, 0, 1, "value")]
1206 fn test_check_in_range_inclusive_u8_when_in_range(
1207 #[case] value: u8,
1208 #[case] l: u8,
1209 #[case] r: u8,
1210 #[case] desc: &str,
1211 ) {
1212 assert!(check_in_range_inclusive_u8(value, l, r, desc).is_ok());
1213 }
1214
1215 #[rstest]
1216 #[case(0, 1, 2, "value")]
1217 #[case(3, 1, 2, "value")]
1218 fn test_check_in_range_inclusive_u8_when_out_of_range(
1219 #[case] value: u8,
1220 #[case] l: u8,
1221 #[case] r: u8,
1222 #[case] param: &str,
1223 ) {
1224 assert!(check_in_range_inclusive_u8(value, l, r, param).is_err());
1225 }
1226
1227 #[rstest]
1228 #[case(0, 0, 0, "value")]
1229 #[case(0, 0, 1, "value")]
1230 #[case(1, 0, 1, "value")]
1231 fn test_check_in_range_inclusive_u64_when_in_range(
1232 #[case] value: u64,
1233 #[case] l: u64,
1234 #[case] r: u64,
1235 #[case] param: &str,
1236 ) {
1237 assert!(check_in_range_inclusive_u64(value, l, r, param).is_ok());
1238 }
1239
1240 #[rstest]
1241 #[case(0, 1, 2, "value")]
1242 #[case(3, 1, 2, "value")]
1243 fn test_check_in_range_inclusive_u64_when_out_of_range(
1244 #[case] value: u64,
1245 #[case] l: u64,
1246 #[case] r: u64,
1247 #[case] param: &str,
1248 ) {
1249 assert!(check_in_range_inclusive_u64(value, l, r, param).is_err());
1250 }
1251
1252 #[rstest]
1253 #[case(0, 0, 0, "value")]
1254 #[case(0, 0, 1, "value")]
1255 #[case(1, 0, 1, "value")]
1256 fn test_check_in_range_inclusive_i64_when_in_range(
1257 #[case] value: i64,
1258 #[case] l: i64,
1259 #[case] r: i64,
1260 #[case] param: &str,
1261 ) {
1262 assert!(check_in_range_inclusive_i64(value, l, r, param).is_ok());
1263 }
1264
1265 #[rstest]
1266 #[case(0.0, 0.0, 0.0, "value")]
1267 #[case(0.0, 0.0, 1.0, "value")]
1268 #[case(1.0, 0.0, 1.0, "value")]
1269 fn test_check_in_range_inclusive_f64_when_in_range(
1270 #[case] value: f64,
1271 #[case] l: f64,
1272 #[case] r: f64,
1273 #[case] param: &str,
1274 ) {
1275 assert!(check_in_range_inclusive_f64(value, l, r, param).is_ok());
1276 }
1277
1278 #[rstest]
1279 #[case(-1e16, 0.0, 0.0, "value")]
1280 #[case(1.0 + 1e16, 0.0, 1.0, "value")]
1281 fn test_check_in_range_inclusive_f64_when_out_of_range(
1282 #[case] value: f64,
1283 #[case] l: f64,
1284 #[case] r: f64,
1285 #[case] param: &str,
1286 ) {
1287 assert!(check_in_range_inclusive_f64(value, l, r, param).is_err());
1288 }
1289
1290 #[rstest]
1291 #[case(0, 1, 2, "value")]
1292 #[case(3, 1, 2, "value")]
1293 fn test_check_in_range_inclusive_i64_when_out_of_range(
1294 #[case] value: i64,
1295 #[case] l: i64,
1296 #[case] r: i64,
1297 #[case] param: &str,
1298 ) {
1299 assert!(check_in_range_inclusive_i64(value, l, r, param).is_err());
1300 }
1301
1302 #[rstest]
1303 #[case(0, 0, 0, "value")]
1304 #[case(0, 0, 1, "value")]
1305 #[case(1, 0, 1, "value")]
1306 fn test_check_in_range_inclusive_usize_when_in_range(
1307 #[case] value: usize,
1308 #[case] l: usize,
1309 #[case] r: usize,
1310 #[case] param: &str,
1311 ) {
1312 assert!(check_in_range_inclusive_usize(value, l, r, param).is_ok());
1313 }
1314
1315 #[rstest]
1316 #[case(0, 1, 2, "value")]
1317 #[case(3, 1, 2, "value")]
1318 fn test_check_in_range_inclusive_usize_when_out_of_range(
1319 #[case] value: usize,
1320 #[case] l: usize,
1321 #[case] r: usize,
1322 #[case] param: &str,
1323 ) {
1324 assert!(check_in_range_inclusive_usize(value, l, r, param).is_err());
1325 }
1326
1327 #[rstest]
1328 fn test_check_in_range_inclusive_usize_returns_out_of_range_error_with_stable_display() {
1329 let error = check_in_range_inclusive_usize(3, 1, 2, "value").unwrap_err();
1330
1331 assert_eq!(
1332 error,
1333 CorrectnessError::OutOfRange {
1334 param: "value".to_string(),
1335 min: "1".to_string(),
1336 max: "2".to_string(),
1337 value: "3".to_string(),
1338 type_name: "usize",
1339 }
1340 );
1341 assert_eq!(
1342 error.to_string(),
1343 "invalid usize for 'value' not in range [1, 2], was 3"
1344 );
1345 }
1346
1347 #[rstest]
1348 #[case(vec![], true)]
1349 #[case(vec![1_u8], false)]
1350 fn test_check_slice_empty(#[case] collection: Vec<u8>, #[case] expected: bool) {
1351 let result = check_slice_empty(collection.as_slice(), "param").is_ok();
1352 assert_eq!(result, expected);
1353 }
1354
1355 #[rstest]
1356 #[case(vec![], false)]
1357 #[case(vec![1_u8], true)]
1358 fn test_check_slice_not_empty(#[case] collection: Vec<u8>, #[case] expected: bool) {
1359 let result = check_slice_not_empty(collection.as_slice(), "param").is_ok();
1360 assert_eq!(result, expected);
1361 }
1362
1363 #[rstest]
1364 fn test_check_slice_not_empty_returns_collection_empty_error_with_stable_display() {
1365 let error = check_slice_not_empty::<u8>(&[], "param").unwrap_err();
1366
1367 assert_eq!(
1368 error,
1369 CorrectnessError::CollectionEmpty {
1370 param: "param".to_string(),
1371 collection_kind: "slice",
1372 type_repr: "&[u8]".to_string(),
1373 }
1374 );
1375 assert_eq!(error.to_string(), "the 'param' slice `&[u8]` was empty");
1376 }
1377
1378 #[rstest]
1379 #[case(HashMap::new(), true)]
1380 #[case(HashMap::from([("A".to_string(), 1_u8)]), false)]
1381 fn test_check_map_empty(#[case] map: HashMap<String, u8>, #[case] expected: bool) {
1382 let result = check_map_empty(&map, "param").is_ok();
1383 assert_eq!(result, expected);
1384 }
1385
1386 #[rstest]
1387 #[case(HashMap::new(), false)]
1388 #[case(HashMap::from([("A".to_string(), 1_u8)]), true)]
1389 fn test_check_map_not_empty(#[case] map: HashMap<String, u8>, #[case] expected: bool) {
1390 let result = check_map_not_empty(&map, "param").is_ok();
1391 assert_eq!(result, expected);
1392 }
1393
1394 #[rstest]
1395 #[case(&HashMap::<u32, u32>::new(), 5, "key", "map", true)] #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", false)] #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", true)] fn test_check_key_not_in_map(
1399 #[case] map: &HashMap<u32, u32>,
1400 #[case] key: u32,
1401 #[case] key_name: &str,
1402 #[case] map_name: &str,
1403 #[case] expected: bool,
1404 ) {
1405 let result = check_key_not_in_map(&key, map, key_name, map_name).is_ok();
1406 assert_eq!(result, expected);
1407 }
1408
1409 #[rstest]
1410 #[case(&HashMap::<u32, u32>::new(), 5, "key", "map", false)] #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", true)] #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", false)] fn test_check_key_in_map(
1414 #[case] map: &HashMap<u32, u32>,
1415 #[case] key: u32,
1416 #[case] key_name: &str,
1417 #[case] map_name: &str,
1418 #[case] expected: bool,
1419 ) {
1420 let result = check_key_in_map(&key, map, key_name, map_name).is_ok();
1421 assert_eq!(result, expected);
1422 }
1423
1424 #[rstest]
1425 fn test_check_key_in_map_returns_key_missing_error_with_stable_display() {
1426 let map = HashMap::<u32, u32>::new();
1427 let error = check_key_in_map(&5, &map, "key", "map").unwrap_err();
1428
1429 assert_eq!(
1430 error,
1431 CorrectnessError::KeyMissing {
1432 key_name: "key".to_string(),
1433 map_name: "map".to_string(),
1434 key: "5".to_string(),
1435 map_type_repr: "&<u32, u32>".to_string(),
1436 }
1437 );
1438 assert_eq!(
1439 error.to_string(),
1440 "the 'key' key 5 was not in the 'map' map `&<u32, u32>`"
1441 );
1442 }
1443
1444 #[rstest]
1445 #[case(&HashSet::<u32>::new(), 5, "member", "set", true)] #[case(&HashSet::from([1, 2]), 1, "member", "set", false)] #[case(&HashSet::from([1, 2]), 5, "member", "set", true)] fn test_check_member_not_in_set(
1449 #[case] set: &HashSet<u32>,
1450 #[case] member: u32,
1451 #[case] member_name: &str,
1452 #[case] set_name: &str,
1453 #[case] expected: bool,
1454 ) {
1455 let result = check_member_not_in_set(&member, set, member_name, set_name).is_ok();
1456 assert_eq!(result, expected);
1457 }
1458
1459 #[rstest]
1460 #[case(&HashSet::<u32>::new(), 5, "member", "set", false)] #[case(&HashSet::from([1, 2]), 1, "member", "set", true)] #[case(&HashSet::from([1, 2]), 5, "member", "set", false)] fn test_check_member_in_set(
1464 #[case] set: &HashSet<u32>,
1465 #[case] member: u32,
1466 #[case] member_name: &str,
1467 #[case] set_name: &str,
1468 #[case] expected: bool,
1469 ) {
1470 let result = check_member_in_set(&member, set, member_name, set_name).is_ok();
1471 assert_eq!(result, expected);
1472 }
1473
1474 #[rstest]
1475 #[case("1", true)] #[case("0.0000000000000000000000000001", true)] #[case("79228162514264337593543950335", true)] #[case("0", false)] #[case("-0.0000000000000000000000000001", false)] #[case("-1", false)] fn test_check_positive_decimal(#[case] raw: &str, #[case] expected: bool) {
1482 let value = Decimal::from_str(raw).expect("valid decimal literal");
1483 let result = super::check_positive_decimal(value, "param").is_ok();
1484 assert_eq!(result, expected);
1485 }
1486
1487 #[rstest]
1488 #[case(1, true)]
1489 #[case(u128::MAX, true)]
1490 #[case(0, false)]
1491 fn test_check_positive_u128(#[case] value: u128, #[case] expected: bool) {
1492 assert_eq!(check_positive_u128(value, "value").is_ok(), expected);
1493 }
1494
1495 #[rstest]
1496 #[case(1, true)]
1497 #[case(i128::MAX, true)]
1498 #[case(0, false)]
1499 #[case(-1, false)]
1500 #[case(i128::MIN, false)]
1501 fn test_check_positive_i128(#[case] value: i128, #[case] expected: bool) {
1502 assert_eq!(check_positive_i128(value, "value").is_ok(), expected);
1503 }
1504
1505 #[rstest]
1506 fn test_check_positive_decimal_returns_not_positive_error_with_stable_display() {
1507 let error = check_positive_decimal(Decimal::ZERO, "param").unwrap_err();
1508
1509 assert_eq!(
1510 error,
1511 CorrectnessError::NotPositive {
1512 param: "param".to_string(),
1513 value: "0".to_string(),
1514 type_name: "Decimal",
1515 }
1516 );
1517 assert_eq!(
1518 error.to_string(),
1519 "invalid Decimal for 'param' not positive, was 0"
1520 );
1521 }
1522}