Skip to content

Commit

Permalink
feat: add support for Alarms
Browse files Browse the repository at this point in the history
  • Loading branch information
hoodie committed Nov 13, 2022
1 parent ce57466 commit eb4a1bd
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 1 deletion.
24 changes: 24 additions & 0 deletions examples/alarm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use chrono::*;
use icalendar::*;

fn main() {
// lets create a calendar
let my_calendar = Calendar::new()
.name("example calendar")
.push(
Event::new()
.summary("test event")
.description("here I have something really important to do")
.starts(Utc::now())
.class(Class::Confidential)
.ends(Utc::now() + Duration::days(1))
.alarm(Duration::days(1), Action::Audio)
.append_component(
Alarm::with_trigger(Trigger::from(Duration::days(1))).and_action(Action::Audio),
)
.done(),
)
.done();

println!("{}", my_calendar);
}
9 changes: 9 additions & 0 deletions src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ use std::{collections::BTreeMap, fmt, mem};
use crate::properties::*;
use date_time::{format_utc_date_time, naive_date_to_property, parse_utc_date_time};

pub mod alarm;
pub(crate) mod date_time;
mod event;
mod other;
mod todo;
mod venue;

use alarm::*;
pub use date_time::{CalendarDateTime, DatePerhapsTime};
pub use event::*;
pub use other::*;
Expand Down Expand Up @@ -305,6 +307,12 @@ pub trait EventLike: Component {
fn get_location(&self) -> Option<&str> {
self.property_value("LOCATION")
}

/// Set the ALARM for this event
/// [3.6.6. Alarm Component](https://datatracker.ietf.org/doc/html/rfc5545#section-3.6.6)
fn alarm(&mut self, trigger: impl Into<Trigger>, action: Action) -> &mut Self {
self.append_component(Alarm::with_trigger(trigger.into()).and_action(action))
}
}

