use winnow::ascii::{alpha1, digit1, multispace0};
use winnow::combinator::{alt, delimited, eof, opt, separated, separated_pair, terminated};
use winnow::prelude::*;
use std::borrow::Cow;
use std::convert::TryFrom;
use std::str::{self, FromStr};
use crate::error::{Error, ErrorKind};
use crate::ordinal::*;
use crate::schedule::{Schedule, ScheduleFields};
use crate::specifier::*;
use crate::time_unit::*;
impl TryFrom<Cow<'_, str>> for Schedule {
type Error = Error;
fn try_from(expression: Cow<'_, str>) -> Result<Self, Self::Error> {
match schedule.parse(&expression) {
Ok(schedule_fields) => Ok(Schedule::new(expression.into_owned(), schedule_fields)), Err(parse_error) => Err(ErrorKind::Expression(format!("{parse_error}")).into()),
}
}
}
impl TryFrom<String> for Schedule {
type Error = Error;
fn try_from(expression: String) -> Result<Self, Self::Error> {
Self::try_from(Cow::Owned(expression))
}
}
impl TryFrom<&str> for Schedule {
type Error = Error;
fn try_from(expression: &str) -> Result<Self, Self::Error> {
Self::try_from(Cow::Borrowed(expression))
}
}
impl FromStr for Schedule {
type Err = Error;
fn from_str(expression: &str) -> Result<Self, Self::Err> {
Self::try_from(Cow::Borrowed(expression))
}
}
#[derive(Debug, PartialEq)]
pub struct Field {
pub specifiers: Vec<RootSpecifier>, }
trait FromField
where
Self: Sized,
{
fn from_field(field: Field) -> Result<Self, Error>;
}
impl<T> FromField for T
where
T: TimeUnitField,
{
fn from_field(field: Field) -> Result<T, Error> {
if field.specifiers.len() == 1
&& field.specifiers.first().unwrap() == &RootSpecifier::from(Specifier::All)
{
return Ok(T::all());
}
let mut ordinals = OrdinalSet::new();
for specifier in field.specifiers {
let specifier_ordinals: OrdinalSet = T::ordinals_from_root_specifier(&specifier)?;
for ordinal in specifier_ordinals {
ordinals.insert(T::validate_ordinal(ordinal)?);
}
}
Ok(T::from_ordinal_set(ordinals))
}
}
fn ordinal(i: &mut &str) -> winnow::Result<u32> {
delimited(multispace0, digit1, multispace0)
.try_map(u32::from_str)
.parse_next(i)
}
fn name(i: &mut &str) -> winnow::Result<String> {
delimited(multispace0, alpha1, multispace0)
.map(ToOwned::to_owned)
.parse_next(i)
}
fn point(i: &mut &str) -> winnow::Result<Specifier> {
ordinal.map(Specifier::Point).parse_next(i)
}
fn named_point(i: &mut &str) -> winnow::Result<RootSpecifier> {
name.map(RootSpecifier::NamedPoint).parse_next(i)
}
fn period(i: &mut &str) -> winnow::Result<RootSpecifier> {
separated_pair(specifier, "/", ordinal)
.map(|(start, step)| RootSpecifier::Period(start, step))
.parse_next(i)
}
fn period_with_any(i: &mut &str) -> winnow::Result<RootSpecifier> {
separated_pair(specifier_with_any, "/", ordinal)
.map(|(start, step)| RootSpecifier::Period(start, step))
.parse_next(i)
}
fn range(i: &mut &str) -> winnow::Result<Specifier> {
separated_pair(ordinal, "-", ordinal)
.map(|(start, end)| Specifier::Range(start, end))
.parse_next(i)
}
fn named_range(i: &mut &str) -> winnow::Result<Specifier> {
separated_pair(name, "-", name)
.map(|(start, end)| Specifier::NamedRange(start, end))
.parse_next(i)
}
fn all(i: &mut &str) -> winnow::Result<Specifier> {
"*".map(|_| Specifier::All).parse_next(i)
}
fn any(i: &mut &str) -> winnow::Result<Specifier> {
"?".map(|_| Specifier::All).parse_next(i)
}
fn specifier(i: &mut &str) -> winnow::Result<Specifier> {
alt((all, range, point, named_range)).parse_next(i)
}
fn specifier_with_any(i: &mut &str) -> winnow::Result<Specifier> {
alt((any, specifier)).parse_next(i)
}
fn root_specifier(i: &mut &str) -> winnow::Result<RootSpecifier> {
alt((period, specifier.map(RootSpecifier::from), named_point)).parse_next(i)
}
fn root_specifier_with_any(i: &mut &str) -> winnow::Result<RootSpecifier> {
alt((
period_with_any,
specifier_with_any.map(RootSpecifier::from),
named_point,
))
.parse_next(i)
}
fn root_specifier_list(i: &mut &str) -> winnow::Result<Vec<RootSpecifier>> {
let list = separated(1.., root_specifier, ",");
let single_item = root_specifier.map(|spec| vec![spec]);
delimited(multispace0, alt((list, single_item)), multispace0).parse_next(i)
}
fn root_specifier_list_with_any(i: &mut &str) -> winnow::Result<Vec<RootSpecifier>> {
let list = separated(1.., root_specifier_with_any, ",");
let single_item = root_specifier_with_any.map(|spec| vec![spec]);
delimited(multispace0, alt((list, single_item)), multispace0).parse_next(i)
}
fn field(i: &mut &str) -> winnow::Result<Field> {
let specifiers = root_specifier_list.parse_next(i)?;
Ok(Field { specifiers })
}
fn field_with_any(i: &mut &str) -> winnow::Result<Field> {
let specifiers = root_specifier_list_with_any.parse_next(i)?;
Ok(Field { specifiers })
}
fn shorthand_yearly(i: &mut &str) -> winnow::Result<ScheduleFields> {
"@yearly".parse_next(i)?;
let fields = ScheduleFields::new(
Seconds::from_ordinal(0),
Minutes::from_ordinal(0),
Hours::from_ordinal(0),
DaysOfMonth::from_ordinal(1),
Months::from_ordinal(1),
DaysOfWeek::all(),
Years::all(),
);
Ok(fields)
}
fn shorthand_monthly(i: &mut &str) -> winnow::Result<ScheduleFields> {
"@monthly".parse_next(i)?;
let fields = ScheduleFields::new(
Seconds::from_ordinal(0),
Minutes::from_ordinal(0),
Hours::from_ordinal(0),
DaysOfMonth::from_ordinal(1),
Months::all(),
DaysOfWeek::all(),
Years::all(),
);
Ok(fields)
}
fn shorthand_weekly(i: &mut &str) -> winnow::Result<ScheduleFields> {
"@weekly".parse_next(i)?;
let fields = ScheduleFields::new(
Seconds::from_ordinal(0),
Minutes::from_ordinal(0),
Hours::from_ordinal(0),
DaysOfMonth::all(),
Months::all(),
DaysOfWeek::from_ordinal(1),
Years::all(),
);
Ok(fields)
}
fn shorthand_daily(i: &mut &str) -> winnow::Result<ScheduleFields> {
"@daily".parse_next(i)?;
let fields = ScheduleFields::new(
Seconds::from_ordinal(0),
Minutes::from_ordinal(0),
Hours::from_ordinal(0),
DaysOfMonth::all(),
Months::all(),
DaysOfWeek::all(),
Years::all(),
);
Ok(fields)
}
fn shorthand_hourly(i: &mut &str) -> winnow::Result<ScheduleFields> {
"@hourly".parse_next(i)?;
let fields = ScheduleFields::new(
Seconds::from_ordinal(0),
Minutes::from_ordinal(0),
Hours::all(),
DaysOfMonth::all(),
Months::all(),
DaysOfWeek::all(),
Years::all(),
);
Ok(fields)
}
fn shorthand(i: &mut &str) -> winnow::Result<ScheduleFields> {
let keywords = alt((
shorthand_yearly,
shorthand_monthly,
shorthand_weekly,
shorthand_daily,
shorthand_hourly,
));
delimited(multispace0, keywords, multispace0).parse_next(i)
}
fn longhand(i: &mut &str) -> winnow::Result<ScheduleFields> {
let seconds = field.try_map(Seconds::from_field);
let minutes = field.try_map(Minutes::from_field);
let hours = field.try_map(Hours::from_field);
let days_of_month = field_with_any.try_map(DaysOfMonth::from_field);
let months = field.try_map(Months::from_field);
let days_of_week = field_with_any.try_map(DaysOfWeek::from_field);
let years = opt(field.try_map(Years::from_field));
let fields = (
seconds,
minutes,
hours,
days_of_month,
months,
days_of_week,
years,
);
terminated(fields, eof)
.map(
|(seconds, minutes, hours, days_of_month, months, days_of_week, years)| {
let years = years.unwrap_or_else(Years::all);
ScheduleFields::new(
seconds,
minutes,
hours,
days_of_month,
months,
days_of_week,
years,
)
},
)
.parse_next(i)
}
fn schedule(i: &mut &str) -> winnow::Result<ScheduleFields> {
alt((shorthand, longhand)).parse_next(i)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_nom_valid_number() {
let expression = "1997";
point.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_point() {
let expression = "a";
assert!(point.parse(expression).is_err());
}
#[test]
fn test_nom_valid_named_point() {
let expression = "WED";
named_point.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_named_point() {
let expression = "8";
assert!(named_point.parse(expression).is_err());
}
#[test]
fn test_nom_valid_period() {
let expression = "1/2";
period.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_period() {
let expression = "Wed/4";
assert!(period.parse(expression).is_err());
}
#[test]
fn test_nom_valid_number_list() {
let expression = "1,2";
field.parse(expression).unwrap();
field_with_any.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_number_list() {
let expression = ",1,2";
assert!(field.parse(expression).is_err());
assert!(field_with_any.parse(expression).is_err());
}
#[test]
fn test_nom_field_with_any_valid_any() {
let expression = "?";
field_with_any.parse(expression).unwrap();
}
#[test]
fn test_nom_field_invalid_any() {
let expression = "?";
assert!(field.parse(expression).is_err());
}
#[test]
fn test_nom_valid_range_field() {
let expression = "1-4";
range.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_period_all() {
let expression = "*/2";
period.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_period_range() {
let expression = "10-20/2";
period.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_period_named_range() {
let expression = "Mon-Thurs/2";
period.parse(expression).unwrap();
let expression = "February-November/2";
period.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_period_point() {
let expression = "10/2";
period.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_period_any() {
let expression = "?/2";
assert!(period.parse(expression).is_err());
}
#[test]
fn test_nom_invalid_period_named_point() {
let expression = "Tues/2";
assert!(period.parse(expression).is_err());
let expression = "February/2";
assert!(period.parse(expression).is_err());
}
#[test]
fn test_nom_invalid_period_specifier_range() {
let expression = "10-12/*";
assert!(period.parse(expression).is_err());
}
#[test]
fn test_nom_valid_period_with_any_all() {
let expression = "*/2";
period_with_any.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_period_with_any_range() {
let expression = "10-20/2";
period_with_any.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_period_with_any_named_range() {
let expression = "Mon-Thurs/2";
period_with_any.parse(expression).unwrap();
let expression = "February-November/2";
period_with_any.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_period_with_any_point() {
let expression = "10/2";
period_with_any.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_period_with_any_any() {
let expression = "?/2";
period_with_any.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_period_with_any_named_point() {
let expression = "Tues/2";
assert!(period_with_any.parse(expression).is_err());
let expression = "February/2";
assert!(period_with_any.parse(expression).is_err());
}
#[test]
fn test_nom_invalid_period_with_any_specifier_range() {
let expression = "10-12/*";
assert!(period_with_any.parse(expression).is_err());
}
#[test]
fn test_nom_invalid_range_field() {
let expression = "-4";
assert!(range.parse(expression).is_err());
}
#[test]
fn test_nom_valid_named_range_field() {
let expression = "TUES-THURS";
named_range.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_named_range_field() {
let expression = "3-THURS";
assert!(named_range.parse(expression).is_err());
}
#[test]
fn test_nom_valid_schedule() {
let expression = "* * * * * *";
schedule.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_schedule() {
let expression = "* * * *";
assert!(schedule.parse(expression).is_err());
}
#[test]
fn test_nom_valid_seconds_list() {
let expression = "0,20,40 * * * * *";
schedule.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_seconds_range() {
let expression = "0-40 * * * * *";
schedule.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_seconds_mix() {
let expression = "0-5,58 * * * * *";
schedule.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_seconds_range() {
let expression = "0-65 * * * * *";
assert!(schedule.parse(expression).is_err());
}
#[test]
fn test_nom_invalid_seconds_list() {
let expression = "103,12 * * * * *";
assert!(schedule.parse(expression).is_err());
}
#[test]
fn test_nom_invalid_seconds_mix() {
let expression = "0-5,102 * * * * *";
assert!(schedule.parse(expression).is_err());
}
#[test]
fn test_nom_valid_days_of_week_list() {
let expression = "* * * * * MON,WED,FRI";
schedule.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_days_of_week_list() {
let expression = "* * * * * MON,TURTLE";
assert!(schedule.parse(expression).is_err());
}
#[test]
fn test_nom_valid_days_of_week_range() {
let expression = "* * * * * MON-FRI";
schedule.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_days_of_week_range() {
let expression = "* * * * * BEAR-OWL";
assert!(schedule.parse(expression).is_err());
}
#[test]
fn test_nom_invalid_period_with_range_specifier() {
let expression = "10-12/10-12 * * * * ?";
assert!(schedule.parse(expression).is_err());
}
#[test]
fn test_nom_valid_days_of_month_any() {
let expression = "* * * ? * *";
schedule.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_days_of_week_any() {
let expression = "* * * * * ?";
schedule.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_days_of_month_any_days_of_week_specific() {
let expression = "* * * ? * Mon,Thu";
schedule.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_days_of_week_any_days_of_month_specific() {
let expression = "* * * 1,2 * ?";
schedule.parse(expression).unwrap();
}
#[test]
fn test_nom_valid_dom_and_dow_any() {
let expression = "* * * ? * ?";
schedule.parse(expression).unwrap();
}
#[test]
fn test_nom_invalid_other_fields_any() {
let expression = "? * * * * *";
assert!(schedule.parse(expression).is_err());
let expression = "* ? * * * *";
assert!(schedule.parse(expression).is_err());
let expression = "* * ? * * *";
assert!(schedule.parse(expression).is_err());
let expression = "* * * * ? *";
assert!(schedule.parse(expression).is_err());
}
#[test]
fn test_nom_invalid_trailing_characters() {
let expression = "* * * * * *foo *";
assert!(schedule.parse(expression).is_err());
let expression = "* * * * * * * foo";
assert!(schedule.parse(expression).is_err());
}
#[test]
fn shorthand_must_match_whole_input() {
let expression = "@dailyBla";
assert!(schedule.parse(expression).is_err());
let expression = " @dailyBla ";
assert!(schedule.parse(expression).is_err());
}
#[test]
fn test_try_from_cow_str_owned() {
let expression = Cow::Owned(String::from("* * * ? * ?"));
Schedule::try_from(expression).unwrap();
}
#[test]
fn test_try_from_cow_str_borrowed() {
let expression = Cow::Borrowed("* * * ? * ?");
Schedule::try_from(expression).unwrap();
}
#[test]
fn test_try_from_string() {
let expression = String::from("* * * ? * ?");
Schedule::try_from(expression).unwrap();
}
#[test]
fn test_try_from_str() {
let expression = "* * * ? * ?";
Schedule::try_from(expression).unwrap();
}
#[test]
fn test_from_str() {
let expression = "* * * ? * ?";
Schedule::from_str(expression).unwrap();
}
#[test]
fn test_reject_invalid_interval() {
for invalid_expression in [
"1-5/61 * * * * *",
"*/61 2 3 4 5 6",
"* */61 * * * *",
"* * */25 * * *",
"* * * */32 * *",
"* * * * */13 *",
"1,2,3/60 * * * * *",
"0 0 0 1 1 ? 2020-2040/2200",
] {
assert!(schedule.parse(invalid_expression).is_err());
}
for valid_expression in [
"1-5/59 * * * * *",
"*/10 2 3 4 5 6",
"* */30 * * * *",
"* * */23 * * *",
"* * * */30 * *",
"* * * * */10 *",
"1,2,3/5 * * * * *",
"0 0 0 1 1 ? 2020-2040/10",
] {
assert!(schedule.parse(valid_expression).is_ok());
}
}
}