Skip to main content

cranpose_core/
debug_toggles.rs

1//! Values named by environment variables, cached until a programmatic
2//! override of the same name changes. A platform sets overrides for values it
3//! learns at run time, and a test for the switches it flips.
4
5use std::{
6    collections::BTreeMap,
7    ffi::{OsStr, OsString},
8    str::FromStr,
9    sync::{Mutex, OnceLock, PoisonError, atomic::Ordering},
10};
11
12use portable_atomic::AtomicU64;
13
14static GENERATION: AtomicU64 = AtomicU64::new(0);
15
16/// A debug toggle cached until an override changes. Unset values can be
17/// read without acquiring the cache lock.
18pub struct DebugToggle {
19    name: &'static str,
20    cached: Mutex<Option<(u64, Option<String>)>>,
21    absent_generation: AtomicU64,
22}
23
24impl DebugToggle {
25    /// Creates a toggle named after the environment variable `name`; a programmatic override
26    /// of the same name takes precedence over the environment.
27    pub const fn new(name: &'static str) -> Self {
28        Self {
29            name,
30            cached: Mutex::new(None),
31            absent_generation: AtomicU64::new(u64::MAX),
32        }
33    }
34
35    /// Reads the toggle's value through `read`.
36    pub fn with<R>(&self, read: impl FnOnce(Option<&str>) -> R) -> R {
37        let generation = GENERATION.load(Ordering::Acquire);
38        if self.absent_generation.load(Ordering::Acquire) == generation {
39            return read(None);
40        }
41        let mut cached = self.cached.lock().unwrap_or_else(PoisonError::into_inner);
42        if cached.as_ref().is_none_or(|(seen, _)| *seen != generation) {
43            *cached = Some((generation, debug_toggle(self.name)));
44        }
45        let value = cached.as_ref().and_then(|(_, value)| value.as_deref());
46        if value.is_none() {
47            self.absent_generation.store(generation, Ordering::Release);
48        }
49        read(value)
50    }
51
52    /// Whether the toggle holds any value.
53    pub fn is_set(&self) -> bool {
54        self.with(|value| value.is_some())
55    }
56
57    /// Whether the toggle is switched on: `1`, `true` or `yes`.
58    pub fn flag(&self) -> bool {
59        self.with(|value| matches!(value, Some("1" | "true" | "yes")))
60    }
61
62    /// Whether the toggle holds exactly `expected`.
63    pub fn equals(&self, expected: &str) -> bool {
64        self.with(|value| value == Some(expected))
65    }
66
67    /// The toggle parsed as `T`, when set and well formed.
68    pub fn parse<T: FromStr>(&self) -> Option<T> {
69        self.with(|value| value.and_then(|value| value.parse().ok()))
70    }
71}
72
73fn overrides() -> &'static Mutex<BTreeMap<&'static str, OsString>> {
74    static OVERRIDES: OnceLock<Mutex<BTreeMap<&'static str, OsString>>> = OnceLock::new();
75    OVERRIDES.get_or_init(|| Mutex::new(BTreeMap::new()))
76}
77
78#[doc(hidden)]
79pub fn debug_toggle(name: &'static str) -> Option<String> {
80    let map = overrides().lock().unwrap_or_else(PoisonError::into_inner);
81    if let Some(value) = map.get(name) {
82        return value.to_str().map(str::to_owned);
83    }
84    drop(map);
85    std::env::var(name).ok()
86}
87
88#[doc(hidden)]
89pub fn debug_toggle_os(name: &'static str) -> Option<OsString> {
90    let map = overrides().lock().unwrap_or_else(PoisonError::into_inner);
91    if let Some(value) = map.get(name) {
92        return Some(value.clone());
93    }
94    drop(map);
95    std::env::var_os(name)
96}
97
98#[doc(hidden)]
99pub fn set_debug_toggle(name: &'static str, value: Option<&str>) {
100    set_debug_toggle_os(name, value.map(OsStr::new));
101}
102
103#[doc(hidden)]
104pub fn set_debug_toggle_os(name: &'static str, value: Option<&OsStr>) {
105    let mut map = overrides().lock().unwrap_or_else(PoisonError::into_inner);
106    match value {
107        Some(value) => map.insert(name, value.to_owned()),
108        None => map.remove(name),
109    };
110    GENERATION.fetch_add(1, Ordering::Release);
111}
112
113/// The number of overrides set so far: every toggle reads the same value
114/// while it stays the same.
115#[doc(hidden)]
116pub fn debug_toggle_generation() -> u64 {
117    GENERATION.load(Ordering::Acquire)
118}
119
120#[cfg(all(test, unix))]
121#[path = "tests/debug_toggles_tests.rs"]
122mod tests;