claptrap 0.1.0

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

#[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<String>),
}

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![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::<String>()? {
                    values.push(value);
                }
                match values.as_slice() {
                    [magic] => match parse_typed_value_parser(magic) {
                        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) => {
                Self::new(clap::builder::PossibleValuesParser::new(values))
            }
        }
    }
}

#[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!["foo".to_string(), "bar".to_string()])); "possible values")]
    #[test_case(serde_json::json!([":unknown:"]), Ok(ValueParser::PossibleValues(vec![":unknown:".to_string()])); "unknown single magic possible value")]
    #[test_case(serde_json::json!([":bool:", ":falsey:"]), Ok(ValueParser::PossibleValues(vec![":bool:".to_string(), ":falsey:".to_string()])); "known multiple magic values")]
    #[test_case(serde_json::json!([":falsey"]), Ok(ValueParser::PossibleValues(vec![":falsey".to_string()])); "prefix possible value")]
    #[test_case(serde_json::json!(["falsey:"]), Ok(ValueParser::PossibleValues(vec!["falsey:".to_string()])); "suffix possible value")]
    #[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"),
        }
    }
}