xlog_core/
config_value.rs1use crate::{Result, XlogError};
4
5const BOOLEAN_VALUES: &str = "one of 1/true/yes/on or 0/false/no/off";
6
7pub 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
20pub 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
36pub 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}