ezcal 0.3.4

Ergonomic iCalendar + vCard library for Rust
Documentation
use crate::common::property::Property;
use crate::datetime::DateTimeValue;
use crate::error::Result;
use crate::ical::alarm::Alarm;
use crate::ical::event::{escape_text, unescape_text};
use crate::ical::recurrence::RecurrenceRule;

/// An iCalendar VTODO component.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Todo {
    pub(crate) uid: Option<String>,
    pub(crate) dtstamp: Option<DateTimeValue>,
    pub(crate) dtstart: Option<DateTimeValue>,
    pub(crate) due: Option<DateTimeValue>,
    pub(crate) completed: Option<DateTimeValue>,
    pub(crate) summary: Option<String>,
    pub(crate) description: Option<String>,
    pub(crate) location: Option<String>,
    pub(crate) status: Option<String>,
    pub(crate) categories: Vec<String>,
    pub(crate) priority: Option<u8>,
    pub(crate) percent_complete: Option<u8>,
    pub(crate) rrule: Option<RecurrenceRule>,
    pub(crate) alarms: Vec<Alarm>,
    pub(crate) extra_properties: Vec<Property>,
}

impl Todo {
    /// Create a new todo builder.
    pub fn new() -> Self {
        Self {
            uid: None,
            dtstamp: None,
            dtstart: None,
            due: None,
            completed: None,
            summary: None,
            description: None,
            location: None,
            status: None,
            categories: Vec::new(),
            priority: None,
            percent_complete: None,
            rrule: None,
            alarms: Vec::new(),
            extra_properties: Vec::new(),
        }
    }

    pub fn uid(mut self, uid: impl Into<String>) -> Self {
        self.uid = Some(uid.into());
        self
    }

    pub fn summary(mut self, summary: impl Into<String>) -> Self {
        self.summary = Some(summary.into());
        self
    }

    pub fn description(mut self, description: impl Into<String>) -> Self {
        self.description = Some(description.into());
        self
    }

    pub fn location(mut self, location: impl Into<String>) -> Self {
        self.location = Some(location.into());
        self
    }

    pub fn starts(mut self, dt: &str) -> Self {
        self.dtstart = Some(DateTimeValue::parse(dt).expect("invalid start date-time"));
        self
    }

    pub fn starts_dt(mut self, dt: DateTimeValue) -> Self {
        self.dtstart = Some(dt);
        self
    }

    pub fn due_date(mut self, dt: &str) -> Self {
        self.due = Some(DateTimeValue::parse(dt).expect("invalid due date-time"));
        self
    }

    pub fn due_dt(mut self, dt: DateTimeValue) -> Self {
        self.due = Some(dt);
        self
    }

    pub fn completed_date(mut self, dt: &str) -> Self {
        self.completed = Some(DateTimeValue::parse(dt).expect("invalid completed date-time"));
        self
    }

    pub fn status(mut self, status: impl Into<String>) -> Self {
        self.status = Some(status.into());
        self
    }

    pub fn categories(mut self, categories: Vec<String>) -> Self {
        self.categories = categories;
        self
    }

    pub fn add_category(mut self, category: impl Into<String>) -> Self {
        self.categories.push(category.into());
        self
    }

    pub fn priority(mut self, priority: u8) -> Self {
        self.priority = Some(priority);
        self
    }

    pub fn percent_complete(mut self, pct: u8) -> Self {
        self.percent_complete = Some(pct);
        self
    }

    pub fn rrule(mut self, rrule: RecurrenceRule) -> Self {
        self.rrule = Some(rrule);
        self
    }

    pub fn alarm(mut self, alarm: Alarm) -> Self {
        self.alarms.push(alarm);
        self
    }

    pub fn property(mut self, prop: Property) -> Self {
        self.extra_properties.push(prop);
        self
    }

    // --- Accessors ---

    pub fn get_uid(&self) -> Option<&str> {
        self.uid.as_deref()
    }

    pub fn get_summary(&self) -> Option<&str> {
        self.summary.as_deref()
    }

    pub fn get_description(&self) -> Option<&str> {
        self.description.as_deref()
    }

    pub fn get_location(&self) -> Option<&str> {
        self.location.as_deref()
    }

    pub fn get_starts(&self) -> Option<&DateTimeValue> {
        self.dtstart.as_ref()
    }

    pub fn get_due(&self) -> Option<&DateTimeValue> {
        self.due.as_ref()
    }

    pub fn get_completed(&self) -> Option<&DateTimeValue> {
        self.completed.as_ref()
    }

