zulip-api 0.0.8

Client library for interacting with the Zulip API
Documentation
use std::borrow::Cow;

use serde::{Deserialize, Serialize};

use crate::message::{MessageTo, MessageType};
use crate::uuid::Uuid;
use crate::{field_access, impl_default, impl_display};

/// Represents a message to send to a Zulip server.
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct Request {
    #[serde(rename = "type")]
    msg_type: MessageType,
    to: MessageTo,
    content: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    topic: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    queue_id: Option<Uuid>,
    #[serde(skip_serializing_if = "Option::is_none")]
    local_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    read_by_sender: Option<bool>,
}

impl Request {
    /// Creates a new [Request].
    pub const fn new() -> Self {
        Self {
            msg_type: MessageType::new(),
            to: MessageTo::new(),
            content: String::new(),
            topic: None,
            queue_id: None,
            local_id: None,
            read_by_sender: None,
        }
    }

    /// Gets the message `queue_id` field.
    ///
    /// For clients supporting
    /// [local echo](https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html#local-echo),
    /// the [event queue](https://zulip.com/api/register-queue)
    /// ID for the client.
    ///
    /// If passed, `local_id` is required. If the message is
    /// successfully sent, the server will include `local_id` in the `message` event
    /// that the client with this `queue_id` will receive notifying it of the new message
    /// via [`GET /events`](https://zulip.com/api/get-events). This lets the client know unambiguously
    /// that it should replace the locally echoed message, rather than adding this new
    /// message (which would be correct if the user had sent the new message from another
    /// device).
    pub const fn queue_id(&self) -> Option<Uuid> {
        self.queue_id
    }

    /// Sets the message `queue_id` and `local_id` fields.
    ///
    /// If set, the `queue_id` field requires setting the `local_id` field.
    ///
    /// For clients supporting
    /// [local echo](https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html#local-echo),
    /// the [event queue](https://zulip.com/api/register-queue)
    /// ID for the client.
    ///
    /// If passed, `local_id` is required. If the message is
    /// successfully sent, the server will include `local_id` in the `message` event
    /// that the client with this `queue_id` will receive notifying it of the new message
    /// via [`GET /events`](https://zulip.com/api/get-events). This lets the client know unambiguously
    /// that it should replace the locally echoed message, rather than adding this new
    /// message (which would be correct if the user had sent the new message from another
    /// device).
    pub fn set_queue_id<'a, U: Into<Uuid>, S: Into<Cow<'a, str>>>(
        &mut self,
        queue_id: U,
        local_id: S,
    ) {
        self.queue_id = Some(queue_id.into());
        self.local_id = Some(S::into(local_id).into());
    }

    /// Unsets the message `queue_id` and `local_id` fields.
    ///
    /// If set, the `queue_id` field requires setting the `local_id` field.
    ///
    /// For clients supporting
    /// [local echo](https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html#local-echo),
    /// the [event queue](https://zulip.com/api/register-queue)
    /// ID for the client.
    ///
    /// If passed, `local_id` is required. If the message is
    /// successfully sent, the server will include `local_id` in the `message` event
    /// that the client with this `queue_id` will receive notifying it of the new message
    /// via [`GET /events`](https://zulip.com/api/get-events). This lets the client know unambiguously
    /// that it should replace the locally echoed message, rather than adding this new
    /// message (which would be correct if the user had sent the new message from another
    /// device).
    pub fn unset_queue_id(&mut self) -> (Option<Uuid>, Option<String>) {
        (self.queue_id.take(), self.local_id.take())
    }

    /// Builder function that sets the message `queue_id` and `local_id` fields.
    ///
    /// If set, the `queue_id` field requires setting the `local_id` field.
    ///
    /// For clients supporting
    /// [local echo](https://zulip.readthedocs.io/en/latest/subsystems/sending-messages.html#local-echo),
    /// the [event queue](https://zulip.com/api/register-queue)
    /// ID for the client.
    ///
    /// If passed, `local_id` is required. If the message is
    /// successfully sent, the server will include `local_id` in the `message` event
    /// that the client with this `queue_id` will receive notifying it of the new message
    /// via [`GET /events`](https://zulip.com/api/get-events). This lets the client know unambiguously
    /// that it should replace the locally echoed message, rather than adding this new
    /// message (which would be correct if the user had sent the new message from another
    /// device).
    pub fn with_queue_id<'a, U: Into<Uuid>, S: Into<Cow<'a, str>>>(
        self,
        queue_id: U,
        local_id: S,
    ) -> Self {
        Self {
            queue_id: Some(queue_id.into()),
            local_id: Some(S::into(local_id).into()),
            ..self
        }
    }

    /// Gets the message `local_id` field.
    ///
    /// For clients supporting local echo, a unique string-format identifier
    /// chosen freely by the client; the server will pass it back to the client without
    /// inspecting it, as described in the `queue_id` description.
    pub fn local_id(&self) -> Option<&str> {
        self.local_id.as_deref()
    }
}

field_access! {
    Request {
        /// The type of message to be sent.
        ///
        /// `"direct"` for a direct message and `"stream"` or `"channel"` for a
        /// channel message.
        ///
        /// **Changes**: In Zulip 9.0 (feature level 248), `"channel"` was added as
        /// an additional value for this parameter to request a channel message.
        ///
        /// In Zulip 7.0 (feature level 174), `"direct"` was added as
        /// the preferred way to request a direct message, deprecating the original
        /// `"private"`. While `"private"` is still supported for requesting direct
        /// messages, clients are encouraged to use to the modern convention with
        /// servers that support it, because support for `"private"` will eventually
        /// be removed.
        msg_type: MessageType,
    }
}

