timelog/cli/
args.rs

1//! Structs defining different sets of arguments supplied on command line
2//!
3//! # Description
4//!
5//! - [`DateRangeArgs`] - representation of the start/end dates for a report.
6//! - [`FilterArgs`] - representation of the start/end dates and project list for reports.
7use regex::Regex;
8
9#[doc(inline)]
10use crate::date::DateRange;
11#[doc(inline)]
12use crate::date::RangeParser;
13#[doc(inline)]
14use crate::error::Error;
15use crate::Day;
16
17/// Trait specifying common functionality for the different filter arguments.
18pub trait DayFilter {
19    /// Return the start date as a [`String`]
20    fn start(&self) -> String;
21    /// Return the end date as a [`String`]
22    fn end(&self) -> String;
23    /// Return a [`Day`] object after appropriate filtering
24    fn filter_day(&self, day: Day) -> Option<Day>;
25}
26
27// Return Some [`Day`] if the supplied day is not empty.
28fn day_with_entries(day: Day) -> Option<Day> { (!day.is_empty()).then_some(day) }
29
30/// Representation of the start/end date and project list arguments for reports.
31#[derive(Debug)]
32pub struct FilterArgs {
33    range:    DateRange,
34    projects: Option<Regex>
35}
36
37// Return a [`Regex`] one of the supplied projects
38fn regex_from_projs(projs: &[&str]) -> crate::Result<Regex> {
39    Regex::new(&projs.join("|")).map_err(|_| Error::BadProjectFilter)
40}
41
42fn make_projects_regex_opt(projs: &[&str]) -> crate::Result<Option<Regex>> {
43    if projs.is_empty() {
44        Ok(None)
45    }
46    else {
47        regex_from_projs(projs).map(|r| Some(r))
48    }
49}
50
51impl FilterArgs {
52    /// Create the [`FilterArgs`] from an array of date range description strings, and an array of
53    /// projects.
54    ///
55    /// # Errors
56    ///
57    /// - Return [`Error::BadProjectFilter`] if the supplied project Regexes are not valid
58    /// - Return [`Error::DateError`] if the start date is not before the end date
59    pub fn new(dates: &[String], projs: &[String]) -> crate::Result<Self> {
60        let project_list: Vec<&str> = projs.iter().map(String::as_str).collect();
61
62        let mut date_iter = dates.iter().map(String::as_str);
63        let parser = RangeParser::default();
64        let (range, _token) = parser.parse(&mut date_iter)?;
65
66        Ok(Self { range, projects: make_projects_regex_opt(&project_list)? })
67    }
68
69    // Return an [`Option<&Regex>`] that determines how to match projects
70    fn projects(&self) -> Option<&Regex> { self.projects.as_ref() }
71}
72
73impl DayFilter for FilterArgs {
74    /// Return the start date as a [`String`]
75    fn start(&self) -> String { self.range.start().to_string() }
76
77    /// Return the end date as a [`String`]
78    fn end(&self) -> String { self.range.end().to_string() }
79
80    /// Return a [`Day`] object filtered as needed
81    fn filter_day(&self, day: Day) -> Option<Day> {
82        day_with_entries(
83            self.projects()
84                .map(|re| day.filtered_by_project(re))
85                .unwrap_or(day)
86        )
87    }
88}
89
90/// Representation of the start and end date arguments for reports.
91#[derive(Debug, PartialEq, Eq)]
92pub struct DateRangeArgs {
93    range: DateRange
94}
95
96impl DateRangeArgs {
97    /// Create the [`DateRangeArgs`] from an array of strings.
98    ///
99    /// # Errors
100    ///
101    /// - Return [`Error::DateError`] if the start date is not before the end date
102    pub fn new(dates: &[String]) -> crate::Result<Self> {
103        let mut date_iter = dates.iter().map(String::as_str);
104        let parser = RangeParser::default();
105        let (range, _token) = parser.parse(&mut date_iter)?;
106
107        Ok(Self { range })
108    }
109
110    /// Return the start date as a [`String`]
111    pub fn start(&self) -> String { self.range.start().to_string() }
112
113    /// Return the end date as a [`String`]
114    pub fn end(&self) -> String { self.range.end().to_string() }
115}
116
117impl DayFilter for DateRangeArgs {
118    /// Return the start date as a [`String`]
119    fn start(&self) -> String { self.start() }
120
121    /// Return the end date as a [`String`]
122    fn end(&self) -> String { self.end() }
123
124    /// Return a [`Day`] object filtered as needed. If the day is empty,
125    /// return None.
126    fn filter_day(&self, day: Day) -> Option<Day> { day_with_entries(day) }
127}
128
129// Only used for testing, not particularly performant.
130#[cfg(test)]
131impl PartialEq for FilterArgs {
132    fn eq(&self, other: &Self) -> bool {
133        (self.start() == other.start())
134            && (self.end() == other.end())
135            && match (self.projects(), other.projects()) {
136                (None, None) => true,
137                (None, _) | (_, None) => false,
138                (Some(lhs), Some(rhs)) => format!("{lhs:?}") == format!("{rhs:?}")
139            }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use spectral::prelude::*;
146
147    use super::*;
148    use crate::Date;
149
150    // Test Filter
151
152    #[test]
153    fn test_filter_no_args() {
154        let args = vec![];
155        let expected = FilterArgs {
156            range:    DateRange::new(Date::today(), Date::today().succ()),
157            projects: None
158        };
159
160        assert_that!(&FilterArgs::new(&args, &[]))
161            .is_ok()
162            .is_equal_to(&expected);
163    }
164
165    #[test]
166    fn test_filter_just_one_date() {
167        let args = vec!["yesterday".to_string()];
168        let expected = FilterArgs {
169            range:    DateRange::new(Date::today().pred(), Date::today()),
170            projects: None
171        };
172
173        assert_that!(&FilterArgs::new(&args, &[]))
174            .is_ok()
175            .is_equal_to(&expected);
176    }
177
178    #[test]
179    fn test_filter_just_two_dates() {
180        let args = vec!["2021-12-01".to_string(), "2021-12-07".to_string()];
181        let expected = FilterArgs {
182            range:    DateRange::new(
183                Date::new(2021, 12, 1).unwrap(),
184                Date::new(2021, 12, 8).unwrap()
185            ),
186            projects: None
187        };
188
189        assert_that!(&FilterArgs::new(&args, &[]))
190            .is_ok()
191            .is_equal_to(&expected);
192    }
193
194    #[test]
195    fn test_filter_just_project() {
196        let dates = vec![];
197        let proj = vec!["project1".to_string()];
198        let expected = FilterArgs {
199            range:    DateRange::new(Date::today(), Date::today().succ()),
200            projects: Some(Regex::new(r"project1").unwrap())
201        };
202
203        assert_that!(&FilterArgs::new(&dates, &proj))
204            .is_ok()
205            .is_equal_to(&expected);
206    }
207
208    #[test]
209    fn test_filter_just_multiple_projects() {
210        let dates = vec![];
211        let projs = vec![
212            "project1".to_string(),
213            "cleanup".to_string(),
214            "profit".to_string(),
215        ];
216        let expected = FilterArgs {
217            range:    DateRange::new(Date::today(), Date::today().succ()),
218            projects: Some(Regex::new(r"project1|cleanup|profit").unwrap())
219        };
220
221        assert_that!(&FilterArgs::new(&dates, &projs))
222            .is_ok()
223            .is_equal_to(&expected);
224    }
225
226    #[test]
227    fn test_filter_start_and_project() {
228        let dates = vec!["2021-12-01".to_string()];
229        let projs = vec!["project1".to_string()];
230        let expected = FilterArgs {
231            range:    DateRange::new(
232                Date::new(2021, 12, 1).unwrap(),
233                Date::new(2021, 12, 2).unwrap()
234            ),
235            projects: Some(Regex::new(r"project1").unwrap())
236        };
237
238        assert_that!(&FilterArgs::new(&dates, &projs))
239            .is_ok()
240            .is_equal_to(&expected);
241    }
242
243    #[test]
244    fn test_filter_both_dates_and_project() {
245        let dates = vec!["2021-12-01".to_string(), "2021-12-07".to_string()];
246        let projs = vec!["project1".to_string()];
247        let expected = FilterArgs {
248            range:    DateRange::new(
249                Date::new(2021, 12, 1).unwrap(),
250                Date::new(2021, 12, 8).unwrap()
251            ),
252            projects: Some(Regex::new(r"project1").unwrap())
253        };
254
255        assert_that!(&FilterArgs::new(&dates, &projs))
256            .is_ok()
257            .is_equal_to(&expected);
258    }
259
260    // Test DateRange
261
262    #[test]
263    fn test_dates_no_args() {
264        let args = vec![];
265        #[rustfmt::skip]
266        let expected = DateRangeArgs {
267            range: DateRange::new(Date::today(), Date::today().succ())
268        };
269
270        assert_that!(&DateRangeArgs::new(&args))
271            .is_ok()
272            .is_equal_to(&expected);
273    }
274
275    #[test]
276    fn test_dates_just_one_date() {
277        let args = vec!["yesterday".to_string()];
278        #[rustfmt::skip]
279        let expected = DateRangeArgs {
280            range: DateRange::new(Date::today().pred(), Date::today())
281        };
282
283        assert_that!(&DateRangeArgs::new(&args))
284            .is_ok()
285            .is_equal_to(&expected);
286    }
287
288    #[test]
289    fn test_dates_both_dates() {
290        let args = vec!["2021-12-01".to_string(), "2021-12-07".to_string()];
291        let expected = DateRangeArgs {
292            range: DateRange::new(
293                Date::new(2021, 12, 1).unwrap(),
294                Date::new(2021, 12, 8).unwrap()
295            )
296        };
297
298        assert_that!(&DateRangeArgs::new(&args))
299            .is_ok()
300            .is_equal_to(&expected);
301    }
302}