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    check_predicate_true(!predicate, fail_msg)
259}
260
261/// Checks if the string `s` is not empty.
262///
263/// This function performs a basic check to ensure the string has at least one character.
264/// Unlike `check_valid_string`, it does not validate ASCII characters or check for whitespace.
265///
266/// # Errors
267///
268/// Returns an error if `s` is empty.
269#[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/// Checks the string `s` has semantic meaning and contains only ASCII characters.
280///
281/// # Errors
282///
283/// Returns an error if:
284/// - `s` is an empty string.
285/// - `s` consists solely of whitespace characters.
286/// - `s` contains one or more non-ASCII characters.
287#[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    // Ensure string is only traversed once
298    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/// Checks the string `s` has semantic meaning and allows UTF-8 characters.
323///
324/// This is a relaxed version of [`check_valid_string_ascii`] that permits non-ASCII UTF-8 characters.
325/// Use this for external identifiers (e.g., exchange symbols) that may contain Unicode characters.
326///
327/// # Errors
328///
329/// Returns an error if:
330/// - `s` is an empty string.
331/// - `s` consists solely of whitespace characters.
332#[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/// Checks the string `s` if Some, contains only ASCII characters and has semantic meaning.
354///
355/// # Errors
356///
357/// Returns an error if:
358/// - `s` is an empty string.
359/// - `s` consists solely of whitespace characters.
360/// - `s` contains one or more non-ASCII characters.
361#[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/// Checks the string `s` contains the pattern `pat`.
370///
371/// # Errors
372///
373/// Returns an error if the validation check fails.
374#[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/// Checks the values are equal.
388///
389/// # Errors
390///
391/// Returns an error if the validation check fails.
392#[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/// Checks the `u8` values are equal.
412///
413/// # Errors
414///
415/// Returns an error if the validation check fails.
416#[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/// Checks the `usize` values are equal.
431///
432/// # Errors
433///
434/// Returns an error if the validation check fails.
435#[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/// Checks the `usize` value is positive (> 0).
450///
451/// # Errors
452///
453/// Returns an error if the validation check fails.
454#[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/// Checks the `u64` value is positive (> 0).
467///
468/// # Errors
469///
470/// Returns an error if the validation check fails.
471#[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/// Checks the `u128` value is positive (> 0).
484///
485/// # Errors
486///
487/// Returns an error if the validation check fails.
488#[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/// Checks the `i64` value is positive (> 0).
501///
502/// # Errors
503///
504/// Returns an error if the validation check fails.
505#[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/// Checks the `i128` value is positive (> 0).
518///
519/// # Errors
520///
521/// Returns an error if the validation check fails.
522#[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/// Checks the `f64` value is non-negative (>= 0).
535///
536/// # Errors
537///
538/// Returns an error if the validation check fails.
539#[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/// Checks the `u8` value is in range [`l`, `r`] (inclusive).
560///
561/// # Errors
562///
563/// Returns an error if the validation check fails.
564#[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/// Checks the `u64` value is range [`l`, `r`] (inclusive).
579///
580/// # Errors
581///
582/// Returns an error if the validation check fails.
583#[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/// Checks the `i64` value is in range [`l`, `r`] (inclusive).
598///
599/// # Errors
600///
601/// Returns an error if the validation check fails.
602#[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/// Checks the `f64` value is in range [`l`, `r`] (inclusive).
617///
618/// # Errors
619///
620/// Returns an error if the validation check fails.
621#[inline(always)]
622pub fn check_in_range_inclusive_f64(value: f64, l: f64, r: f64, param: &str) -> Result<()> {
623    // Hardcoded epsilon is intentional and appropriate here because:
624    // - 1e-15 is conservative for IEEE 754 double precision (machine epsilon ~2.22e-16)
625    // - This function is used for validation, not high-precision calculations
626    // - The epsilon prevents spurious failures due to floating-point representation
627    // - Making it configurable would complicate the API for minimal benefit
628    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/// Checks the `usize` value is in range [`l`, `r`] (inclusive).
651///
652/// # Errors
653///
654/// Returns an error if the validation check fails.
655#[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/// Checks the slice is empty.
670///
671/// # Errors
672///
673/// Returns an error if the validation check fails.
674#[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/// Checks the slice is **not** empty.
687///
688/// # Errors
689///
690/// Returns an error if the validation check fails.
691#[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/// Checks the hashmap is empty.
704///
705/// # Errors
706///
707/// Returns an error if the validation check fails.
708#[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/// Checks the map is **not** empty.
724///
725/// # Errors
726///
727/// Returns an error if the validation check fails.
728#[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/// Checks the `key` is **not** in the `map`.
744///
745/// # Errors
746///
747/// Returns an error if the validation check fails.
748#[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/// Checks the `key` is in the `map`.
765///
766/// # Errors
767///
768/// Returns an error if the validation check fails.
769#[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/// Checks the `member` is **not** in the `set`.
786///
787/// # Errors
788///
789/// Returns an error if the validation check fails.
790#[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/// Checks the `member` is in the `set`.
811///
812/// # Errors
813///
814/// Returns an error if the validation check fails.
815#[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/// Checks the `Decimal` value is positive (> 0).
836///
837/// # Errors
838///
839/// Returns an error if the validation check fails.
840#[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(" ")] // <-- whitespace is allowed
943    #[case("  ")] // <-- multiple whitespace is allowed
944    #[case("🦀")] // <-- non-ASCII is allowed
945    #[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("")] // empty string
954    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("")] // <-- empty string
970    #[case(" ")] // <-- whitespace-only
971    #[case("  ")] // <-- whitespace-only string
972    #[case("🦀")] // <-- contains non-ASCII char
973    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("")] // <-- empty string
1034    #[case(" ")] // <-- whitespace-only
1035    #[case("  ")] // <-- whitespace-only string
1036    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)] // empty map
1396    #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", false)] // key exists
1397    #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", true)] // key doesn't exist
1398    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)] // empty map
1411    #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", true)] // key exists
1412    #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", false)] // key doesn't exist
1413    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)] // Empty set
1446    #[case(&HashSet::from([1, 2]), 1, "member", "set", false)] // Member exists
1447    #[case(&HashSet::from([1, 2]), 5, "member", "set", true)] // Member doesn't exist
1448    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)] // Empty set
1461    #[case(&HashSet::from([1, 2]), 1, "member", "set", true)] // Member exists
1462    #[case(&HashSet::from([1, 2]), 5, "member", "set", false)] // Member doesn't exist
1463    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)] // simple positive integer
1476    #[case("0.0000000000000000000000000001", true)] // smallest positive (1 × 10⁻²⁸)
1477    #[case("79228162514264337593543950335", true)] // very large positive (≈ Decimal::MAX)
1478    #[case("0", false)] // zero should fail
1479    #[case("-0.0000000000000000000000000001", false)] // tiny negative
1480    #[case("-1", false)] // simple negative integer
1481    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}