sfr-types 0.1.2

The crate has shared types in `slack-framework-rs`.
Documentation
//! The type that represents a request of a slash command.
//!
//! <https://api.slack.com/interactivity/slash-commands#app_command_handling>

use crate::{Block, Layouts, MessagePayloads};
use serde::{Deserialize, Serialize};

/// The type that represents a request of a slash command.
///
/// <https://api.slack.com/interactivity/slash-commands#app_command_handling>
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub struct SlashCommandBody {
    /// (Deprecated) This is a verification token, a deprecated feature that you shouldn't use any more.
    pub token: String,

    /// The command that was entered to trigger this request.
    pub command: String,

    /// This is the part of the slash command after the command itself, and it can contain absolutely anything the user might decide to type.
    pub text: String,

    /// A temporary [webhook URL](https://api.slack.com/messaging/webhooks) that you can use to [generate message responses](https://api.slack.com/interactivity/handling#message_responses).
    pub response_url: String,

    /// A short-lived ID that will allow your app to open [a modal](https://api.slack.com/surfaces/modals).
    pub trigger_id: String,

    /// The ID of the user who triggered the command.
    pub user_id: String,

    /// (Deprecated) The plain text name of the user who triggered the command.
    pub user_name: String,

    /// These IDs provide context about where the user was in Slack when they triggered your app's command (e.g. the workspace, Enterprise Grid, or channel).
    pub team_id: String,

    /// (Deprecated)
    pub team_domain: String,

    /// These IDs provide context about where the user was in Slack when they triggered your app's command (e.g. the workspace, Enterprise Grid, or channel).
    pub enterprise_id: Option<String>,

    /// (Deprecated)
    pub enterprise_name: Option<String>,

    /// These IDs provide context about where the user was in Slack when they triggered your app's command (e.g. the workspace, Enterprise Grid, or channel).
    pub channel_id: String,

    /// (Deprecated)
    pub channel_name: String,

    /// Your Slack app's unique identifier.
    pub api_app_id: String,

    /// `is_enterprise_install`
    pub is_enterprise_install: bool,
}

/// The response type for slash command. <https://api.slack.com/interactivity/slash-commands#responding_to_commands>
#[derive(Debug, Clone)]
pub enum SlashCommandResponse {
    /// no response
    Empty,

    /// as plain text
    String(String),

    /// complex response
    Layouts(Layouts),

    /// complex response and in channel
    LayoutsInChannel(Layouts),
}

impl SlashCommandResponse {
    /// A short-hand to create [`SlashCommandResponse::Empty`].
    pub fn empty() -> Self {
        Self::Empty
    }

    /// A short-hand to create [`SlashCommandResponse::String`].
    pub fn string(string: String) -> Self {
        Self::String(string)
    }
}

mod inner {
    //! The inner module to define serializing and deserializing.

    use super::*;
    use serde::ser::{Error as SerError, Serialize, Serializer};

    /// The response JSON type for slash command. <https://api.slack.com/interactivity/slash-commands#responding_to_commands>
    #[derive(Serialize)]
    struct TmpLayouts<'a> {
        /// The `response_type` parameter in the JSON payload controls this visibility; by default it is set to `ephemeral`, but you can specify a value of `in_channel` to post the response into the channel.
        ///
        /// <https://api.slack.com/interactivity/slash-commands#app_command_handling:~:text=range%20of%20possibilities.-,Message%20Visibility,-There%27s%20one%20special>
        #[serde(skip_serializing_if = "Option::is_none")]
        response_type: Option<SlashCommandResponseType>,

