ironflow_engine/
schedule.rs1use std::fmt;
9use std::str::FromStr;
10
11use croner::Cron;
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone)]
34pub struct CronSchedule {
35 inner: Cron,
36 raw: String,
37}
38
39impl CronSchedule {
40 pub fn new(expression: &str) -> Result<Self, String> {
58 let inner = Cron::from_str(expression)
59 .map_err(|e| format!("invalid cron expression '{expression}': {e}"))?;
60 Ok(Self {
61 inner,
62 raw: expression.to_string(),
63 })
64 }
65
66 pub fn as_str(&self) -> &str {
68 &self.raw
69 }
70}
71
72impl fmt::Display for CronSchedule {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.write_str(&self.raw)
75 }
76}
77
78impl PartialEq for CronSchedule {
79 fn eq(&self, other: &Self) -> bool {
80 self.inner == other.inner
81 }
82}
83
84impl Eq for CronSchedule {}
85
86impl Serialize for CronSchedule {
87 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
88 where
89 S: serde::Serializer,
90 {
91 serializer.serialize_str(&self.raw)
92 }
93}
94
95impl<'de> Deserialize<'de> for CronSchedule {
96 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
97 where
98 D: serde::Deserializer<'de>,
99 {
100 let s = String::deserialize(deserializer)?;
101 Self::new(&s).map_err(serde::de::Error::custom)
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn valid_six_field_expression() {
111 let sched = CronSchedule::new("0 0 * * * *").unwrap();
112 assert_eq!(sched.as_str(), "0 0 * * * *");
113 }
114
115 #[test]
116 fn valid_five_field_expression() {
117 let sched = CronSchedule::new("*/5 * * * *").unwrap();
118 assert_eq!(sched.as_str(), "*/5 * * * *");
119 }
120
121 #[test]
122 fn valid_complex_expression() {
123 let sched = CronSchedule::new("0 30 9 * * MON-FRI").unwrap();
124 assert_eq!(sched.as_str(), "0 30 9 * * MON-FRI");
125 }
126
127 #[test]
128 fn invalid_expression_returns_error() {
129 let result = CronSchedule::new("not a cron");
130 assert!(result.is_err());
131 let err = result.unwrap_err();
132 assert!(err.contains("invalid cron expression"));
133 }
134
135 #[test]
136 fn empty_expression_returns_error() {
137 assert!(CronSchedule::new("").is_err());
138 }
139
140 #[test]
141 fn display_shows_original_expression() {
142 let sched = CronSchedule::new("0 0 12 * * *").unwrap();
143 assert_eq!(format!("{sched}"), "0 0 12 * * *");
144 }
145
146 #[test]
147 fn semantic_equality() {
148 let a = CronSchedule::new("0 0 * * * MON-FRI").unwrap();
149 let b = CronSchedule::new("0 0 * * * 1-5").unwrap();
150 assert_eq!(a, b);
151 }
152
153 #[test]
154 fn inequality_on_different_expressions() {
155 let a = CronSchedule::new("0 0 * * * *").unwrap();
156 let b = CronSchedule::new("0 30 * * * *").unwrap();
157 assert_ne!(a, b);
158 }
159
160 #[test]
161 fn serde_roundtrip() {
162 let sched = CronSchedule::new("0 */5 * * * *").unwrap();
163 let json = serde_json::to_string(&sched).unwrap();
164 assert_eq!(json, "\"0 */5 * * * *\"");
165 let back: CronSchedule = serde_json::from_str(&json).unwrap();
166 assert_eq!(back, sched);
167 }
168
169 #[test]
170 fn deserialize_invalid_expression_fails() {
171 let result: Result<CronSchedule, _> = serde_json::from_str("\"garbage\"");
172 assert!(result.is_err());
173 }
174}