Skip to main content

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