Implement shared RFC 5545 VEvent library with workspace restructuring
- Created calendar-models/ shared library with RFC 5545-compliant VEvent structures - Migrated backend to use shared VEvent with proper field mappings (dtstart/dtend, rrule, exdate, etc.) - Converted CalDAV client to parse into VEvent structures with structured types - Updated all CRUD handlers to use VEvent with CalendarUser, Attendee, VAlarm types - Restructured project as Cargo workspace with frontend/, backend/, calendar-models/ - Updated Trunk configuration for new directory structure - Fixed all compilation errors and field references throughout codebase - Updated documentation and build instructions for workspace structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
13
calendar-models/Cargo.toml
Normal file
13
calendar-models/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "calendar-models"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
wasm = ["chrono/wasm-bindgen", "uuid/wasm-bindgen"]
|
||||
220
calendar-models/src/common.rs
Normal file
220
calendar-models/src/common.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
//! Common types and enums used across calendar components
|
||||
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ==================== ENUMS AND COMMON TYPES ====================
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum EventStatus {
|
||||
Tentative,
|
||||
Confirmed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum EventClass {
|
||||
Public,
|
||||
Private,
|
||||
Confidential,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum TimeTransparency {
|
||||
Opaque, // OPAQUE - time is not available
|
||||
Transparent, // TRANSPARENT - time is available
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum TodoStatus {
|
||||
NeedsAction,
|
||||
Completed,
|
||||
InProcess,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum AttendeeRole {
|
||||
Chair,
|
||||
ReqParticipant,
|
||||
OptParticipant,
|
||||
NonParticipant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ParticipationStatus {
|
||||
NeedsAction,
|
||||
Accepted,
|
||||
Declined,
|
||||
Tentative,
|
||||
Delegated,
|
||||
Completed,
|
||||
InProcess,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum AlarmAction {
|
||||
Audio,
|
||||
Display,
|
||||
Email,
|
||||
Procedure,
|
||||
}
|
||||
|
||||
// ==================== STRUCTURED TYPES ====================
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct CalendarUser {
|
||||
pub cal_address: String, // Calendar user address (usually email)
|
||||
pub common_name: Option<String>, // CN parameter - display name
|
||||
pub dir_entry_ref: Option<String>, // DIR parameter - directory entry
|
||||
pub sent_by: Option<String>, // SENT-BY parameter
|
||||
pub language: Option<String>, // LANGUAGE parameter
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Attendee {
|
||||
pub cal_address: String, // Calendar user address
|
||||
pub common_name: Option<String>, // CN parameter
|
||||
pub role: Option<AttendeeRole>, // ROLE parameter
|
||||
pub part_stat: Option<ParticipationStatus>, // PARTSTAT parameter
|
||||
pub rsvp: Option<bool>, // RSVP parameter
|
||||
pub cu_type: Option<String>, // CUTYPE parameter (INDIVIDUAL, GROUP, RESOURCE, ROOM, UNKNOWN)
|
||||
pub member: Vec<String>, // MEMBER parameter
|
||||
pub delegated_to: Vec<String>, // DELEGATED-TO parameter
|
||||
pub delegated_from: Vec<String>, // DELEGATED-FROM parameter
|
||||
pub sent_by: Option<String>, // SENT-BY parameter
|
||||
pub dir_entry_ref: Option<String>, // DIR parameter
|
||||
pub language: Option<String>, // LANGUAGE parameter
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct VAlarm {
|
||||
pub action: AlarmAction, // Action (ACTION) - REQUIRED
|
||||
pub trigger: AlarmTrigger, // Trigger (TRIGGER) - REQUIRED
|
||||
pub duration: Option<Duration>, // Duration (DURATION)
|
||||
pub repeat: Option<u32>, // Repeat count (REPEAT)
|
||||
pub description: Option<String>, // Description for DISPLAY/EMAIL
|
||||
pub summary: Option<String>, // Summary for EMAIL
|
||||
pub attendees: Vec<Attendee>, // Attendees for EMAIL
|
||||
pub attach: Vec<Attachment>, // Attachments for AUDIO/EMAIL
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AlarmTrigger {
|
||||
DateTime(DateTime<Utc>), // Absolute trigger time
|
||||
Duration(Duration), // Duration relative to start/end
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Attachment {
|
||||
pub format_type: Option<String>, // FMTTYPE parameter (MIME type)
|
||||
pub encoding: Option<String>, // ENCODING parameter
|
||||
pub value: Option<String>, // VALUE parameter (BINARY or URI)
|
||||
pub uri: Option<String>, // URI reference
|
||||
pub binary_data: Option<Vec<u8>>, // Binary data (when ENCODING=BASE64)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct GeographicPosition {
|
||||
pub latitude: f64, // Latitude in decimal degrees
|
||||
pub longitude: f64, // Longitude in decimal degrees
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct VTimeZone {
|
||||
pub tzid: String, // Time zone ID (TZID) - REQUIRED
|
||||
pub last_modified: Option<DateTime<Utc>>, // Last modified (LAST-MODIFIED)
|
||||
pub tzurl: Option<String>, // Time zone URL (TZURL)
|
||||
pub standard_components: Vec<TimeZoneComponent>, // STANDARD components
|
||||
pub daylight_components: Vec<TimeZoneComponent>, // DAYLIGHT components
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TimeZoneComponent {
|
||||
pub dtstart: DateTime<Utc>, // Start of this time zone definition
|
||||
pub tzoffset_to: String, // UTC offset for this component
|
||||
pub tzoffset_from: String, // UTC offset before this component
|
||||
pub rrule: Option<String>, // Recurrence rule
|
||||
pub rdate: Vec<DateTime<Utc>>, // Recurrence dates
|
||||
pub tzname: Vec<String>, // Time zone names
|
||||
pub comment: Vec<String>, // Comments
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct VJournal {
|
||||
// Required properties
|
||||
pub dtstamp: DateTime<Utc>, // Date-time stamp (DTSTAMP) - REQUIRED
|
||||
pub uid: String, // Unique identifier (UID) - REQUIRED
|
||||
|
||||
// Optional properties
|
||||
pub dtstart: Option<DateTime<Utc>>, // Start date-time (DTSTART)
|
||||
pub summary: Option<String>, // Summary/title (SUMMARY)
|
||||
pub description: Option<String>, // Description (DESCRIPTION)
|
||||
|
||||
// Classification and status
|
||||
pub class: Option<EventClass>, // Classification (CLASS)
|
||||
pub status: Option<String>, // Status (STATUS)
|
||||
|
||||
// People and organization
|
||||
pub organizer: Option<CalendarUser>, // Organizer (ORGANIZER)
|
||||
pub attendees: Vec<Attendee>, // Attendees (ATTENDEE)
|
||||
|
||||
// Categorization
|
||||
pub categories: Vec<String>, // Categories (CATEGORIES)
|
||||
|
||||
// Versioning and modification
|
||||
pub sequence: Option<u32>, // Sequence number (SEQUENCE)
|
||||
pub created: Option<DateTime<Utc>>, // Creation time (CREATED)
|
||||
pub last_modified: Option<DateTime<Utc>>, // Last modified (LAST-MODIFIED)
|
||||
|
||||
// Recurrence
|
||||
pub rrule: Option<String>, // Recurrence rule (RRULE)
|
||||
pub rdate: Vec<DateTime<Utc>>, // Recurrence dates (RDATE)
|
||||
pub exdate: Vec<DateTime<Utc>>, // Exception dates (EXDATE)
|
||||
pub recurrence_id: Option<DateTime<Utc>>, // Recurrence ID (RECURRENCE-ID)
|
||||
|
||||
// Attachments
|
||||
pub attachments: Vec<Attachment>, // Attachments (ATTACH)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct VFreeBusy {
|
||||
// Required properties
|
||||
pub dtstamp: DateTime<Utc>, // Date-time stamp (DTSTAMP) - REQUIRED
|
||||
pub uid: String, // Unique identifier (UID) - REQUIRED
|
||||
|
||||
// Optional date-time properties
|
||||
pub dtstart: Option<DateTime<Utc>>, // Start date-time (DTSTART)
|
||||
pub dtend: Option<DateTime<Utc>>, // End date-time (DTEND)
|
||||
|
||||
// People
|
||||
pub organizer: Option<CalendarUser>, // Organizer (ORGANIZER)
|
||||
pub attendees: Vec<Attendee>, // Attendees (ATTENDEE)
|
||||
|
||||
// Free/busy time
|
||||
pub freebusy: Vec<FreeBusyTime>, // Free/busy time periods
|
||||
pub url: Option<String>, // URL (URL)
|
||||
pub comment: Vec<String>, // Comments (COMMENT)
|
||||
pub contact: Option<String>, // Contact information (CONTACT)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct FreeBusyTime {
|
||||
pub fb_type: FreeBusyType, // Free/busy type
|
||||
pub periods: Vec<Period>, // Time periods
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum FreeBusyType {
|
||||
Free,
|
||||
Busy,
|
||||
BusyUnavailable,
|
||||
BusyTentative,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Period {
|
||||
pub start: DateTime<Utc>, // Period start
|
||||
pub end: Option<DateTime<Utc>>, // Period end
|
||||
pub duration: Option<Duration>, // Period duration (alternative to end)
|
||||
}
|
||||
10
calendar-models/src/lib.rs
Normal file
10
calendar-models/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! RFC 5545 Compliant Calendar Models
|
||||
//!
|
||||
//! This crate provides shared data structures for calendar applications
|
||||
//! that comply with RFC 5545 (iCalendar) specification.
|
||||
|
||||
pub mod vevent;
|
||||
pub mod common;
|
||||
|
||||
pub use vevent::*;
|
||||
pub use common::*;
|
||||
149
calendar-models/src/vevent.rs
Normal file
149
calendar-models/src/vevent.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
//! VEvent - RFC 5545 compliant calendar event structure
|
||||
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::common::*;
|
||||
|
||||
// ==================== VEVENT COMPONENT ====================
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct VEvent {
|
||||
// Required properties
|
||||
pub dtstamp: DateTime<Utc>, // Date-time stamp (DTSTAMP) - REQUIRED
|
||||
pub uid: String, // Unique identifier (UID) - REQUIRED
|
||||
pub dtstart: DateTime<Utc>, // Start date-time (DTSTART) - REQUIRED
|
||||
|
||||
// Optional properties (commonly used)
|
||||
pub dtend: Option<DateTime<Utc>>, // End date-time (DTEND)
|
||||
pub duration: Option<Duration>, // Duration (DURATION) - alternative to DTEND
|
||||
pub summary: Option<String>, // Summary/title (SUMMARY)
|
||||
pub description: Option<String>, // Description (DESCRIPTION)
|
||||
pub location: Option<String>, // Location (LOCATION)
|
||||
|
||||
// Classification and status
|
||||
pub class: Option<EventClass>, // Classification (CLASS)
|
||||
pub status: Option<EventStatus>, // Status (STATUS)
|
||||
pub transp: Option<TimeTransparency>, // Time transparency (TRANSP)
|
||||
pub priority: Option<u8>, // Priority 0-9 (PRIORITY)
|
||||
|
||||
// People and organization
|
||||
pub organizer: Option<CalendarUser>, // Organizer (ORGANIZER)
|
||||
pub attendees: Vec<Attendee>, // Attendees (ATTENDEE)
|
||||
pub contact: Option<String>, // Contact information (CONTACT)
|
||||
|
||||
// Categorization and relationships
|
||||
pub categories: Vec<String>, // Categories (CATEGORIES)
|
||||
pub comment: Option<String>, // Comment (COMMENT)
|
||||
pub resources: Vec<String>, // Resources (RESOURCES)
|
||||
pub related_to: Option<String>, // Related component (RELATED-TO)
|
||||
pub url: Option<String>, // URL (URL)
|
||||
|
||||
// Geographical
|
||||
pub geo: Option<GeographicPosition>, // Geographic position (GEO)
|
||||
|
||||
// Versioning and modification
|
||||
pub sequence: Option<u32>, // Sequence number (SEQUENCE)
|
||||
pub created: Option<DateTime<Utc>>, // Creation time (CREATED)
|
||||
pub last_modified: Option<DateTime<Utc>>, // Last modified (LAST-MODIFIED)
|
||||
|
||||
// Recurrence
|
||||
pub rrule: Option<String>, // Recurrence rule (RRULE)
|
||||
pub rdate: Vec<DateTime<Utc>>, // Recurrence dates (RDATE)
|
||||
pub exdate: Vec<DateTime<Utc>>, // Exception dates (EXDATE)
|
||||
pub recurrence_id: Option<DateTime<Utc>>, // Recurrence ID (RECURRENCE-ID)
|
||||
|
||||
// Alarms and attachments
|
||||
pub alarms: Vec<VAlarm>, // VALARM components
|
||||
pub attachments: Vec<Attachment>, // Attachments (ATTACH)
|
||||
|
||||
// CalDAV specific (for implementation)
|
||||
pub etag: Option<String>, // ETag for CalDAV
|
||||
pub href: Option<String>, // Href for CalDAV
|
||||
pub calendar_path: Option<String>, // Calendar path
|
||||
pub all_day: bool, // All-day event flag
|
||||
}
|
||||
|
||||
impl VEvent {
|
||||
/// Create a new VEvent with required fields
|
||||
pub fn new(uid: String, dtstart: DateTime<Utc>) -> Self {
|
||||
Self {
|
||||
dtstamp: Utc::now(),
|
||||
uid,
|
||||
dtstart,
|
||||
dtend: None,
|
||||
duration: None,
|
||||
summary: None,
|
||||
description: None,
|
||||
location: None,
|
||||
class: None,
|
||||
status: None,
|
||||
transp: None,
|
||||
priority: None,
|
||||
organizer: None,
|
||||
attendees: Vec::new(),
|
||||
contact: None,
|
||||
categories: Vec::new(),
|
||||
comment: None,
|
||||
resources: Vec::new(),
|
||||
related_to: None,
|
||||
url: None,
|
||||
geo: None,
|
||||
sequence: None,
|
||||
created: Some(Utc::now()),
|
||||
last_modified: Some(Utc::now()),
|
||||
rrule: None,
|
||||
rdate: Vec::new(),
|
||||
exdate: Vec::new(),
|
||||
recurrence_id: None,
|
||||
alarms: Vec::new(),
|
||||
attachments: Vec::new(),
|
||||
etag: None,
|
||||
href: None,
|
||||
calendar_path: None,
|
||||
all_day: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to get effective end time (dtend or dtstart + duration)
|
||||
pub fn get_end_time(&self) -> DateTime<Utc> {
|
||||
if let Some(dtend) = self.dtend {
|
||||
dtend
|
||||
} else if let Some(duration) = self.duration {
|
||||
self.dtstart + duration
|
||||
} else {
|
||||
// Default to 1 hour if no end or duration specified
|
||||
self.dtstart + Duration::hours(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to get event duration
|
||||
pub fn get_duration(&self) -> Duration {
|
||||
if let Some(duration) = self.duration {
|
||||
duration
|
||||
} else if let Some(dtend) = self.dtend {
|
||||
dtend - self.dtstart
|
||||
} else {
|
||||
Duration::hours(1) // Default duration
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to get display title (summary or "Untitled Event")
|
||||
pub fn get_title(&self) -> String {
|
||||
self.summary.clone().unwrap_or_else(|| "Untitled Event".to_string())
|
||||
}
|
||||
|
||||
/// Helper method to get start date for UI compatibility
|
||||
pub fn get_date(&self) -> chrono::NaiveDate {
|
||||
self.dtstart.date_naive()
|
||||
}
|
||||
|
||||
/// Check if event is recurring
|
||||
pub fn is_recurring(&self) -> bool {
|
||||
self.rrule.is_some()
|
||||
}
|
||||
|
||||
/// Check if this is an exception to a recurring series
|
||||
pub fn is_exception(&self) -> bool {
|
||||
self.recurrence_id.is_some()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user