Skip to main content

libpetri_event/
event_store.rs

1use crate::net_event::NetEvent;
2
3/// Trait for event storage during Petri net execution.
4///
5/// Two const flags govern zero-cost opt-in behaviour. The compiler
6/// monomorphizes the executor against the concrete store type and eliminates
7/// any branch guarded by a const whose value is known at compile time.
8///
9/// - [`ENABLED`](Self::ENABLED) — when `false`, the executor skips every
10///   `append()` call (including argument construction) entirely. Used by
11///   `NoopEventStore` in production hot paths.
12/// - [`CAPTURES_TOKENS`](Self::CAPTURES_TOKENS) — when `true`, the executor
13///   attaches the live token payload to `TokenAdded` / `TokenRemoved` events
14///   via [`NetEvent::token_added_with`](crate::net_event::NetEvent::token_added_with).
15///   Opt-in because cloning the payload costs one `Arc` bump per token
16///   event and a heap allocation for the erased payload wrapper — acceptable
17///   under debug inspection, wasteful otherwise.
18pub trait EventStore: Default + Send {
19    const ENABLED: bool;
20
21    /// Whether this store wants token payloads attached to `TokenAdded` /
22    /// `TokenRemoved` events. Defaults to `false` so existing implementations
23    /// opt in explicitly (debug-aware stores in `libpetri-debug`).
24    const CAPTURES_TOKENS: bool = false;
25
26    fn append(&mut self, event: NetEvent);
27    fn events(&self) -> &[NetEvent];
28    fn size(&self) -> usize;
29    fn is_empty(&self) -> bool;
30}
31
32/// No-op event store for production use.
33///
34/// All methods are no-ops; the compiler eliminates recording branches
35/// via the `ENABLED = false` constant.
36#[derive(Debug, Default)]
37pub struct NoopEventStore;
38
39impl EventStore for NoopEventStore {
40    const ENABLED: bool = false;
41
42    #[inline(always)]
43    fn append(&mut self, _event: NetEvent) {}
44
45    #[inline(always)]
46    fn events(&self) -> &[NetEvent] {
47        &[]
48    }
49
50    #[inline(always)]
51    fn size(&self) -> usize {
52        0
53    }
54
55    #[inline(always)]
56    fn is_empty(&self) -> bool {
57        true
58    }
59}
60
61/// In-memory event store for debugging and testing.
62#[derive(Debug, Default)]
63pub struct InMemoryEventStore {
64    events: Vec<NetEvent>,
65}
66
67impl InMemoryEventStore {
68    pub fn new() -> Self {
69        Self { events: Vec::new() }
70    }
71
72    /// Filter events by predicate.
73    pub fn filter(&self, predicate: impl Fn(&NetEvent) -> bool) -> Vec<&NetEvent> {
74        self.events.iter().filter(|e| predicate(e)).collect()
75    }
76
77    /// Get events for a specific transition.
78    pub fn transition_events(&self, name: &str) -> Vec<&NetEvent> {
79        self.filter(|e| e.transition_name() == Some(name))
80    }
81
82    /// Get all failure events.
83    pub fn failures(&self) -> Vec<&NetEvent> {
84        self.filter(|e| e.is_failure())
85    }
86}
87
88impl EventStore for InMemoryEventStore {
89    const ENABLED: bool = true;
90
91    fn append(&mut self, event: NetEvent) {
92        self.events.push(event);
93    }
94
95    fn events(&self) -> &[NetEvent] {
96        &self.events
97    }
98
99    fn size(&self) -> usize {
100        self.events.len()
101    }
102
103    fn is_empty(&self) -> bool {
104        self.events.is_empty()
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use std::sync::Arc;
112
113    #[test]
114    fn noop_store_is_always_empty() {
115        let mut store = NoopEventStore;
116        store.append(NetEvent::ExecutionStarted {
117            net_name: Arc::from("test"),
118            timestamp: 0,
119        });
120        assert!(store.is_empty());
121        assert_eq!(store.size(), 0);
122        assert!(store.events().is_empty());
123    }
124
125    #[test]
126    fn in_memory_store_records_events() {
127        let mut store = InMemoryEventStore::new();
128        store.append(NetEvent::ExecutionStarted {
129            net_name: Arc::from("test"),
130            timestamp: 0,
131        });
132        store.append(NetEvent::TransitionStarted {
133            transition_name: Arc::from("t1"),
134            timestamp: 1,
135        });
136        assert_eq!(store.size(), 2);
137        assert!(!store.is_empty());
138    }
139
140    #[test]
141    fn in_memory_store_transition_events() {
142        let mut store = InMemoryEventStore::new();
143        store.append(NetEvent::TransitionStarted {
144            transition_name: Arc::from("t1"),
145            timestamp: 1,
146        });
147        store.append(NetEvent::TransitionCompleted {
148            transition_name: Arc::from("t1"),
149            timestamp: 2,
150        });
151        store.append(NetEvent::TransitionStarted {
152            transition_name: Arc::from("t2"),
153            timestamp: 3,
154        });
155        assert_eq!(store.transition_events("t1").len(), 2);
156        assert_eq!(store.transition_events("t2").len(), 1);
157    }
158
159    #[test]
160    fn enabled_constants() {
161        const { assert!(!NoopEventStore::ENABLED) };
162        const { assert!(InMemoryEventStore::ENABLED) };
163    }
164
165    #[test]
166    fn capture_tokens_defaults_off() {
167        // NoopEventStore opts out of token capture (monomorphization elides the
168        // Arc::new payload on hot paths). InMemoryEventStore is a testing store and
169        // inherits the default — debug-aware stores override CAPTURES_TOKENS = true.
170        const { assert!(!NoopEventStore::CAPTURES_TOKENS) };
171        const { assert!(!InMemoryEventStore::CAPTURES_TOKENS) };
172    }
173}