1mod content_line;
4mod datetime;
5mod error;
6mod regex;
7mod utils;
8
9use std::str::FromStr;
10
11pub(crate) use content_line::{ContentLine, ContentLineCaptures};
12pub(crate) use datetime::str_to_weekday;
13pub use error::ParseError;
14
15use crate::RRule;
16
17use self::content_line::{PropertyName, StartDateContentLine};
18
19#[derive(Debug, PartialEq)]
21pub(crate) struct Grammar {
22 pub start: Option<StartDateContentLine>,
23 pub content_lines: Vec<ContentLine>,
24}
25
26impl FromStr for Grammar {
27 type Err = ParseError;
28
29 fn from_str(s: &str) -> Result<Self, Self::Err> {
30 let content_lines_parts = s
31 .lines()
32 .map(ContentLineCaptures::new)
33 .collect::<Result<Vec<_>, _>>()?;
34
35 let start = content_lines_parts
36 .iter()
37 .find(|parts| matches!(parts.property_name, PropertyName::DtStart))
38 .map(StartDateContentLine::try_from)
39 .transpose()?;
40
41 let mut content_lines = vec![];
42
43 for parts in content_lines_parts {
44 let line = match parts.property_name {
45 PropertyName::RRule => {
46 let rrule = RRule::try_from(parts)?;
47 ContentLine::RRule(rrule)
48 }
49 PropertyName::ExRule => {
50 let rrule = RRule::try_from(parts)?;
51 ContentLine::ExRule(rrule)
52 }
53 PropertyName::RDate => ContentLine::RDate(TryFrom::try_from(parts)?),
54 PropertyName::ExDate => ContentLine::ExDate(TryFrom::try_from(parts)?),
55 PropertyName::DtStart => {
56 continue;
58 }
59 };
60 content_lines.push(line);
61 }
62
63 if !content_lines
65 .iter()
66 .any(|line| matches!(line, ContentLine::RRule(_) | ContentLine::RDate(_)))
67 {
68 return Err(ParseError::MissingDateGenerationRules);
69 }
70
71 Ok(Self {
72 start,
73 content_lines,
74 })
75 }
76}
77
78#[cfg(test)]
79mod test {
80 use chrono::{TimeZone, Weekday};
81
82 use super::*;
83 use crate::{core::Tz, parser::content_line::ContentLine, Frequency, NWeekday, RRule};
84
85 const UTC: Tz = Tz::UTC;
86 const BERLIN: Tz = Tz::Europe__Berlin;
87
88 #[test]
89 fn parses_valid_input_to_grammar() {
90 let tests = [
91(
92 "DTSTART:19970902T090000Z\nRRULE:FREQ=YEARLY;COUNT=3\n", Grammar {
93 start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(1997, 9, 2,9, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }),
94 content_lines: vec![
95 ContentLine::RRule(RRule {
96 freq: Frequency::Yearly,
97 count: Some(3),
98 ..Default::default()
99 })
100 ]
101}
102),
103("DTSTART:20120201T093000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR", Grammar {
104 start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,9, 30, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }),
105 content_lines: vec![
106 ContentLine::RRule(RRule {
107 freq: Frequency::Weekly,
108 interval: 5,
109 until: Some(UTC.with_ymd_and_hms(2013, 1, 30,23, 0, 0).unwrap()),
110 by_weekday: vec![NWeekday::Every(Weekday::Mon), NWeekday::Every(Weekday::Fri)],
111 ..Default::default()
112 })
113 ]
114}),
115("DTSTART:20120201T120000Z\nRRULE:FREQ=DAILY;COUNT=5\nEXDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000", Grammar {
116 start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,12, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }),
117 content_lines: vec![
118 ContentLine::RRule(RRule {
119 freq: Frequency::Daily,
120 count: Some(5),
121 ..Default::default()
122 }),
123 ContentLine::ExDate(vec![
124 BERLIN.with_ymd_and_hms(2012, 2, 2,13, 0, 0).unwrap(),
125 BERLIN.with_ymd_and_hms(2012, 2, 3,13, 0, 0).unwrap(),
126 ])
127 ]
128}),
129("DTSTART:20120201T120000Z\nRRULE:FREQ=DAILY;COUNT=5\nEXDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000\nEXRULE:FREQ=WEEKLY;COUNT=10", Grammar {
130 start: Some(StartDateContentLine { datetime: UTC.with_ymd_and_hms(2012, 2, 1,12, 0, 0).unwrap(), timezone: Some(UTC), value: "DATE-TIME" }),
131 content_lines: vec![
132 ContentLine::RRule(RRule {
133 freq: Frequency::Daily,
134 count: Some(5),
135 ..Default::default()
136 }),
137 ContentLine::ExDate(vec![
138 BERLIN.with_ymd_and_hms(2012, 2, 2,13, 0, 0).unwrap(),
139 BERLIN.with_ymd_and_hms(2012, 2, 3,13, 0, 0).unwrap(),
140 ]),
141 ContentLine::ExRule(RRule {
142 freq: Frequency::Weekly,
143 count: Some(10),
144 ..Default::default()
145 }),
146 ]
147})
148 ];
149 for (input, expected_grammar) in tests {
150 let grammar = Grammar::from_str(input);
151 assert_eq!(grammar, Ok(expected_grammar));
152 }
153 }
154
155 #[test]
156 fn rejects_input_without_date_generation() {
157 let tests = [
158"DTSTART:19970902T090000Z",
159"DTSTART:20120201T093000Z\nEXRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR",
160"DTSTART:20120201T120000Z\nEXDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000",
161"DTSTART:20120201T120000Z\nEXRULE:FREQ=DAILY;COUNT=5\nEXDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000"
162 ];
163 for input in tests {
164 let res = Grammar::from_str(input);
165 assert_eq!(res, Err(ParseError::MissingDateGenerationRules));
166 }
167 }
168
169 #[test]
170 fn allows_input_without_start_date() {
171 let tests = [
172 "RRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20130130T230000Z;BYDAY=MO,FR",
173 "RDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000",
174 "RRULE:FREQ=DAILY;COUNT=5\nEXDATE;TZID=Europe/Berlin:20120202T130000,20120203T130000",
175 ];
176 for input in tests {
177 let res = Grammar::from_str(input);
178 assert!(res.is_ok());
179 }
180 }
181}