nylas-types 0.1.1

Type definitions for Nylas API v3
Documentation
//! Common types used across the Nylas API.

use serde::{Deserialize, Serialize};
use std::fmt;

/// Macro to generate ID newtypes with consistent implementation.
macro_rules! define_id_type {
    ($name:ident, $doc:expr) => {
        #[doc = $doc]
        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
        #[serde(transparent)]
        pub struct $name(String);

        impl $name {
            /// Create a new ID.
            pub fn new(id: impl Into<String>) -> Self {
                Self(id.into())
            }

            /// Get the ID as a string slice.
            pub fn as_str(&self) -> &str {
                &self.0
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "{}", self.0)
            }
        }

        impl From<String> for $name {
            fn from(s: String) -> Self {
                Self(s)
            }
        }

        impl From<&str> for $name {
            fn from(s: &str) -> Self {
                Self(s.to_string())
            }
        }
    };
}

// Define all ID types using the macro
define_id_type!(GrantId, "Grant ID newtype for type safety.");
define_id_type!(MessageId, "Message ID newtype for type safety.");
define_id_type!(ThreadId, "Thread ID newtype for type safety.");
define_id_type!(DraftId, "Draft ID newtype for type safety.");
define_id_type!(CalendarId, "Calendar ID newtype for type safety.");
define_id_type!(EventId, "Event ID newtype for type safety.");
define_id_type!(ContactId, "Contact ID newtype for type safety.");
define_id_type!(FolderId, "Folder ID newtype for type safety.");
define_id_type!(WebhookId, "Webhook ID newtype for type safety.");
define_id_type!(
    SchedulerConfigId,
    "Scheduler configuration ID newtype for type safety."
);
define_id_type!(
    SchedulerSessionId,
    "Scheduler session ID newtype for type safety."
);
define_id_type!(
    SchedulerBookingId,
    "Scheduler booking ID newtype for type safety."
);
define_id_type!(NotetakerId, "Notetaker ID newtype for type safety.");
define_id_type!(RecordingId, "Recording ID newtype for type safety.");
define_id_type!(ResourceId, "Resource ID newtype for type safety.");

/// Provider enum.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Provider {
    /// Google (Gmail, Calendar)
    Google,
    /// Microsoft (Outlook, Office 365)
    Microsoft,
    /// IMAP provider
    Imap,
    /// Yahoo
    Yahoo,
    /// iCloud
    Icloud,
    /// Virtual Calendar
    #[serde(rename = "virtual-calendar")]
    VirtualCalendar,
}

/// Email address with optional name.
///
/// # Example
///
/// ```
/// # use nylas_types::EmailAddress;
/// let addr = EmailAddress::new("user@example.com").unwrap();
/// let addr_with_name = EmailAddress::new("user@example.com")
///     .unwrap()
///     .with_name("User Name");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EmailAddress {
    /// Email address.
    pub email: String,

    /// Display name (optional).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
}

impl EmailAddress {
    /// Create a new email address.
    ///
    /// # Errors
    ///
    /// Returns an error if the email address is invalid.
    pub fn new(email: impl Into<String>) -> Result<Self, ValidationError> {
        let email = email.into();
        Self::validate(&email)?;

        Ok(Self { email, name: None })
    }

    /// Add a display name to the email address.
    pub fn with_name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    /// Simple email validation.
    fn validate(email: &str) -> Result<(), ValidationError> {
        if email.is_empty() {
            return Err(ValidationError::Empty);
        }

        if !email.contains('@') {
            return Err(ValidationError::MissingAt);
        }

        let parts: Vec<&str> = email.split('@').collect();
        if parts.len() != 2 {
            return Err(ValidationError::InvalidFormat);
        }

        if parts[0].is_empty() || parts[1].is_empty() {
            return Err(ValidationError::InvalidFormat);
        }

        if !parts[1].contains('.') {
            return Err(ValidationError::InvalidDomain);
        }

        Ok(())
    }

    /// Get the email address.
    pub fn email(&self) -> &str {
        &self.email
    }

    /// Get the display name.
    pub fn name(&self) -> Option<&str> {
        self.name.as_deref()
    }
}

/// Email validation error.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationError {
    /// Email is empty.
    Empty,
    /// Email is missing the @ symbol.
    MissingAt,
    /// Email has invalid format.
    InvalidFormat,
    /// Email has invalid domain.
    InvalidDomain,
}

impl fmt::Display for ValidationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => write!(f, "Email address cannot be empty"),
            Self::MissingAt => write!(f, "Email address must contain '@'"),
            Self::InvalidFormat => write!(f, "Email address has invalid format"),
            Self::InvalidDomain => write!(f, "Email domain is invalid"),
        }
    }
}

impl std::error::Error for ValidationError {}

