nautilus_core/
correctness.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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//! An [`anyhow::Result`] is returned with a descriptive message when the
24//! condition check fails.
25
26use std::fmt::{Debug, Display};
27
28use rust_decimal::Decimal;
29
30use crate::collections::{MapLike, SetLike};
31
32/// A message prefix that can be used with calls to `expect` or other assertion-related functions.
33///
34/// This constant provides a standard message that can be used to indicate a failure condition
35/// when a predicate or condition does not hold true. It is typically used in conjunction with
36/// functions like `expect` to provide a consistent error message.
37pub const FAILED: &str = "Condition failed";
38
39/// Checks the `predicate` is true.
40///
41/// # Errors
42///
43/// Returns an error if the validation check fails.
44#[inline(always)]
45pub fn check_predicate_true(predicate: bool, fail_msg: &str) -> anyhow::Result<()> {
46    if !predicate {
47        anyhow::bail!("{fail_msg}")
48    }
49    Ok(())
50}
51
52/// Checks the `predicate` is false.
53///
54/// # Errors
55///
56/// Returns an error if the validation check fails.
57#[inline(always)]
58pub fn check_predicate_false(predicate: bool, fail_msg: &str) -> anyhow::Result<()> {
59    if predicate {
60        anyhow::bail!("{fail_msg}")
61    }
62    Ok(())
63}
64
65/// Checks if the string `s` is not empty.
66///
67/// This function performs a basic check to ensure the string has at least one character.
68/// Unlike `check_valid_string`, it does not validate ASCII characters or check for whitespace.
69///
70/// # Errors
71///
72/// Returns an error if `s` is empty.
73#[inline(always)]
74pub fn check_nonempty_string<T: AsRef<str>>(s: T, param: &str) -> anyhow::Result<()> {
75    if s.as_ref().is_empty() {
76        anyhow::bail!("invalid string for '{param}', was empty");
77    }
78    Ok(())
79}
80
81/// Checks the string `s` has semantic meaning and contains only ASCII characters.
82///
83/// # Errors
84///
85/// Returns an error if:
86/// - `s` is an empty string.
87/// - `s` consists solely of whitespace characters.
88/// - `s` contains one or more non-ASCII characters.
89#[inline(always)]
90pub fn check_valid_string_ascii<T: AsRef<str>>(s: T, param: &str) -> anyhow::Result<()> {
91    let s = s.as_ref();
92
93    if s.is_empty() {
94        anyhow::bail!("invalid string for '{param}', was empty");
95    }
96
97    // Ensure string is only traversed once
98    let mut has_non_whitespace = false;
99    for c in s.chars() {
100        if !c.is_whitespace() {
101            has_non_whitespace = true;
102        }
103        if !c.is_ascii() {
104            anyhow::bail!("invalid string for '{param}' contained a non-ASCII char, was '{s}'");
105        }
106    }
107
108    if !has_non_whitespace {
109        anyhow::bail!("invalid string for '{param}', was all whitespace");
110    }
111
112    Ok(())
113}
114
115/// Checks the string `s` has semantic meaning and allows UTF-8 characters.
116///
117/// This is a relaxed version of [`check_valid_string_ascii`] that permits non-ASCII UTF-8 characters.
118/// Use this for external identifiers (e.g., exchange symbols) that may contain Unicode characters.
119///
120/// # Errors
121///
122/// Returns an error if:
123/// - `s` is an empty string.
124/// - `s` consists solely of whitespace characters.
125#[inline(always)]
126pub fn check_valid_string_utf8<T: AsRef<str>>(s: T, param: &str) -> anyhow::Result<()> {
127    let s = s.as_ref();
128
129    if s.is_empty() {
130        anyhow::bail!("invalid string for '{param}', was empty");
131    }
132
133    let has_non_whitespace = s.chars().any(|c| !c.is_whitespace());
134
135    if !has_non_whitespace {
136        anyhow::bail!("invalid string for '{param}', was all whitespace");
137    }
138
139    Ok(())
140}
141
142/// Checks the string `s` if Some, contains only ASCII characters and has semantic meaning.
143///
144/// # Errors
145///
146/// Returns an error if:
147/// - `s` is an empty string.
148/// - `s` consists solely of whitespace characters.
149/// - `s` contains one or more non-ASCII characters.
150#[inline(always)]
151pub fn check_valid_string_ascii_optional<T: AsRef<str>>(
152    s: Option<T>,
153    param: &str,
154) -> anyhow::Result<()> {
155    if let Some(s) = s {
156        check_valid_string_ascii(s, param)?;
157    }
158    Ok(())
159}
160
161/// Checks the string `s` contains the pattern `pat`.
162///
163/// # Errors
164///
165/// Returns an error if the validation check fails.
166#[inline(always)]
167pub fn check_string_contains<T: AsRef<str>>(s: T, pat: &str, param: &str) -> anyhow::Result<()> {
168    let s = s.as_ref();
169    if !s.contains(pat) {
170        anyhow::bail!("invalid string for '{param}' did not contain '{pat}', was '{s}'")
171    }
172    Ok(())
173}
174
175/// Checks the values are equal.
176///
177/// # Errors
178///
179/// Returns an error if the validation check fails.
180#[inline(always)]
181pub fn check_equal<T: PartialEq + Debug + Display>(
182    lhs: &T,
183    rhs: &T,
184    lhs_param: &str,
185    rhs_param: &str,
186) -> anyhow::Result<()> {
187    if lhs != rhs {
188        anyhow::bail!("'{lhs_param}' value of {lhs} was not equal to '{rhs_param}' value of {rhs}");
189    }
190    Ok(())
191}
192
193/// Checks the `u8` values are equal.
194///
195/// # Errors
196///
197/// Returns an error if the validation check fails.
198#[inline(always)]
199pub fn check_equal_u8(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> anyhow::Result<()> {
200    if lhs != rhs {
201        anyhow::bail!("'{lhs_param}' u8 of {lhs} was not equal to '{rhs_param}' u8 of {rhs}")
202    }
203    Ok(())
204}
205
206/// Checks the `usize` values are equal.
207///
208/// # Errors
209///
210/// Returns an error if the validation check fails.
211#[inline(always)]
212pub fn check_equal_usize(
213    lhs: usize,
214    rhs: usize,
215    lhs_param: &str,
216    rhs_param: &str,
217) -> anyhow::Result<()> {
218    if lhs != rhs {
219        anyhow::bail!("'{lhs_param}' usize of {lhs} was not equal to '{rhs_param}' usize of {rhs}")
220    }
221    Ok(())
222}
223
224/// Checks the `u64` value is positive (> 0).
225///
226/// # Errors
227///
228/// Returns an error if the validation check fails.
229#[inline(always)]
230pub fn check_positive_u64(value: u64, param: &str) -> anyhow::Result<()> {
231    if value == 0 {
232        anyhow::bail!("invalid u64 for '{param}' not positive, was {value}")
233    }
234    Ok(())
235}
236
237/// Checks the `u128` value is positive (> 0).
238///
239/// # Errors
240///
241/// Returns an error if the validation check fails.
242#[inline(always)]
243pub fn check_positive_u128(value: u128, param: &str) -> anyhow::Result<()> {
244    if value == 0 {
245        anyhow::bail!("invalid u128 for '{param}' not positive, was {value}")
246    }
247    Ok(())
248}
249
250/// Checks the `i64` value is positive (> 0).
251///
252/// # Errors
253///
254/// Returns an error if the validation check fails.
255#[inline(always)]
256pub fn check_positive_i64(value: i64, param: &str) -> anyhow::Result<()> {
257    if value <= 0 {
258        anyhow::bail!("invalid i64 for '{param}' not positive, was {value}")
259    }
260    Ok(())
261}
262
263/// Checks the `i64` value is positive (> 0).
264///
265/// # Errors
266///
267/// Returns an error if the validation check fails.
268#[inline(always)]
269pub fn check_positive_i128(value: i128, param: &str) -> anyhow::Result<()> {
270    if value <= 0 {
271        anyhow::bail!("invalid i128 for '{param}' not positive, was {value}")
272    }
273    Ok(())
274}
275
276/// Checks the `f64` value is non-negative (>= 0).
277///
278/// # Errors
279///
280/// Returns an error if the validation check fails.
281#[inline(always)]
282pub fn check_non_negative_f64(value: f64, param: &str) -> anyhow::Result<()> {
283    if value.is_nan() || value.is_infinite() {
284        anyhow::bail!("invalid f64 for '{param}', was {value}")
285    }
286    if value < 0.0 {
287        anyhow::bail!("invalid f64 for '{param}' negative, was {value}")
288    }
289    Ok(())
290}
291
292/// Checks the `u8` value is in range [`l`, `r`] (inclusive).
293///
294/// # Errors
295///
296/// Returns an error if the validation check fails.
297#[inline(always)]
298pub fn check_in_range_inclusive_u8(value: u8, l: u8, r: u8, param: &str) -> anyhow::Result<()> {
299    if value < l || value > r {
300        anyhow::bail!("invalid u8 for '{param}' not in range [{l}, {r}], was {value}")
301    }
302    Ok(())
303}
304
305/// Checks the `u64` value is range [`l`, `r`] (inclusive).
306///
307/// # Errors
308///
309/// Returns an error if the validation check fails.
310#[inline(always)]
311pub fn check_in_range_inclusive_u64(value: u64, l: u64, r: u64, param: &str) -> anyhow::Result<()> {
312    if value < l || value > r {
313        anyhow::bail!("invalid u64 for '{param}' not in range [{l}, {r}], was {value}")
314    }
315    Ok(())
316}
317
318/// Checks the `i64` value is in range [`l`, `r`] (inclusive).
319///
320/// # Errors
321///
322/// Returns an error if the validation check fails.
323#[inline(always)]
324pub fn check_in_range_inclusive_i64(value: i64, l: i64, r: i64, param: &str) -> anyhow::Result<()> {
325    if value < l || value > r {
326        anyhow::bail!("invalid i64 for '{param}' not in range [{l}, {r}], was {value}")
327    }
328    Ok(())
329}
330
331/// Checks the `f64` value is in range [`l`, `r`] (inclusive).
332///
333/// # Errors
334///
335/// Returns an error if the validation check fails.
336#[inline(always)]
337pub fn check_in_range_inclusive_f64(value: f64, l: f64, r: f64, param: &str) -> anyhow::Result<()> {
338    // SAFETY: Hardcoded epsilon is intentional and appropriate here because:
339    // - 1e-15 is conservative for IEEE 754 double precision (machine epsilon ~2.22e-16)
340    // - This function is used for validation, not high-precision calculations
341    // - The epsilon prevents spurious failures due to floating-point representation
342    // - Making it configurable would complicate the API for minimal benefit
343    const EPSILON: f64 = 1e-15;
344
345    if value.is_nan() || value.is_infinite() {
346        anyhow::bail!("invalid f64 for '{param}', was {value}")
347    }
348    if value < l - EPSILON || value > r + EPSILON {
349        anyhow::bail!("invalid f64 for '{param}' not in range [{l}, {r}], was {value}")
350    }
351    Ok(())
352}
353
354/// Checks the `usize` value is in range [`l`, `r`] (inclusive).
355///
356/// # Errors
357///
358/// Returns an error if the validation check fails.
359#[inline(always)]
360pub fn check_in_range_inclusive_usize(
361    value: usize,
362    l: usize,
363    r: usize,
364    param: &str,
365) -> anyhow::Result<()> {
366    if value < l || value > r {
367        anyhow::bail!("invalid usize for '{param}' not in range [{l}, {r}], was {value}")
368    }
369    Ok(())
370}
371
372/// Checks the slice is empty.
373///
374/// # Errors
375///
376/// Returns an error if the validation check fails.
377#[inline(always)]
378pub fn check_slice_empty<T>(slice: &[T], param: &str) -> anyhow::Result<()> {
379    if !slice.is_empty() {
380        anyhow::bail!(
381            "the '{param}' slice `&[{}]` was not empty",
382            std::any::type_name::<T>()
383        )
384    }
385    Ok(())
386}
387
388/// Checks the slice is **not** empty.
389///
390/// # Errors
391///
392/// Returns an error if the validation check fails.
393#[inline(always)]
394pub fn check_slice_not_empty<T>(slice: &[T], param: &str) -> anyhow::Result<()> {
395    if slice.is_empty() {
396        anyhow::bail!(
397            "the '{param}' slice `&[{}]` was empty",
398            std::any::type_name::<T>()
399        )
400    }
401    Ok(())
402}
403
404/// Checks the hashmap is empty.
405///
406/// # Errors
407///
408/// Returns an error if the validation check fails.
409#[inline(always)]
410pub fn check_map_empty<M>(map: &M, param: &str) -> anyhow::Result<()>
411where
412    M: MapLike,
413{
414    if !map.is_empty() {
415        anyhow::bail!(
416            "the '{param}' map `&<{}, {}>` was not empty",
417            std::any::type_name::<M::Key>(),
418            std::any::type_name::<M::Value>(),
419        );
420    }
421    Ok(())
422}
423
424/// Checks the map is **not** empty.
425///
426/// # Errors
427///
428/// Returns an error if the validation check fails.
429#[inline(always)]
430pub fn check_map_not_empty<M>(map: &M, param: &str) -> anyhow::Result<()>
431where
432    M: MapLike,
433{
434    if map.is_empty() {
435        anyhow::bail!(
436            "the '{param}' map `&<{}, {}>` was empty",
437            std::any::type_name::<M::Key>(),
438            std::any::type_name::<M::Value>(),
439        );
440    }
441    Ok(())
442}
443
444/// Checks the `key` is **not** in the `map`.
445///
446/// # Errors
447///
448/// Returns an error if the validation check fails.
449#[inline(always)]
450pub fn check_key_not_in_map<M>(
451    key: &M::Key,
452    map: &M,
453    key_name: &str,
454    map_name: &str,
455) -> anyhow::Result<()>
456where
457    M: MapLike,
458{
459    if map.contains_key(key) {
460        anyhow::bail!(
461            "the '{key_name}' key {key} was already in the '{map_name}' map `&<{}, {}>`",
462            std::any::type_name::<M::Key>(),
463            std::any::type_name::<M::Value>(),
464        );
465    }
466    Ok(())
467}
468
469/// Checks the `key` is in the `map`.
470///
471/// # Errors
472///
473/// Returns an error if the validation check fails.
474#[inline(always)]
475pub fn check_key_in_map<M>(
476    key: &M::Key,
477    map: &M,
478    key_name: &str,
479    map_name: &str,
480) -> anyhow::Result<()>
481where
482    M: MapLike,
483{
484    if !map.contains_key(key) {
485        anyhow::bail!(
486            "the '{key_name}' key {key} was not in the '{map_name}' map `&<{}, {}>`",
487            std::any::type_name::<M::Key>(),
488            std::any::type_name::<M::Value>(),
489        );
490    }
491    Ok(())
492}
493
494/// Checks the `member` is **not** in the `set`.
495///
496/// # Errors
497///
498/// Returns an error if the validation check fails.
499#[inline(always)]
500pub fn check_member_not_in_set<S>(
501    member: &S::Item,
502    set: &S,
503    member_name: &str,
504    set_name: &str,
505) -> anyhow::Result<()>
506where
507    S: SetLike,
508{
509    if set.contains(member) {
510        anyhow::bail!(
511            "the '{member_name}' member was already in the '{set_name}' set `&<{}>`",
512            std::any::type_name::<S::Item>(),
513        );
514    }
515    Ok(())
516}
517
518/// Checks the `member` is in the `set`.
519///
520/// # Errors
521///
522/// Returns an error if the validation check fails.
523#[inline(always)]
524pub fn check_member_in_set<S>(
525    member: &S::Item,
526    set: &S,
527    member_name: &str,
528    set_name: &str,
529) -> anyhow::Result<()>
530where
531    S: SetLike,
532{
533    if !set.contains(member) {
534        anyhow::bail!(
535            "the '{member_name}' member was not in the '{set_name}' set `&<{}>`",
536            std::any::type_name::<S::Item>(),
537        );
538    }
539    Ok(())
540}
541
542/// Checks the `Decimal` value is positive (> 0).
543///
544/// # Errors
545///
546/// Returns an error if the validation check fails.
547#[inline(always)]
548pub fn check_positive_decimal(value: Decimal, param: &str) -> anyhow::Result<()> {
549    if value <= Decimal::ZERO {
550        anyhow::bail!("invalid Decimal for '{param}' not positive, was {value}")
551    }
552    Ok(())
553}
554
555#[cfg(test)]
556mod tests {
557    use std::{
558        collections::{HashMap, HashSet},
559        fmt::Display,
560        str::FromStr,
561    };
562
563    use rstest::rstest;
564    use rust_decimal::Decimal;
565
566    use super::*;
567
568    #[rstest]
569    #[case(false, false)]
570    #[case(true, true)]
571    fn test_check_predicate_true(#[case] predicate: bool, #[case] expected: bool) {
572        let result = check_predicate_true(predicate, "the predicate was false").is_ok();
573        assert_eq!(result, expected);
574    }
575
576    #[rstest]
577    #[case(false, true)]
578    #[case(true, false)]
579    fn test_check_predicate_false(#[case] predicate: bool, #[case] expected: bool) {
580        let result = check_predicate_false(predicate, "the predicate was true").is_ok();
581        assert_eq!(result, expected);
582    }
583
584    #[rstest]
585    #[case("a")]
586    #[case(" ")] // <-- whitespace is allowed
587    #[case("  ")] // <-- multiple whitespace is allowed
588    #[case("🦀")] // <-- non-ASCII is allowed
589    #[case(" a")]
590    #[case("a ")]
591    #[case("abc")]
592    fn test_check_nonempty_string_with_valid_values(#[case] s: &str) {
593        assert!(check_nonempty_string(s, "value").is_ok());
594    }
595
596    #[rstest]
597    #[case("")] // empty string
598    fn test_check_nonempty_string_with_invalid_values(#[case] s: &str) {
599        assert!(check_nonempty_string(s, "value").is_err());
600    }
601
602    #[rstest]
603    #[case(" a")]
604    #[case("a ")]
605    #[case("a a")]
606    #[case(" a ")]
607    #[case("abc")]
608    fn test_check_valid_string_ascii_with_valid_value(#[case] s: &str) {
609        assert!(check_valid_string_ascii(s, "value").is_ok());
610    }
611
612    #[rstest]
613    #[case("")] // <-- empty string
614    #[case(" ")] // <-- whitespace-only
615    #[case("  ")] // <-- whitespace-only string
616    #[case("🦀")] // <-- contains non-ASCII char
617    fn test_check_valid_string_ascii_with_invalid_values(#[case] s: &str) {
618        assert!(check_valid_string_ascii(s, "value").is_err());
619    }
620
621    #[rstest]
622    #[case(" a")]
623    #[case("a ")]
624    #[case("abc")]
625    #[case("ETHUSDT")]
626    fn test_check_valid_string_utf8_with_valid_values(#[case] s: &str) {
627        assert!(check_valid_string_utf8(s, "value").is_ok());
628    }
629
630    #[rstest]
631    #[case("")] // <-- empty string
632    #[case(" ")] // <-- whitespace-only
633    #[case("  ")] // <-- whitespace-only string
634    fn test_check_valid_string_utf8_with_invalid_values(#[case] s: &str) {
635        assert!(check_valid_string_utf8(s, "value").is_err());
636    }
637
638    #[rstest]
639    #[case(None)]
640    #[case(Some(" a"))]
641    #[case(Some("a "))]
642    #[case(Some("a a"))]
643    #[case(Some(" a "))]
644    #[case(Some("abc"))]
645    fn test_check_valid_string_ascii_optional_with_valid_value(#[case] s: Option<&str>) {
646        assert!(check_valid_string_ascii_optional(s, "value").is_ok());
647    }
648
649    #[rstest]
650    #[case("a", "a")]
651    fn test_check_string_contains_when_does_contain(#[case] s: &str, #[case] pat: &str) {
652        assert!(check_string_contains(s, pat, "value").is_ok());
653    }
654
655    #[rstest]
656    #[case("a", "b")]
657    fn test_check_string_contains_when_does_not_contain(#[case] s: &str, #[case] pat: &str) {
658        assert!(check_string_contains(s, pat, "value").is_err());
659    }
660
661    #[rstest]
662    #[case(0u8, 0u8, "left", "right", true)]
663    #[case(1u8, 1u8, "left", "right", true)]
664    #[case(0u8, 1u8, "left", "right", false)]
665    #[case(1u8, 0u8, "left", "right", false)]
666    #[case(10i32, 10i32, "left", "right", true)]
667    #[case(10i32, 20i32, "left", "right", false)]
668    #[case("hello", "hello", "left", "right", true)]
669    #[case("hello", "world", "left", "right", false)]
670    fn test_check_equal<T: PartialEq + Debug + Display>(
671        #[case] lhs: T,
672        #[case] rhs: T,
673        #[case] lhs_param: &str,
674        #[case] rhs_param: &str,
675        #[case] expected: bool,
676    ) {
677        let result = check_equal(&lhs, &rhs, lhs_param, rhs_param).is_ok();
678        assert_eq!(result, expected);
679    }
680
681    #[rstest]
682    #[case(0, 0, "left", "right", true)]
683    #[case(1, 1, "left", "right", true)]
684    #[case(0, 1, "left", "right", false)]
685    #[case(1, 0, "left", "right", false)]
686    fn test_check_equal_u8_when_equal(
687        #[case] lhs: u8,
688        #[case] rhs: u8,
689        #[case] lhs_param: &str,
690        #[case] rhs_param: &str,
691        #[case] expected: bool,
692    ) {
693        let result = check_equal_u8(lhs, rhs, lhs_param, rhs_param).is_ok();
694        assert_eq!(result, expected);
695    }
696
697    #[rstest]
698    #[case(0, 0, "left", "right", true)]
699    #[case(1, 1, "left", "right", true)]
700    #[case(0, 1, "left", "right", false)]
701    #[case(1, 0, "left", "right", false)]
702    fn test_check_equal_usize_when_equal(
703        #[case] lhs: usize,
704        #[case] rhs: usize,
705        #[case] lhs_param: &str,
706        #[case] rhs_param: &str,
707        #[case] expected: bool,
708    ) {
709        let result = check_equal_usize(lhs, rhs, lhs_param, rhs_param).is_ok();
710        assert_eq!(result, expected);
711    }
712
713    #[rstest]
714    #[case(1, "value")]
715    fn test_check_positive_u64_when_positive(#[case] value: u64, #[case] param: &str) {
716        assert!(check_positive_u64(value, param).is_ok());
717    }
718
719    #[rstest]
720    #[case(0, "value")]
721    fn test_check_positive_u64_when_not_positive(#[case] value: u64, #[case] param: &str) {
722        assert!(check_positive_u64(value, param).is_err());
723    }
724
725    #[rstest]
726    #[case(1, "value")]
727    fn test_check_positive_i64_when_positive(#[case] value: i64, #[case] param: &str) {
728        assert!(check_positive_i64(value, param).is_ok());
729    }
730
731    #[rstest]
732    #[case(0, "value")]
733    #[case(-1, "value")]
734    fn test_check_positive_i64_when_not_positive(#[case] value: i64, #[case] param: &str) {
735        assert!(check_positive_i64(value, param).is_err());
736    }
737
738    #[rstest]
739    #[case(0.0, "value")]
740    #[case(1.0, "value")]
741    fn test_check_non_negative_f64_when_not_negative(#[case] value: f64, #[case] param: &str) {
742        assert!(check_non_negative_f64(value, param).is_ok());
743    }
744
745    #[rstest]
746    #[case(f64::NAN, "value")]
747    #[case(f64::INFINITY, "value")]
748    #[case(f64::NEG_INFINITY, "value")]
749    #[case(-0.1, "value")]
750    fn test_check_non_negative_f64_when_negative(#[case] value: f64, #[case] param: &str) {
751        assert!(check_non_negative_f64(value, param).is_err());
752    }
753
754    #[rstest]
755    #[case(0, 0, 0, "value")]
756    #[case(0, 0, 1, "value")]
757    #[case(1, 0, 1, "value")]
758    fn test_check_in_range_inclusive_u8_when_in_range(
759        #[case] value: u8,
760        #[case] l: u8,
761        #[case] r: u8,
762        #[case] desc: &str,
763    ) {
764        assert!(check_in_range_inclusive_u8(value, l, r, desc).is_ok());
765    }
766
767    #[rstest]
768    #[case(0, 1, 2, "value")]
769    #[case(3, 1, 2, "value")]
770    fn test_check_in_range_inclusive_u8_when_out_of_range(
771        #[case] value: u8,
772        #[case] l: u8,
773        #[case] r: u8,
774        #[case] param: &str,
775    ) {
776        assert!(check_in_range_inclusive_u8(value, l, r, param).is_err());
777    }
778
779    #[rstest]
780    #[case(0, 0, 0, "value")]
781    #[case(0, 0, 1, "value")]
782    #[case(1, 0, 1, "value")]
783    fn test_check_in_range_inclusive_u64_when_in_range(
784        #[case] value: u64,
785        #[case] l: u64,
786        #[case] r: u64,
787        #[case] param: &str,
788    ) {
789        assert!(check_in_range_inclusive_u64(value, l, r, param).is_ok());
790    }
791
792    #[rstest]
793    #[case(0, 1, 2, "value")]
794    #[case(3, 1, 2, "value")]
795    fn test_check_in_range_inclusive_u64_when_out_of_range(
796        #[case] value: u64,
797        #[case] l: u64,
798        #[case] r: u64,
799        #[case] param: &str,
800    ) {
801        assert!(check_in_range_inclusive_u64(value, l, r, param).is_err());
802    }
803
804    #[rstest]
805    #[case(0, 0, 0, "value")]
806    #[case(0, 0, 1, "value")]
807    #[case(1, 0, 1, "value")]
808    fn test_check_in_range_inclusive_i64_when_in_range(
809        #[case] value: i64,
810        #[case] l: i64,
811        #[case] r: i64,
812        #[case] param: &str,
813    ) {
814        assert!(check_in_range_inclusive_i64(value, l, r, param).is_ok());
815    }
816
817    #[rstest]
818    #[case(0.0, 0.0, 0.0, "value")]
819    #[case(0.0, 0.0, 1.0, "value")]
820    #[case(1.0, 0.0, 1.0, "value")]
821    fn test_check_in_range_inclusive_f64_when_in_range(
822        #[case] value: f64,
823        #[case] l: f64,
824        #[case] r: f64,
825        #[case] param: &str,
826    ) {
827        assert!(check_in_range_inclusive_f64(value, l, r, param).is_ok());
828    }
829
830    #[rstest]
831    #[case(-1e16, 0.0, 0.0, "value")]
832    #[case(1.0 + 1e16, 0.0, 1.0, "value")]
833    fn test_check_in_range_inclusive_f64_when_out_of_range(
834        #[case] value: f64,
835        #[case] l: f64,
836        #[case] r: f64,
837        #[case] param: &str,
838    ) {
839        assert!(check_in_range_inclusive_f64(value, l, r, param).is_err());
840    }
841
842    #[rstest]
843    #[case(0, 1, 2, "value")]
844    #[case(3, 1, 2, "value")]
845    fn test_check_in_range_inclusive_i64_when_out_of_range(
846        #[case] value: i64,
847        #[case] l: i64,
848        #[case] r: i64,
849        #[case] param: &str,
850    ) {
851        assert!(check_in_range_inclusive_i64(value, l, r, param).is_err());
852    }
853
854    #[rstest]
855    #[case(0, 0, 0, "value")]
856    #[case(0, 0, 1, "value")]
857    #[case(1, 0, 1, "value")]
858    fn test_check_in_range_inclusive_usize_when_in_range(
859        #[case] value: usize,
860        #[case] l: usize,
861        #[case] r: usize,
862        #[case] param: &str,
863    ) {
864        assert!(check_in_range_inclusive_usize(value, l, r, param).is_ok());
865    }
866
867    #[rstest]
868    #[case(0, 1, 2, "value")]
869    #[case(3, 1, 2, "value")]
870    fn test_check_in_range_inclusive_usize_when_out_of_range(
871        #[case] value: usize,
872        #[case] l: usize,
873        #[case] r: usize,
874        #[case] param: &str,
875    ) {
876        assert!(check_in_range_inclusive_usize(value, l, r, param).is_err());
877    }
878
879    #[rstest]
880    #[case(vec![], true)]
881    #[case(vec![1_u8], false)]
882    fn test_check_slice_empty(#[case] collection: Vec<u8>, #[case] expected: bool) {
883        let result = check_slice_empty(collection.as_slice(), "param").is_ok();
884        assert_eq!(result, expected);
885    }
886
887    #[rstest]
888    #[case(vec![], false)]
889    #[case(vec![1_u8], true)]
890    fn test_check_slice_not_empty(#[case] collection: Vec<u8>, #[case] expected: bool) {
891        let result = check_slice_not_empty(collection.as_slice(), "param").is_ok();
892        assert_eq!(result, expected);
893    }
894
895    #[rstest]
896    #[case(HashMap::new(), true)]
897    #[case(HashMap::from([("A".to_string(), 1_u8)]), false)]
898    fn test_check_map_empty(#[case] map: HashMap<String, u8>, #[case] expected: bool) {
899        let result = check_map_empty(&map, "param").is_ok();
900        assert_eq!(result, expected);
901    }
902
903    #[rstest]
904    #[case(HashMap::new(), false)]
905    #[case(HashMap::from([("A".to_string(), 1_u8)]), true)]
906    fn test_check_map_not_empty(#[case] map: HashMap<String, u8>, #[case] expected: bool) {
907        let result = check_map_not_empty(&map, "param").is_ok();
908        assert_eq!(result, expected);
909    }
910
911    #[rstest]
912    #[case(&HashMap::<u32, u32>::new(), 5, "key", "map", true)] // empty map
913    #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", false)] // key exists
914    #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", true)] // key doesn't exist
915    fn test_check_key_not_in_map(
916        #[case] map: &HashMap<u32, u32>,
917        #[case] key: u32,
918        #[case] key_name: &str,
919        #[case] map_name: &str,
920        #[case] expected: bool,
921    ) {
922        let result = check_key_not_in_map(&key, map, key_name, map_name).is_ok();
923        assert_eq!(result, expected);
924    }
925
926    #[rstest]
927    #[case(&HashMap::<u32, u32>::new(), 5, "key", "map", false)] // empty map
928    #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", true)] // key exists
929    #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", false)] // key doesn't exist
930    fn test_check_key_in_map(
931        #[case] map: &HashMap<u32, u32>,
932        #[case] key: u32,
933        #[case] key_name: &str,
934        #[case] map_name: &str,
935        #[case] expected: bool,
936    ) {
937        let result = check_key_in_map(&key, map, key_name, map_name).is_ok();
938        assert_eq!(result, expected);
939    }
940
941    #[rstest]
942    #[case(&HashSet::<u32>::new(), 5, "member", "set", true)] // Empty set
943    #[case(&HashSet::from([1, 2]), 1, "member", "set", false)] // Member exists
944    #[case(&HashSet::from([1, 2]), 5, "member", "set", true)] // Member doesn't exist
945    fn test_check_member_not_in_set(
946        #[case] set: &HashSet<u32>,
947        #[case] member: u32,
948        #[case] member_name: &str,
949        #[case] set_name: &str,
950        #[case] expected: bool,
951    ) {
952        let result = check_member_not_in_set(&member, set, member_name, set_name).is_ok();
953        assert_eq!(result, expected);
954    }
955
956    #[rstest]
957    #[case(&HashSet::<u32>::new(), 5, "member", "set", false)] // Empty set
958    #[case(&HashSet::from([1, 2]), 1, "member", "set", true)] // Member exists
959    #[case(&HashSet::from([1, 2]), 5, "member", "set", false)] // Member doesn't exist
960    fn test_check_member_in_set(
961        #[case] set: &HashSet<u32>,
962        #[case] member: u32,
963        #[case] member_name: &str,
964        #[case] set_name: &str,
965        #[case] expected: bool,
966    ) {
967        let result = check_member_in_set(&member, set, member_name, set_name).is_ok();
968        assert_eq!(result, expected);
969    }
970
971    #[rstest]
972    #[case("1", true)] // simple positive integer
973    #[case("0.0000000000000000000000000001", true)] // smallest positive (1 × 10⁻²⁸)
974    #[case("79228162514264337593543950335", true)] // very large positive (≈ Decimal::MAX)
975    #[case("0", false)] // zero should fail
976    #[case("-0.0000000000000000000000000001", false)] // tiny negative
977    #[case("-1", false)] // simple negative integer
978    fn test_check_positive_decimal(#[case] raw: &str, #[case] expected: bool) {
979        let value = Decimal::from_str(raw).expect("valid decimal literal");
980        let result = super::check_positive_decimal(value, "param").is_ok();
981        assert_eq!(result, expected);
982    }
983}