use serde::{Deserialize, Serialize};
use std::fmt;
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 {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
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_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.");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Provider {
Google,
Microsoft,
Imap,
Yahoo,
Icloud,
#[serde(rename = "virtual-calendar")]
VirtualCalendar,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EmailAddress {
pub email: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl EmailAddress {
pub fn new(email: impl Into<String>) -> Result<Self, ValidationError> {
let email = email.into();
Self::validate(&email)?;
Ok(Self { email, name: None })
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
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(())
}
pub fn email(&self) -> &str {
&self.email
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationError {
Empty,
MissingAt,
InvalidFormat,
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 {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApiResponse<T> {
pub data: Vec<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
impl<T> ApiResponse<T> {
pub fn new(data: Vec<T>) -> Self {
Self {
data,
request_id: None,
next_cursor: None,
}
}
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");
assert_eq!(grant_id.as_str(), message_id.as_str());
}
#[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);
}
}