use chrono::Duration;
use std::{fmt, mem, ops::Deref};
#[cfg(feature = "recurrence")]
use crate::components::build_recurrence_set;
use crate::{Parameter, Property, components::*};
pub trait IntoTimezoneId: private::Sealed {
fn into_timezone_id(self) -> String;
}
mod private {
pub trait Sealed {}
impl Sealed for &str {}
impl Sealed for String {}
#[cfg(feature = "chrono-tz")]
impl Sealed for chrono_tz::Tz {}
}
impl IntoTimezoneId for &str {
fn into_timezone_id(self) -> String {
self.to_owned()
}
}
impl IntoTimezoneId for String {
fn into_timezone_id(self) -> String {
self
}
}
#[cfg(feature = "chrono-tz")]
impl IntoTimezoneId for chrono_tz::Tz {
fn into_timezone_id(self) -> String {
self.name().to_owned()
}
}
mod calendar_component;
pub use calendar_component::CalendarComponent;
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(into = "crate::parser::Calendar"))]
#[cfg_attr(feature = "serde", serde(from = "crate::parser::Calendar"))]
pub struct Calendar {
pub properties: Vec<Property>,
pub components: Vec<CalendarComponent>,
}
impl Default for Calendar {
fn default() -> Self {
Self {
properties: Property::from_array([
("VERSION", "2.0"),
("PRODID", "ICALENDAR-RS"),
("CALSCALE", "GREGORIAN"),
]),
components: Default::default(),
}
}
}
impl<U> Extend<U> for Calendar
where
U: Into<CalendarComponent>,
{
fn extend<T>(&mut self, other: T)
where
T: IntoIterator<Item = U>,
{
Calendar::extend(self, other);
}
}
impl Calendar {
pub fn new() -> Self {
Default::default()
}
pub fn empty() -> Self {
Self {
properties: Default::default(),
components: Default::default(),
}
}
#[deprecated(note = "Use .push() instead")]
#[doc(hidden)]
pub fn add<T: Into<CalendarComponent>>(&mut self, component: T) -> &mut Self {
self.push(component)
}
pub fn append(&mut self, other: &mut Calendar) {
self.components.append(&mut other.components);
}
pub fn append_property(&mut self, property: impl Into<Property>) -> &mut Self {
self.properties.push(property.into());
self
}
pub fn property_value(&self, key: &str) -> Option<&str> {
Some(
self.properties
.iter()
.find(|property| property.key() == key)?
.value(),
)
}
pub fn extend<T, U>(&mut self, other: T)
where
T: IntoIterator<Item = U>,
U: Into<CalendarComponent>,
{
self.components.extend(other.into_iter().map(Into::into));
}
pub fn push<T: Into<CalendarComponent>>(&mut self, component: T) -> &mut Self {
self.components.push(component.into());
self
}
pub fn name(&mut self, name: &str) -> &mut Self {
self.append_property(Property::new("NAME", name));
self.append_property(Property::new("X-WR-CALNAME", name));
self
}
pub fn get_name(&self) -> Option<&str> {
self.property_value("NAME")
.or_else(|| self.property_value("X-WR-CALNAME"))
}
pub fn description(&mut self, description: &str) -> &mut Self {
self.append_property(Property::new("DESCRIPTION", description));
self.append_property(Property::new("X-WR-CALDESC", description));
self
}
pub fn get_description(&self) -> Option<&str> {
self.property_value("DESCRIPTION")
.or_else(|| self.property_value("X-WR-CALDESC"))
}
pub fn timezone(&mut self, timezone: impl IntoTimezoneId) -> &mut Self {
self.append_property(Property::new("X-WR-TIMEZONE", timezone.into_timezone_id()));
self
}
pub fn get_timezone(&self) -> Option<&str> {
self.property_value("X-WR-TIMEZONE")
}
pub fn ttl(&mut self, duration: &Duration) -> &mut Self {
let duration_string = duration.to_string();
self.append_property(
Property::new("REFRESH-INTERVAL", &duration_string)
.append_parameter(Parameter::new("VALUE", "DURATION"))
.done(),
);
self.append_property(Property::new("X-PUBLISHED-TTL", &duration_string));
self
}
pub fn get_ttl(&self) -> Option<Duration> {
self.property_value("REFRESH-INTERVAL")
.and_then(|refresh_interval| iso8601::duration(refresh_interval).ok())
.or_else(|| {
self.property_value("X-PUBLISHED-TTL")
.and_then(|published_ttl| iso8601::duration(published_ttl).ok())
})
.map(std::time::Duration::from)
.map(Duration::from_std)
.transpose()
.ok()
.flatten()
}
pub fn done(&mut self) -> Self {
Calendar {
properties: mem::take(&mut self.properties),
components: mem::take(&mut self.components),
}
}
fn fmt_write<W: fmt::Write>(&self, out: &mut W) -> Result<(), fmt::Error> {
write_crlf!(out, "BEGIN:VCALENDAR")?;
for property in &self.properties {
property.fmt_write(out)?;
}
for component in &self.components {
component.fmt_write(out)?;
}
write_crlf!(out, "END:VCALENDAR")?;
Ok(())
}
pub fn print(&self) -> Result<(), fmt::Error> {
print_crlf!("{}", self);
Ok(())
}
pub fn events(&self) -> impl Iterator<Item = &Event> {
self.components
.iter()
.filter_map(|component| match component {
CalendarComponent::Event(event) => Some(event),
_ => None,
})
}
pub fn events_mut(&mut self) -> impl Iterator<Item = &mut Event> {
self.components
.iter_mut()
.filter_map(|component| match component {
CalendarComponent::Event(event) => Some(event),
_ => None,
})
}
pub fn todos(&self) -> impl Iterator<Item = &Todo> {
self.components
.iter()
.filter_map(|component| match component {
CalendarComponent::Todo(todo) => Some(todo),
_ => None,
})
}
pub fn todos_mut(&mut self) -> impl Iterator<Item = &mut Todo> {
self.components
.iter_mut()
.filter_map(|component| match component {
CalendarComponent::Todo(todo) => Some(todo),
_ => None,
})
}
#[cfg(feature = "recurrence")]
pub fn calendar_events(&self) -> impl Iterator<Item = CalendarEvent<'_>> {
let tz = self.get_timezone();
self.events().map(move |event| CalendarEvent {
event,
calendar_tz: tz,
})
}
#[cfg(feature = "recurrence")]
pub fn calendar_todos(&self) -> impl Iterator<Item = CalendarTodo<'_>> {
let tz = self.get_timezone();
self.todos().map(move |todo| CalendarTodo {
todo,
calendar_tz: tz,
})
}
}
#[cfg(feature = "recurrence")]
#[derive(Debug, Clone, Copy)]
pub struct CalendarEvent<'a> {
event: &'a Event,
calendar_tz: Option<&'a str>,
}
#[cfg(feature = "recurrence")]
impl<'a> CalendarEvent<'a> {
pub fn event(&self) -> &'a Event {
self.event
}
pub fn calendar_tz(&self) -> Option<&str> {
self.calendar_tz
}
#[cfg(feature = "recurrence")]
pub fn get_recurrence(&self) -> Result<rrule::RRuleSet, crate::RecurrenceError> {
build_recurrence_set(self.event, self.calendar_tz)
}
}
#[cfg(feature = "recurrence")]
impl<'a> Deref for CalendarEvent<'a> {
type Target = Event;
fn deref(&self) -> &Event {
self.event
}
}
#[cfg(feature = "recurrence")]
#[derive(Debug, Clone, Copy)]
pub struct CalendarTodo<'a> {
todo: &'a Todo,
calendar_tz: Option<&'a str>,
}
#[cfg(feature = "recurrence")]
impl<'a> CalendarTodo<'a> {
#[cfg(feature = "recurrence")]
pub fn todo(&self) -> &'a Todo {
self.todo
}
pub fn calendar_tz(&self) -> Option<&str> {
self.calendar_tz
}
#[cfg(feature = "recurrence")]
pub fn get_recurrence(&self) -> Result<rrule::RRuleSet, crate::RecurrenceError> {
build_recurrence_set(self.todo, self.calendar_tz)
}
}
#[cfg(feature = "recurrence")]
impl<'a> Deref for CalendarTodo<'a> {
type Target = Todo;
fn deref(&self) -> &Todo {
self.todo
}
}
impl fmt::Display for Calendar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt_write(f)
}
}
impl TryInto<String> for &Calendar {
type Error = fmt::Error;
fn try_into(self) -> Result<String, Self::Error> {
let mut out_string = String::new();
self.fmt_write(&mut out_string)?;
Ok(out_string)
}
}
impl Deref for Calendar {
type Target = [CalendarComponent];
fn deref(&self) -> &[CalendarComponent] {
self.components.deref()
}
}
impl AsRef<[CalendarComponent]> for Calendar {
fn as_ref(&self) -> &[CalendarComponent] {
self.components.deref()
}
}
impl<T: Into<CalendarComponent>, const N: usize> From<[T; N]> for Calendar {
fn from(elements: [T; N]) -> Self {
elements.into_iter().collect()
}
}
impl<C: Into<CalendarComponent>> From<C> for Calendar {
fn from(element: C) -> Self {
Calendar {
components: vec![element.into()],
..Default::default()
}
}
}
impl<C: Into<CalendarComponent>> FromIterator<C> for Calendar {
fn from_iter<T: IntoIterator<Item = C>>(iter: T) -> Self {
Calendar {
components: iter.into_iter().map(Into::into).collect(),
..Default::default()
}
}
}
#[test]
fn from_adds_default_properties() {
let todo = Todo::default();
let cal = Calendar::from([todo]);
assert!(cal.property_value("VERSION").is_some());
assert!(cal.property_value("CALSCALE").is_some());
assert!(cal.property_value("PRODID").is_some());
assert!(
cal.property_value("VERSION")
.and(cal.property_value("PRODID"))
.and(cal.property_value("CALSCALE"))
.is_some()
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calendar_extend_components() {
let mut calendar = Calendar::new();
let components = vec![
CalendarComponent::Event(Event::new()),
CalendarComponent::Event(Event::new()),
];
calendar.extend(components);
assert_eq!(calendar.components.len(), 2);
}
#[test]
fn calendar_extend_events() {
let mut calendar = Calendar::new();
let events = vec![Event::new(), Event::new()];
calendar.extend(events);
assert_eq!(calendar.components.len(), 2);
}
#[test]
fn get_properties_unset() {
let calendar = Calendar::new();
assert_eq!(calendar.get_name(), None);
assert_eq!(calendar.get_description(), None);
assert_eq!(calendar.get_timezone(), None);
}
#[test]
fn get_properties_set() {
let calendar = Calendar::new()
.name("name")
.description("description")
.done();
assert_eq!(calendar.get_name(), Some("name"));
assert_eq!(calendar.get_description(), Some("description"));
assert_eq!(calendar.get_timezone(), None);
}
#[test]
fn timezone_accepts_str() {
let calendar = Calendar::new().timezone("Europe/Berlin").done();
assert_eq!(calendar.get_timezone(), Some("Europe/Berlin"));
}
#[test]
#[cfg(feature = "chrono-tz")]
fn timezone_accepts_chrono_tz() {
let calendar = Calendar::new().timezone(chrono_tz::Europe::Berlin).done();
assert_eq!(calendar.get_timezone(), Some("Europe/Berlin"));
}
#[test]
fn timezone_writes_only_xwr_timezone() {
let calendar = Calendar::new().timezone("Europe/Berlin").done();
let has_timezone_id = calendar.properties.iter().any(|p| p.key() == "TIMEZONE-ID");
let xwr_count = calendar
.properties
.iter()
.filter(|p| p.key() == "X-WR-TIMEZONE")
.count();
assert!(!has_timezone_id, "TIMEZONE-ID must not be written");
assert_eq!(xwr_count, 1, "exactly one X-WR-TIMEZONE property expected");
assert_eq!(calendar.get_timezone(), Some("Europe/Berlin"));
}
#[test]
fn get_timezone_ignores_timezone_id() {
let calendar = Calendar::new()
.append_property(Property::new("TIMEZONE-ID", "Europe/Berlin"))
.done();
assert_eq!(
calendar.get_timezone(),
None,
"get_timezone() must not read TIMEZONE-ID"
);
}
#[test]
fn get_properties_alternate() {
let calendar = Calendar::new()
.append_property(Property::new("X-WR-CALNAME", "name"))
.append_property(Property::new("X-WR-CALDESC", "description"))
.append_property(Property::new("X-WR-TIMEZONE", "timezone"))
.done();
assert_eq!(calendar.get_name(), Some("name"));
assert_eq!(calendar.get_description(), Some("description"));
assert_eq!(calendar.get_timezone(), Some("timezone"));
}
#[test]
#[cfg(feature = "parser")]
fn emit_parse_icalendar() {
use std::str::FromStr;
let mut original = Calendar::new();
original.append_property(Property::new("FOOBAR", "foobar"));
let emitted = original.to_string();
let parsed = Calendar::from_str(&emitted).unwrap();
pretty_assertions::assert_eq!(parsed.property_value("FOOBAR"), Some("foobar"));
}
}