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////////////////////////////////////////////////////////////////////////////////
556// Tests
557////////////////////////////////////////////////////////////////////////////////
558#[cfg(test)]
559mod tests {
560    use std::{
561        collections::{HashMap, HashSet},
562        fmt::Display,
563        str::FromStr,
564    };
565
566    use rstest::rstest;
567    use rust_decimal::Decimal;
568
569    use super::*;
570
571    #[rstest]
572    #[case(false, false)]
573    #[case(true, true)]
574    fn test_check_predicate_true(#[case] predicate: bool, #[case] expected: bool) {
575        let result = check_predicate_true(predicate, "the predicate was false").is_ok();
576        assert_eq!(result, expected);
577    }
578
579    #[rstest]
580    #[case(false, true)]
581    #[case(true, false)]
582    fn test_check_predicate_false(#[case] predicate: bool, #[case] expected: bool) {
583        let result = check_predicate_false(predicate, "the predicate was true").is_ok();
584        assert_eq!(result, expected);
585    }
586
587    #[rstest]
588    #[case("a")]
589    #[case(" ")] // <-- whitespace is allowed
590    #[case("  ")] // <-- multiple whitespace is allowed
591    #[case("🦀")] // <-- non-ASCII is allowed
592    #[case(" a")]
593    #[case("a ")]
594    #[case("abc")]
595    fn test_check_nonempty_string_with_valid_values(#[case] s: &str) {
596        assert!(check_nonempty_string(s, "value").is_ok());
597    }
598
599    #[rstest]
600    #[case("")] // empty string
601    fn test_check_nonempty_string_with_invalid_values(#[case] s: &str) {
602        assert!(check_nonempty_string(s, "value").is_err());
603    }
604
605    #[rstest]
606    #[case(" a")]
607    #[case("a ")]
608    #[case("a a")]
609    #[case(" a ")]
610    #[case("abc")]
611    fn test_check_valid_string_ascii_with_valid_value(#[case] s: &str) {
612        assert!(check_valid_string_ascii(s, "value").is_ok());
613    }
614
615    #[rstest]
616    #[case("")] // <-- empty string
617    #[case(" ")] // <-- whitespace-only
618    #[case("  ")] // <-- whitespace-only string
619    #[case("🦀")] // <-- contains non-ASCII char
620    fn test_check_valid_string_ascii_with_invalid_values(#[case] s: &str) {
621        assert!(check_valid_string_ascii(s, "value").is_err());
622    }
623
624    #[rstest]
625    #[case(" a")]
626    #[case("a ")]
627    #[case("abc")]
628    #[case("ETHUSDT")]
629    fn test_check_valid_string_utf8_with_valid_values(#[case] s: &str) {
630        assert!(check_valid_string_utf8(s, "value").is_ok());
631    }
632
633    #[rstest]
634    #[case("")] // <-- empty string
635    #[case(" ")] // <-- whitespace-only
636    #[case("  ")] // <-- whitespace-only string
637    fn test_check_valid_string_utf8_with_invalid_values(#[case] s: &str) {
638        assert!(check_valid_string_utf8(s, "value").is_err());
639    }
640
641    #[rstest]
642    #[case(None)]
643    #[case(Some(" a"))]
644    #[case(Some("a "))]
645    #[case(Some("a a"))]
646    #[case(Some(" a "))]
647    #[case(Some("abc"))]
648    fn test_check_valid_string_ascii_optional_with_valid_value(#[case] s: Option<&str>) {
649        assert!(check_valid_string_ascii_optional(s, "value").is_ok());
650    }
651
652    #[rstest]
653    #[case("a", "a")]
654    fn test_check_string_contains_when_does_contain(#[case] s: &str, #[case] pat: &str) {
655        assert!(check_string_contains(s, pat, "value").is_ok());
656    }
657
658    #[rstest]
659    #[case("a", "b")]
660    fn test_check_string_contains_when_does_not_contain(#[case] s: &str, #[case] pat: &str) {
661        assert!(check_string_contains(s, pat, "value").is_err());
662    }
663
664    #[rstest]
665    #[case(0u8, 0u8, "left", "right", true)]
666    #[case(1u8, 1u8, "left", "right", true)]
667    #[case(0u8, 1u8, "left", "right", false)]
668    #[case(1u8, 0u8, "left", "right", false)]
669    #[case(10i32, 10i32, "left", "right", true)]
670    #[case(10i32, 20i32, "left", "right", false)]
671    #[case("hello", "hello", "left", "right", true)]
672    #[case("hello", "world", "left", "right", false)]
673    fn test_check_equal<T: PartialEq + Debug + Display>(
674        #[case] lhs: T,
675        #[case] rhs: T,
676        #[case] lhs_param: &str,
677        #[case] rhs_param: &str,
678        #[case] expected: bool,
679    ) {
680        let result = check_equal(&lhs, &rhs, lhs_param, rhs_param).is_ok();
681        assert_eq!(result, expected);
682    }
683
684    #[rstest]
685    #[case(0, 0, "left", "right", true)]
686    #[case(1, 1, "left", "right", true)]
687    #[case(0, 1, "left", "right", false)]
688    #[case(1, 0, "left", "right", false)]
689    fn test_check_equal_u8_when_equal(
690        #[case] lhs: u8,
691        #[case] rhs: u8,
692        #[case] lhs_param: &str,
693        #[case] rhs_param: &str,
694        #[case] expected: bool,
695    ) {
696        let result = check_equal_u8(lhs, rhs, lhs_param, rhs_param).is_ok();
697        assert_eq!(result, expected);
698    }
699
700    #[rstest]
701    #[case(0, 0, "left", "right", true)]
702    #[case(1, 1, "left", "right", true)]
703    #[case(0, 1, "left", "right", false)]
704    #[case(1, 0, "left", "right", false)]
705    fn test_check_equal_usize_when_equal(
706        #[case] lhs: usize,
707        #[case] rhs: usize,
708        #[case] lhs_param: &str,
709        #[case] rhs_param: &str,
710        #[case] expected: bool,
711    ) {
712        let result = check_equal_usize(lhs, rhs, lhs_param, rhs_param).is_ok();
713        assert_eq!(result, expected);
714    }
715
716    #[rstest]
717    #[case(1, "value")]
718    fn test_check_positive_u64_when_positive(#[case] value: u64, #[case] param: &str) {
719        assert!(check_positive_u64(value, param).is_ok());
720    }
721
722    #[rstest]
723    #[case(0, "value")]
724    fn test_check_positive_u64_when_not_positive(#[case] value: u64, #[case] param: &str) {
725        assert!(check_positive_u64(value, param).is_err());
726    }
727
728    #[rstest]
729    #[case(1, "value")]
730    fn test_check_positive_i64_when_positive(#[case] value: i64, #[case] param: &str) {
731        assert!(check_positive_i64(value, param).is_ok());
732    }
733
734    #[rstest]
735    #[case(0, "value")]
736    #[case(-1, "value")]
737    fn test_check_positive_i64_when_not_positive(#[case] value: i64, #[case] param: &str) {
738        assert!(check_positive_i64(value, param).is_err());
739    }
740
741    #[rstest]
742    #[case(0.0, "value")]
743    #[case(1.0, "value")]
744    fn test_check_non_negative_f64_when_not_negative(#[case] value: f64, #[case] param: &str) {
745        assert!(check_non_negative_f64(value, param).is_ok());
746    }
747
748    #[rstest]
749    #[case(f64::NAN, "value")]
750    #[case(f64::INFINITY, "value")]
751    #[case(f64::NEG_INFINITY, "value")]
752    #[case(-0.1, "value")]
753    fn test_check_non_negative_f64_when_negative(#[case] value: f64, #[case] param: &str) {
754        assert!(check_non_negative_f64(value, param).is_err());
755    }
756
757    #[rstest]
758    #[case(0, 0, 0, "value")]
759    #[case(0, 0, 1, "value")]
760    #[case(1, 0, 1, "value")]
761    fn test_check_in_range_inclusive_u8_when_in_range(
762        #[case] value: u8,
763        #[case] l: u8,
764        #[case] r: u8,
765        #[case] desc: &str,
766    ) {
767        assert!(check_in_range_inclusive_u8(value, l, r, desc).is_ok());
768    }
769
770    #[rstest]
771    #[case(0, 1, 2, "value")]
772    #[case(3, 1, 2, "value")]
773    fn test_check_in_range_inclusive_u8_when_out_of_range(
774        #[case] value: u8,
775        #[case] l: u8,
776        #[case] r: u8,
777        #[case] param: &str,
778    ) {
779        assert!(check_in_range_inclusive_u8(value, l, r, param).is_err());
780    }
781
782    #[rstest]
783    #[case(0, 0, 0, "value")]
784    #[case(0, 0, 1, "value")]
785    #[case(1, 0, 1, "value")]
786    fn test_check_in_range_inclusive_u64_when_in_range(
787        #[case] value: u64,
788        #[case] l: u64,
789        #[case] r: u64,
790        #[case] param: &str,
791    ) {
792        assert!(check_in_range_inclusive_u64(value, l, r, param).is_ok());
793    }
794
795    #[rstest]
796    #[case(0, 1, 2, "value")]
797    #[case(3, 1, 2, "value")]
798    fn test_check_in_range_inclusive_u64_when_out_of_range(
799        #[case] value: u64,
800        #[case] l: u64,
801        #[case] r: u64,
802        #[case] param: &str,
803    ) {
804        assert!(check_in_range_inclusive_u64(value, l, r, param).is_err());
805    }
806
807    #[rstest]
808    #[case(0, 0, 0, "value")]
809    #[case(0, 0, 1, "value")]
810    #[case(1, 0, 1, "value")]
811    fn test_check_in_range_inclusive_i64_when_in_range(
812        #[case] value: i64,
813        #[case] l: i64,
814        #[case] r: i64,
815        #[case] param: &str,
816    ) {
817        assert!(check_in_range_inclusive_i64(value, l, r, param).is_ok());
818    }
819
820    #[rstest]
821    #[case(0.0, 0.0, 0.0, "value")]
822    #[case(0.0, 0.0, 1.0, "value")]
823    #[case(1.0, 0.0, 1.0, "value")]
824    fn test_check_in_range_inclusive_f64_when_in_range(
825        #[case] value: f64,
826        #[case] l: f64,
827        #[case] r: f64,
828        #[case] param: &str,
829    ) {
830        assert!(check_in_range_inclusive_f64(value, l, r, param).is_ok());
831    }
832
833    #[rstest]
834    #[case(-1e16, 0.0, 0.0, "value")]
835    #[case(1.0 + 1e16, 0.0, 1.0, "value")]
836    fn test_check_in_range_inclusive_f64_when_out_of_range(
837        #[case] value: f64,
838        #[case] l: f64,
839        #[case] r: f64,
840        #[case] param: &str,
841    ) {
842        assert!(check_in_range_inclusive_f64(value, l, r, param).is_err());
843    }
844
845    #[rstest]
846    #[case(0, 1, 2, "value")]
847    #[case(3, 1, 2, "value")]
848    fn test_check_in_range_inclusive_i64_when_out_of_range(
849        #[case] value: i64,
850        #[case] l: i64,
851        #[case] r: i64,
852        #[case] param: &str,
853    ) {
854        assert!(check_in_range_inclusive_i64(value, l, r, param).is_err());
855    }
856
857    #[rstest]
858    #[case(0, 0, 0, "value")]
859    #[case(0, 0, 1, "value")]
860    #[case(1, 0, 1, "value")]
861    fn test_check_in_range_inclusive_usize_when_in_range(
862        #[case] value: usize,
863        #[case] l: usize,
864        #[case] r: usize,
865        #[case] param: &str,
866    ) {
867        assert!(check_in_range_inclusive_usize(value, l, r, param).is_ok());
868    }
869
870    #[rstest]
871    #[case(0, 1, 2, "value")]
872    #[case(3, 1, 2, "value")]
873    fn test_check_in_range_inclusive_usize_when_out_of_range(
874        #[case] value: usize,
875        #[case] l: usize,
876        #[case] r: usize,
877        #[case] param: &str,
878    ) {
879        assert!(check_in_range_inclusive_usize(value, l, r, param).is_err());
880    }
881
882    #[rstest]
883    #[case(vec![], true)]
884    #[case(vec![1_u8], false)]
885    fn test_check_slice_empty(#[case] collection: Vec<u8>, #[case] expected: bool) {
886        let result = check_slice_empty(collection.as_slice(), "param").is_ok();
887        assert_eq!(result, expected);
888    }
889
890    #[rstest]
891    #[case(vec![], false)]
892    #[case(vec![1_u8], true)]
893    fn test_check_slice_not_empty(#[case] collection: Vec<u8>, #[case] expected: bool) {
894        let result = check_slice_not_empty(collection.as_slice(), "param").is_ok();
895        assert_eq!(result, expected);
896    }
897
898    #[rstest]
899    #[case(HashMap::new(), true)]
900    #[case(HashMap::from([("A".to_string(), 1_u8)]), false)]
901    fn test_check_map_empty(#[case] map: HashMap<String, u8>, #[case] expected: bool) {
902        let result = check_map_empty(&map, "param").is_ok();
903        assert_eq!(result, expected);
904    }
905
906    #[rstest]
907    #[case(HashMap::new(), false)]
908    #[case(HashMap::from([("A".to_string(), 1_u8)]), true)]
909    fn test_check_map_not_empty(#[case] map: HashMap<String, u8>, #[case] expected: bool) {
910        let result = check_map_not_empty(&map, "param").is_ok();
911        assert_eq!(result, expected);
912    }
913
914    #[rstest]
915    #[case(&HashMap::<u32, u32>::new(), 5, "key", "map", true)] // empty map
916    #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", false)] // key exists
917    #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", true)] // key doesn't exist
918    fn test_check_key_not_in_map(
919        #[case] map: &HashMap<u32, u32>,
920        #[case] key: u32,
921        #[case] key_name: &str,
922        #[case] map_name: &str,
923        #[case] expected: bool,
924    ) {
925        let result = check_key_not_in_map(&key, map, key_name, map_name).is_ok();
926        assert_eq!(result, expected);
927    }
928
929    #[rstest]
930    #[case(&HashMap::<u32, u32>::new(), 5, "key", "map", false)] // empty map
931    #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", true)] // key exists
932    #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", false)] // key doesn't exist
933    fn test_check_key_in_map(
934        #[case] map: &HashMap<u32, u32>,
935        #[case] key: u32,
936        #[case] key_name: &str,
937        #[case] map_name: &str,
938        #[case] expected: bool,
939    ) {
940        let result = check_key_in_map(&key, map, key_name, map_name).is_ok();
941        assert_eq!(result, expected);
942    }
943
944    #[rstest]
945    #[case(&HashSet::<u32>::new(), 5, "member", "set", true)] // Empty set
946    #[case(&HashSet::from([1, 2]), 1, "member", "set", false)] // Member exists
947    #[case(&HashSet::from([1, 2]), 5, "member", "set", true)] // Member doesn't exist
948    fn test_check_member_not_in_set(
949        #[case] set: &HashSet<u32>,
950        #[case] member: u32,
951        #[case] member_name: &str,
952        #[case] set_name: &str,
953        #[case] expected: bool,
954    ) {
955        let result = check_member_not_in_set(&member, set, member_name, set_name).is_ok();
956        assert_eq!(result, expected);
957    }
958
959    #[rstest]
960    #[case(&HashSet::<u32>::new(), 5, "member", "set", false)] // Empty set
961    #[case(&HashSet::from([1, 2]), 1, "member", "set", true)] // Member exists
962    #[case(&HashSet::from([1, 2]), 5, "member", "set", false)] // Member doesn't exist
963    fn test_check_member_in_set(
964        #[case] set: &HashSet<u32>,
965        #[case] member: u32,
966        #[case] member_name: &str,
967        #[case] set_name: &str,
968        #[case] expected: bool,
969    ) {
970        let result = check_member_in_set(&member, set, member_name, set_name).is_ok();
971        assert_eq!(result, expected);
972    }
973
974    #[rstest]
975    #[case("1", true)] // simple positive integer
976    #[case("0.0000000000000000000000000001", true)] // smallest positive (1 × 10⁻²⁸)
977    #[case("79228162514264337593543950335", true)] // very large positive (≈ Decimal::MAX)
978    #[case("0", false)] // zero should fail
979    #[case("-0.0000000000000000000000000001", false)] // tiny negative
980    #[case("-1", false)] // simple negative integer
981    fn test_check_positive_decimal(#[case] raw: &str, #[case] expected: bool) {
982        let value = Decimal::from_str(raw).expect("valid decimal literal");
983        let result = super::check_positive_decimal(value, "param").is_ok();
984        assert_eq!(result, expected);
985    }
986}