field_access! {
    Request {
        /// For channel messages, either the name or integer ID of the channel. For
        /// direct messages, either a list containing integer user IDs or a list
        /// containing string Zulip API email addresses.
        ///
        /// **Changes**: Support for using user/channel IDs was added in Zulip 2.0.0.
        to: as_ref { &MessageTo, MessageTo },
    }
}

field_access! {
    Request {
        /// The content of the message.
        ///
        /// Clients should use the `max_message_length` returned by the
        /// [`POST /register`](https://zulip.com/api/register-queue) endpoint to determine
        /// the maximum message size.
        content: cow { str },
    }
}

field_access! {
    Request {
        /// The topic of the message.
        ///
        /// Only required for channel messages
        /// (`"type": "stream"` or `"type": "channel"`), ignored otherwise.
        ///
        /// Clients should use the `max_topic_length` returned by the
        /// [`POST /register`](https://zulip.com/api/register-queue) endpoint to determine
        /// the maximum topic length.
        ///
        /// Note: When `"(no topic)"` or the value of `realm_empty_topic_display_name`
        /// found in the [POST /register](https://zulip.com/api/register-queue) response is used for this
        /// parameter, it is interpreted as an empty string.
        ///
        /// When [topics are required](https://zulip.com/help/require-topics), this parameter can't
        /// be `"(no topic)"`, an empty string, or the value of `realm_empty_topic_display_name`.
        ///
        /// **Changes**: Before Zulip 10.0 (feature level 370), `"(no topic)"`
        /// was not interpreted as an empty string.
        ///
        /// Before Zulip 10.0 (feature level 334), empty string
        /// was not a valid topic name for channel messages.
        ///
        /// New in Zulip 2.0.0. Previous Zulip releases encoded
        /// this as `subject`, which is currently a deprecated alias.
        topic: option_cow_deref { str, String },
    }
}

field_access! {
    Request {
        /// Whether the message should be initially marked read by its
        /// sender. If unspecified, the server uses a heuristic based
        /// on the client name.
        ///
        /// **Changes**: New in Zulip 8.0 (feature level 236).
        read_by_sender: option { bool },
    }
}

impl_default!(Request);
impl_display!(Request);

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

    #[test]
    fn test_message_minimal() {
        let message_ty = MessageType::Channel;
        let ty_json = serde_json::to_string(&message_ty).unwrap();

        let message_to = MessageTo::ChannelId(42);
        let to_json = serde_json::to_string(&message_to).unwrap();

        let content = "test message";
        let content_json = serde_json::to_string(&content).unwrap();

        let message = Request::new()
            .with_msg_type(message_ty)
            .with_to(message_to.clone())
            .with_content(content);

        let message_json =
            format!(r#"{{"type":{ty_json},"to":{to_json},"content":{content_json}}}"#);

        assert_eq!(serde_json::to_string(&message).unwrap(), message_json);
        assert_eq!(
            serde_json::from_str::<Request>(&message_json).unwrap(),
            message
        );
        assert_eq!(message.msg_type(), message_ty);
        assert_eq!(message.to(), &message_to);
        assert_eq!(message.content(), content);
    }

    #[test]
    fn test_message_full() {
        let message_ty = MessageType::Channel;
        let ty_json = serde_json::to_string(&message_ty).unwrap();

        let message_to = MessageTo::ChannelId(42);
        let to_json = serde_json::to_string(&message_to).unwrap();

        let content = "test message";
        let content_json = serde_json::to_string(&content).unwrap();

        let topic = "test topic";
        let topic_json = serde_json::to_string(&topic).unwrap();

        let queue_id = Uuid::try_from("936da01f-9abd-4d9d-80c7-02af85c822a8").unwrap();
        let queue_id_json = serde_json::to_string(&queue_id).unwrap();

        let local_id = "test.local_id";
        let local_id_json = serde_json::to_string(&local_id).unwrap();

        let read_by_sender = false;
        let read_by_sender_json = serde_json::to_string(&read_by_sender).unwrap();

        let message = Request::new()
            .with_msg_type(message_ty)
            .with_to(message_to.clone())
            .with_content(content)
            .with_topic(topic)
            .with_queue_id(queue_id, local_id)
            .with_read_by_sender(read_by_sender);

        let message_json = format!(
            r#"{{"type":{ty_json},"to":{to_json},"content":{content_json},"topic":{topic_json},"queue_id":{queue_id_json},"local_id":{local_id_json},"read_by_sender":{read_by_sender_json}}}"#
        );

        assert_eq!(serde_json::to_string(&message).unwrap(), message_json);
        assert_eq!(
            serde_json::from_str::<Request>(&message_json).unwrap(),
            message
        );
        assert_eq!(message.msg_type(), message_ty);
        assert_eq!(message.to(), &message_to);
        assert_eq!(message.content(), content);
        assert_eq!(message.topic(), Some(topic));
        assert_eq!(message.queue_id(), Some(queue_id));
        assert_eq!(message.local_id(), Some(local_id));
        assert_eq!(message.read_by_sender(), Some(read_by_sender));
    }
}