Skip to main content

xet_runtime/utils/
config_enum.rs

1use std::fmt;
2use std::ops::Deref;
3use std::str::FromStr;
4
5use tracing::{event, info, warn};
6
7use super::configuration_utils::{INFORMATION_LOG_LEVEL, ParsableConfigValue};
8
9/// A config value restricted to a fixed set of valid lowercase string options.
10///
11/// Stores the normalized (lowercased) value and validates against `valid_values`
12/// at parse time. If the user provides an invalid value via environment variable,
13/// a warning is logged and the default is used instead.
14///
15/// # Usage in `config_group!`
16///
17/// ```rust,ignore
18/// use crate::utils::ConfigEnum;
19///
20/// crate::config_group!({
21///     ref compression_policy: ConfigEnum = ConfigEnum::new("auto", &["", "auto", "none", "lz4"]);
22/// });
23/// ```
24#[derive(Clone)]
25pub struct ConfigEnum {
26    value: String,
27    valid_values: &'static [&'static str],
28}
29
30impl ConfigEnum {
31    pub fn new(default: &str, valid_values: &'static [&'static str]) -> Self {
32        let lower = default.to_lowercase();
33        debug_assert!(
34            valid_values.iter().any(|v| v.to_lowercase() == lower),
35            "Default value \"{default}\" is not in the valid values list: {valid_values:?}"
36        );
37        ConfigEnum {
38            value: lower,
39            valid_values,
40        }
41    }
42
43    /// Creates a ConfigEnum with a string value and an empty `valid_values` list.
44    /// Because `valid_values` is empty, `try_set` will reject every value on the
45    /// resulting instance.  This is intended only for deserialization paths
46    /// (e.g. Python's `from_python`) where the caller validates through the
47    /// *existing* field value's `try_set` rather than through this instance.
48    pub fn new_unchecked(value: impl Into<String>) -> Self {
49        ConfigEnum {
50            value: value.into().to_lowercase(),
51            valid_values: &[],
52        }
53    }
54
55    pub fn as_str(&self) -> &str {
56        &self.value
57    }
58
59    pub fn valid_values(&self) -> &'static [&'static str] {
60        self.valid_values
61    }
62
63    /// Set the value if it is valid (case-insensitive), otherwise return an error.
64    pub fn try_set(&mut self, value: &str) -> Result<(), String> {
65        let lower = value.to_lowercase();
66        if self.valid_values.iter().any(|v| v.to_lowercase() == lower) {
67            self.value = lower;
68            Ok(())
69        } else {
70            Err(format!("\"{value}\" is not a valid value. Valid values are: {:?}", self.valid_values))
71        }
72    }
73
74    /// Parse the stored value into a target type via `FromStr`, matching the
75    /// familiar `str::parse::<T>()` signature.
76    ///
77    /// In debug builds, asserts that *every* entry in `valid_values` can be
78    /// successfully parsed into `T`, catching mismatches between the config's
79    /// allowed strings and the target type's parser at development time.
80    pub fn parse<T>(&self) -> Result<T, T::Err>
81    where
82        T: FromStr,
83        T::Err: fmt::Debug + fmt::Display,
84    {
85        #[cfg(debug_assertions)]
86        for v in self.valid_values {
87            if let Err(e) = v.parse::<T>() {
88                panic!("ConfigEnum valid value \"{v}\" cannot be parsed into {}: {e}", std::any::type_name::<T>());
89            }
90        }
91        self.value.parse::<T>()
92    }
93}
94
95impl fmt::Debug for ConfigEnum {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(f, "{:?}", self.value)
98    }
99}
100
101impl fmt::Display for ConfigEnum {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(f, "{}", self.value)
104    }
105}
106
107impl Deref for ConfigEnum {
108    type Target = str;
109    fn deref(&self) -> &Self::Target {
110        &self.value
111    }
112}
113
114impl AsRef<str> for ConfigEnum {
115    fn as_ref(&self) -> &str {
116        &self.value
117    }
118}
119
120impl PartialEq<str> for ConfigEnum {
121    fn eq(&self, other: &str) -> bool {
122        self.value == other.to_lowercase()
123    }
124}
125
126impl PartialEq<&str> for ConfigEnum {
127    fn eq(&self, other: &&str) -> bool {
128        self.value == other.to_lowercase()
129    }
130}
131
132impl PartialEq for ConfigEnum {
133    fn eq(&self, other: &Self) -> bool {
134        self.value == other.value
135    }
136}
137
138impl Eq for ConfigEnum {}
139
140impl ParsableConfigValue for ConfigEnum {
141    fn parse_user_value(_value: &str) -> Option<Self> {
142        None
143    }
144
145    fn to_config_string(&self) -> String {
146        self.value.clone()
147    }
148
149    fn try_update_in_place(&mut self, value: &str) -> bool {
150        self.try_set(value).is_ok()
151    }
152
153    fn parse_config_value(variable_name: &str, value: Option<String>, default: Self) -> Self {
154        match value {
155            Some(v) => {
156                let lower = v.to_lowercase();
157                if default.valid_values.iter().any(|valid| valid.to_lowercase() == lower) {
158                    info!("Config: {variable_name} = {lower:?} (user set)");
159                    ConfigEnum {
160                        value: lower,
161                        valid_values: default.valid_values,
162                    }
163                } else {
164                    warn!(
165                        "Configuration value \"{v}\" for {variable_name} is not valid. \
166                         Valid values are: {:?}. Reverting to default \"{}\".",
167                        default.valid_values, default.value
168                    );
169                    info!("Config: {variable_name} = {:?} (default due to invalid value)", default.value);
170                    default
171                }
172            },
173            None => {
174                event!(INFORMATION_LOG_LEVEL, "Config: {variable_name} = {:?} (default)", default.value);
175                default
176            },
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use std::num::ParseIntError;
184
185    use super::*;
186
187    const VALID: &[&str] = &["", "auto", "none", "lz4", "bg4-lz4"];
188    const INT_VALID: &[&str] = &["1", "2", "3"];
189
190    #[test]
191    fn test_new_default_value() {
192        let ce = ConfigEnum::new("auto", VALID);
193        assert_eq!(ce.as_str(), "auto");
194    }
195
196    #[test]
197    fn test_new_normalizes_to_lowercase() {
198        let ce = ConfigEnum::new("AUTO", VALID);
199        assert_eq!(ce.as_str(), "auto");
200    }
201
202    #[test]
203    fn test_deref_and_asref() {
204        let ce = ConfigEnum::new("lz4", VALID);
205        let s: &str = &ce;
206        assert_eq!(s, "lz4");
207        assert_eq!(ce.as_ref(), "lz4");
208    }
209
210    #[test]
211    fn test_partial_eq_str() {
212        let ce = ConfigEnum::new("lz4", VALID);
213        assert_eq!(ce, "lz4");
214        assert_eq!(ce, "LZ4");
215        assert_ne!(ce, "auto");
216    }
217
218    #[test]
219    fn test_partial_eq_str_ref() {
220        let ce = ConfigEnum::new("lz4", VALID);
221        assert_eq!(ce, "lz4");
222    }
223
224    #[test]
225    fn test_partial_eq_self() {
226        let a = ConfigEnum::new("lz4", VALID);
227        let b = ConfigEnum::new("lz4", VALID);
228        assert_eq!(a, b);
229    }
230
231    #[test]
232    fn test_display() {
233        let ce = ConfigEnum::new("bg4-lz4", VALID);
234        assert_eq!(format!("{ce}"), "bg4-lz4");
235    }
236
237    #[test]
238    fn test_debug() {
239        let ce = ConfigEnum::new("bg4-lz4", VALID);
240        assert_eq!(format!("{ce:?}"), "\"bg4-lz4\"");
241    }
242
243    #[test]
244    fn test_parse_valid_value() {
245        let default = ConfigEnum::new("auto", VALID);
246        let result = ConfigEnum::parse_config_value("test", Some("LZ4".to_string()), default);
247        assert_eq!(result.as_str(), "lz4");
248    }
249
250    #[test]
251    fn test_parse_invalid_value_returns_default() {
252        let default = ConfigEnum::new("auto", VALID);
253        let result = ConfigEnum::parse_config_value("test", Some("zstd".to_string()), default);
254        assert_eq!(result.as_str(), "auto");
255    }
256
257    #[test]
258    fn test_parse_none_returns_default() {
259        let default = ConfigEnum::new("auto", VALID);
260        let result = ConfigEnum::parse_config_value("test", None, default);
261        assert_eq!(result.as_str(), "auto");
262    }
263
264    #[test]
265    fn test_parse_empty_string_valid() {
266        let default = ConfigEnum::new("auto", VALID);
267        let result = ConfigEnum::parse_config_value("test", Some("".to_string()), default);
268        assert_eq!(result.as_str(), "");
269    }
270
271    #[test]
272    #[should_panic(expected = "not in the valid values list")]
273    #[cfg(debug_assertions)]
274    fn test_new_invalid_default_panics() {
275        let _ = ConfigEnum::new("invalid", VALID);
276    }
277
278    #[test]
279    fn test_valid_values_accessor() {
280        let ce = ConfigEnum::new("auto", VALID);
281        assert_eq!(ce.valid_values(), VALID);
282    }
283
284    #[test]
285    fn test_try_set_valid() {
286        let mut ce = ConfigEnum::new("auto", VALID);
287        assert!(ce.try_set("lz4").is_ok());
288        assert_eq!(ce.as_str(), "lz4");
289    }
290
291    #[test]
292    fn test_try_set_case_insensitive() {
293        let mut ce = ConfigEnum::new("auto", VALID);
294        assert!(ce.try_set("LZ4").is_ok());
295        assert_eq!(ce.as_str(), "lz4");
296    }
297
298    #[test]
299    fn test_try_set_invalid() {
300        let mut ce = ConfigEnum::new("auto", VALID);
301        assert!(ce.try_set("zstd").is_err());
302        assert_eq!(ce.as_str(), "auto");
303    }
304
305    #[test]
306    fn test_try_set_empty_string() {
307        let mut ce = ConfigEnum::new("auto", VALID);
308        assert!(ce.try_set("").is_ok());
309        assert_eq!(ce.as_str(), "");
310    }
311
312    #[test]
313    fn test_parse_success() {
314        let ce = ConfigEnum::new("2", INT_VALID);
315        let val: Result<u32, ParseIntError> = ce.parse();
316        assert_eq!(val.unwrap(), 2);
317    }
318
319    #[test]
320    fn test_parse_all_values_parseable() {
321        let ce = ConfigEnum::new("1", INT_VALID);
322        let _: u32 = ce.parse().unwrap();
323    }
324
325    #[test]
326    #[should_panic(expected = "cannot be parsed")]
327    #[cfg(debug_assertions)]
328    fn test_parse_panics_on_unparseable_valid_value() {
329        let ce = ConfigEnum::new("auto", VALID);
330        let _: Result<u32, ParseIntError> = ce.parse();
331    }
332}