From 744e04c32665021bdcf61c4219a955998f669252 Mon Sep 17 00:00:00 2001 From: Samuel Cormier-Iijima Date: Thu, 21 Mar 2024 21:18:01 -0400 Subject: [PATCH 1/2] Add `RRuleSet::set_from_string` to support loading rules without DTSTART --- rrule/src/core/rruleset.rs | 58 ++++++++++++++++++++++++++------------ rrule/src/parser/mod.rs | 16 +++++------ 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/rrule/src/core/rruleset.rs b/rrule/src/core/rruleset.rs index 99aedc0..ec1ae37 100644 --- a/rrule/src/core/rruleset.rs +++ b/rrule/src/core/rruleset.rs @@ -2,7 +2,7 @@ use crate::core::datetime::datetime_to_ical_format; use crate::core::utils::collect_with_error; use crate::core::DateTime; use crate::parser::{ContentLine, Grammar}; -use crate::{RRule, RRuleError}; +use crate::{ParseError, RRule, RRuleError}; #[cfg(feature = "serde")] use serde_with::{serde_as, DeserializeFromStr, SerializeDisplay}; use std::fmt::Display; @@ -211,34 +211,22 @@ impl RRuleSet { pub fn all_unchecked(self) -> Vec { collect_with_error(self.into_iter(), &self.after, &self.before, true, None).dates } -} -impl FromStr for RRuleSet { - type Err = RRuleError; - - /// Creates an [`RRuleSet`] from a string if input is valid. - /// - /// # Errors - /// - /// Returns [`RRuleError`], if iCalendar string contains invalid parts. - fn from_str(s: &str) -> Result { - let Grammar { - start, - content_lines, - } = Grammar::from_str(s)?; + fn set_from_content_lines(self, content_lines: Vec) -> Result { + let dt_start = self.dt_start; content_lines.into_iter().try_fold( - Self::new(start.datetime), + self, |rrule_set, content_line| match content_line { ContentLine::RRule(rrule) => rrule - .validate(start.datetime) + .validate(dt_start) .map(|rrule| rrule_set.rrule(rrule)), #[allow(unused_variables)] ContentLine::ExRule(exrule) => { #[cfg(feature = "exrule")] { exrule - .validate(start.datetime) + .validate(dt_start) .map(|exrule| rrule_set.exrule(exrule)) } #[cfg(not(feature = "exrule"))] @@ -256,6 +244,40 @@ impl FromStr for RRuleSet { }, ) } + + /// Load an [`RRuleSet`] from a string. + pub fn set_from_string(mut self, s: &str) -> Result { + let Grammar { + start, + content_lines, + } = Grammar::from_str(s)?; + + if let Some(dtstart) = start { + self.dt_start = dtstart.datetime; + } + + self.set_from_content_lines(content_lines) + } +} + +impl FromStr for RRuleSet { + type Err = RRuleError; + + /// Creates an [`RRuleSet`] from a string if input is valid. + /// + /// # Errors + /// + /// Returns [`RRuleError`], if iCalendar string contains invalid parts. + fn from_str(s: &str) -> Result { + let Grammar { + start, + content_lines, + } = Grammar::from_str(s)?; + + let start = start.ok_or(ParseError::MissingStartDate)?; + + Self::new(start.datetime).set_from_content_lines(content_lines) + } } impl Display for RRuleSet { diff --git a/rrule/src/parser/mod.rs b/rrule/src/parser/mod.rs index d990f92..fcaffde 100644 --- a/rrule/src/parser/mod.rs +++ b/rrule/src/parser/mod.rs @@ -19,7 +19,7 @@ use self::content_line::{PropertyName, StartDateContentLine}; /// Grammar represents a well-formatted rrule input. #[derive(Debug, PartialEq)] pub(crate) struct Grammar { - pub start: StartDateContentLine, + pub start: Option, pub content_lines: Vec, } @@ -36,7 +36,7 @@ impl FromStr for Grammar { .iter() .find(|parts| matches!(parts.property_name, PropertyName::DtStart)) .map(StartDateContentLine::try_from) - .ok_or(ParseError::MissingStartDate)??; + .transpose()?; let mut content_lines = vec![]; @@ -90,7 +90,7 @@ mod test { let tests = [ ( "DTSTART:19970902T090000Z\nRRULE:FREQ=YEARLY;COUNT=3\n", Grammar { - start: StartDateContentLine { datetime: UTC.with_ymd_and_hms(1997, 9, 2,9, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }, + start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(1997, 9, 2,9, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }), content_lines: vec![ ContentLine::RRule(RRule { freq: Frequency::Yearly, @@ -101,7 +101,7 @@ mod test { } ), ("DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR", Grammar { - start: StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,9, 30, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }, + start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,9, 30, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }), content_lines: vec![ ContentLine::RRule(RRule { freq: Frequency::Weekly, @@ -113,7 +113,7 @@ mod test { ] }), ("DTSTART:20120201T120000Z\nRRULE:FREQ=DAILY;COUNT=5\nEXDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000", Grammar { - start: StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,12, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }, + start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,12, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }), content_lines: vec![ ContentLine::RRule(RRule { freq: Frequency::Daily, @@ -127,7 +127,7 @@ mod test { ] }), ("DTSTART:20120201T120000Z\nRRULE:FREQ=DAILY;COUNT=5\nEXDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000\nEXRULE:FREQ=WEEKLY;COUNT=10", Grammar { - start: StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,12, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }, + start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,12, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }), content_lines: vec![ ContentLine::RRule(RRule { freq: Frequency::Daily, @@ -167,7 +167,7 @@ mod test { } #[test] - fn rejects_input_without_start_date() { + fn allows_input_without_start_date() { let tests = [ "RRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR", "RDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000", @@ -175,7 +175,7 @@ mod test { ]; for input in tests { let res = Grammar::from_str(input); - assert_eq!(res, Err(ParseError::MissingStartDate)); + assert!(res.is_ok()); } } } From 1c506abb8eacadf6f5c5ff6fe6d662bdfff89864 Mon Sep 17 00:00:00 2001 From: Samuel Cormier-Iijima Date: Mon, 1 Apr 2024 13:50:41 -0400 Subject: [PATCH 2/2] Address review comment and add entry to changelog --- CHANGELOG.md | 1 + rrule/src/core/rruleset.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a20b042..9614914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - MSRV is bumped to `v1.74.0` from `v1.64.0` - Make `ParseError` and `ValidationError` public - `EXRULE`s are now correctly added as exrules on the `RRuleSet` when parsed from a string, instead of being incorrectly added as an rrule. +- Add a `RRuleSet::set_from_string` method to support loading rules without DTSTART. This is useful particularly when working with the Google Calendar API. ## 0.11.0 (2023-07-18) diff --git a/rrule/src/core/rruleset.rs b/rrule/src/core/rruleset.rs index ec1ae37..3bb2b1f 100644 --- a/rrule/src/core/rruleset.rs +++ b/rrule/src/core/rruleset.rs @@ -245,7 +245,7 @@ impl RRuleSet { ) } - /// Load an [`RRuleSet`] from a string. + /// Set the [`RRuleSet`] properties from a string. If a DTSTART is found, it will be used as the start datetime. pub fn set_from_string(mut self, s: &str) -> Result { let Grammar { start,