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