template_model/
schedule.rs1#[must_use]
11pub fn schedule_expression_errors(expression: &str) -> Vec<String> {
12 if let Some(inner) = wrapped_body(expression, "rate(") {
13 check_rate(inner)
14 } else if let Some(inner) = wrapped_body(expression, "cron(") {
15 check_cron(inner)
16 } else {
17 vec![format!("'{expression}' has to be either 'cron()' or 'rate()'")]
18 }
19}
20
21fn wrapped_body<'a>(expression: &'a str, prefix: &str) -> Option<&'a str> {
23 expression.strip_prefix(prefix).and_then(|rest| rest.strip_suffix(')'))
24}
25
26fn check_rate(body: &str) -> Vec<String> {
27 let mut errors = Vec::new();
28 if body.is_empty() {
29 errors.push("'' is not of type 'string'".to_string());
30 return errors;
31 }
32 let items: Vec<&str> = body.split(' ').collect();
33 if items.len() != 2 {
34 errors.push(format!("'{body}' has to be of format rate(Value Unit)"));
35 return errors;
36 }
37 let (value, unit) = (items[0], items[1]);
38 if !is_ascii_digits(value) {
39 errors.push(format!("'{value}' is not of type 'integer'"));
40 return errors;
41 }
42 let amount: u64 = value.parse().unwrap_or(0);
43 if amount == 0 {
44 errors.push(format!("'{value}' is less than the minimum of 0"));
45 }
46 let valid_units: [&str; 3] = if amount <= 1 { ["minute", "hour", "day"] } else { ["minutes", "hours", "days"] };
47 if !valid_units.contains(&unit) {
48 errors.push(format!("'{unit}' is not one of {valid_units:?}"));
49 }
50 errors
51}
52
53fn check_cron(body: &str) -> Vec<String> {
54 let mut errors = Vec::new();
55 if body.is_empty() {
56 errors.push("'' is not of type 'string'".to_string());
57 return errors;
58 }
59 let fields: Vec<&str> = body.split(' ').collect();
60 if fields.len() != 6 {
61 errors.push(format!("'{}' is not of length 6. (Minutes Hours Day-of-month Month Day-of-week Year)", fields[0]));
62 return errors;
63 }
64 let day_of_month = fields[2];
65 let day_of_week = fields[4];
66 if day_of_month != "?" && day_of_week != "?" {
67 errors.push(format!(
68 "'{}' specifies both Day-of-month and Day-of-week. (Minutes Hours Day-of-month Month Day-of-week Year)",
69 body.chars().next().unwrap_or_default()
70 ));
71 }
72 errors
73}
74
75fn is_ascii_digits(value: &str) -> bool {
78 !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit())
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn valid_rate_and_cron_have_no_errors() {
87 assert!(schedule_expression_errors("rate(1 minute)").is_empty());
88 assert!(schedule_expression_errors("rate(5 minutes)").is_empty());
89 assert!(schedule_expression_errors("cron(0 12 * * ? *)").is_empty());
90 assert!(schedule_expression_errors("cron(0 12 ? * MON *)").is_empty());
91 }
92
93 #[test]
94 fn rate_unit_must_agree_in_number_with_value() {
95 assert_eq!(
96 schedule_expression_errors("rate(1 minutes)"),
97 vec!["'minutes' is not one of [\"minute\", \"hour\", \"day\"]".to_string()]
98 );
99 assert_eq!(
100 schedule_expression_errors("rate(5 minute)"),
101 vec!["'minute' is not one of [\"minutes\", \"hours\", \"days\"]".to_string()]
102 );
103 }
104
105 #[test]
106 fn rate_value_must_be_positive_integer() {
107 assert_eq!(schedule_expression_errors("rate(1.5 hours)"), vec!["'1.5' is not of type 'integer'".to_string()]);
108 assert_eq!(
109 schedule_expression_errors("rate(0 minutes)"),
110 vec![
111 "'0' is less than the minimum of 0".to_string(),
112 "'minutes' is not one of [\"minute\", \"hour\", \"day\"]".to_string(),
113 ]
114 );
115 }
116
117 #[test]
118 fn rate_must_have_two_space_separated_items() {
119 assert_eq!(
120 schedule_expression_errors("rate(10 minutes)"),
121 vec!["'10 minutes' has to be of format rate(Value Unit)".to_string()]
122 );
123 assert_eq!(schedule_expression_errors("rate(5)"), vec!["'5' has to be of format rate(Value Unit)".to_string()]);
124 }
125
126 #[test]
127 fn empty_rate_reports_missing_string() {
128 assert_eq!(schedule_expression_errors("rate()"), vec!["'' is not of type 'string'".to_string()]);
129 }
130
131 #[test]
132 fn cron_must_have_six_fields() {
133 assert_eq!(
134 schedule_expression_errors("cron(bad)"),
135 vec!["'bad' is not of length 6. (Minutes Hours Day-of-month Month Day-of-week Year)".to_string()]
136 );
137 }
138
139 #[test]
140 fn cron_cannot_pin_both_day_of_month_and_day_of_week() {
141 assert_eq!(
142 schedule_expression_errors("cron(0 12 * * * *)"),
143 vec![
144 "'0' specifies both Day-of-month and Day-of-week. (Minutes Hours Day-of-month Month Day-of-week Year)"
145 .to_string()
146 ]
147 );
148 }
149
150 #[test]
151 fn non_rate_non_cron_is_rejected() {
152 assert_eq!(
153 schedule_expression_errors("every minute"),
154 vec!["'every minute' has to be either 'cron()' or 'rate()'".to_string()]
155 );
156 }
157}