refluxer 0.1.0

Rust API wrapper for Fluxer
Documentation
use serde::{Deserialize, Serialize, Serializer};

use crate::error::HttpError;
use crate::http::client::HttpClient;
use crate::http::routing::Route;
use crate::model::id::{AttachmentId, ChannelId, MessageId, UserId};

impl HttpClient {
    /// Save a message attachment or embed as a favorite meme.
    pub async fn create_meme_from_message(
        &self,
        channel_id: ChannelId,
        message_id: MessageId,
        params: &CreateFavoriteMemeFromMessage,
    ) -> Result<FavoriteMeme, HttpError> {
        self.request(
            Route::CreateMemeFromMessage {
                channel_id,
                message_id,
            },
            Some(params),
        )
        .await
    }

    /// List memes saved by the authenticated user.
    pub async fn list_favorite_memes(&self) -> Result<Vec<FavoriteMeme>, HttpError> {
        self.request_no_body(Route::ListFavoriteMemes).await
    }

    /// Save an image or video URL as a favorite meme.
    pub async fn create_meme_from_url(
        &self,
        params: &CreateFavoriteMemeFromUrl,
    ) -> Result<FavoriteMeme, HttpError> {
        self.request(Route::CreateMemeFromUrl, Some(params)).await
    }

    pub async fn get_favorite_meme(&self, meme_id: &str) -> Result<FavoriteMeme, HttpError> {
        self.request_no_body(Route::GetFavoriteMeme {
            meme_id: meme_id.into(),
        })
        .await
    }

    pub async fn update_favorite_meme(
        &self,
        meme_id: &str,
        params: &UpdateFavoriteMeme,
    ) -> Result<FavoriteMeme, HttpError> {
        self.request(
            Route::UpdateFavoriteMeme {
                meme_id: meme_id.into(),
            },
            Some(params),
        )
        .await
    }

    pub async fn delete_favorite_meme(&self, meme_id: &str) -> Result<(), HttpError> {
        self.request_empty(
            Route::DeleteFavoriteMeme {
                meme_id: meme_id.into(),
            },
            None::<&()>,
        )
        .await
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct FavoriteMeme {
    pub id: String,
    pub user_id: UserId,
    pub name: String,
    pub alt_text: Option<String>,
    pub tags: Vec<String>,
    pub attachment_id: AttachmentId,
    pub filename: String,
    pub content_type: String,
    pub content_hash: Option<String>,
    pub size: u64,
    pub width: Option<u64>,
    pub height: Option<u64>,
    pub duration: Option<f64>,
    pub url: String,
    pub is_gifv: Option<bool>,
    pub klipy_slug: Option<String>,
    pub tenor_slug_id: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct CreateFavoriteMemeFromMessage {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub alt_text: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tags: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attachment_id: Option<AttachmentId>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub embed_index: Option<u64>,
}

impl CreateFavoriteMemeFromMessage {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            alt_text: None,
            tags: None,
            attachment_id: None,
            embed_index: None,
        }
    }

    pub fn alt_text(mut self, alt_text: impl Into<String>) -> Self {
        self.alt_text = Some(alt_text.into());
        self
    }

    pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
        self.tags = Some(tags.into_iter().map(Into::into).collect());
        self
    }

    pub fn attachment_id(mut self, attachment_id: AttachmentId) -> Self {
        self.attachment_id = Some(attachment_id);
        self
    }

    pub fn embed_index(mut self, embed_index: u64) -> Self {
        self.embed_index = Some(embed_index);
        self
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct CreateFavoriteMemeFromUrl {
    pub url: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub alt_text: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tags: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub klipy_slug: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tenor_slug_id: Option<String>,
}

impl CreateFavoriteMemeFromUrl {
    pub fn new(url: impl Into<String>) -> Self {
        Self {
            url: url.into(),
            name: None,
            alt_text: None,
            tags: None,
            klipy_slug: None,
            tenor_slug_id: None,
        }
    }

    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    pub fn alt_text(mut self, alt_text: impl Into<String>) -> Self {
        self.alt_text = Some(alt_text.into());
        self
    }

    pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
        self.tags = Some(tags.into_iter().map(Into::into).collect());
        self
    }

    pub fn klipy_slug(mut self, slug: impl Into<String>) -> Self {
        self.klipy_slug = Some(slug.into());
        self
    }

    pub fn tenor_slug_id(mut self, slug_id: impl Into<String>) -> Self {
        self.tenor_slug_id = Some(slug_id.into());
        self
    }
}

#[derive(Debug, Default, Clone, Serialize)]
pub struct UpdateFavoriteMeme {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(skip_serializing_if = "NullableUpdate::is_unset")]
    alt_text: NullableUpdate<String>,
    #[serde(skip_serializing_if = "NullableUpdate::is_unset")]
    tags: NullableUpdate<Vec<String>>,
}

impl UpdateFavoriteMeme {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    pub fn alt_text(mut self, alt_text: impl Into<String>) -> Self {
        self.alt_text = NullableUpdate::Value(alt_text.into());
        self
    }

    pub fn clear_alt_text(mut self) -> Self {
        self.alt_text = NullableUpdate::Clear;
        self
    }

    pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
        self.tags = NullableUpdate::Value(tags.into_iter().map(Into::into).collect());
        self
    }

    pub fn clear_tags(mut self) -> Self {
        self.tags = NullableUpdate::Clear;
        self
    }
}

#[derive(Debug, Default, Clone)]
enum NullableUpdate<T> {
    #[default]
    Unset,
    Value(T),
    Clear,
}

impl<T> NullableUpdate<T> {
    fn is_unset(&self) -> bool {
        matches!(self, Self::Unset)
    }
}

impl<T: Serialize> Serialize for NullableUpdate<T> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            Self::Unset | Self::Clear => serializer.serialize_none(),
            Self::Value(value) => value.serialize(serializer),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::UpdateFavoriteMeme;
    use serde_json::json;

    #[test]
    fn update_favorite_meme_omits_unset_nullable_fields() {
        let payload = serde_json::to_value(UpdateFavoriteMeme::new().name("renamed")).unwrap();

        assert_eq!(payload, json!({ "name": "renamed" }));
    }

    #[test]
    fn update_favorite_meme_serializes_nullable_field_values() {
        let payload = serde_json::to_value(
            UpdateFavoriteMeme::new()
                .alt_text("description")
                .tags(["tag-a", "tag-b"]),
        )
        .unwrap();

        assert_eq!(
            payload,
            json!({
                "alt_text": "description",
                "tags": ["tag-a", "tag-b"],
            })
        );
    }

    #[test]
    fn update_favorite_meme_serializes_nullable_field_clears() {
        let payload =
            serde_json::to_value(UpdateFavoriteMeme::new().clear_alt_text().clear_tags()).unwrap();

        assert_eq!(
            payload,
            json!({
                "alt_text": null,
                "tags": null,
            })
        );
    }
}