claptrap 0.3.0

Bring the power of Clap to shell scripts.
Documentation
use serde::de::value::BorrowedStrDeserializer;
use serde::Deserialize;

#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum ValueParser {
    Bool,
    Boolish,
    Falsey,
    String,
    U8,
    U16,
    U32,
    U64,
    U128,
    Usize,
    I8,
    I16,
    I32,
    I64,
    I128,
    Isize,
    F32,
    F64,
    PossibleValues(Vec<PossibleValue>),
}

impl<'de> serde::Deserialize<'de> for ValueParser {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        fn parse_typed_value_parser(value: &str) -> Option<TypedValueParser> {
            value
                .strip_prefix(':')
                .and_then(|s| s.strip_suffix(':'))
                .and_then(|inner| {
                    TypedValueParser::deserialize(
                        BorrowedStrDeserializer::<serde::de::value::Error>::new(inner),
                    )
                    .ok()
                })
        }
        struct ValueParserVisitor;
        impl<'de> serde::de::Visitor<'de> for ValueParserVisitor {
            type Value = ValueParser;

            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                formatter.write_str("a value parser type")
            }

            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match parse_typed_value_parser(value) {
                    Some(tv) => Ok(ValueParser::from(tv)),
                    None => Ok(ValueParser::PossibleValues(vec![PossibleValue::from(
                        value.to_string(),
                    )])),
                }
            }

            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
            where
                A: serde::de::SeqAccess<'de>,
            {
                let mut values = Vec::new();
                while let Some(value) = seq.next_element::<PossibleValue>()? {
                    values.push(value);
                }
                match values.as_slice() {
                    [PossibleValue::Value(pv)] => match parse_typed_value_parser(pv) {
                        Some(tv) => Ok(ValueParser::from(tv)),
                        None => Ok(ValueParser::PossibleValues(values)),
                    },
                    _ => Ok(ValueParser::PossibleValues(values)),
                }
            }
        }
        deserializer.deserialize_any(ValueParserVisitor)
    }
}

