nylas_types/
common.rs

1//! Common types used across the Nylas API.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Macro to generate ID newtypes with consistent implementation.
7macro_rules! define_id_type {
8    ($name:ident, $doc:expr) => {
9        #[doc = $doc]
10        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11        #[serde(transparent)]
12        pub struct $name(String);
13
14        impl $name {
15            /// Create a new ID.
16            pub fn new(id: impl Into<String>) -> Self {
17                Self(id.into())
18            }
19
20            /// Get the ID as a string slice.
21            pub fn as_str(&self) -> &str {
22                &self.0
23            }
24        }
25
26        impl fmt::Display for $name {
27            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28                write!(f, "{}", self.0)
29            }
30        }
31
32        impl From<String> for $name {
33            fn from(s: String) -> Self {
34                Self(s)
35            }
36        }
37
38        impl From<&str> for $name {
39            fn from(s: &str) -> Self {
40                Self(s.to_string())
41            }
42        }
43    };
44}
45
46// Define all ID types using the macro
47define_id_type!(GrantId, "Grant ID newtype for type safety.");
48define_id_type!(MessageId, "Message ID newtype for type safety.");
49define_id_type!(ThreadId, "Thread ID newtype for type safety.");
50define_id_type!(DraftId, "Draft ID newtype for type safety.");
51define_id_type!(CalendarId, "Calendar ID newtype for type safety.");
52define_id_type!(EventId, "Event ID newtype for type safety.");
53define_id_type!(ContactId, "Contact ID newtype for type safety.");
54define_id_type!(FolderId, "Folder ID newtype for type safety.");
55define_id_type!(WebhookId, "Webhook ID newtype for type safety.");
56
57/// Provider enum.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "lowercase")]
60pub enum Provider {
61    /// Google (Gmail, Calendar)
62    Google,
63    /// Microsoft (Outlook, Office 365)
64    Microsoft,
65    /// IMAP provider
66    Imap,
67    /// Yahoo
68    Yahoo,
69    /// iCloud
70    Icloud,
71    /// Virtual Calendar
72    #[serde(rename = "virtual-calendar")]
73    VirtualCalendar,
74}
75
76/// Email address with optional name.
77///
78/// # Example
79///
80/// ```
81/// # use nylas_types::EmailAddress;
82/// let addr = EmailAddress::new("user@example.com").unwrap();
83/// let addr_with_name = EmailAddress::new("user@example.com")
84///     .unwrap()
85///     .with_name("User Name");
86/// ```
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct EmailAddress {
89    /// Email address.
90    pub email: String,
91
92    /// Display name (optional).
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub name: Option<String>,
95}
96
97impl EmailAddress {
98    /// Create a new email address.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the email address is invalid.
103    pub fn new(email: impl Into<String>) -> Result<Self, ValidationError> {
104        let email = email.into();
105        Self::validate(&email)?;
106
107        Ok(Self { email, name: None })
108    }
109
110    /// Add a display name to the email address.
111    pub fn with_name(mut self, name: impl Into<String>) -> Self {
112        self.name = Some(name.into());
113        self
114    }
115
116    /// Simple email validation.
117    fn validate(email: &str) -> Result<(), ValidationError> {
118        if email.is_empty() {
119            return Err(ValidationError::Empty);
120        }
121
122        if !email.contains('@') {
123            return Err(ValidationError::MissingAt);
124        }
125
126        let parts: Vec<&str> = email.split('@').collect();
127        if parts.len() != 2 {
128            return Err(ValidationError::InvalidFormat);
129        }
130
131        if parts[0].is_empty() || parts[1].is_empty() {
132            return Err(ValidationError::InvalidFormat);
133        }
134
135        if !parts[1].contains('.') {
136            return Err(ValidationError::InvalidDomain);
137        }
138
139        Ok(())
140    }
141
142    /// Get the email address.
143    pub fn email(&self) -> &str {
144        &self.email
145    }
146
147    /// Get the display name.
148    pub fn name(&self) -> Option<&str> {
149        self.name.as_deref()
150    }
151}
152
153/// Email validation error.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum ValidationError {
156    /// Email is empty.
157    Empty,
158    /// Email is missing the @ symbol.
159    MissingAt,
160    /// Email has invalid format.
161    InvalidFormat,
162    /// Email has invalid domain.
163    InvalidDomain,
164}
165
166impl fmt::Display for ValidationError {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        match self {
169            Self::Empty => write!(f, "Email address cannot be empty"),
170            Self::MissingAt => write!(f, "Email address must contain '@'"),
171            Self::InvalidFormat => write!(f, "Email address has invalid format"),
172            Self::InvalidDomain => write!(f, "Email domain is invalid"),
173        }
174    }
175}
176
177impl std::error::Error for ValidationError {}
178
179/// Common API response wrapper for list operations.
180///
181/// Nylas v3 API returns data in a consistent format with pagination support.
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct ApiResponse<T> {
184    /// The data items returned by the API.
185    pub data: Vec<T>,
186
187    /// Request ID for debugging.
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub request_id: Option<String>,
190
191    /// Next page cursor for pagination.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub next_cursor: Option<String>,
194}
195
196impl<T> ApiResponse<T> {
197    /// Create a new API response.
198    pub fn new(data: Vec<T>) -> Self {
199        Self {
200            data,
201            request_id: None,
202            next_cursor: None,
203        }
204    }
205
206    /// Check if there are more pages.
207    pub fn has_next_page(&self) -> bool {
208        self.next_cursor.is_some()
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_grant_id_creation() {
218        let id = GrantId::new("grant_123");
219        assert_eq!(id.as_str(), "grant_123");
220        assert_eq!(id.to_string(), "grant_123");
221    }
222
223    #[test]
224    fn test_message_id_from_string() {
225        let id = MessageId::from("msg_456");
226        assert_eq!(id.as_str(), "msg_456");
227    }
228
229    #[test]
230    fn test_id_types_are_distinct() {
231        let grant_id = GrantId::new("123");
232        let message_id = MessageId::new("123");
233
234        // They have the same value but are different types
235        assert_eq!(grant_id.as_str(), message_id.as_str());
236        // Note: We can't compare them directly because they're different types
237        // This is the whole point of the NewType pattern!
238    }
239
240    #[test]
241    fn test_email_address_valid() {
242        let addr = EmailAddress::new("user@example.com");
243        assert!(addr.is_ok());
244
245        let addr = addr.unwrap();
246        assert_eq!(addr.email(), "user@example.com");
247        assert_eq!(addr.name(), None);
248    }
249
250    #[test]
251    fn test_email_address_with_name() {
252        let addr = EmailAddress::new("user@example.com")
253            .unwrap()
254            .with_name("User Name");
255
256        assert_eq!(addr.email(), "user@example.com");
257        assert_eq!(addr.name(), Some("User Name"));
258    }
259
260    #[test]
261    fn test_email_address_empty() {
262        let addr = EmailAddress::new("");
263        assert!(addr.is_err());
264        assert_eq!(addr.unwrap_err(), ValidationError::Empty);
265    }
266
267    #[test]
268    fn test_email_address_missing_at() {
269        let addr = EmailAddress::new("invalid");
270        assert!(addr.is_err());
271        assert_eq!(addr.unwrap_err(), ValidationError::MissingAt);
272    }
273
274    #[test]
275    fn test_email_address_invalid_format() {
276        let addr = EmailAddress::new("@example.com");
277        assert!(addr.is_err());
278        assert_eq!(addr.unwrap_err(), ValidationError::InvalidFormat);
279
280        let addr = EmailAddress::new("user@");
281        assert!(addr.is_err());
282        assert_eq!(addr.unwrap_err(), ValidationError::InvalidFormat);
283    }
284
285    #[test]
286    fn test_email_address_invalid_domain() {
287        let addr = EmailAddress::new("user@domain");
288        assert!(addr.is_err());
289        assert_eq!(addr.unwrap_err(), ValidationError::InvalidDomain);
290    }
291
292    #[test]
293    fn test_email_address_serialization() {
294        let addr = EmailAddress::new("user@example.com")
295            .unwrap()
296            .with_name("User");
297
298        let json = serde_json::to_string(&addr).unwrap();
299        assert!(json.contains("user@example.com"));
300        assert!(json.contains("User"));
301
302        let deserialized: EmailAddress = serde_json::from_str(&json).unwrap();
303        assert_eq!(deserialized, addr);
304    }
305
306    #[test]
307    fn test_api_response_creation() {
308        let response = ApiResponse::new(vec!["item1".to_string(), "item2".to_string()]);
309        assert_eq!(response.data.len(), 2);
310        assert!(!response.has_next_page());
311    }
312
313    #[test]
314    fn test_api_response_with_pagination() {
315        let mut response = ApiResponse::new(vec!["item1".to_string()]);
316        response.next_cursor = Some("cursor_123".to_string());
317
318        assert!(response.has_next_page());
319    }
320
321    #[test]
322    fn test_api_response_serialization() {
323        let response = ApiResponse {
324            data: vec!["test".to_string()],
325            request_id: Some("req_123".to_string()),
326            next_cursor: Some("cursor_456".to_string()),
327        };
328
329        let json = serde_json::to_string(&response).unwrap();
330        assert!(json.contains("test"));
331        assert!(json.contains("req_123"));
332        assert!(json.contains("cursor_456"));
333
334        let deserialized: ApiResponse<String> = serde_json::from_str(&json).unwrap();
335        assert_eq!(deserialized, response);
336    }
337
338    #[test]
339    fn test_provider_serialization() {
340        let provider = Provider::Google;
341        let json = serde_json::to_string(&provider).unwrap();
342        assert_eq!(json, "\"google\"");
343
344        let provider = Provider::VirtualCalendar;
345        let json = serde_json::to_string(&provider).unwrap();
346        assert_eq!(json, "\"virtual-calendar\"");
347    }
348
349    #[test]
350    fn test_id_serialization() {
351        let grant_id = GrantId::new("grant_123");
352        let json = serde_json::to_string(&grant_id).unwrap();
353        assert_eq!(json, "\"grant_123\"");
354
355        let deserialized: GrantId = serde_json::from_str(&json).unwrap();
356        assert_eq!(deserialized, grant_id);
357    }
358}