summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml7
-rw-r--r--README.md43
-rw-r--r--examples/readme.rs34
-rw-r--r--examples/tasks.rs9
-rw-r--r--src/components.rs92
-rw-r--r--src/lib.rs14
-rw-r--r--tests/calendar.rs32
7 files changed, 182 insertions, 49 deletions
diff --git a/Cargo.toml b/Cargo.toml
index e98133f..58f8a0d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
authors = ["Hendrik Sollich <hendrik@hoodie.de>"]
name = "icalendar"
-version = "0.6.0"
+version = "0.7.0"
license = "MIT/Apache-2.0"
edition = "2018"
@@ -25,4 +25,7 @@ chrono = "0.4"
[dependencies.uuid]
features = ["v4"]
-version = "0.5"
+version = "0.8"
+
+[dev-dependencies]
+pretty_assertions = "0.6"
diff --git a/README.md b/README.md
index 9cd132f..0356d4a 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+<div align="center">
+
# iCalendar in Rust
[![Travis](https://img.shields.io/travis/hoodie/icalendar-rs.svg)](https://travis-ci.org/hoodie/icalendar-rs/)
@@ -5,10 +7,12 @@
[![Crates.io](https://img.shields.io/crates/d/icalendar.svg)](https://crates.io/crates/icalendar)
[![version](https://img.shields.io/crates/v/icalendar.svg)](https://crates.io/crates/icalendar/)
[![documentation](https://docs.rs/icalendar/badge.svg)](https://docs.rs/icalendar/)
+![maintenance](https://img.shields.io/maintenance/yes/2021)
+[![contributors](https://img.shields.io/github/contributors/hoodie/notify-rust)](https://github.com/hoodie/notify-rust/graphs/contributors)
+</div>
-A very WIP library to generate rfc5545 calendars.
-This is still just an early idea, there is not much implemented yet.
-I haven't even read the full [spec](http://tools.ietf.org/html/rfc5545) yet.
+A very simple library to generate [`rfc5545`](http://tools.ietf.org/html/rfc5545) calendars.
+Please double check the [spec](http://tools.ietf.org/html/rfc5545).
You want to help make this more mature? Please talk to me, Pull Requests and suggestions are very welcome.
@@ -18,17 +22,17 @@ You want to help make this more mature? Please talk to me, Pull Requests and sug
let event = Event::new()
.summary("test event")
.description("here I have something really important to do")
- .starts(UTC::now())
+ .starts(Utc::now())
.class(Class::Confidential)
- .ends(UTC::now() + Duration::days(1))
+ .ends(Utc::now() + Duration::days(1))
.append_property(Property::new("TEST", "FOOBAR")
- .add_parameter("IMPORTANCE", "very")
- .add_parameter("DUE", "tomorrow")
- .done())
+ .add_parameter("IMPORTANCE", "very")
+ .add_parameter("DUE", "tomorrow")
+ .done())
.done();
let bday = Event::new()
- .all_day(UTC.ymd(2016, 3, 15))
+ .all_day(Utc.ymd(2020, 3, 15))
.summary("My Birthday")
.description(
r#"Hey, I'm gonna have a party
@@ -41,9 +45,22 @@ let todo = Todo::new().summary("Buy some milk").done();
let mut calendar = Calendar::new();
-calendar.add(event);
-calendar.add(todo);
-calendar.add(bday);
+calendar.push(event);
+calendar.push(todo);
+calendar.push(bday);
```
-# License
+## License
+
+icalendar-rs is licensed under either of
+
+* Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
+* MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
+
+at your option.
+
+## Contribution
+
+Any help in form of descriptive and friendly [issues](https://github.com/hoodie/icalendar-rs/issues) or comprehensive pull requests are welcome!
+
+Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in icalendar-rs by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
diff --git a/examples/readme.rs b/examples/readme.rs
new file mode 100644
index 0000000..b92ad05
--- /dev/null
+++ b/examples/readme.rs
@@ -0,0 +1,34 @@
+use chrono::*;
+use icalendar::*;
+
+fn main() {
+ let event = 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))
+ .append_property(Property::new("TEST", "FOOBAR")
+ .add_parameter("IMPORTANCE", "very")
+ .add_parameter("DUE", "tomorrow")
+ .done())
+ .done();
+
+ let bday = Event::new()
+ .all_day(Utc.ymd(2016, 3, 15))
+ .summary("My Birthday")
+ .description(
+ r#"Hey, I'm gonna have a party
+ BYOB: Bring your own beer.
+ Hendrik"#
+ )
+ .done();
+
+ let todo = Todo::new().summary("Buy some milk").done();
+
+
+ let mut calendar = Calendar::new();
+ calendar.push(event);
+ calendar.push(todo);
+ calendar.push(bday);
+} \ No newline at end of file
diff --git a/examples/tasks.rs b/examples/tasks.rs
index b323123..e74f6f7 100644
--- a/examples/tasks.rs
+++ b/examples/tasks.rs
@@ -4,14 +4,13 @@ use chrono::*;
fn main(){
let todo = Todo::new()
- .starts(Local::now())
- .ends(Local::now())
+ .starts(Local::now().naive_local())
+ .ends(Local::now().naive_local())
.priority(12)
.percent_complete(28)
.status(TodoStatus::Completed)
- .completed(&Local::now())
- .due(&Local::now())
- .due(&Local::now())
+ .completed(Utc::now())
+ .due(Local::now().with_timezone(&Utc))
.done();
println!("{}", todo.to_string());
diff --git a/src/components.rs b/src/components.rs
index b4efb91..0462329 100644
--- a/src/components.rs
+++ b/src/components.rs
@@ -8,6 +8,52 @@ use std::collections::BTreeMap;
use crate::properties::*;
+/// Representation of various forms of `DATE-TIME` per
+/// [RFC 5545, Section 3.3.5](https://tools.ietf.org/html/rfc5545#section-3.3.5)
+///
+/// Conversions from [chrono] types are provided in form of [From] implementations, see
+/// documentation of individual variants.
+///
+/// In addition to readily implemented `FORM #1` and `FORM #2`, the RFC also specifies
+/// `FORM #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE`. This variant is not yet implemented.
+/// Adding it will require adding support for `VTIMEZONE` and referencing it using `TZID`.
+#[derive(Clone, Copy, Debug)]
+pub enum CalendarDateTime {
+ /// `FORM #1: DATE WITH LOCAL TIME`: floating, follows current time-zone of the attendee.
+ ///
+ /// Conversion from [`chrono::NaiveDateTime`] results in this variant.
+ Floating(NaiveDateTime),
+ /// `FORM #2: DATE WITH UTC TIME`: rendered with Z suffix character.
+ ///
+ /// Conversion from [`chrono::DateTime<Utc>`](DateTime) results in this variant. Use
+ /// `date_time.with_timezone(&Utc)` to convert `date_time` from arbitrary time zone to UTC.
+ Utc(DateTime<Utc>),
+}
+
+impl fmt::Display for CalendarDateTime {
+ /// Format date-time in RFC 5545 compliant manner.
+ fn fmt(self: &Self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+ match self {
+ CalendarDateTime::Floating(naive_dt) => naive_dt.format("%Y%m%dT%H%M%S").fmt(f),
+ CalendarDateTime::Utc(utc_dt) => utc_dt.format("%Y%m%dT%H%M%SZ").fmt(f),
+ }
+ }
+}
+
+/// Converts from time zone-aware UTC date-time to [CalendarDateTime::Utc].
+impl From<DateTime<Utc>> for CalendarDateTime {
+ fn from(dt: DateTime<Utc>) -> Self {
+ Self::Utc(dt)
+ }
+}
+
+/// Converts from time zone-less date-time to [CalendarDateTime::Floating].
+impl From<NaiveDateTime> for CalendarDateTime {
+ fn from(dt: NaiveDateTime) -> Self {
+ Self::Floating(dt)
+ }
+}
+
/// VEVENT [(RFC 5545, Section 3.6.1 )](https://tools.ietf.org/html/rfc5545#section-3.6.1)
#[derive(Debug, Default)]
pub struct Event { inner: InnerComponent }
@@ -77,20 +123,21 @@ impl Todo {
self
}
-
- /// Set the COMPLETED `Property`, date only
- pub fn due<TZ:TimeZone>(&mut self, dt: &DateTime<TZ>) -> &mut Self
- where TZ::Offset: fmt::Display
- {
- self.add_property("DUE", dt.format("%Y%m%dT%H%M%S").to_string().as_ref());
+ /// Set the DUE `Property`
+ ///
+ /// See [CalendarDateTime] for info how are different [chrono] types converted automatically.
+ pub fn due<T: Into<CalendarDateTime>>(&mut self, dt: T) -> &mut Self {
+ let calendar_dt: CalendarDateTime = dt.into();
+ self.add_property("DUE", &calendar_dt.to_string());
self
}
- /// Set the COMPLETED `Property`, date only
- pub fn completed<TZ:TimeZone>(&mut self, dt: &DateTime<TZ>) -> &mut Self
- where TZ::Offset: fmt::Display
- {
- self.add_property("COMPLETED", dt.format("%Y%m%dT%H%M%S").to_string().as_ref());
+ /// Set the COMPLETED `Property`
+ ///
+ /// Per [RFC 5545, Section 3.8.2.1](https://tools.ietf.org/html/rfc5545#section-3.8.2.1), this
+ /// must be a date-time in UTC format.
+ pub fn completed(&mut self, dt: DateTime<Utc>) -> &mut Self {
+ self.add_property("COMPLETED", &CalendarDateTime::Utc(dt).to_string());
self
}
@@ -129,7 +176,7 @@ pub trait Component {
write_crlf!(out, "BEGIN:{}", Self::component_kind())?;
if !self.properties().contains_key("DTSTAMP") {
- let now = Local::now().format("%Y%m%dT%H%M%S");
+ let now = CalendarDateTime::Utc(Utc::now());
write_crlf!(out, "DTSTAMP:{}", now)?;
}
@@ -174,22 +221,21 @@ pub trait Component {
self
}
-
/// Set the DTSTART `Property`
- fn starts<TZ:TimeZone>(&mut self, dt: DateTime<TZ>) -> &mut Self
- where TZ::Offset: fmt::Display
- {
- // DTSTART
- self.add_property("DTSTART", dt.format("%Y%m%dT%H%M%S").to_string().as_ref());
+ ///
+ /// See [CalendarDateTime] for info how are different [chrono] types converted automatically.
+ fn starts<T: Into<CalendarDateTime>>(&mut self, dt: T) -> &mut Self {
+ let calendar_dt = dt.into();
+ self.add_property("DTSTART", &calendar_dt.to_string());
self
}
/// Set the DTEND `Property`
- fn ends<TZ:TimeZone>(&mut self, dt: DateTime<TZ>) -> &mut Self
- where TZ::Offset: fmt::Display
- {
- // TODO don't manually use format but the rfc method, but test timezone behaviour
- self.add_property("DTEND", dt.format("%Y%m%dT%H%M%S").to_string().as_ref());
+ ///
+ /// See [CalendarDateTime] for info how are different [chrono] types converted automatically.
+ fn ends<T: Into<CalendarDateTime>>(&mut self, dt: T) -> &mut Self {
+ let calendar_dt = dt.into();
+ self.add_property("DTEND", &calendar_dt.to_string());
self
}
diff --git a/src/lib.rs b/src/lib.rs
index fe9ee3b..31916b1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -47,6 +47,18 @@
//! calendar.add(bday);
//! # }
//! ```
+//!
+//! ## Breaking API Changes in version 0.7.0
+//!
+//! - [Todo::due] and [Todo::completed] now take their date-time argument by value rather than by
+//! reference
+//! - [Todo::completed] now requires its [chrono::DateTime] argument to have exactly [chrono::Utc]
+//! specified as its time zone as mandated by the RFC.
+//! - [Component::starts], [Component::ends] and [Todo::due] now take newly introduced
+//! [CalendarDateTime] (through `Into<CalendarDateTime>` indirection). This allows callers to
+//! define time zone handling. Conversions from [`chrono::NaiveDateTime`] and
+//! [`chrono::DateTime<Utc>`](chrono::DateTime) are provided for ergonomics, the latter also restoring API
+//! compatibility in case of UTC date-times.
#![warn(missing_docs,
missing_copy_implementations,
@@ -83,7 +95,7 @@ mod calendar;
//pub mod repeats;
pub use crate::properties::{Property, Parameter, Class, ValueType};
pub use crate::properties::{TodoStatus, EventStatus};
-pub use crate::components::{Event, Todo, Component};
+pub use crate::components::{CalendarDateTime, Event, Todo, Component};
pub use crate::calendar::Calendar;
// TODO Calendar TimeZone VTIMEZONE STANDARD DAYLIGHT (see thunderbird exports)
diff --git a/tests/calendar.rs b/tests/calendar.rs
index 785bec5..badcb15 100644
--- a/tests/calendar.rs
+++ b/tests/calendar.rs
@@ -1,5 +1,6 @@
use chrono::prelude::*;
-use icalendar::{Calendar, Class, Component, Event, EventStatus};
+use icalendar::{Calendar, Class, Component, Event, EventStatus, Todo};
+use pretty_assertions::assert_eq;
const EXPECTED_CAL_CONTENT: &str = "\
BEGIN:VCALENDAR\r
@@ -9,25 +10,37 @@ CALSCALE:GREGORIAN\r
BEGIN:VEVENT\r
CLASS:CONFIDENTIAL\r
DESCRIPTION:Description\r
-DTEND:20140709T091011\r
+DTEND:20140709T091011Z\r
DTSTAMP:20190307T181159\r
-DTSTART:20140708T091011\r
+DTSTART:20140708T071011Z\r
LOCATION:Somewhere\r
PRIORITY:10\r
STATUS:TENTATIVE\r
SUMMARY:summary\r
UID:euid\r
END:VEVENT\r
+BEGIN:VTODO\r
+COMPLETED:20140709T091011Z\r
+DTSTAMP:20190307T181159\r
+DUE:20140708T091011\r
+PERCENT-COMPLETE:95\r
+SUMMARY:A Todo\r
+UID:todouid\r
+END:VTODO\r
END:VCALENDAR\r
";
#[test]
fn test_calendar_to_string() {
let mut calendar = Calendar::new();
+ let cest_date = FixedOffset::east(2 * 3600)
+ .ymd(2014, 7, 8)
+ .and_hms(9, 10, 11);
+ let utc_date = Utc.ymd(2014, 7, 9).and_hms(9, 10, 11);
let event = Event::new()
.status(EventStatus::Tentative)
- .starts(Local.ymd(2014, 7, 8).and_hms(9, 10, 11))
- .ends(Local.ymd(2014, 7, 9).and_hms(9, 10, 11))
+ .starts(cest_date.with_timezone(&Utc))
+ .ends(utc_date)
.priority(11) // converted to 10
.summary("summary")
.description("Description")
@@ -37,5 +50,14 @@ fn test_calendar_to_string() {
.add_property("DTSTAMP", "20190307T181159")
.done();
calendar.push(event);
+ let todo = Todo::new()
+ .percent_complete(95)
+ .due(cest_date.naive_local())
+ .completed(utc_date)
+ .summary("A Todo")
+ .uid("todouid")
+ .add_property("DTSTAMP", "20190307T181159")
+ .done();
+ calendar.push(todo);
assert_eq!(calendar.to_string(), EXPECTED_CAL_CONTENT);
}