impl From<ValueParser> for clap::builder::ValueParser {
    fn from(value_parser: ValueParser) -> Self {
        match value_parser {
            ValueParser::Bool => Self::new(clap::builder::BoolValueParser::new()),
            ValueParser::Boolish => Self::new(clap::builder::BoolishValueParser::new()),
            ValueParser::Falsey => Self::new(clap::builder::FalseyValueParser::new()),
            ValueParser::String => Self::new(clap::builder::StringValueParser::new()),
            ValueParser::U8 => clap::value_parser!(u8).into(),
            ValueParser::U16 => clap::value_parser!(u16).into(),
            ValueParser::U32 => clap::value_parser!(u32).into(),
            ValueParser::U64 => clap::value_parser!(u64).into(),
            ValueParser::U128 => clap::value_parser!(u128).into(),
            ValueParser::Usize => clap::value_parser!(usize).into(),
            ValueParser::I8 => clap::value_parser!(i8).into(),
            ValueParser::I16 => clap::value_parser!(i16).into(),
            ValueParser::I32 => clap::value_parser!(i32).into(),
            ValueParser::I64 => clap::value_parser!(i64).into(),
            ValueParser::I128 => clap::value_parser!(i128).into(),
            ValueParser::Isize => clap::value_parser!(isize).into(),
            ValueParser::F32 => clap::value_parser!(f32).into(),
            ValueParser::F64 => clap::value_parser!(f64).into(),
            ValueParser::PossibleValues(values) => {
                let pvs = values
                    .into_iter()
                    .map(clap::builder::PossibleValue::from)
                    .collect::<Vec<_>>();
                Self::new(clap::builder::PossibleValuesParser::new(pvs))
            }
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
#[non_exhaustive]
pub enum PossibleValue {
    Value(String),
    #[serde(rename_all = "kebab-case")]
    Detailed {
        name: String,
        help: Option<String>,
        alias: Option<String>,
        aliases: Option<Vec<String>>,
        hide: Option<bool>,
    },
}

impl From<PossibleValue> for clap::builder::PossibleValue {
    fn from(value: PossibleValue) -> Self {
        match value {
            PossibleValue::Value(v) => Self::new(v),
            PossibleValue::Detailed {
                name,
                help,
                alias,
                aliases,
                hide,
            } => {
                let mut pv = Self::new(name);
                if let Some(help) = help {
                    pv = pv.help(help);
                }
                if let Some(alias) = alias {
                    pv = pv.alias(alias);
                }
                if let Some(aliases) = aliases {
                    pv = pv.aliases(aliases);
                }
                if let Some(hide) = hide {
                    pv = pv.hide(hide);
                }
                pv
            }
        }
    }
}

impl From<String> for PossibleValue {
    fn from(value: String) -> Self {
        Self::Value(value)
    }
}

impl From<&str> for PossibleValue {
    fn from(value: &str) -> Self {
        Self::Value(value.to_string())
    }
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
enum TypedValueParser {
    Bool,
    Boolish,
    Falsey,
    String,
    U8,
    U16,
    U32,
    U64,
    U128,
    Usize,
    I8,
    I16,
    I32,
    I64,
    I128,
    Isize,
    F32,
    F64,
}

impl From<TypedValueParser> for ValueParser {
    fn from(tv: TypedValueParser) -> Self {
        match tv {
            TypedValueParser::Bool => Self::Bool,
            TypedValueParser::Boolish => Self::Boolish,
            TypedValueParser::Falsey => Self::Falsey,
            TypedValueParser::String => Self::String,
            TypedValueParser::U8 => Self::U8,
            TypedValueParser::U16 => Self::U16,
            TypedValueParser::U32 => Self::U32,
            TypedValueParser::U64 => Self::U64,
            TypedValueParser::U128 => Self::U128,
            TypedValueParser::Usize => Self::Usize,
            TypedValueParser::I8 => Self::I8,
            TypedValueParser::I16 => Self::I16,
            TypedValueParser::I32 => Self::I32,
            TypedValueParser::I64 => Self::I64,
            TypedValueParser::I128 => Self::I128,
            TypedValueParser::Isize => Self::Isize,
            TypedValueParser::F32 => Self::F32,
            TypedValueParser::F64 => Self::F64,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use test_case::test_case;
    #[test_case(serde_json::json!(":bool:"), Ok(ValueParser::Bool); "bool magic")]
    #[test_case(serde_json::json!(":boolish:"), Ok(ValueParser::Boolish); "boolish magic")]
    #[test_case(serde_json::json!(":falsey:"), Ok(ValueParser::Falsey); "falsey magic")]
    #[test_case(serde_json::json!(":string:"), Ok(ValueParser::String); "string magic")]
    #[test_case(serde_json::json!(":u8:"), Ok(ValueParser::U8); "u8 magic")]
    #[test_case(serde_json::json!(":u16:"), Ok(ValueParser::U16); "u16 magic")]
    #[test_case(serde_json::json!(":u32:"), Ok(ValueParser::U32); "u32 magic")]
    #[test_case(serde_json::json!(":u64:"), Ok(ValueParser::U64); "u64 magic")]
    #[test_case(serde_json::json!(":u128:"), Ok(ValueParser::U128); "u128 magic")]
    #[test_case(serde_json::json!(":usize:"), Ok(ValueParser::Usize); "usize magic")]
    #[test_case(serde_json::json!(":i8:"), Ok(ValueParser::I8); "i8 magic")]
    #[test_case(serde_json::json!(":i16:"), Ok(ValueParser::I16); "i16 magic")]
    #[test_case(serde_json::json!(":i32:"), Ok(ValueParser::I32); "i32 magic")]
    #[test_case(serde_json::json!(":i64:"), Ok(ValueParser::I64); "i64 magic")]
    #[test_case(serde_json::json!(":i128:"), Ok(ValueParser::I128); "i128 magic")]
    #[test_case(serde_json::json!(":isize:"), Ok(ValueParser::Isize); "isize magic")]
    #[test_case(serde_json::json!(":f32:"), Ok(ValueParser::F32); "f32 magic")]
    #[test_case(serde_json::json!(":f64:"), Ok(ValueParser::F64); "f64 magic")]
    #[test_case(serde_json::json!([":bool:"]), Ok(ValueParser::Bool); "bool magic list")]
    #[test_case(serde_json::json!([":boolish:"]), Ok(ValueParser::Boolish); "boolish magic list")]
    #[test_case(serde_json::json!([":falsey:"]), Ok(ValueParser::Falsey); "falsey magic list")]
    #[test_case(serde_json::json!([":string:"]), Ok(ValueParser::String); "string magic list")]
    #[test_case(
        serde_json::json!(["foo", "bar"]),
        Ok(ValueParser::PossibleValues(vec![PossibleValue::from("foo"), PossibleValue::from("bar")]));
        "possible values"
    )]
    #[test_case(
        serde_json::json!([":bool:"]),
        Ok(ValueParser::Bool);
        "known single magic possible value"
    )]
    #[test_case(
        serde_json::json!([":unknown:"]),
        Ok(ValueParser::PossibleValues(vec![PossibleValue::from(":unknown:")]));
        "unknown single magic possible value"
    )]
    #[test_case(
        serde_json::json!([":bool:", ":falsey:"]),
        Ok(ValueParser::PossibleValues(vec![PossibleValue::from(":bool:"), PossibleValue::from(":falsey:")]));
        "known multiple magic values"
    )]
    #[test_case(
        serde_json::json!([":falsey"]),
        Ok(ValueParser::PossibleValues(vec![PossibleValue::from(":falsey")]));
        "prefix possible value"
    )]
    #[test_case(
        serde_json::json!(["falsey:"]),
        Ok(ValueParser::PossibleValues(vec![PossibleValue::from("falsey:")]));
        "suffix possible value"
    )]
    #[test_case(
        serde_json::json!([{"name":"fast", "help":"speed"}]),
        Ok(ValueParser::PossibleValues(vec![PossibleValue::Detailed { name: "fast".into(), help: Some("speed".into()), alias: None, aliases: None, hide: None }]));
        "possible value struct with help"
    )]
    #[test_case(serde_json::json!(false), Err(serde::de::Error::custom("invalid type: boolean `false`, expected a value parser type")); "not a sequence")]
    fn test_value_parser(
        value: serde_json::Value,
        expected: Result<ValueParser, serde::de::value::Error>,
    ) {
        let actual: serde_json::Result<ValueParser> = serde_json::from_value(value);
        match (actual, expected) {
            (Ok(actual), Ok(expected)) => assert_eq!(actual, expected),
            (Err(e), Err(expected)) => assert_eq!(e.to_string(), expected.to_string()),
            _ => panic!("Unexpected result"),
        }
    }
}