Skip to main content

nautilus_core/
correctness.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Functions for correctness checks similar to the *design by contract* philosophy.
17//!
18//! This module provides validation checking of function or method conditions.
19//!
20//! A condition is a predicate which must be true just prior to the execution of
21//! some section of code - for correct behavior as per the design specification.
22//!
23//! A typed [`Result`] is returned with a descriptive message when the condition
24//! check fails.
25
26use std::fmt::{Debug, Display};
27
28use rust_decimal::Decimal;
29use thiserror::Error;
30
31use crate::collections::{MapLike, SetLike};
32
33/// A message prefix that can be used with calls to `expect` or other assertion-related functions.
34///
35/// This constant provides a standard message that can be used to indicate a failure condition
36/// when a predicate or condition does not hold true. It is typically used in conjunction with
37/// functions like `expect` to provide a consistent error message.
38pub const FAILED: &str = "Condition failed";
39
40/// Error type for correctness checks.
41#[derive(Clone, Debug, Error, Eq, PartialEq)]
42pub enum CorrectnessError {
43    /// A predicate or invariant check failed.
44    #[error("{message}")]
45    PredicateViolation {
46        /// The failure message.
47        message: String,
48    },
49    /// A string was empty.
50    #[error("invalid string for '{param}', was empty")]
51    EmptyString {
52        /// The parameter name.
53        param: String,
54    },
55    /// A string was all whitespace.
56    #[error("invalid string for '{param}', was all whitespace")]
57    WhitespaceString {
58        /// The parameter name.
59        param: String,
60    },
61    /// A string contained a non-ASCII character.
62    #[error("invalid string for '{param}' contained a non-ASCII char, was '{value}'")]
63    NonAsciiString {
64        /// The parameter name.
65        param: String,
66        /// The provided value.
67        value: String,
68    },
69    /// A string did not contain an expected pattern.
70    #[error("invalid string for '{param}' did not contain '{pattern}', was '{value}'")]
71    MissingSubstring {
72        /// The parameter name.
73        param: String,
74        /// The expected substring.
75        pattern: String,
76        /// The provided value.
77        value: String,
78    },
79    /// Two values were not equal.
80    #[error(
81        "'{lhs_param}' {type_name} of {lhs} was not equal to '{rhs_param}' {type_name} of {rhs}"
82    )]
83    EqualityMismatch {
84        /// The left parameter name.
85        lhs_param: String,
86        /// The right parameter name.
87        rhs_param: String,
88        /// The left value.
89        lhs: String,
90        /// The right value.
91        rhs: String,
92        /// The displayed type name.
93        type_name: &'static str,
94    },
95    /// A value that must be positive was not positive.
96    #[error("invalid {type_name} for '{param}' not positive, was {value}")]
97    NotPositive {
98        /// The parameter name.
99        param: String,
100        /// The provided value.
101        value: String,
102        /// The displayed type name.
103        type_name: &'static str,
104    },
105    /// A value that must not be negative was negative.
106    #[error("invalid {type_name} for '{param}' negative, was {value}")]
107    NegativeValue {
108        /// The parameter name.
109        param: String,
110        /// The provided value.
111        value: String,
112        /// The displayed type name.
113        type_name: &'static str,
114    },
115    /// A value was invalid for its type.
116    #[error("invalid {type_name} for '{param}', was {value}")]
117    InvalidValue {
118        /// The parameter name.
119        param: String,
120        /// The provided value.
121        value: String,
122        /// The displayed type name.
123        type_name: &'static str,
124    },
125    /// A value was outside an inclusive range.
126    #[error("invalid {type_name} for '{param}' not in range [{min}, {max}], was {value}")]
127    OutOfRange {
128        /// The parameter name.
129        param: String,
130        /// The lower bound.
131        min: String,
132        /// The upper bound.
133        max: String,
134        /// The provided value.
135        value: String,
136        /// The displayed type name.
137        type_name: &'static str,
138    },
139    /// A collection that must be empty was not empty.
140    #[error("the '{param}' {collection_kind} `{type_repr}` was not empty")]
141    CollectionNotEmpty {
142        /// The parameter name.
143        param: String,
144        /// The collection kind.
145        collection_kind: &'static str,
146        /// The collection type representation.
147        type_repr: String,
148    },
149    /// A collection that must not be empty was empty.
150    #[error("the '{param}' {collection_kind} `{type_repr}` was empty")]
151    CollectionEmpty {
152        /// The parameter name.
153        param: String,
154        /// The collection kind.
155        collection_kind: &'static str,
156        /// The collection type representation.
157        type_repr: String,
158    },
159    /// A map key was already present.
160    #[error("the '{key_name}' key {key} was already in the '{map_name}' map `{map_type_repr}`")]
161    KeyPresent {
162        /// The key parameter name.
163        key_name: String,
164        /// The map parameter name.
165        map_name: String,
166        /// The key value.
167        key: String,
168        /// The map type representation.
169        map_type_repr: String,
170    },
171    /// A map key was missing.
172    #[error("the '{key_name}' key {key} was not in the '{map_name}' map `{map_type_repr}`")]
173    KeyMissing {
174        /// The key parameter name.
175        key_name: String,
176        /// The map parameter name.
177        map_name: String,
178        /// The key value.
179        key: String,
180        /// The map type representation.
181        map_type_repr: String,
182    },
183    /// A set member was already present.
184    #[error("the '{member_name}' member was already in the '{set_name}' set `{set_type_repr}`")]
185    MemberPresent {
186        /// The member parameter name.
187        member_name: String,
188        /// The set parameter name.
189        set_name: String,
190        /// The set type representation.
191        set_type_repr: String,
192    },
193    /// A set member was missing.
194    #[error("the '{member_name}' member was not in the '{set_name}' set `{set_type_repr}`")]
195    MemberMissing {
196        /// The member parameter name.
197        member_name: String,
198        /// The set parameter name.
199        set_name: String,
200        /// The set type representation.
201        set_type_repr: String,
202    },
203}
204
205/// Result type for correctness checks.
206pub type Result<T> = std::result::Result<T, CorrectnessError>;
207
208/// Result type alias for APIs that want to name the correctness error domain explicitly.
209pub type CorrectnessResult<T> = Result<T>;
210
211/// Extension trait for [`CorrectnessResult`] that panics with the error's
212/// [`Display`] form rather than its `Debug` form.
213///
214/// Use this instead of [`std::result::Result::expect`] when unwrapping a
215/// correctness result: `expect` formats the error with `{:?}`, which exposes
216/// the internal [`CorrectnessError`] struct layout in panic output, while
217/// [`CorrectnessResultExt::expect_display`] preserves the human-readable
218/// message defined on each variant.
219pub trait CorrectnessResultExt<T> {
220    /// Returns the contained [`Ok`] value, panicking with `msg: <error display>`
221    /// on [`Err`].
222    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/// Checks the `predicate` is true.
237///
238/// # Errors
239///
240/// Returns an error if the validation check fails.
241#[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/// Checks the `predicate` is false.
252///
253/// # Errors
254///
255/// Returns an error if the validation check fails.
256#[inline(always)]
257pub fn check_predicate_false(predicate: bool, fail_msg: &str) -> Result<()> {
258    if predicate {
259        return Err(CorrectnessError::PredicateViolation {
260            message: fail_msg.to_string(),
261        });
262    }
263    Ok(())
264}
265
266/// Checks if the string `s` is not empty.
267///
268/// This function performs a basic check to ensure the string has at least one character.
269/// Unlike `check_valid_string`, it does not validate ASCII characters or check for whitespace.
270///
271/// # Errors
272///
273/// Returns an error if `s` is empty.
274#[inline(always)]
275pub fn check_nonempty_string<T: AsRef<str>>(s: T, param: &str) -> Result<()> {
276    if s.as_ref().is_empty() {
277        return Err(CorrectnessError::EmptyString {
278            param: param.to_string(),
279        });
280    }
281    Ok(())
282}
283
284/// Checks the string `s` has semantic meaning and contains only ASCII characters.
285///
286/// # Errors
287///
288/// Returns an error if:
289/// - `s` is an empty string.
290/// - `s` consists solely of whitespace characters.
291/// - `s` contains one or more non-ASCII characters.
292#[inline(always)]
293pub fn check_valid_string_ascii<T: AsRef<str>>(s: T, param: &str) -> Result<()> {
294    let s = s.as_ref();
295
296    if s.is_empty() {
297        return Err(CorrectnessError::EmptyString {
298            param: param.to_string(),
299        });
300    }
301
302    // Ensure string is only traversed once
303    let mut has_non_whitespace = false;
304
305    for c in s.chars() {
306        if !c.is_whitespace() {
307            has_non_whitespace = true;
308        }
309
310        if !c.is_ascii() {
311            return Err(CorrectnessError::NonAsciiString {
312                param: param.to_string(),
313                value: s.to_string(),
314            });
315        }
316    }
317
318    if !has_non_whitespace {
319        return Err(CorrectnessError::WhitespaceString {
320            param: param.to_string(),
321        });
322    }
323
324    Ok(())
325}
326
327/// Checks the string `s` has semantic meaning and allows UTF-8 characters.
328///
329/// This is a relaxed version of [`check_valid_string_ascii`] that permits non-ASCII UTF-8 characters.
330/// Use this for external identifiers (e.g., exchange symbols) that may contain Unicode characters.
331///
332/// # Errors
333///
334/// Returns an error if:
335/// - `s` is an empty string.
336/// - `s` consists solely of whitespace characters.
337#[inline(always)]
338pub fn check_valid_string_utf8<T: AsRef<str>>(s: T, param: &str) -> Result<()> {
339    let s = s.as_ref();
340
341    if s.is_empty() {
342        return Err(CorrectnessError::EmptyString {
343            param: param.to_string(),
344        });
345    }
346
347    let has_non_whitespace = s.chars().any(|c| !c.is_whitespace());
348
349    if !has_non_whitespace {
350        return Err(CorrectnessError::WhitespaceString {
351            param: param.to_string(),
352        });
353    }
354
355    Ok(())
356}
357
358/// Checks the string `s` if Some, contains only ASCII characters and has semantic meaning.
359///
360/// # Errors
361///
362/// Returns an error if:
363/// - `s` is an empty string.
364/// - `s` consists solely of whitespace characters.
365/// - `s` contains one or more non-ASCII characters.
366#[inline(always)]
367pub fn check_valid_string_ascii_optional<T: AsRef<str>>(s: Option<T>, param: &str) -> Result<()> {
368    if let Some(s) = s {
369        check_valid_string_ascii(s, param)?;
370    }
371    Ok(())
372}
373
374/// Checks the string `s` contains the pattern `pat`.
375///
376/// # Errors
377///
378/// Returns an error if the validation check fails.
379#[inline(always)]
380pub fn check_string_contains<T: AsRef<str>>(s: T, pat: &str, param: &str) -> Result<()> {
381    let s = s.as_ref();
382    if !s.contains(pat) {
383        return Err(CorrectnessError::MissingSubstring {
384            param: param.to_string(),
385            pattern: pat.to_string(),
386            value: s.to_string(),
387        });
388    }
389    Ok(())
390}
391
392/// Checks the values are equal.
393///
394/// # Errors
395///
396/// Returns an error if the validation check fails.
397#[inline(always)]
398pub fn check_equal<T: PartialEq + Debug + Display>(
399    lhs: &T,
400    rhs: &T,
401    lhs_param: &str,
402    rhs_param: &str,
403) -> Result<()> {
404    if lhs != rhs {
405        return Err(CorrectnessError::EqualityMismatch {
406            lhs_param: lhs_param.to_string(),
407            rhs_param: rhs_param.to_string(),
408            lhs: lhs.to_string(),
409            rhs: rhs.to_string(),
410            type_name: "value",
411        });
412    }
413    Ok(())
414}
415
416/// Checks the `u8` values are equal.
417///
418/// # Errors
419///
420/// Returns an error if the validation check fails.
421#[inline(always)]
422pub fn check_equal_u8(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> Result<()> {
423    if lhs != rhs {
424        return Err(CorrectnessError::EqualityMismatch {
425            lhs_param: lhs_param.to_string(),
426            rhs_param: rhs_param.to_string(),
427            lhs: lhs.to_string(),
428            rhs: rhs.to_string(),
429            type_name: "u8",
430        });
431    }
432    Ok(())
433}
434
435/// Checks the `usize` values are equal.
436///
437/// # Errors
438///
439/// Returns an error if the validation check fails.
440#[inline(always)]
441pub fn check_equal_usize(lhs: usize, rhs: usize, lhs_param: &str, rhs_param: &str) -> Result<()> {
442    if lhs != rhs {
443        return Err(CorrectnessError::EqualityMismatch {
444            lhs_param: lhs_param.to_string(),
445            rhs_param: rhs_param.to_string(),
446            lhs: lhs.to_string(),
447            rhs: rhs.to_string(),
448            type_name: "usize",
449        });
450    }
451    Ok(())
452}
453
454/// Checks the `usize` value is positive (> 0).
455///
456/// # Errors
457///
458/// Returns an error if the validation check fails.
459#[inline(always)]
460pub fn check_positive_usize(value: usize, param: &str) -> Result<()> {
461    if value == 0 {
462        return Err(CorrectnessError::NotPositive {
463            param: param.to_string(),
464            value: value.to_string(),
465            type_name: "usize",
466        });
467    }
468    Ok(())
469}
470
471/// Checks the `u64` value is positive (> 0).
472///
473/// # Errors
474///
475/// Returns an error if the validation check fails.
476#[inline(always)]
477pub fn check_positive_u64(value: u64, param: &str) -> Result<()> {
478    if value == 0 {
479        return Err(CorrectnessError::NotPositive {
480            param: param.to_string(),
481            value: value.to_string(),
482            type_name: "u64",
483        });
484    }
485    Ok(())
486}
487
488/// Checks the `u128` value is positive (> 0).
489///
490/// # Errors
491///
492/// Returns an error if the validation check fails.
493#[inline(always)]
494pub fn check_positive_u128(value: u128, param: &str) -> Result<()> {
495    if value == 0 {
496        return Err(CorrectnessError::NotPositive {
497            param: param.to_string(),
498            value: value.to_string(),
499            type_name: "u128",
500        });
501    }
502    Ok(())
503}
504
505/// Checks the `i64` value is positive (> 0).
506///
507/// # Errors
508///
509/// Returns an error if the validation check fails.
510#[inline(always)]
511pub fn check_positive_i64(value: i64, param: &str) -> Result<()> {
512    if value <= 0 {
513        return Err(CorrectnessError::NotPositive {
514            param: param.to_string(),
515            value: value.to_string(),
516            type_name: "i64",
517        });
518    }
519    Ok(())
520}
521
522/// Checks the `i64` value is positive (> 0).
523///
524/// # Errors
525///
526/// Returns an error if the validation check fails.
527#[inline(always)]
528pub fn check_positive_i128(value: i128, param: &str) -> Result<()> {
529    if value <= 0 {
530        return Err(CorrectnessError::NotPositive {
531            param: param.to_string(),
532            value: value.to_string(),
533            type_name: "i128",
534        });
535    }
536    Ok(())
537}
538
539/// Checks the `f64` value is non-negative (>= 0).
540///
541/// # Errors
542///
543/// Returns an error if the validation check fails.
544#[inline(always)]
545pub fn check_non_negative_f64(value: f64, param: &str) -> Result<()> {
546    if value.is_nan() || value.is_infinite() {
547        return Err(CorrectnessError::InvalidValue {
548            param: param.to_string(),
549            value: value.to_string(),
550            type_name: "f64",
551        });
552    }
553
554    if value < 0.0 {
555        return Err(CorrectnessError::NegativeValue {
556            param: param.to_string(),
557            value: value.to_string(),
558            type_name: "f64",
559        });
560    }
561    Ok(())
562}
563
564/// Checks the `u8` value is in range [`l`, `r`] (inclusive).
565///
566/// # Errors
567///
568/// Returns an error if the validation check fails.
569#[inline(always)]
570pub fn check_in_range_inclusive_u8(value: u8, l: u8, r: u8, param: &str) -> Result<()> {
571    if value < l || value > r {
572        return Err(CorrectnessError::OutOfRange {
573            param: param.to_string(),
574            min: l.to_string(),
575            max: r.to_string(),
576            value: value.to_string(),
577            type_name: "u8",
578        });
579    }
580    Ok(())
581}
582
583/// Checks the `u64` value is range [`l`, `r`] (inclusive).
584///
585/// # Errors
586///
587/// Returns an error if the validation check fails.
588#[inline(always)]
589pub fn check_in_range_inclusive_u64(value: u64, l: u64, r: u64, param: &str) -> Result<()> {
590    if value < l || value > r {
591        return Err(CorrectnessError::OutOfRange {
592            param: param.to_string(),
593            min: l.to_string(),
594            max: r.to_string(),
595            value: value.to_string(),
596            type_name: "u64",
597        });
598    }
599    Ok(())
600}
601
602/// Checks the `i64` value is in range [`l`, `r`] (inclusive).
603///
604/// # Errors
605///
606/// Returns an error if the validation check fails.
607#[inline(always)]
608pub fn check_in_range_inclusive_i64(value: i64, l: i64, r: i64, param: &str) -> Result<()> {
609    if value < l || value > r {
610        return Err(CorrectnessError::OutOfRange {
611            param: param.to_string(),
612            min: l.to_string(),
613            max: r.to_string(),
614            value: value.to_string(),
615            type_name: "i64",
616        });
617    }
618    Ok(())
619}
620
621/// Checks the `f64` value is in range [`l`, `r`] (inclusive).
622///
623/// # Errors
624///
625/// Returns an error if the validation check fails.
626#[inline(always)]
627pub fn check_in_range_inclusive_f64(value: f64, l: f64, r: f64, param: &str) -> Result<()> {
628    // Hardcoded epsilon is intentional and appropriate here because:
629    // - 1e-15 is conservative for IEEE 754 double precision (machine epsilon ~2.22e-16)
630    // - This function is used for validation, not high-precision calculations
631    // - The epsilon prevents spurious failures due to floating-point representation
632    // - Making it configurable would complicate the API for minimal benefit
633    const EPSILON: f64 = 1e-15;
634
635    if value.is_nan() || value.is_infinite() {
636        return Err(CorrectnessError::InvalidValue {
637            param: param.to_string(),
638            value: value.to_string(),
639            type_name: "f64",
640        });
641    }
642
643    if value < l - EPSILON || value > r + EPSILON {
644        return Err(CorrectnessError::OutOfRange {
645            param: param.to_string(),
646            min: l.to_string(),
647            max: r.to_string(),
648            value: value.to_string(),
649            type_name: "f64",
650        });
651    }
652    Ok(())
653}
654
655/// Checks the `usize` value is in range [`l`, `r`] (inclusive).
656///
657/// # Errors
658///
659/// Returns an error if the validation check fails.
660#[inline(always)]
661pub fn check_in_range_inclusive_usize(value: usize, l: usize, r: usize, param: &str) -> Result<()> {
662    if value < l || value > r {
663        return Err(CorrectnessError::OutOfRange {
664            param: param.to_string(),
665            min: l.to_string(),
666            max: r.to_string(),
667            value: value.to_string(),
668            type_name: "usize",
669        });
670    }
671    Ok(())
672}
673
674/// Checks the slice is empty.
675///
676/// # Errors
677///
678/// Returns an error if the validation check fails.
679#[inline(always)]
680pub fn check_slice_empty<T>(slice: &[T], param: &str) -> Result<()> {
681    if !slice.is_empty() {
682        return Err(CorrectnessError::CollectionNotEmpty {
683            param: param.to_string(),
684            collection_kind: "slice",
685            type_repr: slice_type_repr::<T>(),
686        });
687    }
688    Ok(())
689}
690
691/// Checks the slice is **not** empty.
692///
693/// # Errors
694///
695/// Returns an error if the validation check fails.
696#[inline(always)]
697pub fn check_slice_not_empty<T>(slice: &[T], param: &str) -> Result<()> {
698    if slice.is_empty() {
699        return Err(CorrectnessError::CollectionEmpty {
700            param: param.to_string(),
701            collection_kind: "slice",
702            type_repr: slice_type_repr::<T>(),
703        });
704    }
705    Ok(())
706}
707
708/// Checks the hashmap is empty.
709///
710/// # Errors
711///
712/// Returns an error if the validation check fails.
713#[inline(always)]
714pub fn check_map_empty<M>(map: &M, param: &str) -> Result<()>
715where
716    M: MapLike,
717{
718    if !map.is_empty() {
719        return Err(CorrectnessError::CollectionNotEmpty {
720            param: param.to_string(),
721            collection_kind: "map",
722            type_repr: map_type_repr::<M>(),
723        });
724    }
725    Ok(())
726}
727
728/// Checks the map is **not** empty.
729///
730/// # Errors
731///
732/// Returns an error if the validation check fails.
733#[inline(always)]
734pub fn check_map_not_empty<M>(map: &M, param: &str) -> Result<()>
735where
736    M: MapLike,
737{
738    if map.is_empty() {
739        return Err(CorrectnessError::CollectionEmpty {
740            param: param.to_string(),
741            collection_kind: "map",
742            type_repr: map_type_repr::<M>(),
743        });
744    }
745    Ok(())
746}
747
748/// Checks the `key` is **not** in the `map`.
749///
750/// # Errors
751///
752/// Returns an error if the validation check fails.
753#[inline(always)]
754pub fn check_key_not_in_map<M>(key: &M::Key, map: &M, key_name: &str, map_name: &str) -> Result<()>
755where
756    M: MapLike,
757{
758    if map.contains_key(key) {
759        return Err(CorrectnessError::KeyPresent {
760            key_name: key_name.to_string(),
761            map_name: map_name.to_string(),
762            key: key.to_string(),
763            map_type_repr: map_type_repr::<M>(),
764        });
765    }
766    Ok(())
767}
768
769/// Checks the `key` is in the `map`.
770///
771/// # Errors
772///
773/// Returns an error if the validation check fails.
774#[inline(always)]
775pub fn check_key_in_map<M>(key: &M::Key, map: &M, key_name: &str, map_name: &str) -> Result<()>
776where
777    M: MapLike,
778{
779    if !map.contains_key(key) {
780        return Err(CorrectnessError::KeyMissing {
781            key_name: key_name.to_string(),
782            map_name: map_name.to_string(),
783            key: key.to_string(),
784            map_type_repr: map_type_repr::<M>(),
785        });
786    }
787    Ok(())
788}
789
790/// Checks the `member` is **not** in the `set`.
791///
792/// # Errors
793///
794/// Returns an error if the validation check fails.
795#[inline(always)]
796pub fn check_member_not_in_set<S>(
797    member: &S::Item,
798    set: &S,
799    member_name: &str,
800    set_name: &str,
801) -> Result<()>
802where
803    S: SetLike,
804{
805    if set.contains(member) {
806        return Err(CorrectnessError::MemberPresent {
807            member_name: member_name.to_string(),
808            set_name: set_name.to_string(),
809            set_type_repr: set_type_repr::<S>(),
810        });
811    }
812    Ok(())
813}
814
815/// Checks the `member` is in the `set`.
816///
817/// # Errors
818///
819/// Returns an error if the validation check fails.
820#[inline(always)]
821pub fn check_member_in_set<S>(
822    member: &S::Item,
823    set: &S,
824    member_name: &str,
825    set_name: &str,
826) -> Result<()>
827where
828    S: SetLike,
829{
830    if !set.contains(member) {
831        return Err(CorrectnessError::MemberMissing {
832            member_name: member_name.to_string(),
833            set_name: set_name.to_string(),
834            set_type_repr: set_type_repr::<S>(),
835        });
836    }
837    Ok(())
838}
839
840/// Checks the `Decimal` value is positive (> 0).
841///
842/// # Errors
843///
844/// Returns an error if the validation check fails.
845#[inline(always)]
846pub fn check_positive_decimal(value: Decimal, param: &str) -> Result<()> {
847    if value <= Decimal::ZERO {
848        return Err(CorrectnessError::NotPositive {
849            param: param.to_string(),
850            value: value.to_string(),
851            type_name: "Decimal",
852        });
853    }
854    Ok(())
855}
856
857fn slice_type_repr<T>() -> String {
858    format!("&[{}]", std::any::type_name::<T>())
859}
860
861fn map_type_repr<M>() -> String
862where
863    M: MapLike,
864{
865    format!(
866        "&<{}, {}>",
867        std::any::type_name::<M::Key>(),
868        std::any::type_name::<M::Value>(),
869    )
870}
871
872fn set_type_repr<S>() -> String
873where
874    S: SetLike,
875{
876    format!("&<{}>", std::any::type_name::<S::Item>())
877}
878
879#[cfg(test)]
880mod tests {
881    use std::{
882        collections::{HashMap, HashSet},
883        fmt::Display,
884        str::FromStr,
885    };
886
887    use rstest::rstest;
888    use rust_decimal::Decimal;
889
890    use super::*;
891
892    #[rstest]
893    fn test_check_predicate_true_returns_typed_error_with_stable_display() {
894        let error = check_predicate_true(false, "the predicate was false").unwrap_err();
895
896        assert_eq!(
897            error,
898            CorrectnessError::PredicateViolation {
899                message: "the predicate was false".to_string(),
900            }
901        );
902        assert_eq!(error.to_string(), "the predicate was false");
903    }
904
905    #[rstest]
906    fn test_expect_display_returns_ok_value() {
907        let result: CorrectnessResult<i32> = Ok(42);
908        assert_eq!(result.expect_display(FAILED), 42);
909    }
910
911    #[rstest]
912    #[should_panic(expected = "Condition failed: invalid string for 'value', was empty")]
913    fn test_expect_display_panics_with_display_form_on_err() {
914        let result: CorrectnessResult<()> = Err(CorrectnessError::EmptyString {
915            param: "value".to_string(),
916        });
917        result.expect_display(FAILED);
918    }
919
920    #[rstest]
921    #[should_panic(expected = "custom prefix: the predicate was false")]
922    fn test_expect_display_uses_provided_prefix() {
923        let result: CorrectnessResult<()> = Err(CorrectnessError::PredicateViolation {
924            message: "the predicate was false".to_string(),
925        });
926        result.expect_display("custom prefix");
927    }
928
929    #[rstest]
930    #[case(false, false)]
931    #[case(true, true)]
932    fn test_check_predicate_true(#[case] predicate: bool, #[case] expected: bool) {
933        let result = check_predicate_true(predicate, "the predicate was false").is_ok();
934        assert_eq!(result, expected);
935    }
936
937    #[rstest]
938    #[case(false, true)]
939    #[case(true, false)]
940    fn test_check_predicate_false(#[case] predicate: bool, #[case] expected: bool) {
941        let result = check_predicate_false(predicate, "the predicate was true").is_ok();
942        assert_eq!(result, expected);
943    }
944
945    #[rstest]
946    #[case("a")]
947    #[case(" ")] // <-- whitespace is allowed
948    #[case("  ")] // <-- multiple whitespace is allowed
949    #[case("🦀")] // <-- non-ASCII is allowed
950    #[case(" a")]
951    #[case("a ")]
952    #[case("abc")]
953    fn test_check_nonempty_string_with_valid_values(#[case] s: &str) {
954        assert!(check_nonempty_string(s, "value").is_ok());
955    }
956
957    #[rstest]
958    #[case("")] // empty string
959    fn test_check_nonempty_string_with_invalid_values(#[case] s: &str) {
960        assert!(check_nonempty_string(s, "value").is_err());
961    }
962
963    #[rstest]
964    #[case(" a")]
965    #[case("a ")]
966    #[case("a a")]
967    #[case(" a ")]
968    #[case("abc")]
969    fn test_check_valid_string_ascii_with_valid_value(#[case] s: &str) {
970        assert!(check_valid_string_ascii(s, "value").is_ok());
971    }
972
973    #[rstest]
974    #[case("")] // <-- empty string
975    #[case(" ")] // <-- whitespace-only
976    #[case("  ")] // <-- whitespace-only string
977    #[case("🦀")] // <-- contains non-ASCII char
978    fn test_check_valid_string_ascii_with_invalid_values(#[case] s: &str) {
979        assert!(check_valid_string_ascii(s, "value").is_err());
980    }
981
982    #[rstest]
983    fn test_check_valid_string_ascii_returns_empty_string_error_with_stable_display() {
984        let error = check_valid_string_ascii("", "value").unwrap_err();
985
986        assert_eq!(
987            error,
988            CorrectnessError::EmptyString {
989                param: "value".to_string(),
990            }
991        );
992        assert_eq!(error.to_string(), "invalid string for 'value', was empty");
993    }
994
995    #[rstest]
996    fn test_check_valid_string_ascii_returns_non_ascii_error_with_stable_display() {
997        let error = check_valid_string_ascii("🦀", "value").unwrap_err();
998
999        assert_eq!(
1000            error,
1001            CorrectnessError::NonAsciiString {
1002                param: "value".to_string(),
1003                value: "🦀".to_string(),
1004            }
1005        );
1006        assert_eq!(
1007            error.to_string(),
1008            "invalid string for 'value' contained a non-ASCII char, was '🦀'"
1009        );
1010    }
1011
1012    #[rstest]
1013    fn test_check_valid_string_ascii_returns_whitespace_string_error_with_stable_display() {
1014        let error = check_valid_string_ascii("   ", "value").unwrap_err();
1015
1016        assert_eq!(
1017            error,
1018            CorrectnessError::WhitespaceString {
1019                param: "value".to_string(),
1020            }
1021        );
1022        assert_eq!(
1023            error.to_string(),
1024            "invalid string for 'value', was all whitespace"
1025        );
1026    }
1027
1028    #[rstest]
1029    #[case(" a")]
1030    #[case("a ")]
1031    #[case("abc")]
1032    #[case("ETHUSDT")]
1033    fn test_check_valid_string_utf8_with_valid_values(#[case] s: &str) {
1034        assert!(check_valid_string_utf8(s, "value").is_ok());
1035    }
1036
1037    #[rstest]
1038    #[case("")] // <-- empty string
1039    #[case(" ")] // <-- whitespace-only
1040    #[case("  ")] // <-- whitespace-only string
1041    fn test_check_valid_string_utf8_with_invalid_values(#[case] s: &str) {
1042        assert!(check_valid_string_utf8(s, "value").is_err());
1043    }
1044
1045    #[rstest]
1046    #[case(None)]
1047    #[case(Some(" a"))]
1048    #[case(Some("a "))]
1049    #[case(Some("a a"))]
1050    #[case(Some(" a "))]
1051    #[case(Some("abc"))]
1052    fn test_check_valid_string_ascii_optional_with_valid_value(#[case] s: Option<&str>) {
1053        assert!(check_valid_string_ascii_optional(s, "value").is_ok());
1054    }
1055
1056    #[rstest]
1057    #[case("a", "a")]
1058    fn test_check_string_contains_when_does_contain(#[case] s: &str, #[case] pat: &str) {
1059        assert!(check_string_contains(s, pat, "value").is_ok());
1060    }
1061
1062    #[rstest]
1063    #[case("a", "b")]
1064    fn test_check_string_contains_when_does_not_contain(#[case] s: &str, #[case] pat: &str) {
1065        assert!(check_string_contains(s, pat, "value").is_err());
1066    }
1067
1068    #[rstest]
1069    #[case(0u8, 0u8, "left", "right", true)]
1070    #[case(1u8, 1u8, "left", "right", true)]
1071    #[case(0u8, 1u8, "left", "right", false)]
1072    #[case(1u8, 0u8, "left", "right", false)]
1073    #[case(10i32, 10i32, "left", "right", true)]
1074    #[case(10i32, 20i32, "left", "right", false)]
1075    #[case("hello", "hello", "left", "right", true)]
1076    #[case("hello", "world", "left", "right", false)]
1077    fn test_check_equal<T: PartialEq + Debug + Display>(
1078        #[case] lhs: T,
1079        #[case] rhs: T,
1080        #[case] lhs_param: &str,
1081        #[case] rhs_param: &str,
1082        #[case] expected: bool,
1083    ) {
1084        let result = check_equal(&lhs, &rhs, lhs_param, rhs_param).is_ok();
1085        assert_eq!(result, expected);
1086    }
1087
1088    #[rstest]
1089    #[case(0, 0, "left", "right", true)]
1090    #[case(1, 1, "left", "right", true)]
1091    #[case(0, 1, "left", "right", false)]
1092    #[case(1, 0, "left", "right", false)]
1093    fn test_check_equal_u8_when_equal(
1094        #[case] lhs: u8,
1095        #[case] rhs: u8,
1096        #[case] lhs_param: &str,
1097        #[case] rhs_param: &str,
1098        #[case] expected: bool,
1099    ) {
1100        let result = check_equal_u8(lhs, rhs, lhs_param, rhs_param).is_ok();
1101        assert_eq!(result, expected);
1102    }
1103
1104    #[rstest]
1105    fn test_check_equal_u8_returns_equality_mismatch_with_stable_display() {
1106        let error = check_equal_u8(1, 2, "left", "right").unwrap_err();
1107
1108        assert_eq!(
1109            error,
1110            CorrectnessError::EqualityMismatch {
1111                lhs_param: "left".to_string(),
1112                rhs_param: "right".to_string(),
1113                lhs: "1".to_string(),
1114                rhs: "2".to_string(),
1115                type_name: "u8",
1116            }
1117        );
1118        assert_eq!(
1119            error.to_string(),
1120            "'left' u8 of 1 was not equal to 'right' u8 of 2"
1121        );
1122    }
1123
1124    #[rstest]
1125    #[case(0, 0, "left", "right", true)]
1126    #[case(1, 1, "left", "right", true)]
1127    #[case(0, 1, "left", "right", false)]
1128    #[case(1, 0, "left", "right", false)]
1129    fn test_check_equal_usize_when_equal(
1130        #[case] lhs: usize,
1131        #[case] rhs: usize,
1132        #[case] lhs_param: &str,
1133        #[case] rhs_param: &str,
1134        #[case] expected: bool,
1135    ) {
1136        let result = check_equal_usize(lhs, rhs, lhs_param, rhs_param).is_ok();
1137        assert_eq!(result, expected);
1138    }
1139
1140    #[rstest]
1141    #[case(1, true)]
1142    #[case(usize::MAX, true)]
1143    #[case(0, false)]
1144    fn test_check_positive_usize(#[case] value: usize, #[case] expected: bool) {
1145        assert_eq!(check_positive_usize(value, "value").is_ok(), expected);
1146    }
1147
1148    #[rstest]
1149    fn test_check_positive_usize_returns_not_positive_error_with_stable_display() {
1150        let error = check_positive_usize(0, "param").unwrap_err();
1151
1152        assert_eq!(
1153            error,
1154            CorrectnessError::NotPositive {
1155                param: "param".to_string(),
1156                value: "0".to_string(),
1157                type_name: "usize",
1158            }
1159        );
1160        assert_eq!(
1161            error.to_string(),
1162            "invalid usize for 'param' not positive, was 0"
1163        );
1164    }
1165
1166    #[rstest]
1167    #[case(1, "value")]
1168    fn test_check_positive_u64_when_positive(#[case] value: u64, #[case] param: &str) {
1169        assert!(check_positive_u64(value, param).is_ok());
1170    }
1171
1172    #[rstest]
1173    #[case(0, "value")]
1174    fn test_check_positive_u64_when_not_positive(#[case] value: u64, #[case] param: &str) {
1175        assert!(check_positive_u64(value, param).is_err());
1176    }
1177
1178    #[rstest]
1179    #[case(1, "value")]
1180    fn test_check_positive_i64_when_positive(#[case] value: i64, #[case] param: &str) {
1181        assert!(check_positive_i64(value, param).is_ok());
1182    }
1183
1184    #[rstest]
1185    #[case(0, "value")]
1186    #[case(-1, "value")]
1187    fn test_check_positive_i64_when_not_positive(#[case] value: i64, #[case] param: &str) {
1188        assert!(check_positive_i64(value, param).is_err());
1189    }
1190
1191    #[rstest]
1192    #[case(0.0, "value")]
1193    #[case(1.0, "value")]
1194    fn test_check_non_negative_f64_when_not_negative(#[case] value: f64, #[case] param: &str) {
1195        assert!(check_non_negative_f64(value, param).is_ok());
1196    }
1197
1198    #[rstest]
1199    #[case(f64::NAN, "value")]
1200    #[case(f64::INFINITY, "value")]
1201    #[case(f64::NEG_INFINITY, "value")]
1202    #[case(-0.1, "value")]
1203    fn test_check_non_negative_f64_when_negative(#[case] value: f64, #[case] param: &str) {
1204        assert!(check_non_negative_f64(value, param).is_err());
1205    }
1206
1207    #[rstest]
1208    #[case(0, 0, 0, "value")]
1209    #[case(0, 0, 1, "value")]
1210    #[case(1, 0, 1, "value")]
1211    fn test_check_in_range_inclusive_u8_when_in_range(
1212        #[case] value: u8,
1213        #[case] l: u8,
1214        #[case] r: u8,
1215        #[case] desc: &str,
1216    ) {
1217        assert!(check_in_range_inclusive_u8(value, l, r, desc).is_ok());
1218    }
1219
1220    #[rstest]
1221    #[case(0, 1, 2, "value")]
1222    #[case(3, 1, 2, "value")]
1223    fn test_check_in_range_inclusive_u8_when_out_of_range(
1224        #[case] value: u8,
1225        #[case] l: u8,
1226        #[case] r: u8,
1227        #[case] param: &str,
1228    ) {
1229        assert!(check_in_range_inclusive_u8(value, l, r, param).is_err());
1230    }
1231
1232    #[rstest]
1233    #[case(0, 0, 0, "value")]
1234    #[case(0, 0, 1, "value")]
1235    #[case(1, 0, 1, "value")]
1236    fn test_check_in_range_inclusive_u64_when_in_range(
1237        #[case] value: u64,
1238        #[case] l: u64,
1239        #[case] r: u64,
1240        #[case] param: &str,
1241    ) {
1242        assert!(check_in_range_inclusive_u64(value, l, r, param).is_ok());
1243    }
1244
1245    #[rstest]
1246    #[case(0, 1, 2, "value")]
1247    #[case(3, 1, 2, "value")]
1248    fn test_check_in_range_inclusive_u64_when_out_of_range(
1249        #[case] value: u64,
1250        #[case] l: u64,
1251        #[case] r: u64,
1252        #[case] param: &str,
1253    ) {
1254        assert!(check_in_range_inclusive_u64(value, l, r, param).is_err());
1255    }
1256
1257    #[rstest]
1258    #[case(0, 0, 0, "value")]
1259    #[case(0, 0, 1, "value")]
1260    #[case(1, 0, 1, "value")]
1261    fn test_check_in_range_inclusive_i64_when_in_range(
1262        #[case] value: i64,
1263        #[case] l: i64,
1264        #[case] r: i64,
1265        #[case] param: &str,
1266    ) {
1267        assert!(check_in_range_inclusive_i64(value, l, r, param).is_ok());
1268    }
1269
1270    #[rstest]
1271    #[case(0.0, 0.0, 0.0, "value")]
1272    #[case(0.0, 0.0, 1.0, "value")]
1273    #[case(1.0, 0.0, 1.0, "value")]
1274    fn test_check_in_range_inclusive_f64_when_in_range(
1275        #[case] value: f64,
1276        #[case] l: f64,
1277        #[case] r: f64,
1278        #[case] param: &str,
1279    ) {
1280        assert!(check_in_range_inclusive_f64(value, l, r, param).is_ok());
1281    }
1282
1283    #[rstest]
1284    #[case(-1e16, 0.0, 0.0, "value")]
1285    #[case(1.0 + 1e16, 0.0, 1.0, "value")]
1286    fn test_check_in_range_inclusive_f64_when_out_of_range(
1287        #[case] value: f64,
1288        #[case] l: f64,
1289        #[case] r: f64,
1290        #[case] param: &str,
1291    ) {
1292        assert!(check_in_range_inclusive_f64(value, l, r, param).is_err());
1293    }
1294
1295    #[rstest]
1296    #[case(0, 1, 2, "value")]
1297    #[case(3, 1, 2, "value")]
1298    fn test_check_in_range_inclusive_i64_when_out_of_range(
1299        #[case] value: i64,
1300        #[case] l: i64,
1301        #[case] r: i64,
1302        #[case] param: &str,
1303    ) {
1304        assert!(check_in_range_inclusive_i64(value, l, r, param).is_err());
1305    }
1306
1307    #[rstest]
1308    #[case(0, 0, 0, "value")]
1309    #[case(0, 0, 1, "value")]
1310    #[case(1, 0, 1, "value")]
1311    fn test_check_in_range_inclusive_usize_when_in_range(
1312        #[case] value: usize,
1313        #[case] l: usize,
1314        #[case] r: usize,
1315        #[case] param: &str,
1316    ) {
1317        assert!(check_in_range_inclusive_usize(value, l, r, param).is_ok());
1318    }
1319
1320    #[rstest]
1321    #[case(0, 1, 2, "value")]
1322    #[case(3, 1, 2, "value")]
1323    fn test_check_in_range_inclusive_usize_when_out_of_range(
1324        #[case] value: usize,
1325        #[case] l: usize,
1326        #[case] r: usize,
1327        #[case] param: &str,
1328    ) {
1329        assert!(check_in_range_inclusive_usize(value, l, r, param).is_err());
1330    }
1331
1332    #[rstest]
1333    fn test_check_in_range_inclusive_usize_returns_out_of_range_error_with_stable_display() {
1334        let error = check_in_range_inclusive_usize(3, 1, 2, "value").unwrap_err();
1335
1336        assert_eq!(
1337            error,
1338            CorrectnessError::OutOfRange {
1339                param: "value".to_string(),
1340                min: "1".to_string(),
1341                max: "2".to_string(),
1342                value: "3".to_string(),
1343                type_name: "usize",
1344            }
1345        );
1346        assert_eq!(
1347            error.to_string(),
1348            "invalid usize for 'value' not in range [1, 2], was 3"
1349        );
1350    }
1351
1352    #[rstest]
1353    #[case(vec![], true)]
1354    #[case(vec![1_u8], false)]
1355    fn test_check_slice_empty(#[case] collection: Vec<u8>, #[case] expected: bool) {
1356        let result = check_slice_empty(collection.as_slice(), "param").is_ok();
1357        assert_eq!(result, expected);
1358    }
1359
1360    #[rstest]
1361    #[case(vec![], false)]
1362    #[case(vec![1_u8], true)]
1363    fn test_check_slice_not_empty(#[case] collection: Vec<u8>, #[case] expected: bool) {
1364        let result = check_slice_not_empty(collection.as_slice(), "param").is_ok();
1365        assert_eq!(result, expected);
1366    }
1367
1368    #[rstest]
1369    fn test_check_slice_not_empty_returns_collection_empty_error_with_stable_display() {
1370        let error = check_slice_not_empty::<u8>(&[], "param").unwrap_err();
1371
1372        assert_eq!(
1373            error,
1374            CorrectnessError::CollectionEmpty {
1375                param: "param".to_string(),
1376                collection_kind: "slice",
1377                type_repr: "&[u8]".to_string(),
1378            }
1379        );
1380        assert_eq!(error.to_string(), "the 'param' slice `&[u8]` was empty");
1381    }
1382
1383    #[rstest]
1384    #[case(HashMap::new(), true)]
1385    #[case(HashMap::from([("A".to_string(), 1_u8)]), false)]
1386    fn test_check_map_empty(#[case] map: HashMap<String, u8>, #[case] expected: bool) {
1387        let result = check_map_empty(&map, "param").is_ok();
1388        assert_eq!(result, expected);
1389    }
1390
1391    #[rstest]
1392    #[case(HashMap::new(), false)]
1393    #[case(HashMap::from([("A".to_string(), 1_u8)]), true)]
1394    fn test_check_map_not_empty(#[case] map: HashMap<String, u8>, #[case] expected: bool) {
1395        let result = check_map_not_empty(&map, "param").is_ok();
1396        assert_eq!(result, expected);
1397    }
1398
1399    #[rstest]
1400    #[case(&HashMap::<u32, u32>::new(), 5, "key", "map", true)] // empty map
1401    #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", false)] // key exists
1402    #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", true)] // key doesn't exist
1403    fn test_check_key_not_in_map(
1404        #[case] map: &HashMap<u32, u32>,
1405        #[case] key: u32,
1406        #[case] key_name: &str,
1407        #[case] map_name: &str,
1408        #[case] expected: bool,
1409    ) {
1410        let result = check_key_not_in_map(&key, map, key_name, map_name).is_ok();
1411        assert_eq!(result, expected);
1412    }
1413
1414    #[rstest]
1415    #[case(&HashMap::<u32, u32>::new(), 5, "key", "map", false)] // empty map
1416    #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", true)] // key exists
1417    #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", false)] // key doesn't exist
1418    fn test_check_key_in_map(
1419        #[case] map: &HashMap<u32, u32>,
1420        #[case] key: u32,
1421        #[case] key_name: &str,
1422        #[case] map_name: &str,
1423        #[case] expected: bool,
1424    ) {
1425        let result = check_key_in_map(&key, map, key_name, map_name).is_ok();
1426        assert_eq!(result, expected);
1427    }
1428
1429    #[rstest]
1430    fn test_check_key_in_map_returns_key_missing_error_with_stable_display() {
1431        let map = HashMap::<u32, u32>::new();
1432        let error = check_key_in_map(&5, &map, "key", "map").unwrap_err();
1433
1434        assert_eq!(
1435            error,
1436            CorrectnessError::KeyMissing {
1437                key_name: "key".to_string(),
1438                map_name: "map".to_string(),
1439                key: "5".to_string(),
1440                map_type_repr: "&<u32, u32>".to_string(),
1441            }
1442        );
1443        assert_eq!(
1444            error.to_string(),
1445            "the 'key' key 5 was not in the 'map' map `&<u32, u32>`"
1446        );
1447    }
1448
1449    #[rstest]
1450    #[case(&HashSet::<u32>::new(), 5, "member", "set", true)] // Empty set
1451    #[case(&HashSet::from([1, 2]), 1, "member", "set", false)] // Member exists
1452    #[case(&HashSet::from([1, 2]), 5, "member", "set", true)] // Member doesn't exist
1453    fn test_check_member_not_in_set(
1454        #[case] set: &HashSet<u32>,
1455        #[case] member: u32,
1456        #[case] member_name: &str,
1457        #[case] set_name: &str,
1458        #[case] expected: bool,
1459    ) {
1460        let result = check_member_not_in_set(&member, set, member_name, set_name).is_ok();
1461        assert_eq!(result, expected);
1462    }
1463
1464    #[rstest]
1465    #[case(&HashSet::<u32>::new(), 5, "member", "set", false)] // Empty set
1466    #[case(&HashSet::from([1, 2]), 1, "member", "set", true)] // Member exists
1467    #[case(&HashSet::from([1, 2]), 5, "member", "set", false)] // Member doesn't exist
1468    fn test_check_member_in_set(
1469        #[case] set: &HashSet<u32>,
1470        #[case] member: u32,
1471        #[case] member_name: &str,
1472        #[case] set_name: &str,
1473        #[case] expected: bool,
1474    ) {
1475        let result = check_member_in_set(&member, set, member_name, set_name).is_ok();
1476        assert_eq!(result, expected);
1477    }
1478
1479    #[rstest]
1480    #[case("1", true)] // simple positive integer
1481    #[case("0.0000000000000000000000000001", true)] // smallest positive (1 × 10⁻²⁸)
1482    #[case("79228162514264337593543950335", true)] // very large positive (≈ Decimal::MAX)
1483    #[case("0", false)] // zero should fail
1484    #[case("-0.0000000000000000000000000001", false)] // tiny negative
1485    #[case("-1", false)] // simple negative integer
1486    fn test_check_positive_decimal(#[case] raw: &str, #[case] expected: bool) {
1487        let value = Decimal::from_str(raw).expect("valid decimal literal");
1488        let result = super::check_positive_decimal(value, "param").is_ok();
1489        assert_eq!(result, expected);
1490    }
1491
1492    #[rstest]
1493    #[case(1, true)]
1494    #[case(u128::MAX, true)]
1495    #[case(0, false)]
1496    fn test_check_positive_u128(#[case] value: u128, #[case] expected: bool) {
1497        assert_eq!(check_positive_u128(value, "value").is_ok(), expected);
1498    }
1499
1500    #[rstest]
1501    #[case(1, true)]
1502    #[case(i128::MAX, true)]
1503    #[case(0, false)]
1504    #[case(-1, false)]
1505    #[case(i128::MIN, false)]
1506    fn test_check_positive_i128(#[case] value: i128, #[case] expected: bool) {
1507        assert_eq!(check_positive_i128(value, "value").is_ok(), expected);
1508    }
1509
1510    #[rstest]
1511    fn test_check_positive_decimal_returns_not_positive_error_with_stable_display() {
1512        let error = check_positive_decimal(Decimal::ZERO, "param").unwrap_err();
1513
1514        assert_eq!(
1515            error,
1516            CorrectnessError::NotPositive {
1517                param: "param".to_string(),
1518                value: "0".to_string(),
1519                type_name: "Decimal",
1520            }
1521        );
1522        assert_eq!(
1523            error.to_string(),
1524            "invalid Decimal for 'param' not positive, was 0"
1525        );
1526    }
1527}