nylas-types 0.1.1

Type definitions for Nylas API v3
Documentation
//! Thread types for Nylas API v3
//!
//! Threads represent email conversations grouped by subject and participants.

use serde::{Deserialize, Serialize};

use crate::common::{EmailAddress, GrantId, ThreadId};

/// An email thread (conversation).
///
/// Threads group related messages together based on subject and participants.
///
/// # Example
///
/// ```
/// # use nylas_types::{Thread, ThreadId, GrantId};
/// let thread = Thread {
///     id: ThreadId::new("thread_123"),
///     grant_id: GrantId::new("grant_456"),
///     subject: Some("Project Discussion".to_string()),
///     participants: vec![],
///     message_ids: vec![],
///     draft_ids: vec![],
///     folders: vec![],
///     snippet: Some("Let's discuss the project timeline...".to_string()),
///     starred: Some(false),
///     unread: Some(false),
///     latest_draft_or_message: None,
///     has_attachments: Some(false),
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Thread {
    /// Unique identifier for the thread.
    pub id: ThreadId,

    /// Grant ID this thread belongs to.
    pub grant_id: GrantId,

    /// Subject of the thread.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subject: Option<String>,

    /// Participants in the thread.
    #[serde(default)]
    pub participants: Vec<EmailAddress>,

    /// IDs of messages in this thread.
    #[serde(default)]
    pub message_ids: Vec<String>,

    /// IDs of drafts in this thread.
    #[serde(default)]
    pub draft_ids: Vec<String>,

    /// Folder IDs this thread belongs to.
    #[serde(default)]
    pub folders: Vec<String>,

    /// Text snippet from the thread.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub snippet: Option<String>,

    /// Whether the thread is starred.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub starred: Option<bool>,

    /// Whether the thread has unread messages.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub unread: Option<bool>,

    /// Unix timestamp of the latest message or draft.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub latest_draft_or_message: Option<i64>,

    /// Whether the thread has attachments.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub has_attachments: Option<bool>,
}

/// Request to update a thread.
///
/// Only certain fields can be updated (starred, unread, folders).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct UpdateThreadRequest {
    /// Update starred status.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub starred: Option<bool>,

    /// Update unread status.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub unread: Option<bool>,

    /// Update folder IDs.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub folders: Option<Vec<String>>,
}

impl UpdateThreadRequest {
    /// Create a builder for UpdateThreadRequest.
    pub fn builder() -> UpdateThreadRequestBuilder {
        UpdateThreadRequestBuilder::default()
    }
}

/// Builder for UpdateThreadRequest.
#[derive(Debug, Clone, Default)]
pub struct UpdateThreadRequestBuilder {
    starred: Option<bool>,
    unread: Option<bool>,
    folders: Option<Vec<String>>,
}

impl UpdateThreadRequestBuilder {
    /// Set starred status.
    pub fn starred(mut self, starred: bool) -> Self {
        self.starred = Some(starred);
        self
    }

    /// Set unread status.
    pub fn unread(mut self, unread: bool) -> Self {
        self.unread = Some(unread);
        self
    }

    /// Set folder IDs.
    pub fn folders(mut self, folders: Vec<String>) -> Self {
        self.folders = Some(folders);
        self
    }

    /// Build the UpdateThreadRequest.
    pub fn build(self) -> UpdateThreadRequest {
        UpdateThreadRequest {
            starred: self.starred,
            unread: self.unread,
            folders: self.folders,
        }
    }
}

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

    #[test]
    fn test_thread_creation() {
        let thread = Thread {
            id: ThreadId::new("thread_123"),
            grant_id: GrantId::new("grant_456"),
            subject: Some("Test Subject".to_string()),
            participants: vec![],
            message_ids: vec!["msg_1".to_string(), "msg_2".to_string()],
            draft_ids: vec![],
            folders: vec!["folder_1".to_string()],
            snippet: Some("Test snippet".to_string()),
            starred: Some(false),
            unread: Some(true),
            latest_draft_or_message: Some(1234567890),
            has_attachments: Some(false),
        };

        assert_eq!(thread.id.as_str(), "thread_123");
        assert_eq!(thread.subject, Some("Test Subject".to_string()));
        assert_eq!(thread.message_ids.len(), 2);
        assert_eq!(thread.unread, Some(true));
    }

    #[test]
    fn test_thread_serialization() {
        let thread = Thread {
            id: ThreadId::new("thread_123"),
            grant_id: GrantId::new("grant_456"),
            subject: Some("Test".to_string()),
            participants: vec![],
            message_ids: vec!["msg_1".to_string()],
            draft_ids: vec![],
            folders: vec![],
            snippet: None,
            starred: Some(true),
            unread: Some(false),
            latest_draft_or_message: None,
            has_attachments: Some(false),
        };

        let json = serde_json::to_string(&thread).unwrap();
        assert!(json.contains("thread_123"));
        assert!(json.contains("Test"));
    }

    #[test]
    fn test_thread_deserialization() {
        let json = r#"{
            "id": "thread_123",
            "grant_id": "grant_456",
            "subject": "Test Subject",
            "participants": [],
            "message_ids": ["msg_1", "msg_2"],
            "draft_ids": [],
            "folders": ["folder_1"],
            "starred": false,
            "unread": true,
            "latest_draft_or_message": 1234567890
        }"#;

        let thread: Thread = serde_json::from_str(json).unwrap();
        assert_eq!(thread.id.as_str(), "thread_123");
        assert_eq!(thread.subject, Some("Test Subject".to_string()));
        assert_eq!(thread.message_ids.len(), 2);
        assert_eq!(thread.unread, Some(true));
    }

    #[test]
    fn test_update_thread_request_builder() {
        let update = UpdateThreadRequest::builder()
            .starred(true)
            .unread(false)
            .folders(vec!["folder_1".to_string()])
            .build();

        assert_eq!(update.starred, Some(true));
        assert_eq!(update.unread, Some(false));
        assert_eq!(update.folders, Some(vec!["folder_1".to_string()]));
    }

    #[test]
    fn test_update_thread_request_partial() {
        let update = UpdateThreadRequest::builder().starred(true).build();

        assert_eq!(update.starred, Some(true));
        assert_eq!(update.unread, None);
        assert_eq!(update.folders, None);
    }

    #[test]
    fn test_update_thread_request_serialization() {
        let update = UpdateThreadRequest::builder()
            .starred(true)
            .unread(false)
            .build();

        let json = serde_json::to_string(&update).unwrap();
        assert!(json.contains("starred"));
        assert!(json.contains("unread"));

        let deserialized: UpdateThreadRequest = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, update);
    }
}