Files
calendar/backend/src/models_v2.rs
Connor Johnstone f266d3f304 Implement RFC 5545-compliant calendar system with v2 API
This major refactor eliminates manual string parsing throughout the
codebase and introduces proper RFC 5545 iCalendar specification
compliance with significant code simplification benefits.

## Backend Improvements
- Add complete RFC 5545-compliant data structures (VEvent, VTodo, etc.)
- Create simplified v2 API endpoints with direct DateTime support
- Eliminate ~150 lines of manual string parsing in handlers
- Add structured attendee and alarm support
- Maintain backward compatibility with existing v1 APIs

## Frontend Improvements
- Replace 16+ parameter create_event calls with single structured request
- Add automatic date/time conversion in EventCreationData
- Eliminate enum-to-string conversions throughout event creation
- Add v2 API models with proper type safety

## Technical Benefits
- Direct DateTime<Utc> usage instead of error-prone string parsing
- Proper RFC 5545 compliance with DTSTAMP, SEQUENCE fields
- Vec<AttendeeV2> instead of comma-separated strings
- Structured alarm system with multiple reminder types
- Enhanced RRULE support for complex recurrence patterns

## Code Quality
- Reduced create_event call from 16 parameters to 1 structured request
- Added comprehensive integration plan documentation
- Both backend and frontend compile successfully
- Maintained full backward compatibility during transition

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-29 17:06:22 -04:00

295 lines
9.8 KiB
Rust