    pub fn get_status(&self) -> Option<&str> {
        self.status.as_deref()
    }

    pub fn get_categories(&self) -> &[String] {
        &self.categories
    }

    pub fn get_priority(&self) -> Option<u8> {
        self.priority
    }

    pub fn get_percent_complete(&self) -> Option<u8> {
        self.percent_complete
    }

    pub fn get_rrule(&self) -> Option<&RecurrenceRule> {
        self.rrule.as_ref()
    }

    pub fn get_alarms(&self) -> &[Alarm] {
        &self.alarms
    }

    pub fn get_extra_properties(&self) -> &[Property] {
        &self.extra_properties
    }

    pub(crate) fn to_properties(&self) -> Vec<Property> {
        let mut props = Vec::new();

        let uid = self
            .uid
            .clone()
            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
        props.push(Property::new("UID", uid));

        if let Some(ref dtstamp) = self.dtstamp {
            props.push(dtstamp.to_property("DTSTAMP"));
        } else {
            #[cfg(feature = "chrono")]
            {
                let now = chrono::Utc::now();
                let dt = DateTimeValue::from_chrono_utc(now);
                props.push(dt.to_property("DTSTAMP"));
            }
            #[cfg(not(feature = "chrono"))]
            {
                props.push(Property::new("DTSTAMP", "19700101T000000Z"));
            }
        }

        if let Some(ref dt) = self.dtstart {
            props.push(dt.to_property("DTSTART"));
        }
        if let Some(ref dt) = self.due {
            props.push(dt.to_property("DUE"));
        }
        if let Some(ref dt) = self.completed {
            props.push(dt.to_property("COMPLETED"));
        }
        if let Some(ref s) = self.summary {
            props.push(Property::new("SUMMARY", escape_text(s)));
        }
        if let Some(ref s) = self.description {
            props.push(Property::new("DESCRIPTION", escape_text(s)));
        }
        if let Some(ref s) = self.location {
            props.push(Property::new("LOCATION", escape_text(s)));
        }
        if let Some(ref s) = self.status {
            props.push(Property::new("STATUS", s));
        }
        if !self.categories.is_empty() {
            props.push(Property::new(
                "CATEGORIES",
                self.categories
                    .iter()
                    .map(|c| escape_text(c))
                    .collect::<Vec<_>>()
                    .join(","),
            ));
        }
        if let Some(p) = self.priority {
            props.push(Property::new("PRIORITY", p.to_string()));
        }
        if let Some(pct) = self.percent_complete {
            props.push(Property::new("PERCENT-COMPLETE", pct.to_string()));
        }
        if let Some(ref rrule) = self.rrule {
            props.push(Property::new("RRULE", rrule.to_string()));
        }

        props.extend(self.extra_properties.clone());

        props
    }

    pub(crate) fn from_properties(props: Vec<Property>, alarms: Vec<Alarm>) -> Result<Self> {
        let mut todo = Todo::new();
        todo.alarms = alarms;
        let mut extra = Vec::new();

        for prop in props {
            match prop.name.as_str() {
                "UID" => todo.uid = Some(prop.value.clone()),
                "DTSTAMP" => todo.dtstamp = Some(DateTimeValue::from_property(&prop)?),
                "DTSTART" => todo.dtstart = Some(DateTimeValue::from_property(&prop)?),
                "DUE" => todo.due = Some(DateTimeValue::from_property(&prop)?),
                "COMPLETED" => todo.completed = Some(DateTimeValue::from_property(&prop)?),
                "SUMMARY" => todo.summary = Some(unescape_text(&prop.value)),
                "DESCRIPTION" => todo.description = Some(unescape_text(&prop.value)),
                "LOCATION" => todo.location = Some(unescape_text(&prop.value)),
                "STATUS" => todo.status = Some(prop.value.clone()),
                "CATEGORIES" => {
                    todo.categories = prop
                        .value
                        .split(',')
                        .map(|s| unescape_text(s.trim()))
                        .collect();
                }
                "PRIORITY" => {
                    todo.priority = prop.value.parse().ok();
                }
                "PERCENT-COMPLETE" => {
                    todo.percent_complete = prop.value.parse().ok();
                }
                "RRULE" => todo.rrule = Some(RecurrenceRule::parse(&prop.value)?),
                _ => extra.push(prop),
            }
        }

        todo.extra_properties = extra;
        Ok(todo)
    }
}

impl Default for Todo {
    fn default() -> Self {
        Self::new()
    }
}