1use std::str::FromStr;
2use std::sync::LazyLock;
3
4use tracing::{Level, event, info, warn};
5
6use super::ByteSize;
7#[cfg(not(target_family = "wasm"))]
8use super::TemplatedPathBuf;
9
10#[cfg(not(feature = "elevated_information_level"))]
11pub const INFORMATION_LOG_LEVEL: Level = Level::DEBUG;
12#[cfg(feature = "elevated_information_level")]
13pub const INFORMATION_LOG_LEVEL: Level = Level::INFO;
14
15pub trait ParsableConfigValue: std::fmt::Debug + Sized {
19 fn parse_user_value(value: &str) -> Option<Self>;
20
21 fn to_config_string(&self) -> String;
23
24 fn try_update_in_place(&mut self, value: &str) -> bool {
28 if let Some(v) = Self::parse_user_value(value) {
29 *self = v;
30 true
31 } else {
32 false
33 }
34 }
35
36 fn parse_config_value(variable_name: &str, value: Option<String>, default: Self) -> Self {
39 match value {
40 Some(v) => match Self::parse_user_value(&v) {
41 Some(v) => {
42 info!("Config: {variable_name} = {v:?} (user set)");
43 v
44 },
45 None => {
46 warn!(
47 "Configuration value {v} for {variable_name} cannot be parsed into correct type; reverting to default."
48 );
49 info!("Config: {variable_name} = {default:?} (default due to parse error)");
50 default
51 },
52 },
53 None => {
54 event!(INFORMATION_LOG_LEVEL, "Config: {variable_name} = {default:?} (default)");
55 default
56 },
57 }
58 }
59}
60
61pub trait FromStrParseable: FromStr + std::fmt::Debug + std::fmt::Display {}
64
65impl<T: FromStrParseable> ParsableConfigValue for T {
66 fn parse_user_value(value: &str) -> Option<Self> {
67 value.parse::<T>().ok()
68 }
69
70 fn to_config_string(&self) -> String {
71 self.to_string()
72 }
73}
74
75impl FromStrParseable for usize {}
77impl FromStrParseable for u8 {}
78impl FromStrParseable for u16 {}
79impl FromStrParseable for u32 {}
80impl FromStrParseable for u64 {}
81impl FromStrParseable for isize {}
82impl FromStrParseable for i8 {}
83impl FromStrParseable for i16 {}
84impl FromStrParseable for i32 {}
85impl FromStrParseable for i64 {}
86impl FromStrParseable for f32 {}
87impl FromStrParseable for f64 {}
88impl FromStrParseable for String {}
89impl FromStrParseable for ByteSize {}
90
91fn parse_bool_value(value: &str) -> Option<bool> {
95 let t = value.trim().to_ascii_lowercase();
96
97 match t.as_str() {
98 "0" | "false" | "no" | "n" | "off" => Some(false),
99 "1" | "true" | "yes" | "y" | "on" => Some(true),
100 _ => None,
101 }
102}
103
104impl ParsableConfigValue for bool {
105 fn parse_user_value(value: &str) -> Option<Self> {
106 parse_bool_value(value)
107 }
108
109 fn to_config_string(&self) -> String {
110 if *self { "true" } else { "false" }.to_owned()
111 }
112}
113
114impl<T: ParsableConfigValue> ParsableConfigValue for Option<T> {
117 fn parse_user_value(value: &str) -> Option<Self> {
118 if value.trim().is_empty() {
119 return Some(None);
120 }
121 T::parse_user_value(value).map(Some)
122 }
123
124 fn to_config_string(&self) -> String {
125 match self {
126 Some(v) => v.to_config_string(),
127 None => String::new(),
128 }
129 }
130}
131
132impl ParsableConfigValue for std::time::Duration {
137 fn parse_user_value(value: &str) -> Option<Self> {
138 humantime::parse_duration(value).ok()
139 }
140
141 fn to_config_string(&self) -> String {
142 let total_ms = self.as_millis();
143 if self.subsec_nanos() == 0 {
144 format!("{}s", self.as_secs())
145 } else if self.subsec_nanos().is_multiple_of(1_000_000) {
146 format!("{total_ms}ms")
147 } else if self.subsec_nanos().is_multiple_of(1_000) {
148 format!("{}us", self.as_micros())
149 } else {
150 format!("{}ns", self.as_nanos())
151 }
152 }
153}
154
155#[cfg(not(target_family = "wasm"))]
156impl ParsableConfigValue for TemplatedPathBuf {
157 fn parse_user_value(value: &str) -> Option<Self> {
158 Some(Self::new(value))
159 }
160
161 fn to_config_string(&self) -> String {
162 self.template_string()
163 }
164}
165
166#[macro_export]
167macro_rules! test_configurable_constants {
168 ($(
169 $(#[$meta:meta])*
170 ref $name:ident : $type:ty = $value:expr;
171 )+) => {
172 $(
173 $(#[$meta])*
174 pub static $name: ::std::sync::LazyLock<$type> = ::std::sync::LazyLock::new(|| {
175 #[cfg(debug_assertions)]
176 {
177 let default_value = $value;
178 let maybe_env_value = ::std::env::var(concat!("HF_XET_", stringify!($name))).ok();
179 <$type as $crate::configuration_utils::ParsableConfigValue>::parse_config_value(
180 stringify!($name),
181 maybe_env_value,
182 default_value,
183 )
184 }
185 #[cfg(not(debug_assertions))]
186 {
187 $value
188 }
189 });
190 )+
191 };
192}
193
194pub use ctor as ctor_reexport;
195
196#[cfg(not(doctest))]
197#[macro_export]
223macro_rules! test_set_constants {
224 ($(
225 $var_name:ident = $val:expr;
226 )+) => {
227 use $crate::configuration_utils::ctor_reexport as ctor;
228
229 #[ctor::ctor(unsafe)]
230 fn set_constants_on_load() {
231 $(
232 let val = $val;
233 let val_str = format!("{val:?}");
234
235 let env_name = concat!("HF_XET_", stringify!($var_name));
237
238 unsafe {
240 std::env::set_var(env_name, &val_str);
241 }
242
243 let actual_value = *$var_name;
245
246 if format!("{actual_value:?}") != val_str {
247 panic!(
248 "test_set_constants! failed: wanted {} to be {:?}, but got {:?}",
249 stringify!($var_name),
250 val,
251 actual_value
252 );
253 }
254 eprintln!("> Set {} to {:?}",
255 stringify!($var_name),
256 val);
257 )+
258 }
259 }
260}
261
262#[cfg(not(doctest))]
263#[macro_export]
290macro_rules! test_set_config {
291 ($(
292 $group_name:ident {
293 $(
294 $field_name:ident = $val:expr;
295 )+
296 }
297 )+) => {
298 use $crate::configuration_utils::ctor_reexport as config_ctor;
299
300 #[config_ctor::ctor(unsafe)]
301 fn set_config_on_load() {
302 $(
303 let group_name_upper = stringify!($group_name).to_uppercase();
304 $(
305 let val = $val;
306 let val_str = format!("{val:?}");
307 let field_name_upper = stringify!($field_name).to_uppercase();
308
309 let env_name = format!("HF_XET_{}_{}", group_name_upper, field_name_upper);
311
312 unsafe {
314 std::env::set_var(&env_name, &val_str);
315 }
316
317 eprintln!("> Set config {}.{} to {:?} (env: {})",
318 stringify!($group_name),
319 stringify!($field_name),
320 val,
321 env_name);
322 )+
323 )+
324 }
325 }
326}
327
328fn get_high_performance_flag() -> bool {
329 if let Ok(val) = std::env::var("HF_XET_HIGH_PERFORMANCE") {
330 parse_bool_value(&val).unwrap_or(false)
331 } else if let Ok(val) = std::env::var("HF_XET_HP") {
332 parse_bool_value(&val).unwrap_or(false)
333 } else {
334 false
335 }
336}
337
338pub static HIGH_PERFORMANCE: LazyLock<bool> = LazyLock::new(get_high_performance_flag);
342
343#[inline]
344pub fn is_high_performance() -> bool {
345 *HIGH_PERFORMANCE
346}
347
348#[cfg(test)]
349mod tests {
350 use std::time::Duration;
351
352 use super::*;
353
354 fn assert_roundtrip<T: ParsableConfigValue + PartialEq + std::fmt::Debug>(value: T) {
355 let s = value.to_config_string();
356 let restored = T::parse_user_value(&s).unwrap_or_else(|| {
357 panic!("Failed to parse config string '{s}' back into {:?}", std::any::type_name::<T>())
358 });
359 assert_eq!(value, restored);
360 }
361
362 macro_rules! assert_roundtrips {
363 ($($value:expr),+ $(,)?) => {
364 $(assert_roundtrip($value);)+
365 };
366 }
367
368 #[test]
369 fn test_roundtrip_numeric_primitives() {
370 assert_roundtrips!(
371 0usize,
372 42usize,
373 usize::MAX,
374 0u8,
375 255u8,
376 0u16,
377 65535u16,
378 0u32,
379 123456u32,
380 0u64,
381 u64::MAX,
382 0isize,
383 -42isize,
384 isize::MAX,
385 -128i8,
386 127i8,
387 -32768i16,
388 32767i16,
389 0i32,
390 -123456i32,
391 0i64,
392 i64::MIN,
393 0.0f32,
394 3.14f32,
395 -1.5f32,
396 0.0f64,
397 std::f64::consts::PI,
398 -1e10f64
399 );
400 }
401
402 #[test]
403 fn test_roundtrip_bool_and_string() {
404 assert_roundtrips!(true, false, String::new(), "hello world".to_owned(), "http://localhost:8080".to_owned());
405 }
406
407 #[test]
408 fn test_roundtrip_byte_size() {
409 assert_roundtrips!(
410 ByteSize::new(0),
411 ByteSize::new(1000),
412 ByteSize::new(1_000_000),
413 ByteSize::new(8_000_000),
414 ByteSize::new(10_000_000_000)
415 );
416 }
417
418 #[test]
419 fn test_roundtrip_duration() {
420 assert_roundtrips!(
421 Duration::from_secs(0),
422 Duration::from_secs(60),
423 Duration::from_secs(120),
424 Duration::from_millis(200),
425 Duration::from_millis(3000),
426 Duration::from_secs(360),
427 Duration::from_micros(123),
428 Duration::from_nanos(123)
429 );
430 }
431
432 #[test]
433 fn test_roundtrip_option_some() {
434 assert_roundtrips!(Some(42usize), Some("hello".to_owned()));
435 }
436
437 #[test]
438 fn test_roundtrip_option_none_string() {
439 let none_val: Option<String> = None;
440 let s = none_val.to_config_string();
441 assert_eq!(s, "");
442 }
443
444 #[test]
445 fn test_parse_option_empty_string_as_none() {
446 assert_eq!(Option::<String>::parse_user_value(""), Some(None));
447 assert_eq!(Option::<String>::parse_user_value(" "), Some(None));
448 assert_eq!(Option::<usize>::parse_user_value(""), Some(None));
449 }
450
451 #[test]
452 fn test_parse_bool_empty_string_as_none() {
453 assert_eq!(bool::parse_user_value(""), None);
454 assert_eq!(bool::parse_user_value(" "), None);
455 }
456
457 #[cfg(not(target_family = "wasm"))]
458 #[test]
459 fn test_roundtrip_templated_path_buf() {
460 let path = TemplatedPathBuf::new("/some/simple/path");
461 let s = path.to_config_string();
462 assert_eq!(s, "/some/simple/path");
463 let restored = TemplatedPathBuf::parse_user_value(&s).unwrap();
464 assert_eq!(path.template_string(), restored.template_string());
465 }
466}