topgg 1.4.2

The official Rust wrapper for the Top.gg API
Documentation
use crate::Error;
use chrono::{DateTime, TimeZone, Utc};
use reqwest::Response;
use serde::{de::DeserializeOwned, Deserialize, Deserializer};

const DISCORD_EPOCH: u64 = 1_420_070_400_000;

macro_rules! debug_struct {
  (
    $(#[$struct_attr:meta])*
    $struct_name:ident {
      $(public {
        $(
          $(#[$pub_prop_attr:meta])*
          $pub_prop_name:ident: $pub_prop_type:ty,
        )*
      })?
      $(protected {
        $(
          $(#[$protected_prop_attr:meta])*
          $protected_prop_name:ident: $protected_prop_type:ty,
        )*
      })?
      $(private {
        $(
          $(#[$priv_prop_attr:meta])*
          $priv_prop_name:ident: $priv_prop_type:ty,
        )*
      })?
      $(getters($self:ident) {
        $(
          $(#[$getter_attr:meta])*
          $getter_name:ident: $getter_type:ty => $getter_code:tt
        )*
      })?
    }
  ) => {
    $(#[$struct_attr])*
    pub struct $struct_name {
      $($(
        $(#[$pub_prop_attr])*
        pub $pub_prop_name: $pub_prop_type,
      )*)?
      $($(
        $(#[$protected_prop_attr])*
        pub(crate) $protected_prop_name: $protected_prop_type,
      )*)?
      $($(
        $(#[$priv_prop_attr])*
        $priv_prop_name: $priv_prop_type,
      )*)?
    }

    $(impl $struct_name {
      $(
        $(#[$getter_attr])*
        pub fn $getter_name(&$self) -> $getter_type $getter_code
      )*
    })?

    impl std::fmt::Debug for $struct_name {
      fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
        fmt
          .debug_struct(stringify!($struct_name))
          $($(
            .field(stringify!($pub_prop_name), &self.$pub_prop_name)
          )*)?
          $($(
            .field(stringify!($getter_name), &self.$getter_name())
          )*)?
          .finish()
      }
    }
  };
}

pub(crate) use debug_struct;

#[inline(always)]
pub(crate) fn deserialize_optional_string<'de, D>(
  deserializer: D,
) -> Result<Option<String>, D::Error>
where
  D: Deserializer<'de>,
{
  Ok(match <String as Deserialize>::deserialize(deserializer) {
    Ok(s) => {
      if s.is_empty() {
        None
      } else {
        Some(s)
      }
    }
    _ => None,
  })
}

#[inline(always)]
pub(crate) fn deserialize_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
  T: Default + Deserialize<'de>,
  D: Deserializer<'de>,
{
  Option::deserialize(deserializer).map(|res| res.unwrap_or_default())
}

#[inline(always)]
pub(crate) fn get_creation_date(id: u64) -> DateTime<Utc> {
  Utc
    .timestamp_millis_opt(((id >> 22) + DISCORD_EPOCH) as _)
    .single()
    .unwrap()
}

#[inline(always)]
pub(crate) async fn parse_json<T>(response: Response) -> crate::Result<T>
where
  T: DeserializeOwned,
{
  if let Ok(bytes) = response.bytes().await {
    if let Ok(json) = serde_json::from_slice(&bytes) {
      return Ok(json);
    }
  }

  Err(Error::InternalServerError)
}

pub(crate) fn get_avatar(hash: &Option<String>, id: u64) -> String {
  match hash {
    Some(hash) => {
      let ext = if hash.starts_with("a_") { "gif" } else { "png" };

      format!("https://cdn.discordapp.com/avatars/{id}/{hash}.{ext}?size=1024")
    }
    _ => format!(
      "https://cdn.discordapp.com/embed/avatars/{}.png",
      (id >> 22) % 5
    ),
  }
}