        /// Sending an immediate response.
        ///
        /// <https://api.slack.com/interactivity/slash-commands#responding_immediate_response>
        #[serde(flatten)]
        layouts: TmpLayoutsInner<'a>,
    }

    /// The inner type of [`TmpLayouts`].
    #[derive(Serialize)]
    #[serde(untagged)]
    enum TmpLayoutsInner<'a> {
        /// The type mapped to [`Layouts::SingleBlock`].
        Block {
            /// The `blocks` field.
            blocks: [&'a Block; 1],
        },

        /// The type mapped to [`Layouts::MultipleBlocks`].
        Blocks {
            /// The `blocks` field.
            blocks: &'a Vec<Block>,
        },

        /// The type mapped to [`Layouts::BlocksArray`].
        BlocksArray(&'a MessagePayloads),
    }

    /// `Message Visibility`: <https://api.slack.com/interactivity/slash-commands#command_payload_descriptions:~:text=range%20of%20possibilities.-,Message%20Visibility,-There%27s%20one%20special>
    #[derive(Serialize, Debug, Clone)]
    #[serde(rename_all = "snake_case")]
    enum SlashCommandResponseType {
        // Ephemeral,
        /// When the `response_type` is `in_channel`, both the response message and the initial slash command entered by the user will be shared in the channel.
        InChannel,
    }

    impl Serialize for SlashCommandResponse {
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            match self {
                Self::Layouts(layouts) | Self::LayoutsInChannel(layouts) => {
                    let response_type = matches!(self, Self::LayoutsInChannel(_))
                        .then_some(SlashCommandResponseType::InChannel);

                    into_tmp_layouts(response_type, layouts).serialize(serializer)
                }

                Self::Empty | Self::String(_) => {
                    Err(S::Error::custom("Empty or String must not serialize"))
                }
            }
        }
    }

    /// Converts into a valid [`TmpLayouts`].
    fn into_tmp_layouts(
        response_type: Option<SlashCommandResponseType>,
        layouts: &Layouts,
    ) -> TmpLayouts<'_> {
        let layouts = match layouts {
            Layouts::SingleBlock(block) => TmpLayoutsInner::Block { blocks: [block] },
            Layouts::MultipleBlocks(blocks) => TmpLayoutsInner::Blocks { blocks },
            Layouts::BlocksArray(payload) => TmpLayoutsInner::BlocksArray(payload),
        };

        TmpLayouts {
            response_type,
            layouts,
        }
    }
}

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

    use crate::SectionBlock;
    use crate::{PlainTextObject, TextObject};
    use anyhow::Result;

    #[test]
    fn test_serialize_empty() -> Result<()> {
        let testdate = SlashCommandResponse::empty();
        assert!(serde_json::to_string(&testdate).is_err());
        Ok(())
    }

    #[test]
    fn test_serialize_text() -> Result<()> {
        let testdate = SlashCommandResponse::String("test".into());
        assert!(serde_json::to_string(&testdate).is_err());
        Ok(())
    }

    fn only_text_layouts(text: String) -> Layouts {
        Layouts::BlocksArray(MessagePayloads {
            text,

            blocks: Default::default(),
            attachments: Default::default(),
            thread_ts: Default::default(),
            mrkdwn: Default::default(),
        })
    }

    #[test]
    fn test_serialize_layouts_to_ephemeral() -> Result<()> {
        let testdate = SlashCommandResponse::Layouts(only_text_layouts("test".into()));
        let expected = r#"{"text":"test"}"#;
        assert_eq!(serde_json::to_string(&testdate)?, expected);

        let testdate = SlashCommandResponse::Layouts(Layouts::MultipleBlocks(vec![]));
        let expected = r#"{"blocks":[]}"#;
        assert_eq!(serde_json::to_string(&testdate)?, expected);

        let testdate = SlashCommandResponse::Layouts(Layouts::SingleBlock(Box::new(
            Block::Section(SectionBlock {
                text: Some(TextObject::PlainText(PlainTextObject {
                    text: "dummy".into(),
                    emoji: None,
                })),
                ..Default::default()
            }),
        )));
        let expected =
            r#"{"blocks":[{"type":"section","text":{"type":"plain_text","text":"dummy"}}]}"#;
        assert_eq!(serde_json::to_string(&testdate)?, expected);

        Ok(())
    }

    #[test]
    fn test_serialize_layouts_to_in_channel() -> Result<()> {
        let testdate = SlashCommandResponse::LayoutsInChannel(only_text_layouts("test".into()));
        let expected = r#"{"response_type":"in_channel","text":"test"}"#;
        assert_eq!(serde_json::to_string(&testdate)?, expected);
        Ok(())
    }
}