1macro_rules! lever {
17 ($env:literal, $path:literal, $ty:ident, $default:literal, None, $class:ident, $reader:literal, $doc:literal) => {
18 $crate::levers::LeverSpec {
19 env: $env,
20 path: $path,
21 ty: $crate::levers::Ty::$ty,
22 default: $default,
23 flag: None,
24 class: $crate::levers::Class::$class,
25 reader: $reader,
26 doc: $doc,
27 }
28 };
29 ($env:literal, $path:literal, $ty:ident, $default:literal, $flag:literal, $class:ident, $reader:literal, $doc:literal) => {
30 $crate::levers::LeverSpec {
31 env: $env,
32 path: $path,
33 ty: $crate::levers::Ty::$ty,
34 default: $default,
35 flag: Some($flag),
36 class: $crate::levers::Class::$class,
37 reader: $reader,
38 doc: $doc,
39 }
40 };
41}
42
43mod table_a;
44mod table_b;
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum Class {
49 Runtime,
52 Harness,
54 Build,
56 Dev,
58 Tool,
60}
61
62impl Class {
63 #[must_use]
64 pub fn as_str(self) -> &'static str {
65 match self {
66 Self::Runtime => "runtime",
67 Self::Harness => "harness",
68 Self::Build => "build",
69 Self::Dev => "dev",
70 Self::Tool => "tool",
71 }
72 }
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum Ty {
77 Switch,
79 Int,
80 Float,
81 Text,
82 Path,
83}
84
85impl Ty {
86 #[must_use]
87 pub fn as_str(self) -> &'static str {
88 match self {
89 Self::Switch => "switch",
90 Self::Int => "int",
91 Self::Float => "float",
92 Self::Text => "text",
93 Self::Path => "path",
94 }
95 }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub struct LeverSpec {
100 pub env: &'static str,
101 pub path: &'static str,
103 pub ty: Ty,
104 pub default: &'static str,
108 pub flag: Option<&'static str>,
110 pub class: Class,
111 pub reader: &'static str,
113 pub doc: &'static str,
114}
115
116pub const PREFIX: &str = "METRALE_";
117
118pub fn all() -> impl Iterator<Item = &'static LeverSpec> {
120 table_a::LEVERS.iter().chain(table_b::LEVERS)
121}
122
123#[must_use]
124pub fn lookup(env: &str) -> Option<&'static LeverSpec> {
125 all().find(|l| l.env == env)
126}
127
128#[must_use]
129pub fn is_harness(env: &str) -> bool {
130 lookup(env).is_some_and(|l| l.class == Class::Harness)
131}
132
133pub fn undeclared<I, S>(names: I) -> Vec<String>
136where
137 I: IntoIterator<Item = S>,
138 S: AsRef<str>,
139{
140 let mut out: Vec<String> = names
141 .into_iter()
142 .map(|n| n.as_ref().to_string())
143 .filter(|n| n.starts_with(PREFIX) && lookup(n).is_none())
144 .collect();
145 out.sort();
146 out.dedup();
147 out
148}
149
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct UndeclaredLevers(pub Vec<String>);
152
153impl std::fmt::Display for UndeclaredLevers {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 write!(
156 f,
157 "undeclared METRALE_* environment variable{}: {}. Nothing reads {}: it is a \
158 typo or a lever that no longer exists, and running on as if it applied would \
159 measure something other than what was asked for. Unset {}. (`met \
160 dump-serve-options` lists every declared lever.)",
161 if self.0.len() == 1 { "" } else { "s" },
162 self.0.join(", "),
163 if self.0.len() == 1 { "it" } else { "them" },
164 if self.0.len() == 1 { "it" } else { "them" },
165 )
166 }
167}
168
169impl std::error::Error for UndeclaredLevers {}
170
171pub fn check<I, S>(names: I) -> Result<(), UndeclaredLevers>
176where
177 I: IntoIterator<Item = S>,
178 S: AsRef<str>,
179{
180 let bad = undeclared(names);
181 if bad.is_empty() {
182 Ok(())
183 } else {
184 Err(UndeclaredLevers(bad))
185 }
186}
187
188pub fn var(name: &str) -> Option<String> {
193 debug_assert!(declared_or_foreign(name), "{name} is not a declared lever");
194 std::env::var(name).ok()
195}
196
197pub fn var_os(name: &str) -> Option<std::ffi::OsString> {
199 debug_assert!(declared_or_foreign(name), "{name} is not a declared lever");
200 std::env::var_os(name)
201}
202
203fn declared_or_foreign(name: &str) -> bool {
204 !name.starts_with(PREFIX) || lookup(name).is_some()
205}
206
207#[cfg(test)]
208#[path = "levers_tests.rs"]
209mod tests;