1use std::borrow::Cow;
7use std::fmt::Debug;
8
9use serde::{Deserialize, Serialize};
10use strum::{Display, EnumString, IntoStaticStr};
11
12#[derive(Clone, Copy, Hash, Debug, Default, Ord, PartialOrd, Eq, PartialEq)]
16#[allow(clippy::exhaustive_enums)] #[derive(Serialize, Deserialize)]
18#[serde(try_from = "BoolOrAutoSerde", into = "BoolOrAutoSerde")]
19pub enum BoolOrAuto {
20 #[default]
21 Auto,
23 Explicit(bool),
25}
26
27impl BoolOrAuto {
28 pub fn as_bool(self) -> Option<bool> {
40 match self {
41 BoolOrAuto::Auto => None,
42 BoolOrAuto::Explicit(v) => Some(v),
43 }
44 }
45}
46
47#[derive(Serialize, Deserialize)]
49#[serde(untagged)]
50enum BoolOrAutoSerde {
51 String(Cow<'static, str>),
53 Bool(bool),
55}
56
57impl From<BoolOrAuto> for BoolOrAutoSerde {
58 fn from(boa: BoolOrAuto) -> BoolOrAutoSerde {
59 use BoolOrAutoSerde as BoAS;
60 boa.as_bool()
61 .map(BoAS::Bool)
62 .unwrap_or_else(|| BoAS::String("auto".into()))
63 }
64}
65
66#[derive(thiserror::Error, Debug, Clone)]
68#[non_exhaustive]
69#[error(r#"Invalid value, expected boolean or "auto""#)]
70pub struct InvalidBoolOrAuto {}
71
72impl TryFrom<BoolOrAutoSerde> for BoolOrAuto {
73 type Error = InvalidBoolOrAuto;
74
75 fn try_from(pls: BoolOrAutoSerde) -> Result<BoolOrAuto, Self::Error> {
76 use BoolOrAuto as BoA;
77 use BoolOrAutoSerde as BoAS;
78 Ok(match pls {
79 BoAS::Bool(v) => BoA::Explicit(v),
80 BoAS::String(s) if s == "false" => BoA::Explicit(false),
81 BoAS::String(s) if s == "true" => BoA::Explicit(true),
82 BoAS::String(s) if s == "auto" => BoA::Auto,
83 _ => return Err(InvalidBoolOrAuto {}),
84 })
85 }
86}
87
88#[macro_export]
116macro_rules! impl_not_auto_value {
117 ($ty:ty) => {
118 $crate::deps::paste! {
119 impl $crate::NotAutoValue for $ty {}
120
121 #[cfg(test)]
122 #[allow(non_snake_case)]
123 mod [<test_not_auto_value_ $ty>] {
124 #[allow(unused_imports)]
125 use super::*;
126
127 #[test]
128 fn [<auto_is_not_a_valid_value_for_ $ty>]() {
129 let res = $crate::deps::serde_value::Value::String(
130 "auto".into()
131 ).deserialize_into::<$ty>();
132
133 assert!(
134 res.is_err(),
135 concat!(
136 stringify!($ty), " is not a valid NotAutoValue type: ",
137 "NotAutoValue types should not be deserializable from \"auto\""
138 ),
139 );
140 }
141 }
142 }
143 };
144}
145
146#[derive(Clone, Copy, Hash, Debug, Default, Ord, PartialOrd, Eq, PartialEq)]
205#[allow(clippy::exhaustive_enums)] #[derive(Serialize, Deserialize)]
207pub enum ExplicitOrAuto<T: NotAutoValue> {
208 #[default]
210 #[serde(rename = "auto")]
211 Auto,
212 #[serde(untagged)]
214 Explicit(T),
215}
216
217impl<T: NotAutoValue> ExplicitOrAuto<T> {
218 pub fn into_value(self) -> Option<T> {
230 match self {
231 ExplicitOrAuto::Auto => None,
232 ExplicitOrAuto::Explicit(v) => Some(v),
233 }
234 }
235
236 pub fn as_value(&self) -> Option<&T> {
240 match self {
241 ExplicitOrAuto::Auto => None,
242 ExplicitOrAuto::Explicit(v) => Some(v),
243 }
244 }
245
246 pub fn map<U: NotAutoValue>(self, f: impl FnOnce(T) -> U) -> ExplicitOrAuto<U> {
249 match self {
250 Self::Auto => ExplicitOrAuto::Auto,
251 Self::Explicit(x) => ExplicitOrAuto::Explicit(f(x)),
252 }
253 }
254}
255
256impl<T> From<T> for ExplicitOrAuto<T>
257where
258 T: NotAutoValue,
259{
260 fn from(x: T) -> Self {
261 Self::Explicit(x)
262 }
263}
264
265pub trait NotAutoValue {}
272
273macro_rules! impl_not_auto_value_for_types {
275 ($($ty:ty)*) => {
276 $(impl_not_auto_value!($ty);)*
277 }
278}
279
280impl_not_auto_value_for_types!(
282 i8 i16 i32 i64 i128 isize
283 u8 u16 u32 u64 u128 usize
284 f32 f64
285 char
286 bool
287);
288
289use tor_basic_utils::ByteQty;
290impl_not_auto_value!(ByteQty);
291
292#[derive(Clone, Copy, Hash, Debug, Ord, PartialOrd, Eq, PartialEq)]
306#[allow(clippy::exhaustive_enums)] #[derive(Serialize, Deserialize)]
308#[serde(try_from = "PaddingLevelSerde", into = "PaddingLevelSerde")]
309#[derive(Display, EnumString, IntoStaticStr)]
310#[strum(serialize_all = "snake_case")]
311#[derive(Default)]
312pub enum PaddingLevel {
313 None,
315 Reduced,
317 #[default]
319 Normal,
320}
321
322#[derive(Serialize, Deserialize)]
324#[serde(untagged)]
325enum PaddingLevelSerde {
326 String(Cow<'static, str>),
330 Bool(bool),
332}
333
334impl From<PaddingLevel> for PaddingLevelSerde {
335 fn from(pl: PaddingLevel) -> PaddingLevelSerde {
336 PaddingLevelSerde::String(<&str>::from(&pl).into())
337 }
338}
339
340#[derive(thiserror::Error, Debug, Clone)]
342#[non_exhaustive]
343#[error("Invalid padding level")]
344struct InvalidPaddingLevel {}
345
346impl TryFrom<PaddingLevelSerde> for PaddingLevel {
347 type Error = InvalidPaddingLevel;
348
349 fn try_from(pls: PaddingLevelSerde) -> Result<PaddingLevel, Self::Error> {
350 Ok(match pls {
351 PaddingLevelSerde::String(s) => {
352 s.as_ref().try_into().map_err(|_| InvalidPaddingLevel {})?
353 }
354 PaddingLevelSerde::Bool(false) => PaddingLevel::None,
355 PaddingLevelSerde::Bool(true) => PaddingLevel::Normal,
356 })
357 }
358}
359
360#[cfg(test)]
361mod test {
362 #![allow(clippy::bool_assert_comparison)]
364 #![allow(clippy::clone_on_copy)]
365 #![allow(clippy::dbg_macro)]
366 #![allow(clippy::mixed_attributes_style)]
367 #![allow(clippy::print_stderr)]
368 #![allow(clippy::print_stdout)]
369 #![allow(clippy::single_char_pattern)]
370 #![allow(clippy::unwrap_used)]
371 #![allow(clippy::unchecked_time_subtraction)]
372 #![allow(clippy::useless_vec)]
373 #![allow(clippy::needless_pass_by_value)]
374 #![allow(clippy::string_slice)] use super::*;
377
378 #[derive(Debug, Default, Deserialize, Serialize)]
379 struct TestConfigFile {
380 #[serde(default)]
381 something_enabled: BoolOrAuto,
382
383 #[serde(default)]
384 padding: PaddingLevel,
385
386 #[serde(default)]
387 auto_or_usize: ExplicitOrAuto<usize>,
388
389 #[serde(default)]
390 auto_or_bool: ExplicitOrAuto<bool>,
391 }
392
393 #[test]
394 fn bool_or_auto() {
395 use BoolOrAuto as BoA;
396
397 let chk = |pl, s| {
398 let tc: TestConfigFile = toml::from_str(s).expect(s);
399 assert_eq!(pl, tc.something_enabled, "{:?}", s);
400 };
401
402 chk(BoA::Auto, "");
403 chk(BoA::Auto, r#"something_enabled = "auto""#);
404 chk(BoA::Explicit(true), r#"something_enabled = true"#);
405 chk(BoA::Explicit(true), r#"something_enabled = "true""#);
406 chk(BoA::Explicit(false), r#"something_enabled = false"#);
407 chk(BoA::Explicit(false), r#"something_enabled = "false""#);
408
409 let chk_e = |s| {
410 let tc: Result<TestConfigFile, _> = toml::from_str(s);
411 let _ = tc.expect_err(s);
412 };
413
414 chk_e(r#"something_enabled = 1"#);
415 chk_e(r#"something_enabled = "unknown""#);
416 chk_e(r#"something_enabled = "True""#);
417 }
418
419 #[test]
420 fn padding_level() {
421 use PaddingLevel as PL;
422
423 let chk = |pl, s| {
424 let tc: TestConfigFile = toml::from_str(s).expect(s);
425 assert_eq!(pl, tc.padding, "{:?}", s);
426 };
427
428 chk(PL::None, r#"padding = "none""#);
429 chk(PL::None, r#"padding = false"#);
430 chk(PL::Reduced, r#"padding = "reduced""#);
431 chk(PL::Normal, r#"padding = "normal""#);
432 chk(PL::Normal, r#"padding = true"#);
433 chk(PL::Normal, "");
434
435 let chk_e = |s| {
436 let tc: Result<TestConfigFile, _> = toml::from_str(s);
437 let _ = tc.expect_err(s);
438 };
439
440 chk_e(r#"padding = 1"#);
441 chk_e(r#"padding = "unknown""#);
442 chk_e(r#"padding = "Normal""#);
443 }
444
445 #[test]
446 fn explicit_or_auto() {
447 use ExplicitOrAuto as EOA;
448
449 let chk = |eoa: EOA<usize>, s| {
450 let tc: TestConfigFile = toml::from_str(s).expect(s);
451 assert_eq!(
452 format!("{:?}", eoa),
453 format!("{:?}", tc.auto_or_usize),
454 "{:?}",
455 s
456 );
457 };
458
459 chk(EOA::Auto, r#"auto_or_usize = "auto""#);
460 chk(EOA::Explicit(20), r#"auto_or_usize = 20"#);
461
462 let chk_e = |s| {
463 let tc: Result<TestConfigFile, _> = toml::from_str(s);
464 let _ = tc.expect_err(s);
465 };
466
467 chk_e(r#"auto_or_usize = """#);
468 chk_e(r#"auto_or_usize = []"#);
469 chk_e(r#"auto_or_usize = {}"#);
470
471 let chk = |eoa: EOA<bool>, s| {
472 let tc: TestConfigFile = toml::from_str(s).expect(s);
473 assert_eq!(
474 format!("{:?}", eoa),
475 format!("{:?}", tc.auto_or_bool),
476 "{:?}",
477 s
478 );
479 };
480
481 chk(EOA::Auto, r#"auto_or_bool = "auto""#);
483 chk(EOA::Explicit(false), r#"auto_or_bool = false"#);
484
485 chk_e(r#"auto_or_bool= "not bool or auto""#);
486
487 let mut config = TestConfigFile::default();
488 let toml = toml::to_string(&config).unwrap();
489 assert_eq!(
490 toml,
491 r#"something_enabled = "auto"
492padding = "normal"
493auto_or_usize = "auto"
494auto_or_bool = "auto"
495"#
496 );
497
498 config.auto_or_bool = ExplicitOrAuto::Explicit(true);
499 let toml = toml::to_string(&config).unwrap();
500 assert_eq!(
501 toml,
502 r#"something_enabled = "auto"
503padding = "normal"
504auto_or_usize = "auto"
505auto_or_bool = true
506"#
507 );
508 }
509}