Skip to main content

aviso_validators/
enum_handler.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//! Enumeration validation and canonicalization handler
10//!
11//! Validates that field values match one of a predefined set of allowed values.
12//! Performs case-insensitive matching and canonicalizes to lowercase for
13//! consistent topic generation and storage.
14
15use anyhow::{Result, bail};
16
17pub struct EnumHandler;
18
19impl EnumHandler {
20    /// Validate and canonicalize an enumeration value
21    ///
22    /// This method performs case-insensitive validation against the allowed
23    /// values list and canonicalizes the result to lowercase for consistency.
24    ///
25    /// # Validation Process
26    /// - Convert input to lowercase for comparison
27    /// - Check if lowercase value exists in allowed values (case-insensitive)
28    /// - Return the lowercase canonical form if valid
29    /// - Provide detailed error with all allowed values if invalid
30    ///
31    /// # Arguments
32    /// * `value` - The input value to validate
33    /// * `allowed_values` - List of allowed values (case-insensitive matching)
34    /// * `field_name` - Name of the field being validated (for error messages)
35    ///
36    /// # Returns
37    /// * `Ok(String)` - The value in canonical lowercase form
38    /// * `Err(anyhow::Error)` - Value not in allowed list with helpful error
39    pub fn validate_and_canonicalize(
40        value: &str,
41        allowed_values: &[String],
42        field_name: &str,
43    ) -> Result<String> {
44        // Convert input to lowercase for case-insensitive comparison
45        let lowercase_value = value.to_lowercase();
46
47        // Check if the lowercase value matches any allowed value (case-insensitive)
48        let is_valid = allowed_values
49            .iter()
50            .any(|allowed| allowed.to_lowercase() == lowercase_value);
51
52        if is_valid {
53            tracing::debug!(
54                field_name = field_name,
55                input_value = value,
56                canonical_value = %lowercase_value,
57                allowed_count = allowed_values.len(),
58                "Enum value successfully validated and canonicalized"
59            );
60
61            Ok(lowercase_value)
62        } else {
63            bail!(
64                "Field '{}' has invalid value '{}'. Allowed: [{}]",
65                field_name,
66                value,
67                allowed_values.join(", ")
68            );
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_valid_enum_value_exact_case() {
79        let allowed = vec![
80            "active".to_string(),
81            "inactive".to_string(),
82            "pending".to_string(),
83        ];
84        let result = EnumHandler::validate_and_canonicalize("active", &allowed, "status");
85        assert!(result.is_ok());
86        assert_eq!(result.unwrap(), "active");
87    }
88
89    #[test]
90    fn test_valid_enum_value_different_case() {
91        let allowed = vec!["active".to_string(), "inactive".to_string()];
92        let result = EnumHandler::validate_and_canonicalize("ACTIVE", &allowed, "status");
93        assert!(result.is_ok());
94        assert_eq!(result.unwrap(), "active");
95    }
96
97    #[test]
98    fn test_valid_enum_value_mixed_case() {
99        let allowed = vec!["Active".to_string(), "InActive".to_string()];
100        let result = EnumHandler::validate_and_canonicalize("active", &allowed, "status");
101        assert!(result.is_ok());
102        assert_eq!(result.unwrap(), "active");
103    }
104
105    #[test]
106    fn test_invalid_enum_value() {
107        let allowed = vec!["active".to_string(), "inactive".to_string()];
108        let result = EnumHandler::validate_and_canonicalize("unknown", &allowed, "status");
109        assert!(result.is_err());
110    }
111
112    #[test]
113    fn test_empty_allowed_values() {
114        let allowed = vec![];
115        let result = EnumHandler::validate_and_canonicalize("any", &allowed, "field");
116        assert!(result.is_err());
117    }
118
119    #[test]
120    fn test_large_allowed_values_list() {
121        let allowed: Vec<String> = (0..20).map(|i| format!("value{}", i)).collect();
122        let result = EnumHandler::validate_and_canonicalize("unknown", &allowed, "field");
123        assert!(result.is_err());
124    }
125}