1use std::collections::HashMap;
18use std::str::FromStr;
19
20use parking_lot::RwLock;
21
22pub use inventory;
23
24pub trait KnobValue: Sized + Clone + Send + Sync + 'static {
27 const TYPE_NAME: &'static str;
28 fn parse_knob(s: &str) -> Option<Self>;
31 fn render_knob(&self) -> String;
32}
33
34impl KnobValue for bool {
35 const TYPE_NAME: &'static str = "bool";
36 fn parse_knob(s: &str) -> Option<Self> {
37 match s.trim().to_ascii_lowercase().as_str() {
38 "1" | "true" | "on" | "yes" => Some(true),
39 "0" | "false" | "off" | "no" => Some(false),
40 _ => None,
41 }
42 }
43 fn render_knob(&self) -> String {
44 if *self { "true" } else { "false" }.to_string()
45 }
46}
47
48macro_rules! impl_knob_value_fromstr {
49 ($ty:ty, $name:expr) => {
50 impl KnobValue for $ty {
51 const TYPE_NAME: &'static str = $name;
52 fn parse_knob(s: &str) -> Option<Self> {
53 <$ty>::from_str(s.trim()).ok()
54 }
55 fn render_knob(&self) -> String {
56 self.to_string()
57 }
58 }
59 };
60}
61impl_knob_value_fromstr!(i64, "i64");
62impl_knob_value_fromstr!(usize, "usize");
63impl_knob_value_fromstr!(f32, "f32");
64
65impl KnobValue for String {
66 const TYPE_NAME: &'static str = "string";
67 fn parse_knob(s: &str) -> Option<Self> {
68 Some(s.to_string())
69 }
70 fn render_knob(&self) -> String {
71 self.clone()
72 }
73}
74
75impl<T: KnobValue> KnobValue for Option<T> {
79 const TYPE_NAME: &'static str = "option";
80 fn parse_knob(s: &str) -> Option<Self> {
81 T::parse_knob(s).map(Some)
82 }
83 fn render_knob(&self) -> String {
84 match self {
85 Some(v) => v.render_knob(),
86 None => "(unset)".to_string(),
87 }
88 }
89}
90
91static OVERRIDES: RwLock<Option<HashMap<&'static str, String>>> = RwLock::new(None);
92
93fn with_override<R>(name: &str, f: impl FnOnce(Option<&String>) -> R) -> R {
94 let guard = OVERRIDES.read();
95 f(guard.as_ref().and_then(|m| m.get(name)))
96}
97
98pub struct Knob<T: KnobValue> {
101 pub name: &'static str,
102 pub doc: &'static str,
103 default: fn() -> T,
104}
105
106impl<T: KnobValue> Knob<T> {
107 pub const fn new(name: &'static str, default: fn() -> T, doc: &'static str) -> Self {
108 Knob { name, doc, default }
109 }
110
111 pub fn default_value(&self) -> T {
113 (self.default)()
114 }
115
116 pub fn get(&self) -> T {
119 if let Some(v) = with_override(self.name, |o| o.and_then(|s| T::parse_knob(s))) {
120 return v;
121 }
122 if let Ok(s) = std::env::var(self.name)
123 && let Some(v) = T::parse_knob(&s)
124 {
125 return v;
126 }
127 self.default_value()
128 }
129
130 pub fn set(&self, value: T) {
134 OVERRIDES.write().get_or_insert_with(HashMap::new).insert(self.name, value.render_knob());
135 }
136
137 pub fn clear(&self) {
139 if let Some(m) = OVERRIDES.write().as_mut() {
140 m.remove(self.name);
141 }
142 }
143}
144
145pub struct KnobInfo {
148 pub name: &'static str,
149 pub doc: &'static str,
150 pub type_name: &'static str,
151 pub default: fn() -> String,
153 pub current: fn() -> String,
155}
156
157inventory::collect!(KnobInfo);
158
159pub fn all() -> Vec<&'static KnobInfo> {
161 let mut v: Vec<&'static KnobInfo> = inventory::iter::<KnobInfo>.into_iter().collect();
162 v.sort_by_key(|k| k.name);
163 v
164}
165
166pub fn set_str(name: &str, value: &str) -> bool {
170 match all().into_iter().find(|k| k.name == name) {
171 Some(info) => {
172 OVERRIDES.write().get_or_insert_with(HashMap::new).insert(info.name, value.to_string());
173 true
174 }
175 None => false,
176 }
177}
178
179pub fn clear_str(name: &str) -> bool {
181 match all().into_iter().find(|k| k.name == name) {
182 Some(info) => {
183 if let Some(m) = OVERRIDES.write().as_mut() {
184 m.remove(info.name);
185 }
186 true
187 }
188 None => false,
189 }
190}
191
192pub fn list() -> String {
194 use std::fmt::Write;
195 let shown = |s: String| if s.is_empty() { "(unset)".to_string() } else { s };
196 let mut out = String::new();
197 for k in all() {
198 let current = shown((k.current)());
199 let default = shown((k.default)());
200 write!(out, "{} = {} [{}", k.name, current, k.type_name).unwrap();
201 if current != default {
202 write!(out, ", default {default}").unwrap();
203 }
204 writeln!(out, "]\n {}\n", k.doc).unwrap();
205 }
206 out
207}
208
209#[macro_export]
223macro_rules! declare_knob {
224 ($ident:ident, $ty:ty, $default:expr, $doc:literal) => {
225 #[doc = $doc]
226 pub static $ident: $crate::knobs::Knob<$ty> =
227 $crate::knobs::Knob::new(stringify!($ident), || $default, $doc);
228
229 $crate::knobs::inventory::submit! {
230 $crate::knobs::KnobInfo {
231 name: stringify!($ident),
232 doc: $doc,
233 type_name: <$ty as $crate::knobs::KnobValue>::TYPE_NAME,
234 default: || $crate::knobs::KnobValue::render_knob(&$ident.default_value()),
235 current: || $crate::knobs::KnobValue::render_knob(&$ident.get()),
236 }
237 }
238 };
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 declare_knob!(TRACT_TEST_KNOB_BOOL, bool, false, "Test bool knob.");
246 declare_knob!(TRACT_TEST_KNOB_INT, usize, 7, "Test int knob.");
247 declare_knob!(TRACT_TEST_KNOB_SET_INT, usize, 7, "Test int knob for set-by-name.");
248
249 #[test]
250 fn default_then_override_then_clear() {
251 assert!(!TRACT_TEST_KNOB_BOOL.get());
252 assert_eq!(TRACT_TEST_KNOB_INT.get(), 7);
253
254 TRACT_TEST_KNOB_BOOL.set(true);
255 assert!(TRACT_TEST_KNOB_BOOL.get());
256 TRACT_TEST_KNOB_BOOL.clear();
257 assert!(!TRACT_TEST_KNOB_BOOL.get());
258 }
259
260 #[test]
261 fn set_by_name() {
262 assert!(set_str("TRACT_TEST_KNOB_SET_INT", "42"));
263 assert_eq!(TRACT_TEST_KNOB_SET_INT.get(), 42);
264 assert!(clear_str("TRACT_TEST_KNOB_SET_INT"));
265 assert_eq!(TRACT_TEST_KNOB_SET_INT.get(), 7);
266 assert!(!set_str("TRACT_TEST_KNOB_DOES_NOT_EXIST", "1"));
267 }
268
269 #[test]
270 fn registered_in_inventory() {
271 let names: Vec<_> = all().iter().map(|k| k.name).collect();
272 assert!(names.contains(&"TRACT_TEST_KNOB_BOOL"));
273 assert!(names.contains(&"TRACT_TEST_KNOB_INT"));
274 }
275
276 #[test]
277 fn bool_parsing() {
278 assert_eq!(bool::parse_knob("1"), Some(true));
279 assert_eq!(bool::parse_knob("Off"), Some(false));
280 assert_eq!(bool::parse_knob("maybe"), None);
281 }
282}