reddit-rs 0.1.1

A wrapper around the Reddit API.
Documentation
use std::convert::TryInto;

use chrono::{DateTime, TimeZone, Utc};
use serde::{
    de::{Error, Unexpected, Visitor},
    Deserialize, Deserializer,
};

pub fn object_empty_as_none<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
    D: Deserializer<'de>,
    for<'a> T: Deserialize<'a>,
{
    #[derive(Deserialize, Debug)]
    #[serde(deny_unknown_fields)]
    struct Empty {}

    #[derive(Deserialize, Debug)]
    #[serde(untagged)]
    enum Aux<T> {
        T(T),
        Empty(Empty),
        Null,
    }

    match Deserialize::deserialize(deserializer)? {
        Aux::T(t) => Ok(Some(t)),
        Aux::Empty(_) | Aux::Null => Ok(None),
    }
}

pub fn false_or_datetime<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
    D: Deserializer<'de>,
{
    struct FalseOrDatetimeVisitor;

    impl Visitor<'_> for FalseOrDatetimeVisitor {
        type Value = Option<DateTime<Utc>>;

        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
            formatter.write_str("`false`, or a unix timestamp in seconds potentially followed by `.0`")
        }

        fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
        where
            E: Error,
        {
            #[allow(clippy::match_bool)]
            match v {
                false => Ok(None),
                true => Err(E::invalid_value(
                    Unexpected::Bool(true),
                    &"`false`, or a unix timestamp potentially followed by `.0`",
                )),
            }
        }

        fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
        where
            E: Error,
        {
            Ok(Some(Utc.timestamp(v.try_into().map_err(E::custom)?, 0)))
        }

        fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
        where
            E: Error,
        {
            self.visit_f64(f64::from(v))
        }

        fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
        where
            E: Error,
        {
            if v.fract() == 0.0 && v > 0.0 {
                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // checked above
                self.visit_u64(v as u64)
            } else {
                Err(E::invalid_value(
                    Unexpected::Float(v as _),
                    &"float with no fractional portion",
                ))
            }
        }
    }

    deserializer.deserialize_any(FalseOrDatetimeVisitor)
}

pub fn integer_or_float_to_datetime<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
    D: Deserializer<'de>,
{
    struct IntegerOrFloatToDatetimeVisitor;

    impl Visitor<'_> for IntegerOrFloatToDatetimeVisitor {
        type Value = DateTime<Utc>;

        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
            formatter.write_str("unix timestamp in seconds or a float with no fractional part")
        }

        fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
        where
            E: Error,
        {
            Ok(Utc.timestamp(v.try_into().map_err(E::custom)?, 0))
        }

        fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
        where
            E: Error,
        {
            self.visit_f64(f64::from(v))
        }

        fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
        where
            E: Error,
        {
            if v.fract() == 0.0 && v > 0.0 {
                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // checked above
                self.visit_u64(v as u64)
            } else {
                Err(E::invalid_value(
                    Unexpected::Float(v as _),
                    &"float with no fractional portion",
                ))
            }
        }
    }

    deserializer.deserialize_any(IntegerOrFloatToDatetimeVisitor)
}

pub(crate) fn bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
    D: Deserializer<'de>,
{
    match u8::deserialize(deserializer)? {
        0 => Ok(false),
        1 => Ok(true),
        other => Err(D::Error::invalid_value(
            Unexpected::Unsigned(u64::from(other)),
            &"zero or one",
        )),
    }
}

pub(crate) mod mime_serde {
    use std::str::FromStr;

    use mime::Mime;
    use serde::{de::Visitor, Deserializer, Serializer};

    pub(crate) fn serialize<S>(mime: &Mime, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(mime.as_ref())
    }

    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Mime, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct MimeVisitor;

        impl<'de> Visitor<'de> for MimeVisitor {
            type Value = Mime;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a valid MIME type")
            }

            fn visit_str<E>(self, value: &str) -> Result<Mime, E>
            where
                E: serde::de::Error,
            {
                Mime::from_str(value).map_err(E::custom)
            }
        }

        deserializer.deserialize_str(MimeVisitor)
    }
}