// Simplified RFC 5545-based API models
// Axum imports removed - not needed for model definitions
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
// ==================== CALENDAR REQUESTS ====================
#[derive(Debug, Deserialize)]
pub struct CreateCalendarRequestV2 {
pub name: String,
pub description: Option<String>,
pub color: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct DeleteCalendarRequestV2 {
pub path: String,
}
// ==================== EVENT REQUESTS ====================
// Simplified create event request using proper DateTime instead of string parsing
#[derive(Debug, Deserialize)]
pub struct CreateEventRequestV2 {
pub summary: String, // title -> summary (RFC 5545 term)
pub description: Option<String>, // Optional in RFC 5545
pub dtstart: DateTime<Utc>, // Direct DateTime, no string parsing!
pub dtend: Option<DateTime<Utc>>, // Optional, alternative to duration
pub location: Option<String>,
pub all_day: bool,
// Status and classification
pub status: Option<EventStatusV2>, // Use enum instead of string
pub class: Option<EventClassV2>, // Use enum instead of string
pub priority: Option<u8>, // 0-9 priority level
// People
pub organizer: Option<String>, // Organizer email
pub attendees: Vec<AttendeeV2>, // Rich attendee objects
// Categorization
pub categories: Vec<String>, // Direct Vec instead of comma-separated
// Recurrence (simplified for now)
pub rrule: Option<String>, // Standard RRULE format
// Reminders (simplified for now)
pub alarms: Vec<AlarmV2>, // Structured alarms
// Calendar context
pub calendar_path: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateEventRequestV2 {
pub uid: String, // Event UID to identify which event to update
pub summary: String,
pub description: Option<String>,
pub dtstart: DateTime<Utc>, // Direct DateTime, no string parsing!
pub dtend: Option<DateTime<Utc>>,
pub location: Option<String>,
pub all_day: bool,
// Status and classification
pub status: Option<EventStatusV2>,
pub class: Option<EventClassV2>,
pub priority: Option<u8>,
// People
pub organizer: Option<String>,
pub attendees: Vec<AttendeeV2>,
// Categorization
pub categories: Vec<String>,
// Recurrence
pub rrule: Option<String>,
// Reminders
pub alarms: Vec<AlarmV2>,
// Context
pub calendar_path: Option<String>,
pub update_action: Option<String>, // "update_series" for recurring events
pub occurrence_date: Option<DateTime<Utc>>, // Specific occurrence
pub exception_dates: Option<Vec<DateTime<Utc>>>, // EXDATE
pub until_date: Option<DateTime<Utc>>, // RRULE UNTIL clause
}
#[derive(Debug, Deserialize)]
pub struct DeleteEventRequestV2 {
pub calendar_path: String,
pub event_href: String,
pub delete_action: DeleteActionV2, // Use enum instead of string
pub occurrence_date: Option<DateTime<Utc>>, // Direct DateTime
}
// ==================== SUPPORTING TYPES ====================
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum EventStatusV2 {
Tentative,
Confirmed,
Cancelled,
}
impl Default for EventStatusV2 {
fn default() -> Self {
EventStatusV2::Confirmed
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum EventClassV2 {
Public,
Private,
Confidential,
}
impl Default for EventClassV2 {
fn default() -> Self {
EventClassV2::Public
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DeleteActionV2 {
DeleteThis, // "delete_this"
DeleteFollowing, // "delete_following"
DeleteSeries, // "delete_series"
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AttendeeV2 {
pub email: String, // Calendar address
pub name: Option<String>, // Common name (CN parameter)
pub role: Option<AttendeeRoleV2>, // Role (ROLE parameter)
pub status: Option<ParticipationStatusV2>, // Participation status (PARTSTAT parameter)
pub rsvp: Option<bool>, // RSVP expectation (RSVP parameter)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AttendeeRoleV2 {
Chair,
Required, // REQ-PARTICIPANT
Optional, // OPT-PARTICIPANT
NonParticipant,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ParticipationStatusV2 {
NeedsAction,
Accepted,
Declined,
Tentative,
Delegated,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AlarmV2 {
pub action: AlarmActionV2, // Action (AUDIO, DISPLAY, EMAIL)
pub trigger_minutes: i32, // Minutes before event (negative = before)
pub description: Option<String>, // Description for display/email
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AlarmActionV2 {
Audio,
Display,
Email,
}
// ==================== RESPONSES ====================
#[derive(Debug, Serialize)]
pub struct CreateEventResponseV2 {
pub success: bool,
pub message: String,
pub event: Option<EventSummaryV2>, // Return created event summary
}
#[derive(Debug, Serialize)]
pub struct UpdateEventResponseV2 {
pub success: bool,
pub message: String,
pub event: Option<EventSummaryV2>, // Return updated event summary
}
#[derive(Debug, Serialize)]
pub struct DeleteEventResponseV2 {
pub success: bool,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EventSummaryV2 {
pub uid: String,
pub summary: Option<String>,
pub dtstart: DateTime<Utc>,
pub dtend: Option<DateTime<Utc>>,
pub location: Option<String>,
pub all_day: bool,
pub href: Option<String>,
pub etag: Option<String>,
}
// ==================== CONVERSION HELPERS ====================
// Convert from old request format to new for backward compatibility
impl From<crate::models::CreateEventRequest> for CreateEventRequestV2 {
fn from(old: crate::models::CreateEventRequest) -> Self {
use chrono::{NaiveDate, NaiveTime, TimeZone, Utc};
// Parse the old string-based date/time format
let start_date = NaiveDate::parse_from_str(&old.start_date, "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Utc::now().date_naive());
let start_time = NaiveTime::parse_from_str(&old.start_time, "%H:%M")
.unwrap_or_else(|_| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
let dtstart = Utc.from_utc_datetime(&start_date.and_time(start_time));
let end_date = NaiveDate::parse_from_str(&old.end_date, "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Utc::now().date_naive());
let end_time = NaiveTime::parse_from_str(&old.end_time, "%H:%M")
.unwrap_or_else(|_| NaiveTime::from_hms_opt(1, 0, 0).unwrap());
let dtend = Some(Utc.from_utc_datetime(&end_date.and_time(end_time)));
// Parse comma-separated categories
let categories: Vec<String> = if old.categories.trim().is_empty() {
Vec::new()
} else {
old.categories.split(',').map(|s| s.trim().to_string()).collect()
};
// Parse comma-separated attendees
let attendees: Vec<AttendeeV2> = if old.attendees.trim().is_empty() {
Vec::new()
} else {
old.attendees.split(',').map(|email| AttendeeV2 {
email: email.trim().to_string(),
name: None,
role: Some(AttendeeRoleV2::Required),
status: Some(ParticipationStatusV2::NeedsAction),
rsvp: Some(true),
}).collect()
};
// Convert status string to enum
let status = match old.status.as_str() {
"tentative" => Some(EventStatusV2::Tentative),
"confirmed" => Some(EventStatusV2::Confirmed),
"cancelled" => Some(EventStatusV2::Cancelled),
_ => Some(EventStatusV2::Confirmed),
};
// Convert class string to enum
let class = match old.class.as_str() {
"public" => Some(EventClassV2::Public),
"private" => Some(EventClassV2::Private),
"confidential" => Some(EventClassV2::Confidential),
_ => Some(EventClassV2::Public),
};
// Create basic alarm if reminder specified
let alarms = if old.reminder == "none" {
Vec::new()
} else {
// Default to 15 minutes before for now
vec![AlarmV2 {
action: AlarmActionV2::Display,
trigger_minutes: 15,
description: Some("Event reminder".to_string()),
}]
};
Self {
summary: old.title,
description: if old.description.trim().is_empty() { None } else { Some(old.description) },
dtstart,
dtend,
location: if old.location.trim().is_empty() { None } else { Some(old.location) },
all_day: old.all_day,
status,
class,
priority: old.priority,
organizer: if old.organizer.trim().is_empty() { None } else { Some(old.organizer) },
attendees,
categories,
rrule: None, // TODO: Convert recurrence string to RRULE
alarms,
calendar_path: old.calendar_path,
}
}
}
// Error handling - ApiError is available through crate::models::ApiError in handlers