Skip to main content

krishiv_plan/cep/
pattern.rs

1//! CEP pattern builder (R16 S2.1).
2
3use std::time::Duration;
4
5/// Unsupported pattern combinator (quantifiers, negation).
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum UnsupportedCombinator {
8    OneOrMore,
9    ZeroOrMore,
10    NotFollowedBy,
11    Branching,
12    ExactCount,
13}
14
15/// Pattern compilation error.
16#[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/// One stage in a linear CEP pattern.
25#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
26pub struct PatternStage {
27    pub name: String,
28    pub max_gap_ms: Option<u64>,
29}
30
31/// Compiled linear pattern.
32#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
33pub struct CompiledPattern {
34    pub stages: Vec<PatternStage>,
35    pub window_ms: u64,
36}
37
38/// Fluent pattern builder.
39#[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    // ── Additional deep-coverage tests ─────────────────────────────────
223
224    #[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}