Skip to main content

aviso_validators/
expver.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
9use anyhow::{Result, bail};
10
11/// Experiment version validation handler
12///
13/// Handles experiment version identifiers for different runs and model versions
14///
15/// - **Numeric versions**: Integers that are zero-padded to 4 digits (e.g., 1 → "0001")
16/// - **String versions**: Alphanumeric identifiers converted to lowercase (e.g., "PROD" → "prod")
17///
18/// The canonicalization ensures consistent representation for topic generation
19/// and database storage while supporting the flexibility needed by different
20/// operational workflows.
21pub struct ExpverHandler;
22
23impl ExpverHandler {
24    /// Validate and canonicalize an experiment version value
25    ///
26    /// This method handles both numeric and string experiment versions:
27    /// - Numeric values are zero-padded to 4 digits for consistency
28    /// - String values are converted to lowercase for standardization
29    /// - Empty values use the configured default if available
30    ///
31    /// # Arguments
32    /// * `value` - The experiment version to validate (can be empty if default provided)
33    /// * `default` - Optional default value to use when input is empty
34    /// * `field_name` - Name of the field being validated (for error messages)
35    ///
36    /// # Returns
37    /// * `Ok(String)` - The canonicalized experiment version
38    /// * `Err(anyhow::Error)` - Empty value with no default provided
39    pub fn validate_and_canonicalize(
40        value: &str,
41        default: Option<&str>,
42        field_name: &str,
43    ) -> Result<String> {
44        // Handle empty values by using default if available
45        if value.is_empty() {
46            if let Some(default_val) = default {
47                let canonicalized_default = Self::canonicalize_expver(default_val);
48                tracing::debug!(
49                    field_name = field_name,
50                    default_value = default_val,
51                    canonical_value = %canonicalized_default,
52                    "Using default experiment version"
53                );
54                return Ok(canonicalized_default);
55            } else {
56                bail!("Field '{}' cannot be empty", field_name);
57            }
58        }
59
60        // Canonicalize the provided value
61        let canonicalized = Self::canonicalize_expver(value);
62
63        tracing::debug!(
64            field_name = field_name,
65            input_value = value,
66            canonical_value = %canonicalized,
67            value_type = if value.parse::<u32>().is_ok() { "numeric" } else { "string" },
68            "Experiment version successfully validated and canonicalized"
69        );
70
71        Ok(canonicalized)
72    }
73
74    /// Canonicalize an experiment version to standard format
75    ///
76    /// This method determines whether the input is numeric or string and
77    /// applies the appropriate canonicalization rules:
78    ///
79    /// - **Numeric**: Zero-pad to 4 digits (supports up to 9999)
80    /// - **String**: Convert to lowercase for consistency
81    ///
82    /// # Arguments
83    /// * `value` - The experiment version value to canonicalize
84    ///
85    /// # Returns
86    /// * `String` - The canonicalized experiment version
87    ///
88    /// # Numeric Canonicalization
89    /// Numeric experiment versions are zero-padded to exactly 4 digits:
90    /// - 1 → "0001"
91    /// - 42 → "0042"
92    /// - 123 → "0123"
93    /// - 9999 → "9999"
94    ///
95    /// # String Canonicalization
96    /// String experiment versions are converted to lowercase:
97    /// - "PROD" → "prod"
98    /// - "Test" → "test"
99    /// - "dev-branch" → "dev-branch"
100    fn canonicalize_expver(value: &str) -> String {
101        // Try to parse as integer first for numeric canonicalization
102        if let Ok(num) = value.parse::<u32>() {
103            // Zero-pad numeric values to 4 digits
104            format!("{:04}", num)
105        } else {
106            // Convert string values to lowercase for consistency
107            value.to_lowercase()
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_numeric_expver_single_digit() {
118        let result = ExpverHandler::validate_and_canonicalize("1", None, "expver");
119        assert!(result.is_ok());
120        assert_eq!(result.unwrap(), "0001");
121    }
122
123    #[test]
124    fn test_numeric_expver_multiple_digits() {
125        let result = ExpverHandler::validate_and_canonicalize("42", None, "expver");
126        assert!(result.is_ok());
127        assert_eq!(result.unwrap(), "0042");
128    }
129
130    #[test]
131    fn test_numeric_expver_four_digits() {
132        let result = ExpverHandler::validate_and_canonicalize("9999", None, "expver");
133        assert!(result.is_ok());
134        assert_eq!(result.unwrap(), "9999");
135    }
136
137    #[test]
138    fn test_string_expver_uppercase() {
139        let result = ExpverHandler::validate_and_canonicalize("PROD", None, "expver");
140        assert!(result.is_ok());
141        assert_eq!(result.unwrap(), "prod");
142    }
143
144    #[test]
145    fn test_string_expver_mixed_case() {
146        let result = ExpverHandler::validate_and_canonicalize("Test", None, "expver");
147        assert!(result.is_ok());
148        assert_eq!(result.unwrap(), "test");
149    }
150
151    #[test]
152    fn test_empty_with_numeric_default() {
153        let result = ExpverHandler::validate_and_canonicalize("", Some("42"), "expver");
154        assert!(result.is_ok());
155        assert_eq!(result.unwrap(), "0042");
156    }
157
158    #[test]
159    fn test_empty_with_string_default() {
160        let result = ExpverHandler::validate_and_canonicalize("", Some("PROD"), "expver");
161        assert!(result.is_ok());
162        assert_eq!(result.unwrap(), "prod");
163    }
164
165    #[test]
166    fn test_empty_without_default() {
167        let result = ExpverHandler::validate_and_canonicalize("", None, "expver");
168        assert!(result.is_err());
169        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
170    }
171
172    #[test]
173    fn test_alphanumeric_string() {
174        let result = ExpverHandler::validate_and_canonicalize("dev-v2.1", None, "expver");
175        assert!(result.is_ok());
176        assert_eq!(result.unwrap(), "dev-v2.1");
177    }
178}