tuning 0.4.0

ansible-like tool with a smaller scope, focused primarily on complementing dotfiles for cross-machine bliss
use std::{num::NonZeroU32, time::Duration};

use serde::{
    de::{Deserializer, Error as SerdeDeError},
    Deserialize,
};
use toml::value::Value;

pub(crate) fn into_bool<'de, D>(deserializer: D) -> std::result::Result<bool, D::Error>
where
    D: Deserializer<'de>,
{
    let value = match toml::value::Value::deserialize(deserializer)? {
        Value::Boolean(b) => b,
        Value::String(s) => s.parse::<bool>().map_err(SerdeDeError::custom)?,
        _ => {
            return Err(SerdeDeError::custom(
                "must provide a boolean or string to convert to a boolean",
            ));
        }
    };

    Ok(value)
}

pub(crate) fn into_option_bool<'de, D>(
    deserializer: D,
) -> std::result::Result<Option<bool>, D::Error>
where
    D: Deserializer<'de>,
{
    let value = into_bool(deserializer)?;
    Ok(Some(value))
}

pub(crate) fn into_option_duration_ms<'de, D>(
    deserializer: D,
) -> std::result::Result<Option<Duration>, D::Error>
where
    D: Deserializer<'de>,
{
    let value = match toml::value::Value::deserialize(deserializer)? {
        Value::Integer(i) => u64::try_from(i).map_err(SerdeDeError::custom)?,
        Value::String(s) => s.parse::<u64>().map_err(SerdeDeError::custom)?,
        _ => {
            return Err(SerdeDeError::custom(
                "must provide an integer or string to convert to a duration in milliseconds",
            ));
        }
    };

    Ok(Some(Duration::from_millis(value)))
}

pub(crate) fn into_option_nonzero_u32<'de, D>(
    deserializer: D,
) -> std::result::Result<Option<NonZeroU32>, D::Error>
where
    D: Deserializer<'de>,
{
    let value = match toml::value::Value::deserialize(deserializer)? {
        Value::Integer(i) => {
            let unsigned = u32::try_from(i).map_err(SerdeDeError::custom)?;
            NonZeroU32::new(unsigned)
                .ok_or_else(|| SerdeDeError::custom("must provide a positive integer"))?
        }
        Value::String(s) => s.parse::<NonZeroU32>().map_err(SerdeDeError::custom)?,
        _ => {
            return Err(SerdeDeError::custom(
                "must provide an integer or string to convert to a duration",
            ));
        }
    };

    Ok(Some(value))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn into_option_bool_from_string() {
        let input = Value::String(String::from("true"));

        let got = into_option_bool(input);

        assert_eq!(got, Ok(Some(true)));
    }

    #[test]
    fn into_option_bool_from_invalid_string() {
        let input = Value::String(String::from("foo"));

        let got = into_option_bool(input);

        assert!(got.is_err());
    }

    #[test]
    fn into_option_bool_from_bool() {
        let input = Value::Boolean(true);

        let got = into_option_bool(input);

        assert_eq!(got, Ok(Some(true)));
    }

    #[test]
    fn into_option_duration_ms_from_string() {
        let input = Value::String(String::from("123"));

        let got = into_option_duration_ms(input);

        assert_eq!(got, Ok(Some(Duration::from_millis(123))));
    }

    #[test]
    fn into_option_duration_ms_from_invalid_string() {
        let input = Value::String(String::from("foo"));

        let got = into_option_duration_ms(input);

        assert!(got.is_err());
    }

    #[test]
    fn into_option_duration_ms_from_i64() {
        let input = Value::Integer(123);

        let got = into_option_duration_ms(input);

        assert_eq!(got, Ok(Some(Duration::from_millis(123))));
    }

    #[test]
    fn into_option_nonzero_u32_from_string() {
        let input = Value::String(String::from("123"));

        let got = into_option_nonzero_u32(input);

        assert_eq!(got, Ok(NonZeroU32::new(123)));
    }

    #[test]
    fn into_option_nonzero_u32_from_invalid_string() {
        let input = Value::String(String::from("foo"));

        let got = into_option_nonzero_u32(input);

        assert!(got.is_err());
    }

    #[test]
    fn into_option_nonzero_u32_from_i64() {
        let input = Value::Integer(123);

        let got = into_option_nonzero_u32(input);

        assert_eq!(got, Ok(NonZeroU32::new(123)));
    }
}