cron/
parsing.rs

1use winnow::ascii::{alpha1, digit1, multispace0};
2use winnow::combinator::{alt, delimited, eof, opt, separated, separated_pair, terminated};
3use winnow::prelude::*;
4use winnow::PResult;
5
6use std::borrow::Cow;
7use std::convert::TryFrom;
8use std::str::{self, FromStr};
9
10use crate::error::{Error, ErrorKind};
11use crate::ordinal::*;
12use crate::schedule::{Schedule, ScheduleFields};
13use crate::specifier::*;
14use crate::time_unit::*;
15
16impl TryFrom<Cow<'_, str>> for Schedule {
17    type Error = Error;
18
19    fn try_from(expression: Cow<'_, str>) -> Result<Self, Self::Error> {
20        match schedule.parse(&expression) {
21            Ok(schedule_fields) => Ok(Schedule::new(expression.into_owned(), schedule_fields)), // Extract from winnow tuple
22            Err(parse_error) => Err(ErrorKind::Expression(format!("{parse_error}")).into()),
23        }
24    }
25}
26
27impl TryFrom<String> for Schedule {
28    type Error = Error;
29
30    fn try_from(expression: String) -> Result<Self, Self::Error> {
31        Self::try_from(Cow::Owned(expression))
32    }
33}
34
35impl TryFrom<&str> for Schedule {
36    type Error = Error;
37
38    fn try_from(expression: &str) -> Result<Self, Self::Error> {
39        Self::try_from(Cow::Borrowed(expression))
40    }
41}
42
43impl FromStr for Schedule {
44    type Err = Error;
45
46    fn from_str(expression: &str) -> Result<Self, Self::Err> {
47        Self::try_from(Cow::Borrowed(expression))
48    }
49}
50
51#[derive(Debug, PartialEq)]
52pub struct Field {
53    pub specifiers: Vec<RootSpecifier>, // TODO: expose iterator?
54}
55
56trait FromField
57where
58    Self: Sized,
59{
60    //TODO: Replace with std::convert::TryFrom when stable
61    fn from_field(field: Field) -> Result<Self, Error>;
62}
63
64impl<T> FromField for T
65where
66    T: TimeUnitField,
67{
68    fn from_field(field: Field) -> Result<T, Error> {
69        if field.specifiers.len() == 1
70            && field.specifiers.first().unwrap() == &RootSpecifier::from(Specifier::All)
71        {
72            return Ok(T::all());
73        }
74        let mut ordinals = OrdinalSet::new();
75        for specifier in field.specifiers {
76            let specifier_ordinals: OrdinalSet = T::ordinals_from_root_specifier(&specifier)?;
77            for ordinal in specifier_ordinals {
78                ordinals.insert(T::validate_ordinal(ordinal)?);
79            }
80        }
81        Ok(T::from_ordinal_set(ordinals))
82    }
83}
84
85fn ordinal(i: &mut &str) -> PResult<u32> {
86    delimited(multispace0, digit1, multispace0)
87        .try_map(u32::from_str)
88        .parse_next(i)
89}
90
91fn name(i: &mut &str) -> PResult<String> {
92    delimited(multispace0, alpha1, multispace0)
93        .map(ToOwned::to_owned)
94        .parse_next(i)
95}
96
97fn point(i: &mut &str) -> PResult<Specifier> {
98    ordinal.map(Specifier::Point).parse_next(i)
99}
100
101fn named_point(i: &mut &str) -> PResult<RootSpecifier> {
102    name.map(RootSpecifier::NamedPoint).parse_next(i)
103}
104
105fn period(i: &mut &str) -> PResult<RootSpecifier> {
106    separated_pair(specifier, "/", ordinal)
107        .map(|(start, step)| RootSpecifier::Period(start, step))
108        .parse_next(i)
109}
110
111fn period_with_any(i: &mut &str) -> PResult<RootSpecifier> {
112    separated_pair(specifier_with_any, "/", ordinal)
113        .map(|(start, step)| RootSpecifier::Period(start, step))
114        .parse_next(i)
115}
116
117fn range(i: &mut &str) -> PResult<Specifier> {
118    separated_pair(ordinal, "-", ordinal)
119        .map(|(start, end)| Specifier::Range(start, end))
120        .parse_next(i)
121}
122
123fn named_range(i: &mut &str) -> PResult<Specifier> {
124    separated_pair(name, "-", name)
125        .map(|(start, end)| Specifier::NamedRange(start, end))
126        .parse_next(i)
127}
128
129fn all(i: &mut &str) -> PResult<Specifier> {
130    "*".map(|_| Specifier::All).parse_next(i)
131}
132
133fn any(i: &mut &str) -> PResult<Specifier> {
134    "?".map(|_| Specifier::All).parse_next(i)
135}
136
137fn specifier(i: &mut &str) -> PResult<Specifier> {
138    alt((all, range, point, named_range)).parse_next(i)
139}
140
141fn specifier_with_any(i: &mut &str) -> PResult<Specifier> {
142    alt((any, specifier)).parse_next(i)
143}
144
145fn root_specifier(i: &mut &str) -> PResult<RootSpecifier> {
146    alt((period, specifier.map(RootSpecifier::from), named_point)).parse_next(i)
147}
148
149fn root_specifier_with_any(i: &mut &str) -> PResult<RootSpecifier> {
150    alt((
151        period_with_any,
152        specifier_with_any.map(RootSpecifier::from),
153        named_point,
154    ))
155    .parse_next(i)
156}
157
158fn root_specifier_list(i: &mut &str) -> PResult<Vec<RootSpecifier>> {
159    let list = separated(1.., root_specifier, ",");
160    let single_item = root_specifier.map(|spec| vec![spec]);
161    delimited(multispace0, alt((list, single_item)), multispace0).parse_next(i)
162}
163
164fn root_specifier_list_with_any(i: &mut &str) -> PResult<Vec<RootSpecifier>> {
165    let list = separated(1.., root_specifier_with_any, ",");
166    let single_item = root_specifier_with_any.map(|spec| vec![spec]);
167    delimited(multispace0, alt((list, single_item)), multispace0).parse_next(i)
168}
169
170fn field(i: &mut &str) -> PResult<Field> {
171    let specifiers = root_specifier_list.parse_next(i)?;
172    Ok(Field { specifiers })
173}
174
175fn field_with_any(i: &mut &str) -> PResult<Field> {
176    let specifiers = root_specifier_list_with_any.parse_next(i)?;
177    Ok(Field { specifiers })
178}
179
180fn shorthand_yearly(i: &mut &str) -> PResult<ScheduleFields> {
181    "@yearly".parse_next(i)?;
182    let fields = ScheduleFields::new(
183        Seconds::from_ordinal(0),
184        Minutes::from_ordinal(0),
185        Hours::from_ordinal(0),
186        DaysOfMonth::from_ordinal(1),
187        Months::from_ordinal(1),
188        DaysOfWeek::all(),
189        Years::all(),
190    );
191    Ok(fields)
192}
193
194fn shorthand_monthly(i: &mut &str) -> PResult<ScheduleFields> {
195    "@monthly".parse_next(i)?;
196    let fields = ScheduleFields::new(
197        Seconds::from_ordinal(0),
198        Minutes::from_ordinal(0),
199        Hours::from_ordinal(0),
200        DaysOfMonth::from_ordinal(1),
201        Months::all(),
202        DaysOfWeek::all(),
203        Years::all(),
204    );
205    Ok(fields)
206}
207
208fn shorthand_weekly(i: &mut &str) -> PResult<ScheduleFields> {
209    "@weekly".parse_next(i)?;
210    let fields = ScheduleFields::new(
211        Seconds::from_ordinal(0),
212        Minutes::from_ordinal(0),
213        Hours::from_ordinal(0),
214        DaysOfMonth::all(),
215        Months::all(),
216        DaysOfWeek::from_ordinal(1),
217        Years::all(),
218    );
219    Ok(fields)
220}
221
222fn shorthand_daily(i: &mut &str) -> PResult<ScheduleFields> {
223    "@daily".parse_next(i)?;
224    let fields = ScheduleFields::new(
225        Seconds::from_ordinal(0),
226        Minutes::from_ordinal(0),
227        Hours::from_ordinal(0),
228        DaysOfMonth::all(),
229        Months::all(),
230        DaysOfWeek::all(),
231        Years::all(),
232    );
233    Ok(fields)
234}
235
236fn shorthand_hourly(i: &mut &str) -> PResult<ScheduleFields> {
237    "@hourly".parse_next(i)?;
238    let fields = ScheduleFields::new(
239        Seconds::from_ordinal(0),
240        Minutes::from_ordinal(0),
241        Hours::all(),
242        DaysOfMonth::all(),
243        Months::all(),
244        DaysOfWeek::all(),
245        Years::all(),
246    );
247    Ok(fields)
248}
249
250fn shorthand(i: &mut &str) -> PResult<ScheduleFields> {
251    let keywords = alt((
252        shorthand_yearly,
253        shorthand_monthly,
254        shorthand_weekly,
255        shorthand_daily,
256        shorthand_hourly,
257    ));
258    delimited(multispace0, keywords, multispace0).parse_next(i)
259}
260
261fn longhand(i: &mut &str) -> PResult<ScheduleFields> {
262    let seconds = field.try_map(Seconds::from_field);
263    let minutes = field.try_map(Minutes::from_field);
264    let hours = field.try_map(Hours::from_field);
265    let days_of_month = field_with_any.try_map(DaysOfMonth::from_field);
266    let months = field.try_map(Months::from_field);
267    let days_of_week = field_with_any.try_map(DaysOfWeek::from_field);
268    let years = opt(field.try_map(Years::from_field));
269    let fields = (
270        seconds,
271        minutes,
272        hours,
273        days_of_month,
274        months,
275        days_of_week,
276        years,
277    );
278
279    terminated(fields, eof)
280        .map(
281            |(seconds, minutes, hours, days_of_month, months, days_of_week, years)| {
282                let years = years.unwrap_or_else(Years::all);
283                ScheduleFields::new(
284                    seconds,
285                    minutes,
286                    hours,
287                    days_of_month,
288                    months,
289                    days_of_week,
290                    years,
291                )
292            },
293        )
294        .parse_next(i)
295}
296
297fn schedule(i: &mut &str) -> PResult<ScheduleFields> {
298    alt((shorthand, longhand)).parse_next(i)
299}
300
301#[cfg(test)]
302mod test {
303    use super::*;
304
305    #[test]
306    fn test_nom_valid_number() {
307        let expression = "1997";
308        point.parse(expression).unwrap();
309    }
310
311    #[test]
312    fn test_nom_invalid_point() {
313        let expression = "a";
314        assert!(point.parse(expression).is_err());
315    }
316
317    #[test]
318    fn test_nom_valid_named_point() {
319        let expression = "WED";
320        named_point.parse(expression).unwrap();
321    }
322
323    #[test]
324    fn test_nom_invalid_named_point() {
325        let expression = "8";
326        assert!(named_point.parse(expression).is_err());
327    }
328
329    #[test]
330    fn test_nom_valid_period() {
331        let expression = "1/2";
332        period.parse(expression).unwrap();
333    }
334
335    #[test]
336    fn test_nom_invalid_period() {
337        let expression = "Wed/4";
338        assert!(period.parse(expression).is_err());
339    }
340
341    #[test]
342    fn test_nom_valid_number_list() {
343        let expression = "1,2";
344        field.parse(expression).unwrap();
345        field_with_any.parse(expression).unwrap();
346    }
347
348    #[test]
349    fn test_nom_invalid_number_list() {
350        let expression = ",1,2";
351        assert!(field.parse(expression).is_err());
352        assert!(field_with_any.parse(expression).is_err());
353    }
354
355    #[test]
356    fn test_nom_field_with_any_valid_any() {
357        let expression = "?";
358        field_with_any.parse(expression).unwrap();
359    }
360
361    #[test]
362    fn test_nom_field_invalid_any() {
363        let expression = "?";
364        assert!(field.parse(expression).is_err());
365    }
366
367    #[test]
368    fn test_nom_valid_range_field() {
369        let expression = "1-4";
370        range.parse(expression).unwrap();
371    }
372
373    #[test]
374    fn test_nom_valid_period_all() {
375        let expression = "*/2";
376        period.parse(expression).unwrap();
377    }
378
379    #[test]
380    fn test_nom_valid_period_range() {
381        let expression = "10-20/2";
382        period.parse(expression).unwrap();
383    }
384
385    #[test]
386    fn test_nom_valid_period_named_range() {
387        let expression = "Mon-Thurs/2";
388        period.parse(expression).unwrap();
389
390        let expression = "February-November/2";
391        period.parse(expression).unwrap();
392    }
393
394    #[test]
395    fn test_nom_valid_period_point() {
396        let expression = "10/2";
397        period.parse(expression).unwrap();
398    }
399
400    #[test]
401    fn test_nom_invalid_period_any() {
402        let expression = "?/2";
403        assert!(period.parse(expression).is_err());
404    }
405
406    #[test]
407    fn test_nom_invalid_period_named_point() {
408        let expression = "Tues/2";
409        assert!(period.parse(expression).is_err());
410
411        let expression = "February/2";
412        assert!(period.parse(expression).is_err());
413    }
414
415    #[test]
416    fn test_nom_invalid_period_specifier_range() {
417        let expression = "10-12/*";
418        assert!(period.parse(expression).is_err());
419    }
420
421    #[test]
422    fn test_nom_valid_period_with_any_all() {
423        let expression = "*/2";
424        period_with_any.parse(expression).unwrap();
425    }
426
427    #[test]
428    fn test_nom_valid_period_with_any_range() {
429        let expression = "10-20/2";
430        period_with_any.parse(expression).unwrap();
431    }
432
433    #[test]
434    fn test_nom_valid_period_with_any_named_range() {
435        let expression = "Mon-Thurs/2";
436        period_with_any.parse(expression).unwrap();
437
438        let expression = "February-November/2";
439        period_with_any.parse(expression).unwrap();
440    }
441
442    #[test]
443    fn test_nom_valid_period_with_any_point() {
444        let expression = "10/2";
445        period_with_any.parse(expression).unwrap();
446    }
447
448    #[test]
449    fn test_nom_valid_period_with_any_any() {
450        let expression = "?/2";
451        period_with_any.parse(expression).unwrap();
452    }
453
454    #[test]
455    fn test_nom_invalid_period_with_any_named_point() {
456        let expression = "Tues/2";
457        assert!(period_with_any.parse(expression).is_err());
458
459        let expression = "February/2";
460        assert!(period_with_any.parse(expression).is_err());
461    }
462
463    #[test]
464    fn test_nom_invalid_period_with_any_specifier_range() {
465        let expression = "10-12/*";
466        assert!(period_with_any.parse(expression).is_err());
467    }
468
469    #[test]
470    fn test_nom_invalid_range_field() {
471        let expression = "-4";
472        assert!(range.parse(expression).is_err());
473    }
474
475    #[test]
476    fn test_nom_valid_named_range_field() {
477        let expression = "TUES-THURS";
478        named_range.parse(expression).unwrap();
479    }
480
481    #[test]
482    fn test_nom_invalid_named_range_field() {
483        let expression = "3-THURS";
484        assert!(named_range.parse(expression).is_err());
485    }
486
487    #[test]
488    fn test_nom_valid_schedule() {
489        let expression = "* * * * * *";
490        schedule.parse(expression).unwrap();
491    }
492
493    #[test]
494    fn test_nom_invalid_schedule() {
495        let expression = "* * * *";
496        assert!(schedule.parse(expression).is_err());
497    }
498
499    #[test]
500    fn test_nom_valid_seconds_list() {
501        let expression = "0,20,40 * * * * *";
502        schedule.parse(expression).unwrap();
503    }
504
505    #[test]
506    fn test_nom_valid_seconds_range() {
507        let expression = "0-40 * * * * *";
508        schedule.parse(expression).unwrap();
509    }
510
511    #[test]
512    fn test_nom_valid_seconds_mix() {
513        let expression = "0-5,58 * * * * *";
514        schedule.parse(expression).unwrap();
515    }
516
517    #[test]
518    fn test_nom_invalid_seconds_range() {
519        let expression = "0-65 * * * * *";
520        assert!(schedule.parse(expression).is_err());
521    }
522
523    #[test]
524    fn test_nom_invalid_seconds_list() {
525        let expression = "103,12 * * * * *";
526        assert!(schedule.parse(expression).is_err());
527    }
528
529    #[test]
530    fn test_nom_invalid_seconds_mix() {
531        let expression = "0-5,102 * * * * *";
532        assert!(schedule.parse(expression).is_err());
533    }
534
535    #[test]
536    fn test_nom_valid_days_of_week_list() {
537        let expression = "* * * * * MON,WED,FRI";
538        schedule.parse(expression).unwrap();
539    }
540
541    #[test]
542    fn test_nom_invalid_days_of_week_list() {
543        let expression = "* * * * * MON,TURTLE";
544        assert!(schedule.parse(expression).is_err());
545    }
546
547    #[test]
548    fn test_nom_valid_days_of_week_range() {
549        let expression = "* * * * * MON-FRI";
550        schedule.parse(expression).unwrap();
551    }
552
553    #[test]
554    fn test_nom_invalid_days_of_week_range() {
555        let expression = "* * * * * BEAR-OWL";
556        assert!(schedule.parse(expression).is_err());
557    }
558
559    #[test]
560    fn test_nom_invalid_period_with_range_specifier() {
561        let expression = "10-12/10-12 * * * * ?";
562        assert!(schedule.parse(expression).is_err());
563    }
564
565    #[test]
566    fn test_nom_valid_days_of_month_any() {
567        let expression = "* * * ? * *";
568        schedule.parse(expression).unwrap();
569    }
570
571    #[test]
572    fn test_nom_valid_days_of_week_any() {
573        let expression = "* * * * * ?";
574        schedule.parse(expression).unwrap();
575    }
576
577    #[test]
578    fn test_nom_valid_days_of_month_any_days_of_week_specific() {
579        let expression = "* * * ? * Mon,Thu";
580        schedule.parse(expression).unwrap();
581    }
582
583    #[test]
584    fn test_nom_valid_days_of_week_any_days_of_month_specific() {
585        let expression = "* * * 1,2 * ?";
586        schedule.parse(expression).unwrap();
587    }
588
589    #[test]
590    fn test_nom_valid_dom_and_dow_any() {
591        let expression = "* * * ? * ?";
592        schedule.parse(expression).unwrap();
593    }
594
595    #[test]
596    fn test_nom_invalid_other_fields_any() {
597        let expression = "? * * * * *";
598        assert!(schedule.parse(expression).is_err());
599
600        let expression = "* ? * * * *";
601        assert!(schedule.parse(expression).is_err());
602
603        let expression = "* * ? * * *";
604        assert!(schedule.parse(expression).is_err());
605
606        let expression = "* * * * ? *";
607        assert!(schedule.parse(expression).is_err());
608    }
609
610    #[test]
611    fn test_nom_invalid_trailing_characters() {
612        let expression = "* * * * * *foo *";
613        assert!(schedule.parse(expression).is_err());
614
615        let expression = "* * * * * * * foo";
616        assert!(schedule.parse(expression).is_err());
617    }
618
619    /// Issue #86
620    #[test]
621    fn shorthand_must_match_whole_input() {
622        let expression = "@dailyBla";
623        assert!(schedule.parse(expression).is_err());
624        let expression = " @dailyBla ";
625        assert!(schedule.parse(expression).is_err());
626    }
627
628    #[test]
629    fn test_try_from_cow_str_owned() {
630        let expression = Cow::Owned(String::from("* * * ? * ?"));
631        Schedule::try_from(expression).unwrap();
632    }
633
634    #[test]
635    fn test_try_from_cow_str_borrowed() {
636        let expression = Cow::Borrowed("* * * ? * ?");
637        Schedule::try_from(expression).unwrap();
638    }
639
640    #[test]
641    fn test_try_from_string() {
642        let expression = String::from("* * * ? * ?");
643        Schedule::try_from(expression).unwrap();
644    }
645
646    #[test]
647    fn test_try_from_str() {
648        let expression = "* * * ? * ?";
649        Schedule::try_from(expression).unwrap();
650    }
651
652    #[test]
653    fn test_from_str() {
654        let expression = "* * * ? * ?";
655        Schedule::from_str(expression).unwrap();
656    }
657
658    /// Issue #59
659    #[test]
660    fn test_reject_invalid_interval() {
661        for invalid_expression in [
662            "1-5/61 * * * * *",
663            "*/61 2 3 4 5 6",
664            "* */61 * * * *",
665            "* * */25 * * *",
666            "* * * */32 * *",
667            "* * * * */13 *",
668            "1,2,3/60 * * * * *",
669            "0 0 0 1 1 ? 2020-2040/2200",
670        ] {
671            assert!(schedule.parse(invalid_expression).is_err());
672        }
673
674        for valid_expression in [
675            "1-5/59 * * * * *",
676            "*/10 2 3 4 5 6",
677            "* */30 * * * *",
678            "* * */23 * * *",
679            "* * * */30 * *",
680            "* * * * */10 *",
681            "1,2,3/5 * * * * *",
682            "0 0 0 1 1 ? 2020-2040/10",
683        ] {
684            assert!(schedule.parse(valid_expression).is_ok());
685        }
686    }
687}