From 9916389af9594ff21e4042dfd3b51c00838066d0 Mon Sep 17 00:00:00 2001 From: Nick Krichevsky Date: Sun, 30 Jan 2022 15:14:33 -0500 Subject: [PATCH] Add step parsing --- src/lib.rs | 106 +++++++++++++++++++++--------------------- src/parse.rs | 4 +- src/parse/compound.rs | 30 +++++++----- src/parse/simple.rs | 15 ++++-- src/parse/validate.rs | 8 ++-- 5 files changed, 90 insertions(+), 73 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7a25fc7..819dee9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,9 +6,9 @@ mod parse; #[derive(Debug, Clone, PartialEq, Eq)] pub enum CronSpecifier { - Any, + Any(Option), Specifically(u8), - Range(u8, u8), + Range(u8, u8, Option), Several(Vec), } @@ -37,59 +37,61 @@ mod tests { use CronSpecifier::{Any, Range, Several, Specifically}; // simple parse checks - #[test_case("* * * * *", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Any})] - #[test_case("0 4 * * *", &CronEntry{minute: Specifically(0), hour: Specifically(4), day_of_month: Any, month: Any, day_of_week: Any})] + #[test_case("* * * * *", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Any(None)})] + #[test_case("0 4 * * *", &CronEntry{minute: Specifically(0), hour: Specifically(4), day_of_month: Any(None), month: Any(None), day_of_week: Any(None)})] #[test_case("0 4 10 5 4", &CronEntry{minute: Specifically(0), hour: Specifically(4), day_of_month: Specifically(10), month: Specifically(5), day_of_week: Specifically(4)})] - #[test_case("* * 10 5 *", &CronEntry{minute: Any, hour: Any, day_of_month: Specifically(10), month: Specifically(5), day_of_week: Any})] - #[test_case("* * * * 7", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Specifically(0)}; "sunday can be seven")] - #[test_case("* 4-6 * * *", &CronEntry{minute: Any, hour: Range(4, 6), day_of_month: Any, month: Any, day_of_week: Any})] - #[test_case("* * * * mon-wed", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Range(1, 3)})] - #[test_case("* * * * mon-5", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Range(1, 5)})] - #[test_case("* * * jan-jun *", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Range(1, 6), day_of_week: Any})] - #[test_case("* * * 1,5 *", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Several(vec![Specifically(1), Specifically(5)]), day_of_week: Any})] - #[test_case("* * * * 1,5", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Several(vec![Specifically(1), Specifically(5)])})] - #[test_case("* * * * mon-wed,1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Several(vec![Range(1, 3), Specifically(1)])})] - #[test_case("* * * * mon-wed,1,3-5", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Any, day_of_week: Several(vec![Range(1, 3), Specifically(1), Range(3, 5)])})] + #[test_case("* * 10 5 *", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Specifically(10), month: Specifically(5), day_of_week: Any(None)})] + #[test_case("* * * * 7", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Specifically(0)}; "sunday can be seven")] + #[test_case("* 4-6 * * *", &CronEntry{minute: Any(None), hour: Range(4, 6, None), day_of_month: Any(None), month: Any(None), day_of_week: Any(None)})] + #[test_case("* * * * mon-wed", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Range(1, 3, None)})] + #[test_case("* * * * mon-5", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Range(1, 5, None)})] + #[test_case("* * * jan-jun *", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Range(1, 6, None), day_of_week: Any(None)})] + #[test_case("* * * 1,5 *", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Several(vec![Specifically(1), Specifically(5)]), day_of_week: Any(None)})] + #[test_case("* * * * 1,5", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Several(vec![Specifically(1), Specifically(5)])})] + #[test_case("* * * * mon-wed,1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Several(vec![Range(1, 3, None), Specifically(1)])})] + #[test_case("* * * * mon-wed,1,3-5", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Several(vec![Range(1, 3, None), Specifically(1), Range(3, 5, None)])})] + #[test_case("*/10 * * * *", &CronEntry{minute: Any(Some(10)), hour: Any(None), day_of_month: Any(None), month: Any(None), day_of_week: Any(None)})] + #[test_case("* */2 * * SUN", &CronEntry{minute: Any(None), hour: Any(Some(2)), day_of_month: Any(None), month: Any(None), day_of_week: Specifically(0)})] // days of week - #[test_case("* * * 1 mon", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(1)})] - #[test_case("* * * 2 MON", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(1)})] - #[test_case("* * * 1 tue", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(2)})] - #[test_case("* * * 2 TUE", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(2)})] - #[test_case("* * * 1 wed", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(3)})] - #[test_case("* * * 2 WED", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(3)})] - #[test_case("* * * 1 thu", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(4)})] - #[test_case("* * * 2 THU", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(4)})] - #[test_case("* * * 1 fri", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(5)})] - #[test_case("* * * 2 FRI", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(5)})] - #[test_case("* * * 1 sat", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(6)})] - #[test_case("* * * 2 SAT", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(6)})] - #[test_case("* * * 1 sun", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(0)})] - #[test_case("* * * 2 SUN", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(0)})] + #[test_case("* * * 1 mon", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(1)})] + #[test_case("* * * 2 MON", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(1)})] + #[test_case("* * * 1 tue", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(2)})] + #[test_case("* * * 2 TUE", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(2)})] + #[test_case("* * * 1 wed", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(3)})] + #[test_case("* * * 2 WED", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(3)})] + #[test_case("* * * 1 thu", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(4)})] + #[test_case("* * * 2 THU", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(4)})] + #[test_case("* * * 1 fri", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(5)})] + #[test_case("* * * 2 FRI", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(5)})] + #[test_case("* * * 1 sat", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(6)})] + #[test_case("* * * 2 SAT", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(6)})] + #[test_case("* * * 1 sun", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(0)})] + #[test_case("* * * 2 SUN", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(0)})] // months - #[test_case("* * * jan 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(0)})] - #[test_case("* * * JAN 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(1), day_of_week: Specifically(1)})] - #[test_case("* * * feb 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(0)})] - #[test_case("* * * FEB 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(2), day_of_week: Specifically(1)})] - #[test_case("* * * mar 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(3), day_of_week: Specifically(0)})] - #[test_case("* * * MAR 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(3), day_of_week: Specifically(1)})] - #[test_case("* * * apr 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(4), day_of_week: Specifically(0)})] - #[test_case("* * * APR 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(4), day_of_week: Specifically(1)})] - #[test_case("* * * may 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(5), day_of_week: Specifically(0)})] - #[test_case("* * * MAY 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(5), day_of_week: Specifically(1)})] - #[test_case("* * * jun 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(6), day_of_week: Specifically(0)})] - #[test_case("* * * JUN 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(6), day_of_week: Specifically(1)})] - #[test_case("* * * jul 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(7), day_of_week: Specifically(0)})] - #[test_case("* * * JUL 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(7), day_of_week: Specifically(1)})] - #[test_case("* * * aug 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(8), day_of_week: Specifically(0)})] - #[test_case("* * * AUG 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(8), day_of_week: Specifically(1)})] - #[test_case("* * * sep 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(9), day_of_week: Specifically(0)})] - #[test_case("* * * SEP 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(9), day_of_week: Specifically(1)})] - #[test_case("* * * oct 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(10), day_of_week: Specifically(0)})] - #[test_case("* * * OCT 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(10), day_of_week: Specifically(1)})] - #[test_case("* * * nov 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(11), day_of_week: Specifically(0)})] - #[test_case("* * * NOV 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(11), day_of_week: Specifically(1)})] - #[test_case("* * * dec 0", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(12), day_of_week: Specifically(0)})] - #[test_case("* * * DEC 1", &CronEntry{minute: Any, hour: Any, day_of_month: Any, month: Specifically(12), day_of_week: Specifically(1)})] + #[test_case("* * * jan 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(0)})] + #[test_case("* * * JAN 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(1), day_of_week: Specifically(1)})] + #[test_case("* * * feb 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(0)})] + #[test_case("* * * FEB 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(2), day_of_week: Specifically(1)})] + #[test_case("* * * mar 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(3), day_of_week: Specifically(0)})] + #[test_case("* * * MAR 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(3), day_of_week: Specifically(1)})] + #[test_case("* * * apr 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(4), day_of_week: Specifically(0)})] + #[test_case("* * * APR 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(4), day_of_week: Specifically(1)})] + #[test_case("* * * may 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(5), day_of_week: Specifically(0)})] + #[test_case("* * * MAY 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(5), day_of_week: Specifically(1)})] + #[test_case("* * * jun 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(6), day_of_week: Specifically(0)})] + #[test_case("* * * JUN 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(6), day_of_week: Specifically(1)})] + #[test_case("* * * jul 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(7), day_of_week: Specifically(0)})] + #[test_case("* * * JUL 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(7), day_of_week: Specifically(1)})] + #[test_case("* * * aug 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(8), day_of_week: Specifically(0)})] + #[test_case("* * * AUG 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(8), day_of_week: Specifically(1)})] + #[test_case("* * * sep 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(9), day_of_week: Specifically(0)})] + #[test_case("* * * SEP 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(9), day_of_week: Specifically(1)})] + #[test_case("* * * oct 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(10), day_of_week: Specifically(0)})] + #[test_case("* * * OCT 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(10), day_of_week: Specifically(1)})] + #[test_case("* * * nov 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(11), day_of_week: Specifically(0)})] + #[test_case("* * * NOV 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(11), day_of_week: Specifically(1)})] + #[test_case("* * * dec 0", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(12), day_of_week: Specifically(0)})] + #[test_case("* * * DEC 1", &CronEntry{minute: Any(None), hour: Any(None), day_of_month: Any(None), month: Specifically(12), day_of_week: Specifically(1)})] fn test_successful_parse(to_parse: &str, expected: &CronEntry) { let parse_res = CronEntry::from_str(to_parse); diff --git a/src/parse.rs b/src/parse.rs index ac7e89e..d328ef2 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,5 +1,5 @@ use itertools::Itertools; -use nom::{branch::alt, character::complete::char, IResult, Parser}; +use nom::{branch::alt, character::complete::char, error::FromExternalError, IResult, Parser}; use thiserror::Error; use crate::{CronEntry, CronSpecifier}; @@ -163,7 +163,7 @@ fn list_or_single_specifier<'a, P, E>( ) -> impl FnMut(&'a str) -> IResult<&'a str, CronSpecifier, E> where P: Parser<&'a str, CronSpecifier, E> + Copy, - E: nom::error::ParseError<&'a str>, + E: nom::error::ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>, { alt(( compound::several_specifiers(list_element_parser), diff --git a/src/parse/compound.rs b/src/parse/compound.rs index ed0229c..6ff119b 100644 --- a/src/parse/compound.rs +++ b/src/parse/compound.rs @@ -3,11 +3,12 @@ use crate::parse::{simple, word}; use crate::CronSpecifier; use nom::combinator::fail; +use nom::error::FromExternalError; use nom::multi::separated_list1; use nom::InputLength; use nom::{ - branch::alt, character::complete::char, sequence::separated_pair, AsChar, IResult, InputIter, - Parser, Slice, + branch::alt, character::complete::char, combinator::opt, sequence::pair, + sequence::separated_pair, AsChar, IResult, InputIter, Parser, Slice, }; use std::ops::RangeFrom; @@ -101,20 +102,25 @@ where /// Build a parser for a ranged specifier, with some kind of parser for the left/right of the range. /// This accepts strings of the form `-`. The left/right parsers must return the /// raw components of a range, rather than the specifiers themselves -fn build_range_parser( +fn build_range_parser<'a, E, L, R>( left_parser: L, right_parser: R, -) -> impl FnMut(I) -> IResult +) -> impl FnMut(&'a str) -> IResult<&'a str, CronSpecifier, E> where - I: InputIter + Slice>, - ::Item: AsChar, - L: Parser + Copy, - R: Parser + Copy, - E: nom::error::ParseError, + L: Parser<&'a str, u8, E> + Copy, + R: Parser<&'a str, u8, E> + Copy, + E: nom::error::ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>, { - move |data: I| { - let (remaining, (start, end)) = separated_pair(left_parser, char('-'), right_parser)(data)?; - let specifier = CronSpecifier::Range(start, end); + // I'd much prefer it if I could make this take a generic input, but I backed myself into a corner with + // `parse_number`. + // TODO: make this take a generic input + move |data: &'a str| { + let (remaining, ((start, end), step)) = pair( + separated_pair(left_parser, char('-'), right_parser), + opt(simple::parse_step), + )(data)?; + + let specifier = CronSpecifier::Range(start, end, step); Ok((remaining, specifier)) } diff --git a/src/parse/simple.rs b/src/parse/simple.rs index b37e3d3..06d5b8e 100644 --- a/src/parse/simple.rs +++ b/src/parse/simple.rs @@ -3,8 +3,9 @@ use nom::{ character::complete::{char, digit1}, - combinator::map_res, + combinator::{map_res, opt}, error::FromExternalError, + sequence::{pair, preceded}, IResult, }; @@ -27,9 +28,15 @@ where pub(super) fn parse_star_specifier<'a, E>(chunk: &'a str) -> IResult<&'a str, CronSpecifier, E> where - E: nom::error::ParseError<&'a str>, + E: nom::error::ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError>, { - let (remaining, _) = char('*')(chunk)?; + let (remaining, (_, step)) = pair(char('*'), opt(parse_step))(chunk)?; - Ok((remaining, CronSpecifier::Any)) + Ok((remaining, CronSpecifier::Any(step))) +} + +pub(super) fn parse_step<'a, E>(chunk: &'a str) -> IResult<&'a str, u8, E> + where + E: nom::error::ParseError<&'a str> + FromExternalError<&'a str, std::num::ParseIntError> { + preceded(char('/'), parse_number)(chunk) } diff --git a/src/parse/validate.rs b/src/parse/validate.rs index dda2477..7807e4f 100644 --- a/src/parse/validate.rs +++ b/src/parse/validate.rs @@ -25,7 +25,7 @@ fn ensure_value_in_range( construct_error: fn(u8) -> Error, ) -> Result<(), Error> { match *specifier { - CronSpecifier::Any => Ok(()), + CronSpecifier::Any(_) => Ok(()), CronSpecifier::Specifically(value) => { if value >= min && value <= max { Ok(()) @@ -33,7 +33,8 @@ fn ensure_value_in_range( Err(construct_error(value)) } } - CronSpecifier::Range(range_min, range_max) => { + // TODO: Handle steps + CronSpecifier::Range(range_min, range_max, _) => { let min_ok = range_min >= min && range_min <= max; let max_ok = range_max >= min && range_max <= max; if max_ok && min_ok { @@ -54,7 +55,8 @@ fn ensure_value_in_range( fn ensure_range_validity(specifier: &CronSpecifier) -> Result<(), Error> { match *specifier { - CronSpecifier::Range(min, max) => { + // TODO: Handle steps + CronSpecifier::Range(min, max, _) => { if min <= max { Ok(()) } else {