Skip to main content

aviso_validators/
int.rs

1// (C) Copyright 2024- ECMWF and individual contributors.
2//
3// This software is licensed under the terms of the Apache Licence Version 2.0
4// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
5// In applying this licence, ECMWF does not waive the privileges and immunities
6// granted to it by virtue of its status as an intergovernmental organisation nor
7// does it submit to any jurisdiction.
8
9//! Integer validation and canonicalization handler
10//!
11//! Validates that field values are valid integers and optionally within
12//! specified numeric ranges. Used for step values, counts, indices,
13//! and other numeric parameters in operational systems.
14
15use anyhow::{Context, Result, bail};
16
17/// Integer validation handler
18///
19/// Validates and canonicalizes integer values with optional range constraints.
20pub struct IntHandler;
21
22impl IntHandler {
23    /// Validate and canonicalize an integer value with optional range checking
24    ///
25    /// This method performs comprehensive integer validation:
26    /// 1. Parses the input string as a signed 64-bit integer
27    /// 2. Validates the value is within the specified range (if configured)
28    /// 3. Returns the canonical string representation of the integer
29    ///
30    /// # Arguments
31    /// * `value` - The string value to validate as an integer
32    /// * `range` - Optional [min, max] range constraint (inclusive bounds)
33    /// * `field_name` - Name of the field being validated (for error messages)
34    ///
35    /// # Returns
36    /// * `Ok(String)` - The canonical string representation of the valid integer
37    /// * `Err(anyhow::Error)` - Invalid integer format or out of range
38    ///
39    /// # Range Validation
40    /// When a range is specified as `[min, max]`, both bounds are inclusive:
41    /// - `[0, 100]` allows values from 0 to 100 (including 0 and 100)
42    /// - `[1, 10]` allows values from 1 to 10 (including 1 and 10)
43    /// - No range means any valid integer is accepted
44    pub fn validate_and_canonicalize(
45        value: &str,
46        range: Option<&[i64; 2]>,
47        field_name: &str,
48    ) -> Result<String> {
49        // Parse the input string as a signed 64-bit integer
50        let parsed_value: i64 = value.parse().context(format!(
51            "Field '{}' must be a valid integer, got: '{}'",
52            field_name, value
53        ))?;
54
55        // Validate range constraints if specified
56        if let Some([min, max]) = range {
57            if parsed_value < *min || parsed_value > *max {
58                bail!(
59                    "Field '{}' value {} is outside allowed range [{}, {}]",
60                    field_name,
61                    parsed_value,
62                    min,
63                    max
64                );
65            }
66
67            tracing::debug!(
68                field_name = field_name,
69                input_value = value,
70                parsed_value = parsed_value,
71                min_allowed = min,
72                max_allowed = max,
73                "Integer successfully validated within range"
74            );
75        } else {
76            tracing::debug!(
77                field_name = field_name,
78                input_value = value,
79                parsed_value = parsed_value,
80                "Integer successfully validated (no range constraint)"
81            );
82        }
83
84        // Return the canonical string representation
85        // This ensures consistent formatting (removes leading zeros, etc.)
86        Ok(parsed_value.to_string())
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_valid_positive_integer() {
96        let result = IntHandler::validate_and_canonicalize("42", None, "count");
97        assert!(result.is_ok());
98        assert_eq!(result.unwrap(), "42");
99    }
100
101    #[test]
102    fn test_valid_negative_integer() {
103        let result = IntHandler::validate_and_canonicalize("-42", None, "offset");
104        assert!(result.is_ok());
105        assert_eq!(result.unwrap(), "-42");
106    }
107
108    #[test]
109    fn test_valid_zero() {
110        let result = IntHandler::validate_and_canonicalize("0", Some(&[-10, 10]), "value");
111        assert!(result.is_ok());
112        assert_eq!(result.unwrap(), "0");
113    }
114
115    #[test]
116    fn test_valid_integer_within_range() {
117        let result = IntHandler::validate_and_canonicalize("50", Some(&[0, 100]), "step");
118        assert!(result.is_ok());
119        assert_eq!(result.unwrap(), "50");
120    }
121
122    #[test]
123    fn test_valid_integer_at_range_boundaries() {
124        // Test minimum boundary
125        let result = IntHandler::validate_and_canonicalize("0", Some(&[0, 100]), "step");
126        assert!(result.is_ok());
127        assert_eq!(result.unwrap(), "0");
128
129        // Test maximum boundary
130        let result = IntHandler::validate_and_canonicalize("100", Some(&[0, 100]), "step");
131        assert!(result.is_ok());
132        assert_eq!(result.unwrap(), "100");
133    }
134
135    #[test]
136    fn test_integer_below_minimum() {
137        let result = IntHandler::validate_and_canonicalize("-5", Some(&[0, 100]), "step");
138        assert!(result.is_err());
139    }
140
141    #[test]
142    fn test_integer_above_maximum() {
143        let result = IntHandler::validate_and_canonicalize("150", Some(&[0, 100]), "step");
144        assert!(result.is_err());
145    }
146
147    #[test]
148    fn test_invalid_integer_format() {
149        let result = IntHandler::validate_and_canonicalize("abc", None, "count");
150        assert!(result.is_err());
151    }
152
153    #[test]
154    fn test_decimal_number_rejected() {
155        let result = IntHandler::validate_and_canonicalize("42.5", None, "count");
156        assert!(result.is_err());
157    }
158
159    #[test]
160    fn test_leading_zeros_canonicalized() {
161        let result = IntHandler::validate_and_canonicalize("0042", None, "count");
162        assert!(result.is_ok());
163        assert_eq!(result.unwrap(), "42"); // Leading zeros removed
164    }
165
166    #[test]
167    fn test_large_integer() {
168        let result = IntHandler::validate_and_canonicalize("9223372036854775807", None, "big");
169        assert!(result.is_ok());
170        assert_eq!(result.unwrap(), "9223372036854775807");
171    }
172}