1use std::time::Duration;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum UnsupportedCombinator {
8 OneOrMore,
9 ZeroOrMore,
10 NotFollowedBy,
11 Branching,
12 ExactCount,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
17pub enum CepCompileError {
18 #[error("unsupported CEP combinator {0:?} — deferred to R17/R18")]
19 UnsupportedCombinator(UnsupportedCombinator),
20 #[error("CEP pattern must have at least one stage")]
21 EmptyPattern,
22}
23
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
26pub struct PatternStage {
27 pub name: String,
28 pub max_gap_ms: Option<u64>,
29}
30
31#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
33pub struct CompiledPattern {
34 pub stages: Vec<PatternStage>,
35 pub window_ms: u64,
36}
37
38#[derive(Debug, Default)]
40pub struct Pattern {
41 stages: Vec<PatternStage>,
42 window_ms: Option<u64>,
43}
44
45impl Pattern {
46 pub fn begin(name: impl Into<String>) -> Self {
47 let mut p = Self::default();
48 p.stages.push(PatternStage {
49 name: name.into(),
50 max_gap_ms: None,
51 });
52 p
53 }
54
55 pub fn followed_by(mut self, name: impl Into<String>) -> Self {
56 self.stages.push(PatternStage {
57 name: name.into(),
58 max_gap_ms: None,
59 });
60 self
61 }
62
63 pub fn within(mut self, duration: Duration) -> Self {
64 self.window_ms = Some(duration.as_millis() as u64);
65 self
66 }
67
68 pub fn times(self, _n: u32) -> Result<Self, CepCompileError> {
69 Err(CepCompileError::UnsupportedCombinator(
70 UnsupportedCombinator::ExactCount,
71 ))
72 }
73
74 pub fn compile(self) -> Result<CompiledPattern, CepCompileError> {
75 if self.stages.is_empty() {
76 return Err(CepCompileError::EmptyPattern);
77 }
78 Ok(CompiledPattern {
79 stages: self.stages,
80 window_ms: self.window_ms.unwrap_or(60_000),
81 })
82 }
83
84 pub fn one_or_more(self) -> Result<Self, CepCompileError> {
85 Err(CepCompileError::UnsupportedCombinator(
86 UnsupportedCombinator::OneOrMore,
87 ))
88 }
89
90 pub fn not_followed_by(self) -> Result<Self, CepCompileError> {
91 Err(CepCompileError::UnsupportedCombinator(
92 UnsupportedCombinator::NotFollowedBy,
93 ))
94 }
95
96 pub fn zero_or_more(self) -> Result<Self, CepCompileError> {
97 Err(CepCompileError::UnsupportedCombinator(
98 UnsupportedCombinator::ZeroOrMore,
99 ))
100 }
101
102 pub fn branching(self) -> Result<Self, CepCompileError> {
103 Err(CepCompileError::UnsupportedCombinator(
104 UnsupportedCombinator::Branching,
105 ))
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn linear_pattern_compiles() {
115 let p = Pattern::begin("start")
116 .followed_by("next")
117 .within(Duration::from_secs(10))
118 .compile()
119 .unwrap();
120 assert_eq!(p.stages.len(), 2);
121 assert_eq!(p.window_ms, 10_000);
122 }
123
124 #[test]
125 fn quantifier_returns_unsupported() {
126 let err = Pattern::begin("a").one_or_more().unwrap_err();
127 assert!(matches!(
128 err,
129 CepCompileError::UnsupportedCombinator(UnsupportedCombinator::OneOrMore)
130 ));
131 }
132
133 #[test]
134 fn empty_pattern_rejected() {
135 let p = Pattern::default();
136 let err = p.compile().unwrap_err();
137 assert!(matches!(err, CepCompileError::EmptyPattern));
138 }
139
140 #[test]
141 fn times_returns_unsupported() {
142 let err = Pattern::begin("a").times(3).unwrap_err();
143 assert!(matches!(
144 err,
145 CepCompileError::UnsupportedCombinator(UnsupportedCombinator::ExactCount)
146 ));
147 }
148
149 #[test]
150 fn not_followed_by_returns_unsupported() {
151 let err = Pattern::begin("a").not_followed_by().unwrap_err();
152 assert!(matches!(
153 err,
154 CepCompileError::UnsupportedCombinator(UnsupportedCombinator::NotFollowedBy)
155 ));
156 }
157
158 #[test]
159 fn zero_or_more_returns_unsupported() {
160 let err = Pattern::begin("a").zero_or_more().unwrap_err();
161 assert!(matches!(
162 err,
163 CepCompileError::UnsupportedCombinator(UnsupportedCombinator::ZeroOrMore)
164 ));
165 }
166
167 #[test]
168 fn branching_returns_unsupported() {
169 let err = Pattern::begin("a").branching().unwrap_err();
170 assert!(matches!(
171 err,
172 CepCompileError::UnsupportedCombinator(UnsupportedCombinator::Branching)
173 ));
174 }
175
176 #[test]
177 fn display_empty_pattern() {
178 let err = CepCompileError::EmptyPattern;
179 let msg = format!("{err}");
180 assert!(msg.contains("at least one stage"));
181 }
182
183 #[test]
184 fn display_unsupported_combinator() {
185 let err = CepCompileError::UnsupportedCombinator(UnsupportedCombinator::OneOrMore);
186 let msg = format!("{err}");
187 assert!(msg.contains("OneOrMore"));
188 }
189
190 #[test]
191 fn single_stage_default_window() {
192 let p = Pattern::begin("only").compile().unwrap();
193 assert_eq!(p.window_ms, 60_000);
194 }
195
196 #[test]
197 fn three_stage_pattern() {
198 let p = Pattern::begin("a")
199 .followed_by("b")
200 .followed_by("c")
201 .within(Duration::from_secs(30))
202 .compile()
203 .unwrap();
204 assert_eq!(p.stages.len(), 3);
205 assert_eq!(p.window_ms, 30_000);
206 assert_eq!(p.stages[0].name, "a");
207 assert_eq!(p.stages[1].name, "b");
208 assert_eq!(p.stages[2].name, "c");
209 }
210
211 #[test]
212 fn stage_names_preserved() {
213 let p = Pattern::begin("login")
214 .followed_by("query")
215 .followed_by("logout")
216 .compile()
217 .unwrap();
218 let names: Vec<&str> = p.stages.iter().map(|s| s.name.as_str()).collect();
219 assert_eq!(names, vec!["login", "query", "logout"]);
220 }
221
222 #[test]
225 fn error_trait_implemented() {
226 let err: Box<dyn std::error::Error> = Box::new(CepCompileError::UnsupportedCombinator(
227 UnsupportedCombinator::Branching,
228 ));
229 assert!(!err.to_string().is_empty());
230 }
231
232 #[test]
233 fn all_unsupported_combinator_variants_display() {
234 let variants = [
235 UnsupportedCombinator::OneOrMore,
236 UnsupportedCombinator::ZeroOrMore,
237 UnsupportedCombinator::NotFollowedBy,
238 UnsupportedCombinator::Branching,
239 UnsupportedCombinator::ExactCount,
240 ];
241 for v in &variants {
242 let err = CepCompileError::UnsupportedCombinator(v.clone());
243 let msg = format!("{err}");
244 assert!(!msg.is_empty());
245 }
246 }
247
248 #[test]
249 fn unsupported_combinator_debug() {
250 let c = UnsupportedCombinator::OneOrMore;
251 let debug = format!("{:?}", c);
252 assert!(debug.contains("OneOrMore"));
253 }
254
255 #[test]
256 fn unsupported_combinator_eq() {
257 assert_eq!(
258 UnsupportedCombinator::OneOrMore,
259 UnsupportedCombinator::OneOrMore
260 );
261 assert_ne!(
262 UnsupportedCombinator::OneOrMore,
263 UnsupportedCombinator::ZeroOrMore
264 );
265 }
266
267 #[test]
268 fn pattern_default_creates_empty() {
269 let p = Pattern::default();
270 assert!(p.stages.is_empty());
271 assert!(p.window_ms.is_none());
272 }
273
274 #[test]
275 fn pattern_builder_into_string() {
276 let p = Pattern::begin(String::from("dynamic_name"))
277 .compile()
278 .unwrap();
279 assert_eq!(p.stages[0].name, "dynamic_name");
280 }
281
282 #[test]
283 fn followed_by_chain_builds_correctly() {
284 let p = Pattern::begin("a")
285 .followed_by("b")
286 .followed_by("c")
287 .followed_by("d")
288 .compile()
289 .unwrap();
290 assert_eq!(p.stages.len(), 4);
291 assert_eq!(p.stages[3].name, "d");
292 }
293
294 #[test]
295 fn within_sets_window_ms() {
296 let p = Pattern::begin("a")
297 .within(Duration::from_millis(42))
298 .compile()
299 .unwrap();
300 assert_eq!(p.window_ms, 42);
301 }
302
303 #[test]
304 fn within_max_duration() {
305 let p = Pattern::begin("a")
306 .within(Duration::from_millis(u64::MAX))
307 .compile()
308 .unwrap();
309 assert_eq!(p.window_ms, u64::MAX);
310 }
311
312 #[test]
313 fn within_zero_duration() {
314 let p = Pattern::begin("a")
315 .within(Duration::from_millis(0))
316 .compile()
317 .unwrap();
318 assert_eq!(p.window_ms, 0);
319 }
320
321 #[test]
322 fn compiled_pattern_clone() {
323 let p = Pattern::begin("x")
324 .followed_by("y")
325 .within(Duration::from_secs(5))
326 .compile()
327 .unwrap();
328 let c = p.clone();
329 assert_eq!(c.stages.len(), 2);
330 assert_eq!(c.window_ms, 5000);
331 }
332
333 #[test]
334 fn pattern_stage_clone() {
335 let stage = PatternStage {
336 name: "test".to_string(),
337 max_gap_ms: Some(1000),
338 };
339 let cloned = stage.clone();
340 assert_eq!(cloned.name, "test");
341 assert_eq!(cloned.max_gap_ms, Some(1000));
342 }
343
344 #[test]
345 fn empty_string_stage_name() {
346 let p = Pattern::begin("").compile().unwrap();
347 assert_eq!(p.stages[0].name, "");
348 }
349
350 #[test]
351 fn single_character_stage_name() {
352 let p = Pattern::begin("x").compile().unwrap();
353 assert_eq!(p.stages[0].name, "x");
354 }
355
356 #[test]
357 fn long_stage_name() {
358 let name = "a".repeat(1000);
359 let p = Pattern::begin(name.clone()).compile().unwrap();
360 assert_eq!(p.stages[0].name, name);
361 }
362
363 #[test]
364 fn compile_returns_ok_for_single_stage() {
365 assert!(Pattern::begin("only").compile().is_ok());
366 }
367
368 #[test]
369 fn compile_returns_ok_for_multi_stage() {
370 assert!(
371 Pattern::begin("a")
372 .followed_by("b")
373 .followed_by("c")
374 .compile()
375 .is_ok()
376 );
377 }
378}