/// Common API response wrapper for list operations.
///
/// Nylas v3 API returns data in a consistent format with pagination support.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApiResponse<T> {
    /// The data items returned by the API.
    pub data: Vec<T>,

    /// Request ID for debugging.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub request_id: Option<String>,

    /// Next page cursor for pagination.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub next_cursor: Option<String>,
}

impl<T> ApiResponse<T> {
    /// Create a new API response.
    pub fn new(data: Vec<T>) -> Self {
        Self {
            data,
            request_id: None,
            next_cursor: None,
        }
    }

    /// Check if there are more pages.
    pub fn has_next_page(&self) -> bool {
        self.next_cursor.is_some()
    }
}

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

    #[test]
    fn test_grant_id_creation() {
        let id = GrantId::new("grant_123");
        assert_eq!(id.as_str(), "grant_123");
        assert_eq!(id.to_string(), "grant_123");
    }

    #[test]
    fn test_message_id_from_string() {
        let id = MessageId::from("msg_456");
        assert_eq!(id.as_str(), "msg_456");
    }

    #[test]
    fn test_id_types_are_distinct() {
        let grant_id = GrantId::new("123");
        let message_id = MessageId::new("123");

        // They have the same value but are different types
        assert_eq!(grant_id.as_str(), message_id.as_str());
        // Note: We can't compare them directly because they're different types
        // This is the whole point of the NewType pattern!
    }

    #[test]
    fn test_email_address_valid() {
        let addr = EmailAddress::new("user@example.com");
        assert!(addr.is_ok());

        let addr = addr.unwrap();
        assert_eq!(addr.email(), "user@example.com");
        assert_eq!(addr.name(), None);
    }

    #[test]
    fn test_email_address_with_name() {
        let addr = EmailAddress::new("user@example.com")
            .unwrap()
            .with_name("User Name");

        assert_eq!(addr.email(), "user@example.com");
        assert_eq!(addr.name(), Some("User Name"));
    }

    #[test]
    fn test_email_address_empty() {
        let addr = EmailAddress::new("");
        assert!(addr.is_err());
        assert_eq!(addr.unwrap_err(), ValidationError::Empty);
    }

    #[test]
    fn test_email_address_missing_at() {
        let addr = EmailAddress::new("invalid");
        assert!(addr.is_err());
        assert_eq!(addr.unwrap_err(), ValidationError::MissingAt);
    }

    #[test]
    fn test_email_address_invalid_format() {
        let addr = EmailAddress::new("@example.com");
        assert!(addr.is_err());
        assert_eq!(addr.unwrap_err(), ValidationError::InvalidFormat);

        let addr = EmailAddress::new("user@");
        assert!(addr.is_err());
        assert_eq!(addr.unwrap_err(), ValidationError::InvalidFormat);
    }

    #[test]
    fn test_email_address_invalid_domain() {
        let addr = EmailAddress::new("user@domain");
        assert!(addr.is_err());
        assert_eq!(addr.unwrap_err(), ValidationError::InvalidDomain);
    }

    #[test]
    fn test_email_address_serialization() {
        let addr = EmailAddress::new("user@example.com")
            .unwrap()
            .with_name("User");

        let json = serde_json::to_string(&addr).unwrap();
        assert!(json.contains("user@example.com"));
        assert!(json.contains("User"));

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

    #[test]
    fn test_api_response_creation() {
        let response = ApiResponse::new(vec!["item1".to_string(), "item2".to_string()]);
        assert_eq!(response.data.len(), 2);
        assert!(!response.has_next_page());
    }

    #[test]
    fn test_api_response_with_pagination() {
        let mut response = ApiResponse::new(vec!["item1".to_string()]);
        response.next_cursor = Some("cursor_123".to_string());

        assert!(response.has_next_page());
    }

    #[test]
    fn test_api_response_serialization() {
        let response = ApiResponse {
            data: vec!["test".to_string()],
            request_id: Some("req_123".to_string()),
            next_cursor: Some("cursor_456".to_string()),
        };

        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains("test"));
        assert!(json.contains("req_123"));
        assert!(json.contains("cursor_456"));

        let deserialized: ApiResponse<String> = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, response);
    }

    #[test]
    fn test_provider_serialization() {
        let provider = Provider::Google;
        let json = serde_json::to_string(&provider).unwrap();
        assert_eq!(json, "\"google\"");

        let provider = Provider::VirtualCalendar;
        let json = serde_json::to_string(&provider).unwrap();
        assert_eq!(json, "\"virtual-calendar\"");
    }

    #[test]
    fn test_id_serialization() {
        let grant_id = GrantId::new("grant_123");
        let json = serde_json::to_string(&grant_id).unwrap();
        assert_eq!(json, "\"grant_123\"");

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