use std::fmt;
use serde::de::value::MapAccessDeserializer;
use serde::de::{self, MapAccess, Unexpected, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Clone, Debug, Default, PartialEq)]
pub enum AutoOption<T> {
#[default]
Auto,
Disabled,
Explicit(T),
}
impl<T> AutoOption<T> {
#[must_use]
pub fn is_disabled(&self) -> bool {
matches!(self, Self::Disabled)
}
#[must_use]
pub fn is_auto(&self) -> bool {
matches!(self, Self::Auto)
}
#[must_use]
pub fn as_explicit(&self) -> Option<&T> {
match self {
Self::Explicit(v) => Some(v),
_ => None,
}
}
}
impl<T: Serialize> Serialize for AutoOption<T> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Auto => serializer.serialize_str("auto"),
Self::Disabled => serializer.serialize_str("disabled"),
Self::Explicit(cfg) => cfg.serialize(serializer),
}
}
}
impl<'de, T: Deserialize<'de>> Deserialize<'de> for AutoOption<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(AutoOptionVisitor(std::marker::PhantomData))
}
}
struct AutoOptionVisitor<T>(std::marker::PhantomData<T>);
impl<'de, T: Deserialize<'de>> Visitor<'de> for AutoOptionVisitor<T> {
type Value = AutoOption<T>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(r#"a string ("auto", "enabled", "disabled"), a boolean, or a map of settings"#)
}
fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
Ok(if v {
AutoOption::Auto
} else {
AutoOption::Disabled
})
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
match v {
"auto" | "default" | "enabled" => Ok(AutoOption::Auto),
"disabled" => Ok(AutoOption::Disabled),
_ => Err(E::invalid_value(Unexpected::Str(v), &self)),
}
}
fn visit_map<M: MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
let cfg = T::deserialize(MapAccessDeserializer::new(map))?;
Ok(AutoOption::Explicit(cfg))
}
}
#[cfg(feature = "unstable-schemas")]
impl<T: schemars::JsonSchema> schemars::JsonSchema for AutoOption<T> {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Owned(format!("AutoOption_{}", T::schema_name()))
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let inner = generator.subschema_for::<T>();
schemars::json_schema!({
"description": format!(
"Conversion configuration:\n\n\
- `\"auto\"`, `\"enabled\"` or boolean `true` - use defaults for this conversion\n\
- `\"disabled\"` or boolean `false` - disable this conversion\n\
- An object `{}` - explicit settings",
T::schema_name()
),
"oneOf": [
{
"type": "string",
"enum": ["auto","enabled"],
"description": "Use the feature with default settings."
},
{
"type": "string",
"enum": ["disabled"],
"description": "Disable the feature."
},
{
"type": "boolean",
"description": "true = auto (defaults), false = disabled."
},
inner,
]
})
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use serde::Deserialize;
use super::*;
#[serde_with::skip_serializing_none]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
struct DummyCfg {
#[serde(default)]
foo: Option<bool>,
#[serde(default)]
bar: Option<u32>,
}
#[rstest]
#[case("auto", AutoOption::Auto)]
#[case("default", AutoOption::Auto)]
#[case("true", AutoOption::Auto)]
#[case("enabled", AutoOption::Auto)]
#[case("disabled", AutoOption::Disabled)]
#[case("false", AutoOption::Disabled)]
fn parse_keyword(#[case] input: &str, #[case] expected: AutoOption<DummyCfg>) {
let v: AutoOption<DummyCfg> = serde_yaml::from_str(input).unwrap();
assert_eq!(v, expected);
}
#[test]
fn parse_explicit() {
let v: AutoOption<DummyCfg> = serde_yaml::from_str("foo: true\nbar: 42").unwrap();
assert_eq!(
v,
AutoOption::Explicit(DummyCfg {
foo: Some(true),
bar: Some(42),
})
);
}
#[rstest]
#[case("nope")]
#[case("42")]
fn parse_invalid(#[case] input: &str) {
assert!(serde_yaml::from_str::<AutoOption<DummyCfg>>(input).is_err());
}
#[rstest]
#[case::auto(AutoOption::<DummyCfg>::Auto)]
#[case::auto(AutoOption::<DummyCfg>::Disabled)]
#[case::auto(AutoOption::Explicit(DummyCfg {foo: Some(true),bar: None,}))]
fn serde_round_trip(#[case] v: AutoOption<DummyCfg>) {
let yaml = serde_yaml::to_string(&v).unwrap();
let parsed: AutoOption<DummyCfg> = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(v, parsed);
}
}