reddit-rs 0.1.1

A wrapper around the Reddit API.
Documentation
use std::{collections::HashMap, fmt::Display, str::FromStr};

use chrono::{DateTime, Utc};
use iri_string::types::{IriAbsoluteString, IriRelativeString, IriString};
use mime::Mime;
use serde::{de::Error, Deserialize, Serialize};
use uuid::Uuid;

pub use iri_string;

use crate::models::{author::Author, award::{Awarding, TopAwardedType}, color::Color, comment::Comment, flair::{FlairTextColor, FlairType}, fullname::FullName, guilding::Gildings, media::{Media, MediaEmbed}, richtext::Richtext};

#[allow(clippy::struct_excessive_bools)] // reddit api sucks
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Link {
    pub approved_at_utc: Option<DateTime<Utc>>,

    pub selftext: String,

    #[serde(deserialize_with = "crate::utils::bool_from_int")]
    pub gilded: bool,
    pub title: String,
    pub pwls: Option<u32>,

    #[serde(flatten)]
    pub thumbnail: LinkThumbnail,

    #[serde(flatten)]
    pub post_link: PostLink,

    #[serde(flatten)]
    pub author: Author,

    #[serde(flatten)]
    pub flair: LinkFlair,

    #[serde(flatten)]
    pub subreddit_info: SubredditInfo,

    #[serde(flatten)]
    pub mod_info: ModInfo,

    pub allow_live_comments: bool,
    pub archived: bool,
    pub can_gild: bool,
    pub can_mod_post: bool,
    pub clicked: bool,
    pub contest_mode: bool,
    pub hidden: bool,
    pub hide_score: bool,
    pub is_created_from_ads_ui: bool,
    pub is_crosspostable: bool,
    pub is_meta: bool,
    pub is_original_content: bool,
    pub is_reddit_media_domain: bool,
    pub is_robot_indexable: bool,
    pub is_self: bool,
    pub is_video: bool,
    pub locked: bool,
    pub no_follow: bool,
    pub over_18: bool,
    pub pinned: bool,
    pub quarantine: bool,
    pub saved: bool,
    pub send_replies: bool,
    pub spoiler: bool,
    pub stickied: bool,
    pub visited: bool,

    pub name: FullName,

    pub ups: u32,
    pub downs: u32,
    pub upvote_ratio: f32,

    pub total_awards_received: u32,

    pub score: u32,
    pub num_comments: u32,
    pub num_crossposts: u32,

    pub media_metadata: Option<MediaMetadatas>,

    pub media_only: bool,
    pub media: Option<Media>,
    #[serde(deserialize_with = "crate::utils::object_empty_as_none")]
    pub media_embed: Option<MediaEmbed>,

    pub secure_media: Option<Media>,
    #[serde(deserialize_with = "crate::utils::object_empty_as_none")]
    pub secure_media_embed: Option<MediaEmbed>,

    pub user_reports: Vec<UserReport>,
    pub category: Option<()>,
    pub approved_by: Option<()>,
    #[serde(deserialize_with = "crate::utils::false_or_datetime")]
    pub edited: Option<DateTime<Utc>>,
    pub gildings: Gildings,
    pub content_categories: Option<Vec<ContentCategories>>,
    pub wls: Option<u32>,
    pub removed_by_category: Option<()>,
    pub banned_by: Option<()>,
    pub domain: String,
    pub selftext_html: Option<String>,
    pub likes: Option<()>,
    pub suggested_sort: Option<Sort>,
    pub banned_at_utc: Option<()>,
    pub view_count: Option<()>,

    // awards
    pub all_awardings: Vec<Awarding>,
    pub awarders: Vec<()>,
    pub top_awarded_type: Option<TopAwardedType>,

    pub treatment_tags: Vec<()>,
    pub removed_by: Option<()>,
    pub num_reports: Option<()>,
    /// "moderator"
    pub distinguished: Option<Distinguished>,
    pub removal_reason: Option<()>,
    /// REVIEW: base 36? "q4550i"
    pub id: String,
    pub report_reasons: Option<()>,
    pub discussion_type: Option<()>,

    pub whitelist_status: Option<WhitelistStatus>,
    pub parent_whitelist_status: Option<WhitelistStatus>,
    pub permalink: IriRelativeString,

    #[serde(deserialize_with = "crate::utils::integer_or_float_to_datetime")]
    pub created: DateTime<Utc>,
    #[serde(deserialize_with = "crate::utils::integer_or_float_to_datetime")]
    pub created_utc: DateTime<Utc>,

    /// REVIEW: Condense `post_hint` and `preview`?
    pub post_hint: Option<PostHint>,
    pub preview: Option<Preview>,

    pub url_overridden_by_dest: Option<IriString>,
    // #[serde(flatten)]
    // pub rest: HashMap<String, serde_json::value::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Distinguished {
    Moderator,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MediaMetadatas(HashMap<MediaMetadataId, MediaMetadata>);

#[derive(Debug, Clone, Serialize, PartialEq, Hash, Eq, PartialOrd, Ord)]
pub struct MediaMetadataId(u128);

impl<'de> Deserialize<'de> for MediaMetadataId {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let id = String::deserialize(deserializer)
            .map(|s| u128::from_str_radix(&s, 36).map_err(D::Error::custom))??;

        Ok(Self(id))
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MediaMetadata {
    status: MediaMetadataStatus,
    #[serde(rename = "e")]
    ty: MediaMetadataType,
    #[serde(rename = "m", with = "crate::utils::mime_serde")]
    mime_type: Mime,
    #[serde(rename = "p")]
    p: Vec<P>,
    #[serde(rename = "s")]
    s: P,
    #[serde(rename = "id")]
    id: MediaMetadataId,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct P {
    y: u16,
    x: u16,
    u: IriString,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
// #[serde(rename_all = "snake_case")]
pub enum MediaMetadataType {
    Image,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum MediaMetadataStatus {
    Valid,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum PostLink {
    Url {
        url: IriString,
    },
    Crosspost {
        #[serde(rename = "crosspost_parent")]
        parent: FullName,
        #[serde(rename = "crosspost_parent_list")]
        list: Vec<Link>,
        url: IriRelativeString,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ModInfo {
    #[serde(rename = "mod_reason_title")]
    pub reason_title: Option<String>,
    #[serde(rename = "mod_reports")]
    pub reports: Vec<()>,
    #[serde(rename = "mod_note")]
    pub note: Option<()>,
    #[serde(rename = "mod_reason_by")]
    pub reason_by: Option<()>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SubredditInfo {
    #[serde(rename = "subreddit")]
    pub name: String,
    #[serde(rename = "subreddit_name_prefixed")]
    pub name_prefixed: String,
    #[serde(rename = "subreddit_type")]
    pub ty: SubredditType,
    #[serde(rename = "subreddit_subscribers")]
    pub subscribers: u32,
    #[serde(rename = "subreddit_id")]
    pub id: FullName,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct LinkFlairTemplateId(Uuid);

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LinkFlair {
    #[serde(deserialize_with = "Color::from_hex_str")]
    #[serde(rename = "link_flair_background_color")]
    pub background_color: Option<Color>,

    #[serde(rename = "link_flair_template_id")]
    pub template_id: Option<LinkFlairTemplateId>,

    #[serde(rename = "link_flair_text_color")]
    pub text_color: FlairTextColor,

    #[serde(rename = "link_flair_text")]
    pub text: Option<String>,

    #[serde(rename = "link_flair_type")]
    pub ty: FlairType,

    #[serde(rename = "link_flair_richtext")]
    pub richtext: Vec<Richtext>,

    #[serde(rename = "link_flair_css_class")]
    pub css_class: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LinkThumbnail {
    #[serde(rename = "thumbnail")]
    pub link: LinkThumbnailType,
    #[serde(rename = "thumbnail_height")]
    pub height: Option<u16>,
    #[serde(rename = "thumbnail_width")]
    pub width: Option<u16>,
}

#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(untagged)]
pub enum LinkThumbnailType {
    /// thumbnail link is `self`
    This,
    Default,
    Nsfw,
    Spoiler,
    Url(IriAbsoluteString),
}

impl FromStr for LinkThumbnailType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "self" => Ok(Self::This),
            "default" => Ok(Self::Default),
            "nsfw" => Ok(Self::Nsfw),
            "spoiler" => Ok(Self::Spoiler),
            _ => Ok(Self::Url(IriAbsoluteString::from_str(s).map_err(
                |why| format!("invalid IRI or thumbnail type: {}, error: {}", s, why),
            )?)),
        }
    }
}

impl<'de> Deserialize<'de> for LinkThumbnailType {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        LinkThumbnailType::from_str(&String::deserialize(deserializer)?)
            .map_err(serde::de::Error::custom)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum PostHint {
    #[serde(rename = "image")]
    Image,

    #[serde(rename = "link")]
    Link,

    #[serde(rename = "gallery")]
    Gallery,

    #[serde(rename = "self")]
    This,

    #[serde(rename = "rich:video")]
    RichVideo,

    #[serde(rename = "hosted:video")]
    HostedVideo,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Preview {
    enabled: bool,
    images: Vec<Images>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Images {
    id: String,
    resolutions: Vec<Image>,
    source: Image,
    variants: Variants,
}

#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Variants {
    obfuscated: Option<Variant>,
    nsfw: Option<Variant>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Variant {
    resolutions: Vec<Image>,
    source: Image,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Image {
    height: u16,
    width: u16,
    url: IriAbsoluteString,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ContentCategories {
    Photography,
    DrawingAndPainting,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UserReport;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", content = "data")]
pub enum RedditListing {
    Listing {
        after: Option<FullName>,
        dist: Option<u32>,
        modhash: String,
        geo_filter: Option<String>,
        children: Vec<RedditListing>,
    },
    #[serde(rename = "t3")]
    Link(Box<Link>),
    #[serde(rename = "t1")]
    Comment(Box<Comment>),
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Sort {
    New,
    Top,
    Confidence,
}

impl Display for Sort {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Sort::New => "new",
            Sort::Top => "top",
            Sort::Confidence => "confidence",
        })
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SubredditType {
    Public,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum WhitelistStatus {
    AllAds,
    SomeAds,
    NoAds,
    PromoAdultNsfw,
}