use std::collections::HashMap;
use std::str::FromStr;
use parking_lot::RwLock;
pub use inventory;
pub trait KnobValue: Sized + Clone + Send + Sync + 'static {
const TYPE_NAME: &'static str;
fn parse_knob(s: &str) -> Option<Self>;
fn render_knob(&self) -> String;
}
impl KnobValue for bool {
const TYPE_NAME: &'static str = "bool";
fn parse_knob(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "on" | "yes" => Some(true),
"0" | "false" | "off" | "no" => Some(false),
_ => None,
}
}
fn render_knob(&self) -> String {
if *self { "true" } else { "false" }.to_string()
}
}
macro_rules! impl_knob_value_fromstr {
($ty:ty, $name:expr) => {
impl KnobValue for $ty {
const TYPE_NAME: &'static str = $name;
fn parse_knob(s: &str) -> Option<Self> {
<$ty>::from_str(s.trim()).ok()
}
fn render_knob(&self) -> String {
self.to_string()
}
}
};
}
impl_knob_value_fromstr!(i64, "i64");
impl_knob_value_fromstr!(usize, "usize");
impl_knob_value_fromstr!(f32, "f32");
impl KnobValue for String {
const TYPE_NAME: &'static str = "string";
fn parse_knob(s: &str) -> Option<Self> {
Some(s.to_string())
}
fn render_knob(&self) -> String {
self.clone()
}
}
impl<T: KnobValue> KnobValue for Option<T> {
const TYPE_NAME: &'static str = "option";
fn parse_knob(s: &str) -> Option<Self> {
T::parse_knob(s).map(Some)
}
fn render_knob(&self) -> String {
match self {
Some(v) => v.render_knob(),
None => "(unset)".to_string(),
}
}
}
static OVERRIDES: RwLock<Option<HashMap<&'static str, String>>> = RwLock::new(None);
fn with_override<R>(name: &str, f: impl FnOnce(Option<&String>) -> R) -> R {
let guard = OVERRIDES.read();
f(guard.as_ref().and_then(|m| m.get(name)))
}
pub struct Knob<T: KnobValue> {
pub name: &'static str,
pub doc: &'static str,
default: fn() -> T,
}
impl<T: KnobValue> Knob<T> {
pub const fn new(name: &'static str, default: fn() -> T, doc: &'static str) -> Self {
Knob { name, doc, default }
}
pub fn default_value(&self) -> T {
(self.default)()
}
pub fn get(&self) -> T {
if let Some(v) = with_override(self.name, |o| o.and_then(|s| T::parse_knob(s))) {
return v;
}
if let Ok(s) = std::env::var(self.name)
&& let Some(v) = T::parse_knob(&s)
{
return v;
}
self.default_value()
}
pub fn set(&self, value: T) {
OVERRIDES.write().get_or_insert_with(HashMap::new).insert(self.name, value.render_knob());
}
pub fn clear(&self) {
if let Some(m) = OVERRIDES.write().as_mut() {
m.remove(self.name);
}
}
}
pub struct KnobInfo {
pub name: &'static str,
pub doc: &'static str,
pub type_name: &'static str,
pub default: fn() -> String,
pub current: fn() -> String,
}
inventory::collect!(KnobInfo);
pub fn all() -> Vec<&'static KnobInfo> {
let mut v: Vec<&'static KnobInfo> = inventory::iter::<KnobInfo>.into_iter().collect();
v.sort_by_key(|k| k.name);
v
}
pub fn set_str(name: &str, value: &str) -> bool {
match all().into_iter().find(|k| k.name == name) {
Some(info) => {
OVERRIDES.write().get_or_insert_with(HashMap::new).insert(info.name, value.to_string());
true
}
None => false,
}
}
pub fn clear_str(name: &str) -> bool {
match all().into_iter().find(|k| k.name == name) {
Some(info) => {
if let Some(m) = OVERRIDES.write().as_mut() {
m.remove(info.name);
}
true
}
None => false,
}
}
pub fn list() -> String {
use std::fmt::Write;
let shown = |s: String| if s.is_empty() { "(unset)".to_string() } else { s };
let mut out = String::new();
for k in all() {
let current = shown((k.current)());
let default = shown((k.default)());
write!(out, "{} = {} [{}", k.name, current, k.type_name).unwrap();
if current != default {
write!(out, ", default {default}").unwrap();
}
writeln!(out, "]\n {}\n", k.doc).unwrap();
}
out
}
#[macro_export]
macro_rules! declare_knob {
($ident:ident, $ty:ty, $default:expr, $doc:literal) => {
#[doc = $doc]
pub static $ident: $crate::knobs::Knob<$ty> =
$crate::knobs::Knob::new(stringify!($ident), || $default, $doc);
$crate::knobs::inventory::submit! {
$crate::knobs::KnobInfo {
name: stringify!($ident),
doc: $doc,
type_name: <$ty as $crate::knobs::KnobValue>::TYPE_NAME,
default: || $crate::knobs::KnobValue::render_knob(&$ident.default_value()),
current: || $crate::knobs::KnobValue::render_knob(&$ident.get()),
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
declare_knob!(TRACT_TEST_KNOB_BOOL, bool, false, "Test bool knob.");
declare_knob!(TRACT_TEST_KNOB_INT, usize, 7, "Test int knob.");
declare_knob!(TRACT_TEST_KNOB_SET_INT, usize, 7, "Test int knob for set-by-name.");
#[test]
fn default_then_override_then_clear() {
assert!(!TRACT_TEST_KNOB_BOOL.get());
assert_eq!(TRACT_TEST_KNOB_INT.get(), 7);
TRACT_TEST_KNOB_BOOL.set(true);
assert!(TRACT_TEST_KNOB_BOOL.get());
TRACT_TEST_KNOB_BOOL.clear();
assert!(!TRACT_TEST_KNOB_BOOL.get());
}
#[test]
fn set_by_name() {
assert!(set_str("TRACT_TEST_KNOB_SET_INT", "42"));
assert_eq!(TRACT_TEST_KNOB_SET_INT.get(), 42);
assert!(clear_str("TRACT_TEST_KNOB_SET_INT"));
assert_eq!(TRACT_TEST_KNOB_SET_INT.get(), 7);
assert!(!set_str("TRACT_TEST_KNOB_DOES_NOT_EXIST", "1"));
}
#[test]
fn registered_in_inventory() {
let names: Vec<_> = all().iter().map(|k| k.name).collect();
assert!(names.contains(&"TRACT_TEST_KNOB_BOOL"));
assert!(names.contains(&"TRACT_TEST_KNOB_INT"));
}
#[test]
fn bool_parsing() {
assert_eq!(bool::parse_knob("1"), Some(true));
assert_eq!(bool::parse_knob("Off"), Some(false));
assert_eq!(bool::parse_knob("maybe"), None);
}
}