macro_rules! event_impl {
Expand Down Expand Up @@ -378,6 +386,7 @@ component_impl! { Todo , String::from("VTODO")}
event_impl! { Todo}

component_impl! { Venue , String::from("VVENUE")}
component_impl! { Alarm, String::from("VALARM") }

#[cfg(test)]
mod tests {
Expand Down
175 changes: 175 additions & 0 deletions src/components/alarm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
use std::collections::HashMap;

use super::*;

/// VALARM [(RFC 5545, Section 3.6.6 )](https://tools.ietf.org/html/rfc5545#section-3.6.6)
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Alarm {
pub(crate) inner: InnerComponent,
// pub(crate) action: Option<Action>,
}

impl Alarm {
/// Creates a new Alarm with the given [`Trigger`]
/// The [`TRIGGER`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.3) property is mandatory.
/// see also [Alarm Trigger Relationship](https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.14)
pub fn with_trigger<T: Into<Trigger>>(trigger: T) -> Self {
let trigger: Trigger = trigger.into();
Alarm::default().append_property(trigger.into()).done()
}

/// add the [`ACTION`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.1) property
pub fn and_action(&mut self, action: Action) -> Self {
// self.add_property("ACTION", action.as_str());
self.append_property(action.into());
self.done()
}

/// add the [`ACTION`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.1) property
pub fn action(&mut self, action: Action) -> &mut Self {
// self.add_property("ACTION", action.as_str());
self.append_property(action.into());
self
}

/// add the [`DURATION`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.5) property
/// TODO: add this to Event and Venue as well
pub fn duration(&mut self, duration: Duration) -> &mut Self {
// self.add_property("ACTION", action.as_str());
self.append_property(duration.into());
self
}

/// add the [`REPEAT`](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.2) property
pub fn repeat<T: Copy + Clone + Into<Repeat>>(&mut self, repeat_count: T) -> &mut Self {
let repeat: Repeat = repeat_count.into();
self.append_property(repeat.into());
self
}

/// End of builder pattern.
/// copies over everything
pub fn done(&mut self) -> Self {
Alarm {
inner: self.inner.done(),
// TODO: add default action = None
}
}

//pub fn repeats<R:Repeater+?Sized>(&mut self, repeat: R) -> &mut Self {
// unimplemented!()
//}
}

/// [rfc5545#section-3.8.6.1](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.1)
#[derive(Debug, PartialEq, Eq)]
pub enum Action {
/// [rfc5545#section-3.8.6.1](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.1)
Audio,
/// [rfc5545#section-3.8.6.1](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.1)
Email,
/// [rfc5545#section-3.8.6.1](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.1)
Display,
// /// [rfc5545#section-3.8.6.1](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.1)
// IanaToken(String),
// /// [rfc5545#section-3.8.6.1](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.1)
// XName{vendor: String, name: String},
/// what ever else
Other(String),
}

impl ToString for Action {
/// convert the ACTION into its serialized representation
fn to_string(&self) -> String {
match self {
Action::Audio => "AUDIO".into(),
Action::Email => "EMAIL".into(),
Action::Display => "DISPLAY".into(),
Action::Other(other) => other.clone(),
}
}
}

impl From<Action> for Property {
fn from(action: Action) -> Self {
Property {
key: String::from("ACTION"),
val: action.to_string(),
params: HashMap::new(),
}
}
}

/// [rfc5545#section-3.8.6.2](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.2)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Repeat(
u32, // technically a signed integer according to spec
);

impl From<u32> for Repeat {
fn from(count: u32) -> Self {
Repeat(count)
}
}

impl From<Repeat> for Property {
fn from(r: Repeat) -> Self {
Property::new_pre_alloc("REPEAT".into(), r.0.to_string())
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Related {
Start,
End,
}

/// [rfc5545#section-3.8.6.3](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.6.3)
/// This property specifies when an alarm will trigger.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Trigger {
/// Duration in relation to either Start or End of the event
Duration(Duration, Option<Related>),
/// Absolute DateTime of the Trigger
DateTime(DatePerhapsTime),
}

impl From<Duration> for Trigger {
fn from(duration: Duration) -> Self {
Trigger::Duration(duration, None)
}
}

impl<T> From<T> for Trigger
where
DatePerhapsTime: From<T>,
{
fn from(dt: T) -> Self {
Trigger::DateTime(DatePerhapsTime::from(dt))
}
}

impl From<(Duration, Related)> for Trigger {
fn from((duration, related): (Duration, Related)) -> Self {
Trigger::Duration(duration, Some(related))
}
}

impl From<Trigger> for Property {
fn from(trigger: Trigger) -> Self {
match trigger {
Trigger::Duration(duration, _) => {
Property::new_pre_alloc("TRIGGER".into(), duration.to_string())
}

Trigger::DateTime(dt) => dt.to_property("TRIGGER"),
}
}
}

#[test]
fn test_trigger() {
let prop: Property = dbg!(Trigger::from(Duration::weeks(14)).into());
let mut out = String::new();
prop.fmt_write(&mut out).unwrap();
dbg!(out);
}
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ mod properties;

pub use crate::{
calendar::{Calendar, CalendarComponent},
components::{CalendarDateTime, Component, DatePerhapsTime, Event, Todo, Venue},
components::{
alarm::{Action, Alarm, Trigger},
CalendarDateTime, Component, DatePerhapsTime, Event, EventLike, Todo, Venue,
},
properties::{Class, EventStatus, Parameter, Property, TodoStatus, ValueType},
};

Expand Down
58 changes: 58 additions & 0 deletions tests/alarm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use chrono::*;
use icalendar::*;
use pretty_assertions::assert_eq;

/// Taken from https://datatracker.ietf.org/doc/html/rfc5545
/// some properties have been removed or reordered
const EXPECTED_CAL_CONTENT: &str = "\
BEGIN:VCALENDAR\r
VERSION:2.0\r
PRODID:ICALENDAR-RS\r
CALSCALE:GREGORIAN\r
BEGIN:VTODO\r
DTSTAMP:19980130T134500Z\r
DTSTART:19980130T134500Z\r
DUE:19980415T000000\r
SEQUENCE:2\r
STATUS:NEEDS-ACTION\r
SUMMARY:Submit Income Taxes\r
UID:uid4@example.com\r
BEGIN:VALARM\r
ACTION:AUDIO\r
DTSTAMP:19980130T134500Z\r
DURATION:PT3600S\r
REPEAT:4\r
TRIGGER:19980403T120000Z\r
UID:OverwriteForConsistency\r
END:VALARM\r
END:VTODO\r
END:VCALENDAR\r
";

#[test]
fn test_alarm_to_string() {
let mut calendar = Calendar::new();
let todo = Todo::new()
.uid("uid4@example.com")
.add_property("DTSTAMP", "19980130T134500Z")
.sequence(2)
//.organizer("")
.starts(Utc.ymd(1998, 1, 30).and_hms(13, 45, 0))
// .due(Utc.ymd(1998, 4, 15).and_hms(0, 0, 0))
.due(NaiveDate::from_ymd(1998, 4, 15).and_hms(0, 0, 0))
//.repeat(4)
.status(TodoStatus::NeedsAction)
.summary("Submit Income Taxes")
.append_component(
Alarm::with_trigger(Trigger::from(Utc.ymd(1998, 4, 3).and_hms(12, 0, 0)))
.duration(chrono::Duration::hours(1))
.uid("OverwriteForConsistency")
.action(Action::Audio)
.repeat(4)
.add_property("DTSTAMP", "19980130T134500Z")
.done(),
)
.done();
calendar.push(todo);
assert_eq!(calendar.to_string(), EXPECTED_CAL_CONTENT);
}

0 comments on commit eb4a1bd

Please sign in to comment.