use std::borrow::Cow;
use std::fmt::Debug;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, IntoStaticStr};
#[derive(Clone, Copy, Hash, Debug, Default, Ord, PartialOrd, Eq, PartialEq)]
#[allow(clippy::exhaustive_enums)] #[derive(Serialize, Deserialize)]
#[serde(try_from = "BoolOrAutoSerde", into = "BoolOrAutoSerde")]
pub enum BoolOrAuto {
#[default]
Auto,
Explicit(bool),
}
impl BoolOrAuto {
pub fn as_bool(self) -> Option<bool> {
match self {
BoolOrAuto::Auto => None,
BoolOrAuto::Explicit(v) => Some(v),
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum BoolOrAutoSerde {
String(Cow<'static, str>),
Bool(bool),
}
impl From<BoolOrAuto> for BoolOrAutoSerde {
fn from(boa: BoolOrAuto) -> BoolOrAutoSerde {
use BoolOrAutoSerde as BoAS;
boa.as_bool()
.map(BoAS::Bool)
.unwrap_or_else(|| BoAS::String("auto".into()))
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
#[error(r#"Invalid value, expected boolean or "auto""#)]
pub struct InvalidBoolOrAuto {}
impl TryFrom<BoolOrAutoSerde> for BoolOrAuto {
type Error = InvalidBoolOrAuto;
fn try_from(pls: BoolOrAutoSerde) -> Result<BoolOrAuto, Self::Error> {
use BoolOrAuto as BoA;
use BoolOrAutoSerde as BoAS;
Ok(match pls {
BoAS::Bool(v) => BoA::Explicit(v),
BoAS::String(s) if s == "false" => BoA::Explicit(false),
BoAS::String(s) if s == "true" => BoA::Explicit(true),
BoAS::String(s) if s == "auto" => BoA::Auto,
_ => return Err(InvalidBoolOrAuto {}),
})
}
}
#[macro_export]
macro_rules! impl_not_auto_value {
($ty:ty) => {
$crate::deps::paste! {
impl $crate::NotAutoValue for $ty {}
#[cfg(test)]
#[allow(non_snake_case)]
mod [<test_not_auto_value_ $ty>] {
#[allow(unused_imports)]
use super::*;
#[test]
fn [<auto_is_not_a_valid_value_for_ $ty>]() {
let res = $crate::deps::serde_value::Value::String(
"auto".into()
).deserialize_into::<$ty>();
assert!(
res.is_err(),
concat!(
stringify!($ty), " is not a valid NotAutoValue type: ",
"NotAutoValue types should not be deserializable from \"auto\""
),
);
}
}
}
};
}
#[derive(Clone, Copy, Hash, Debug, Default, Ord, PartialOrd, Eq, PartialEq)]
#[allow(clippy::exhaustive_enums)] #[derive(Serialize, Deserialize)]
pub enum ExplicitOrAuto<T: NotAutoValue> {
#[default]
#[serde(rename = "auto")]
Auto,
#[serde(untagged)]
Explicit(T),
}
impl<T: NotAutoValue> ExplicitOrAuto<T> {
pub fn into_value(self) -> Option<T> {
match self {
ExplicitOrAuto::Auto => None,
ExplicitOrAuto::Explicit(v) => Some(v),
}
}
pub fn as_value(&self) -> Option<&T> {
match self {
ExplicitOrAuto::Auto => None,
ExplicitOrAuto::Explicit(v) => Some(v),
}
}
pub fn map<U: NotAutoValue>(self, f: impl FnOnce(T) -> U) -> ExplicitOrAuto<U> {
match self {
Self::Auto => ExplicitOrAuto::Auto,
Self::Explicit(x) => ExplicitOrAuto::Explicit(f(x)),
}
}
}
impl<T> From<T> for ExplicitOrAuto<T>
where
T: NotAutoValue,
{
fn from(x: T) -> Self {
Self::Explicit(x)
}
}
pub trait NotAutoValue {}
macro_rules! impl_not_auto_value_for_types {
($($ty:ty)*) => {
$(impl_not_auto_value!($ty);)*
}
}
impl_not_auto_value_for_types!(
i8 i16 i32 i64 i128 isize
u8 u16 u32 u64 u128 usize
f32 f64
char
bool
);
use tor_basic_utils::ByteQty;
impl_not_auto_value!(ByteQty);
#[derive(Clone, Copy, Hash, Debug, Ord, PartialOrd, Eq, PartialEq)]
#[allow(clippy::exhaustive_enums)] #[derive(Serialize, Deserialize)]
#[serde(try_from = "PaddingLevelSerde", into = "PaddingLevelSerde")]
#[derive(Display, EnumString, IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
#[derive(Default)]
pub enum PaddingLevel {
None,
Reduced,
#[default]
Normal,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum PaddingLevelSerde {
String(Cow<'static, str>),
Bool(bool),
}
impl From<PaddingLevel> for PaddingLevelSerde {
fn from(pl: PaddingLevel) -> PaddingLevelSerde {
PaddingLevelSerde::String(<&str>::from(&pl).into())
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
#[error("Invalid padding level")]
struct InvalidPaddingLevel {}
impl TryFrom<PaddingLevelSerde> for PaddingLevel {
type Error = InvalidPaddingLevel;
fn try_from(pls: PaddingLevelSerde) -> Result<PaddingLevel, Self::Error> {
Ok(match pls {
PaddingLevelSerde::String(s) => {
s.as_ref().try_into().map_err(|_| InvalidPaddingLevel {})?
}
PaddingLevelSerde::Bool(false) => PaddingLevel::None,
PaddingLevelSerde::Bool(true) => PaddingLevel::Normal,
})
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
#[derive(Debug, Default, Deserialize, Serialize)]
struct TestConfigFile {
#[serde(default)]
something_enabled: BoolOrAuto,
#[serde(default)]
padding: PaddingLevel,
#[serde(default)]
auto_or_usize: ExplicitOrAuto<usize>,
#[serde(default)]
auto_or_bool: ExplicitOrAuto<bool>,
}
#[test]
fn bool_or_auto() {
use BoolOrAuto as BoA;
let chk = |pl, s| {
let tc: TestConfigFile = toml::from_str(s).expect(s);
assert_eq!(pl, tc.something_enabled, "{:?}", s);
};
chk(BoA::Auto, "");
chk(BoA::Auto, r#"something_enabled = "auto""#);
chk(BoA::Explicit(true), r#"something_enabled = true"#);
chk(BoA::Explicit(true), r#"something_enabled = "true""#);
chk(BoA::Explicit(false), r#"something_enabled = false"#);
chk(BoA::Explicit(false), r#"something_enabled = "false""#);
let chk_e = |s| {
let tc: Result<TestConfigFile, _> = toml::from_str(s);
let _ = tc.expect_err(s);
};
chk_e(r#"something_enabled = 1"#);
chk_e(r#"something_enabled = "unknown""#);
chk_e(r#"something_enabled = "True""#);
}
#[test]
fn padding_level() {
use PaddingLevel as PL;
let chk = |pl, s| {
let tc: TestConfigFile = toml::from_str(s).expect(s);
assert_eq!(pl, tc.padding, "{:?}", s);
};
chk(PL::None, r#"padding = "none""#);
chk(PL::None, r#"padding = false"#);
chk(PL::Reduced, r#"padding = "reduced""#);
chk(PL::Normal, r#"padding = "normal""#);
chk(PL::Normal, r#"padding = true"#);
chk(PL::Normal, "");
let chk_e = |s| {
let tc: Result<TestConfigFile, _> = toml::from_str(s);
let _ = tc.expect_err(s);
};
chk_e(r#"padding = 1"#);
chk_e(r#"padding = "unknown""#);
chk_e(r#"padding = "Normal""#);
}
#[test]
fn explicit_or_auto() {
use ExplicitOrAuto as EOA;
let chk = |eoa: EOA<usize>, s| {
let tc: TestConfigFile = toml::from_str(s).expect(s);
assert_eq!(
format!("{:?}", eoa),
format!("{:?}", tc.auto_or_usize),
"{:?}",
s
);
};
chk(EOA::Auto, r#"auto_or_usize = "auto""#);
chk(EOA::Explicit(20), r#"auto_or_usize = 20"#);
let chk_e = |s| {
let tc: Result<TestConfigFile, _> = toml::from_str(s);
let _ = tc.expect_err(s);
};
chk_e(r#"auto_or_usize = """#);
chk_e(r#"auto_or_usize = []"#);
chk_e(r#"auto_or_usize = {}"#);
let chk = |eoa: EOA<bool>, s| {
let tc: TestConfigFile = toml::from_str(s).expect(s);
assert_eq!(
format!("{:?}", eoa),
format!("{:?}", tc.auto_or_bool),
"{:?}",
s
);
};
chk(EOA::Auto, r#"auto_or_bool = "auto""#);
chk(EOA::Explicit(false), r#"auto_or_bool = false"#);
chk_e(r#"auto_or_bool= "not bool or auto""#);
let mut config = TestConfigFile::default();
let toml = toml::to_string(&config).unwrap();
assert_eq!(
toml,
r#"something_enabled = "auto"
padding = "normal"
auto_or_usize = "auto"
auto_or_bool = "auto"
"#
);
config.auto_or_bool = ExplicitOrAuto::Explicit(true);
let toml = toml::to_string(&config).unwrap();
assert_eq!(
toml,
r#"something_enabled = "auto"
padding = "normal"
auto_or_usize = "auto"
auto_or_bool = true
"#
);
}
}