Skip to main content

wasm4pm_compat/
eventlog.rs

1//! OCEL-compatible event log types for wasm4pm-compat.
2//!
3//! This module is distinct from [`crate::event_log`] (XES-shaped).
4//! Use these types when building object-centric process evidence.
5
6use std::fmt;
7
8/// A single event in a case trace.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Event {
11    activity: String,
12    timestamp_ns: u64,
13    resource: Option<String>,
14    lifecycle: Option<String>,
15}
16
17impl Event {
18    pub fn new(activity: &str) -> Self {
19        Event {
20            activity: activity.to_owned(),
21            timestamp_ns: 0,
22            resource: None,
23            lifecycle: None,
24        }
25    }
26
27    #[must_use]
28    pub fn at_ns(mut self, ns: u64) -> Self {
29        self.timestamp_ns = ns;
30        self
31    }
32
33    #[must_use]
34    pub fn by(mut self, resource: &str) -> Self {
35        self.resource = Some(resource.to_owned());
36        self
37    }
38
39    #[must_use]
40    pub fn with_lifecycle(mut self, lc: &str) -> Self {
41        self.lifecycle = Some(lc.to_owned());
42        self
43    }
44
45    pub fn activity(&self) -> &str {
46        &self.activity
47    }
48    pub fn timestamp_ns(&self) -> Option<u64> {
49        if self.timestamp_ns == 0 {
50            None
51        } else {
52            Some(self.timestamp_ns)
53        }
54    }
55    pub fn resource(&self) -> Option<&str> {
56        self.resource.as_deref()
57    }
58    pub fn lifecycle(&self) -> Option<&str> {
59        self.lifecycle.as_deref()
60    }
61}
62
63/// An ordered sequence of events belonging to one case.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Trace {
66    case_id: String,
67    events: Vec<Event>,
68}
69
70impl Trace {
71    pub fn new(case_id: &str, events: impl IntoIterator<Item = Event>) -> Self {
72        Trace {
73            case_id: case_id.to_owned(),
74            events: events.into_iter().collect(),
75        }
76    }
77
78    pub fn from_events(events: impl IntoIterator<Item = Event>) -> Self {
79        Trace {
80            case_id: "_".to_owned(),
81            events: events.into_iter().collect(),
82        }
83    }
84
85    pub fn case_id(&self) -> &str {
86        &self.case_id
87    }
88    pub fn len(&self) -> usize {
89        self.events.len()
90    }
91    pub fn is_empty(&self) -> bool {
92        self.events.is_empty()
93    }
94    pub fn events(&self) -> &[Event] {
95        &self.events
96    }
97
98    pub fn validate(&self) -> Result<(), EventLogRefusal> {
99        if self.events.is_empty() {
100            return Err(EventLogRefusal::EmptyTrace);
101        }
102        let stamped: Vec<u64> = self
103            .events
104            .iter()
105            .map(|e| e.timestamp_ns)
106            .filter(|&t| t > 0)
107            .collect();
108        for w in stamped.windows(2) {
109            if w[1] < w[0] {
110                return Err(EventLogRefusal::NonMonotonicTrace);
111            }
112        }
113        Ok(())
114    }
115}
116
117/// A collection of traces forming a process event log.
118#[derive(Debug, Clone, PartialEq, Eq, Default)]
119pub struct EventLog {
120    traces: Vec<Trace>,
121}
122
123impl EventLog {
124    pub fn from_traces(traces: impl IntoIterator<Item = Trace>) -> Self {
125        EventLog {
126            traces: traces.into_iter().collect(),
127        }
128    }
129
130    pub fn traces(&self) -> &[Trace] {
131        &self.traces
132    }
133
134    pub fn trace_count(&self) -> usize {
135        self.traces.len()
136    }
137
138    pub fn event_count(&self) -> usize {
139        self.traces.iter().map(|t| t.len()).sum()
140    }
141
142    pub fn validate(&self) -> Result<(), EventLogRefusal> {
143        for trace in &self.traces {
144            trace.validate()?;
145        }
146        Ok(())
147    }
148}
149
150/// A streaming accumulator of events (append-only, in-memory).
151#[derive(Debug, Clone, Default)]
152pub struct EventStream {
153    events: Vec<Event>,
154}
155
156impl EventStream {
157    pub fn new() -> Self {
158        EventStream::default()
159    }
160    pub fn push(&mut self, e: Event) {
161        self.events.push(e);
162    }
163    pub fn is_empty(&self) -> bool {
164        self.events.is_empty()
165    }
166    pub fn len(&self) -> usize {
167        self.events.len()
168    }
169}
170
171/// Named refusal variants for event-log validation laws.
172///
173/// Every variant names a specific law from van der Aalst's process mining
174/// theory. Error messages emit the variant name verbatim (tests use `.contains()`).
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum EventLogRefusal {
177    /// A trace contains no events — violates the log completeness law.
178    EmptyTrace,
179    /// Events with explicit timestamps are not in non-decreasing order.
180    NonMonotonicTrace,
181    MissingCaseId,
182    MissingActivity,
183    MissingTimestamp,
184    DuplicateEvent,
185    InvalidLifecycle,
186}
187
188impl fmt::Display for EventLogRefusal {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        match self {
191            EventLogRefusal::EmptyTrace => write!(f, "EmptyTrace"),
192            EventLogRefusal::NonMonotonicTrace => write!(f, "NonMonotonicTrace"),
193            EventLogRefusal::MissingCaseId => write!(f, "MissingCaseId"),
194            EventLogRefusal::MissingActivity => write!(f, "MissingActivity"),
195            EventLogRefusal::MissingTimestamp => write!(f, "MissingTimestamp"),
196            EventLogRefusal::DuplicateEvent => write!(f, "DuplicateEvent"),
197            EventLogRefusal::InvalidLifecycle => write!(f, "InvalidLifecycle"),
198        }
199    }
200}
201
202impl std::error::Error for EventLogRefusal {}