Skip to main content

xlog_core/
config_value.rs

1//! Strict parsing for boolean configuration values.
2
3use crate::{Result, XlogError};
4
5const BOOLEAN_VALUES: &str = "one of 1/true/yes/on or 0/false/no/off";
6
7/// Parse a boolean configuration value using the repository-wide spelling set.
8pub fn parse_bool_value(name: &str, raw: &str) -> Result<bool> {
9    match raw.trim().to_ascii_lowercase().as_str() {
10        "1" | "true" | "yes" | "on" => Ok(true),
11        "0" | "false" | "no" | "off" => Ok(false),
12        _ => Err(XlogError::Configuration {
13            name: name.to_owned(),
14            value: raw.to_owned(),
15            expected: BOOLEAN_VALUES,
16        }),
17    }
18}
19
20/// Read and strictly parse a boolean environment variable.
21///
22/// An unset variable returns `Ok(None)`. A present empty, unknown, or
23/// non-Unicode value returns a typed configuration error.
24pub fn read_bool_env(name: &str) -> Result<Option<bool>> {
25    match std::env::var(name) {
26        Ok(raw) => parse_bool_value(name, &raw).map(Some),
27        Err(std::env::VarError::NotPresent) => Ok(None),
28        Err(std::env::VarError::NotUnicode(_)) => Err(XlogError::Configuration {
29            name: name.to_owned(),
30            value: "<non-Unicode>".to_owned(),
31            expected: BOOLEAN_VALUES,
32        }),
33    }
34}
35
36/// Resolve a boolean as explicit typed configuration, then environment, then default.
37pub fn resolve_bool(explicit: Option<bool>, env_name: &str, default: bool) -> Result<bool> {
38    match explicit {
39        Some(value) => Ok(value),
40        None => Ok(read_bool_env(env_name)?.unwrap_or(default)),
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::{parse_bool_value, read_bool_env, resolve_bool};
47    use crate::XlogError;
48    use std::sync::{Mutex, OnceLock};
49
50    const TEST_ENV: &str = "XLOG_CORE_TEST_BOOLEAN";
51
52    fn env_lock() -> &'static Mutex<()> {
53        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
54        LOCK.get_or_init(|| Mutex::new(()))
55    }
56
57    struct EnvRestore(Option<std::ffi::OsString>);
58
59    impl EnvRestore {
60        fn capture() -> Self {
61            Self(std::env::var_os(TEST_ENV))
62        }
63    }
64
65    impl Drop for EnvRestore {
66        fn drop(&mut self) {
67            match self.0.take() {
68                Some(value) => std::env::set_var(TEST_ENV, value),
69                None => std::env::remove_var(TEST_ENV),
70            }
71        }
72    }
73
74    #[test]
75    fn accepts_documented_true_and_false_values_case_insensitively() {
76        for value in ["1", "true", "TRUE", " yes ", "On"] {
77            assert!(parse_bool_value(TEST_ENV, value).unwrap(), "{value:?}");
78        }
79        for value in ["0", "false", "FALSE", " no ", "Off"] {
80            assert!(!parse_bool_value(TEST_ENV, value).unwrap(), "{value:?}");
81        }
82    }
83
84    #[test]
85    fn rejects_empty_and_unknown_values_with_variable_diagnostics() {
86        for raw in ["", " ", "enabled", "2"] {
87            let error = parse_bool_value(TEST_ENV, raw).unwrap_err();
88            assert!(matches!(
89                error,
90                XlogError::Configuration { ref name, ref value, .. }
91                    if name == TEST_ENV && value == raw
92            ));
93            assert!(error.to_string().contains(TEST_ENV));
94        }
95    }
96
97    #[test]
98    fn unset_environment_value_is_absent() {
99        let _guard = env_lock().lock().unwrap();
100        let _restore = EnvRestore::capture();
101        std::env::remove_var(TEST_ENV);
102        assert_eq!(read_bool_env(TEST_ENV).unwrap(), None);
103    }
104
105    #[test]
106    fn environment_value_uses_the_same_strict_parser() {
107        let _guard = env_lock().lock().unwrap();
108        let _restore = EnvRestore::capture();
109        std::env::set_var(TEST_ENV, "yes");
110        assert_eq!(read_bool_env(TEST_ENV).unwrap(), Some(true));
111        std::env::set_var(TEST_ENV, "unknown");
112        assert!(matches!(
113            read_bool_env(TEST_ENV),
114            Err(XlogError::Configuration { ref name, .. }) if name == TEST_ENV
115        ));
116    }
117
118    #[test]
119    fn resolution_precedence_is_explicit_then_environment_then_default() {
120        let _guard = env_lock().lock().unwrap();
121        let _restore = EnvRestore::capture();
122
123        std::env::set_var(TEST_ENV, "invalid");
124        assert!(!resolve_bool(Some(false), TEST_ENV, true).unwrap());
125
126        std::env::set_var(TEST_ENV, "on");
127        assert!(resolve_bool(None, TEST_ENV, false).unwrap());
128
129        std::env::remove_var(TEST_ENV);
130        assert!(resolve_bool(None, TEST_ENV, true).unwrap());
131    }
132}