eventix/
event.rs

1//! Event types and builder API
2
3use crate::error::{EventixError, Result};
4use crate::recurrence::{Recurrence, RecurrenceFilter};
5use crate::timezone::{parse_datetime_with_tz, parse_timezone};
6use chrono::{DateTime, Duration, TimeZone};
7use chrono_tz::Tz;
8
9use serde::{Deserialize, Serialize};
10
11/// Status of an event in the booking lifecycle
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13pub enum EventStatus {
14    /// The event is confirmed and occupies time (default)
15    #[default]
16    Confirmed,
17    /// The event is tentative/provisional and occupies time
18    Tentative,
19    /// The event is cancelled and does NOT occupy time
20    Cancelled,
21    /// The time slot is blocked (similar to Confirmed)
22    Blocked,
23}
24
25/// A calendar event with timezone-aware start and end times
26#[derive(Debug, Clone)]
27pub struct Event {
28    /// Event title
29    pub title: String,
30
31    /// Optional description
32    pub description: Option<String>,
33
34    /// Start time with timezone
35    pub start_time: DateTime<Tz>,
36
37    /// End time with timezone
38    pub end_time: DateTime<Tz>,
39
40    /// Timezone for the event
41    pub timezone: Tz,
42
43    /// Optional list of attendees
44    pub attendees: Vec<String>,
45
46    /// Optional recurrence pattern
47    pub recurrence: Option<Recurrence>,
48
49    /// Optional recurrence filter (skip weekends, holidays, etc.)
50    pub recurrence_filter: Option<RecurrenceFilter>,
51
52    /// Specific dates to exclude from recurrence
53    pub exdates: Vec<DateTime<Tz>>,
54
55    /// Location of the event
56    pub location: Option<String>,
57
58    /// Unique identifier for the event
59    pub uid: Option<String>,
60
61    /// Status of the event (Confirmed, Cancelled, etc.)
62    pub status: EventStatus,
63}
64
65impl Event {
66    /// Create a new event builder
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use eventix::Event;
72    ///
73    /// let event = Event::builder()
74    ///     .title("Team Meeting")
75    ///     .start("2025-11-01 10:00:00", "America/New_York")
76    ///     .duration_hours(1)
77    ///     .build()
78    ///     .unwrap();
79    /// ```
80    pub fn builder() -> EventBuilder {
81        EventBuilder::new()
82    }
83
84    /// Get all occurrences of this event within a date range
85    ///
86    /// For non-recurring events, returns a single occurrence.
87    /// For recurring events, generates all occurrences based on the recurrence rule.
88    pub fn occurrences_between(
89        &self,
90        start: DateTime<Tz>,
91        end: DateTime<Tz>,
92        max_occurrences: usize,
93    ) -> Result<Vec<DateTime<Tz>>> {
94        if let Some(ref recurrence) = self.recurrence {
95            let mut occurrences =
96                recurrence.generate_occurrences(self.start_time, max_occurrences)?;
97
98            // Filter by date range
99            occurrences.retain(|dt| *dt >= start && *dt <= end);
100
101            // Apply recurrence filter if present
102            if let Some(ref filter) = self.recurrence_filter {
103                occurrences = filter.filter_occurrences(occurrences);
104            }
105
106            // Remove exception dates
107            occurrences.retain(|dt| {
108                !self.exdates.iter().any(|exdate| exdate.date_naive() == dt.date_naive())
109            });
110
111            Ok(occurrences)
112        } else {
113            // Non-recurring event
114            if self.start_time >= start && self.start_time <= end {
115                Ok(vec![self.start_time])
116            } else {
117                Ok(vec![])
118            }
119        }
120    }
121
122    /// Check if this event occurs on a specific date
123    pub fn occurs_on(&self, date: DateTime<Tz>) -> Result<bool> {
124        let start = date.date_naive().and_hms_opt(0, 0, 0).ok_or_else(|| {
125            EventixError::ValidationError("Invalid start time for date check".to_string())
126        })?;
127        let end = date.date_naive().and_hms_opt(23, 59, 59).ok_or_else(|| {
128            EventixError::ValidationError("Invalid end time for date check".to_string())
129        })?;
130
131        let start_dt = self.timezone.from_local_datetime(&start).earliest().ok_or_else(|| {
132            EventixError::ValidationError("Ambiguous start time for date check".to_string())
133        })?;
134        let end_dt = self.timezone.from_local_datetime(&end).latest().ok_or_else(|| {
135            EventixError::ValidationError("Ambiguous end time for date check".to_string())
136        })?;
137
138        let occurrences = self.occurrences_between(start_dt, end_dt, 1)?;
139        Ok(!occurrences.is_empty())
140    }
141
142    /// Get the duration of this event
143    pub fn duration(&self) -> Duration {
144        self.end_time.signed_duration_since(self.start_time)
145    }
146
147    /// Check if the event is considered "active" (occupies time)
148    ///
149    /// Returns true for Confirmed, Tentative, and Blocked.
150    /// Returns false for Cancelled.
151    pub fn is_active(&self) -> bool {
152        matches!(
153            self.status,
154            EventStatus::Confirmed | EventStatus::Tentative | EventStatus::Blocked
155        )
156    }
157
158    /// Confirm the event
159    pub fn confirm(&mut self) {
160        self.status = EventStatus::Confirmed;
161    }
162
163    /// Cancel the event
164    pub fn cancel(&mut self) {
165        self.status = EventStatus::Cancelled;
166    }
167
168    /// Set the event as tentative
169    pub fn tentative(&mut self) {
170        self.status = EventStatus::Tentative;
171    }
172
173    /// Block the event (similar to Confirmed, but explicit)
174    pub fn block(&mut self) {
175        self.status = EventStatus::Blocked;
176    }
177
178    /// Reschedule the event to a new time
179    ///
180    /// This updates the start and end times. If the event was Cancelled,
181    /// it automatically resets the status to Confirmed.
182    ///
183    /// This also updates the event's timezone to match the new start time.
184    pub fn reschedule(&mut self, new_start: DateTime<Tz>, new_end: DateTime<Tz>) -> Result<()> {
185        if new_end <= new_start {
186            return Err(EventixError::ValidationError(
187                "Event end time must be after start time".to_string(),
188            ));
189        }
190        self.start_time = new_start;
191        self.end_time = new_end;
192        self.timezone = new_start.timezone();
193
194        // If rescheduling a cancelled event, assume it's valid again
195        if self.status == EventStatus::Cancelled {
196            self.status = EventStatus::Confirmed;
197        }
198        Ok(())
199    }
200}
201
202/// Builder for creating events with a fluent API
203pub struct EventBuilder {
204    title: Option<String>,
205    description: Option<String>,
206    start_time: Option<DateTime<Tz>>,
207    end_time: Option<DateTime<Tz>>,
208    timezone: Option<Tz>,
209    attendees: Vec<String>,
210    recurrence: Option<Recurrence>,
211    recurrence_filter: Option<RecurrenceFilter>,
212    exdates: Vec<DateTime<Tz>>,
213    location: Option<String>,
214    uid: Option<String>,
215    status: EventStatus,
216}
217
218impl EventBuilder {
219    /// Create a new event builder
220    pub fn new() -> Self {
221        Self {
222            title: None,
223            description: None,
224            start_time: None,
225            end_time: None,
226            timezone: None,
227            attendees: Vec::new(),
228            recurrence: None,
229            recurrence_filter: None,
230            exdates: Vec::new(),
231            location: None,
232            uid: None,
233            status: EventStatus::default(),
234        }
235    }
236
237    /// Set the event title
238    pub fn title(mut self, title: impl Into<String>) -> Self {
239        self.title = Some(title.into());
240        self
241    }
242
243    /// Set the event description
244    pub fn description(mut self, description: impl Into<String>) -> Self {
245        self.description = Some(description.into());
246        self
247    }
248
249    /// Set the start time using a string and timezone
250    ///
251    /// # Examples
252    ///
253    /// ```
254    /// use eventix::Event;
255    ///
256    /// let event = Event::builder()
257    ///     .title("Meeting")
258    ///     .start("2025-11-01 10:00:00", "America/New_York")
259    ///     .duration_hours(1)
260    ///     .build()
261    ///     .unwrap();
262    /// ```
263    pub fn start(mut self, datetime: &str, timezone: &str) -> Self {
264        if let Ok(tz) = parse_timezone(timezone) {
265            self.timezone = Some(tz);
266            if let Ok(dt) = parse_datetime_with_tz(datetime, tz) {
267                self.start_time = Some(dt);
268            }
269        }
270        self
271    }
272
273    /// Set the start time directly
274    pub fn start_datetime(mut self, datetime: DateTime<Tz>) -> Self {
275        self.timezone = Some(datetime.timezone());
276        self.start_time = Some(datetime);
277        self
278    }
279
280    /// Set the end time using a string
281    pub fn end(mut self, datetime: &str) -> Self {
282        if let Some(tz) = self.timezone {
283            if let Ok(dt) = parse_datetime_with_tz(datetime, tz) {
284                self.end_time = Some(dt);
285            }
286        }
287        self
288    }
289
290    /// Set the end time directly
291    pub fn end_datetime(mut self, datetime: DateTime<Tz>) -> Self {
292        self.end_time = Some(datetime);
293        self
294    }
295
296    /// Set the duration in hours (calculates end_time from start_time)
297    pub fn duration_hours(mut self, hours: i64) -> Self {
298        if let Some(start) = self.start_time {
299            self.end_time = Some(start + Duration::hours(hours));
300        }
301        self
302    }
303
304    /// Set the duration in minutes (calculates end_time from start_time)
305    pub fn duration_minutes(mut self, minutes: i64) -> Self {
306        if let Some(start) = self.start_time {
307            self.end_time = Some(start + Duration::minutes(minutes));
308        }
309        self
310    }
311
312    /// Set the duration (calculates end_time from start_time)
313    pub fn duration(mut self, duration: Duration) -> Self {
314        if let Some(start) = self.start_time {
315            self.end_time = Some(start + duration);
316        }
317        self
318    }
319
320    /// Add an attendee
321    pub fn attendee(mut self, attendee: impl Into<String>) -> Self {
322        self.attendees.push(attendee.into());
323        self
324    }
325
326    /// Set multiple attendees
327    pub fn attendees(mut self, attendees: Vec<String>) -> Self {
328        self.attendees = attendees;
329        self
330    }
331
332    /// Set the recurrence pattern
333    pub fn recurrence(mut self, recurrence: Recurrence) -> Self {
334        self.recurrence = Some(recurrence);
335        self
336    }
337
338    /// Enable skipping weekends for recurring events
339    pub fn skip_weekends(mut self, skip: bool) -> Self {
340        let filter = self.recurrence_filter.unwrap_or_default();
341        self.recurrence_filter = Some(filter.skip_weekends(skip));
342        self
343    }
344
345    /// Add exception dates (dates to skip)
346    pub fn exception_dates(mut self, dates: Vec<DateTime<Tz>>) -> Self {
347        self.exdates = dates;
348        self
349    }
350
351    /// Add a single exception date
352    pub fn exception_date(mut self, date: DateTime<Tz>) -> Self {
353        self.exdates.push(date);
354        self
355    }
356
357    /// Set the location
358    pub fn location(mut self, location: impl Into<String>) -> Self {
359        self.location = Some(location.into());
360        self
361    }
362
363    /// Set a unique identifier
364    pub fn uid(mut self, uid: impl Into<String>) -> Self {
365        self.uid = Some(uid.into());
366        self
367    }
368
369    /// Set the event status
370    pub fn status(mut self, status: EventStatus) -> Self {
371        self.status = status;
372        self
373    }
374
375    /// Build the event
376    pub fn build(self) -> Result<Event> {
377        let title = self
378            .title
379            .ok_or_else(|| EventixError::ValidationError("Event title is required".to_string()))?;
380
381        let start_time = self.start_time.ok_or_else(|| {
382            EventixError::ValidationError("Event start time is required".to_string())
383        })?;
384
385        let end_time = self.end_time.ok_or_else(|| {
386            EventixError::ValidationError("Event end time is required".to_string())
387        })?;
388
389        let timezone = self.timezone.ok_or_else(|| {
390            EventixError::ValidationError("Event timezone is required".to_string())
391        })?;
392
393        if end_time <= start_time {
394            return Err(EventixError::ValidationError(
395                "Event end time must be after start time".to_string(),
396            ));
397        }
398
399        Ok(Event {
400            title,
401            description: self.description,
402            start_time,
403            end_time,
404            timezone,
405            attendees: self.attendees,
406            recurrence: self.recurrence,
407            recurrence_filter: self.recurrence_filter,
408            exdates: self.exdates,
409            location: self.location,
410            uid: self.uid,
411            status: self.status,
412        })
413    }
414}
415
416impl Default for EventBuilder {
417    fn default() -> Self {
418        Self::new()
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    #![allow(clippy::unwrap_used)]
425    use super::*;
426
427    #[test]
428    fn test_event_builder() {
429        let event = Event::builder()
430            .title("Test Event")
431            .description("A test event")
432            .start("2025-11-01 10:00:00", "UTC")
433            .duration_hours(2)
434            .attendee("alice@example.com")
435            .build()
436            .unwrap();
437
438        assert_eq!(event.title, "Test Event");
439        assert_eq!(event.description, Some("A test event".to_string()));
440        assert_eq!(event.attendees.len(), 1);
441        assert_eq!(event.duration(), Duration::hours(2));
442    }
443
444    #[test]
445    fn test_event_builder_duration() {
446        let event = Event::builder()
447            .title("Test Event")
448            .description("A test event")
449            .start("2025-11-01 10:00:00", "UTC")
450            .duration(Duration::hours(1) + Duration::minutes(10))
451            .attendee("alice@example.com")
452            .build()
453            .unwrap();
454
455        assert_eq!(event.title, "Test Event");
456        assert_eq!(event.description, Some("A test event".to_string()));
457        assert_eq!(event.attendees.len(), 1);
458        assert_eq!(event.end_time.to_rfc3339(), "2025-11-01T11:10:00+00:00");
459        let duration_in_secs = (60.0 * 60.0) + (10.0 * 60.0); // 1 hour 10 minutes = 4200 seconds
460        assert_eq!(event.duration().as_seconds_f32(), duration_in_secs);
461    }
462
463    #[test]
464    fn test_event_validation() {
465        // Missing title
466        let result = Event::builder().start("2025-11-01 10:00:00", "UTC").duration_hours(1).build();
467        assert!(result.is_err());
468
469        // End before start
470        let result = Event::builder()
471            .title("Test")
472            .start("2025-11-01 10:00:00", "UTC")
473            .end("2025-11-01 09:00:00")
474            .build();
475        assert!(result.is_err());
476    }
477}