Skip to main content

wasm4pm_compat/
ocel.rs

1
2
3pub mod flatten;
4pub mod intake;
5pub mod validate;
6
7use chrono::{DateTime, FixedOffset};
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, BTreeSet};
10use std::fmt::Display;
11
12#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
13pub struct OCEL {
14    #[serde(rename = "eventTypes")]
15    pub event_types: Vec<OCELType>,
16    #[serde(rename = "objectTypes")]
17    pub object_types: Vec<OCELType>,
18    #[serde(default)]
19    pub events: Vec<OCELEvent>,
20    #[serde(default)]
21    pub objects: Vec<OCELObject>,
22}
23
24#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
25pub struct OCELType {
26    pub name: String,
27    #[serde(default)]
28    pub attributes: Vec<OCELTypeAttribute>,
29}
30
31#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
32pub struct OCELTypeAttribute {
33    pub name: String,
34    #[serde(rename = "type")]
35    pub value_type: String,
36}
37
38#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
39pub struct OCELEventAttribute {
40    pub name: String,
41    pub value: OCELAttributeValue,
42}
43
44#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
45pub struct OCELEvent {
46    pub id: String,
47    #[serde(rename = "type")]
48    pub event_type: String,
49    pub time: DateTime<FixedOffset>,
50    #[serde(default)]
51    pub attributes: Vec<OCELEventAttribute>,
52    #[serde(default)]
53    pub relationships: Vec<OCELRelationship>,
54}
55
56#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
57pub struct OCELRelationship {
58    #[serde(rename = "objectId")]
59    pub object_id: String,
60    pub qualifier: String,
61}
62
63#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
64pub struct OCELObject {
65    pub id: String,
66    #[serde(rename = "type")]
67    pub object_type: String,
68    #[serde(default)]
69    pub attributes: Vec<OCELObjectAttribute>,
70    #[serde(default)]
71    pub relationships: Vec<OCELRelationship>,
72}
73
74#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
75pub struct OCELObjectAttribute {
76    pub name: String,
77    pub value: OCELAttributeValue,
78    pub time: DateTime<FixedOffset>,
79}
80
81#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
82#[serde(untagged)]
83pub enum OCELAttributeValue {
84    Integer(i64),
85    Float(f64),
86    Boolean(bool),
87    Time(DateTime<FixedOffset>),
88    String(String),
89    #[default]
90    Null,
91}
92
93impl Display for OCELAttributeValue {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        let s = match self {
96            OCELAttributeValue::Time(dt) => dt.to_rfc3339(),
97            OCELAttributeValue::Integer(i) => i.to_string(),
98            OCELAttributeValue::Float(f) => f.to_string(),
99            OCELAttributeValue::Boolean(b) => b.to_string(),
100            OCELAttributeValue::String(s) => s.clone(),
101            OCELAttributeValue::Null => String::default(),
102        };
103        write!(f, "{s}")
104    }
105}
106
107/// Cardinality constraint on an object type, mirroring the route `object_types`
108/// schema (`created_by[]`, `terminated_by[]`, `schema`, `min_count`, `max_count`).
109///
110/// In OCEL-v2 / OCEDO terms this is a *meta-model* constraint: it bounds how many
111/// instances of a given object type a lawful log (or route case) may carry, and
112/// records which event types create/terminate the object's lifecycle.
113#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
114pub struct ObjectTypeCardinality {
115    /// Event types that create an instance of this object type (lifecycle open).
116    #[serde(default, skip_serializing_if = "Vec::is_empty")]
117    pub created_by: Vec<String>,
118    /// Event types that terminate an instance of this object type (lifecycle close).
119    #[serde(default, skip_serializing_if = "Vec::is_empty")]
120    pub terminated_by: Vec<String>,
121    /// Optional path to a JSON Schema validating this object type's payload.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub schema: Option<String>,
124    /// Minimum number of instances required (inclusive). `None` = unbounded below.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub min_count: Option<usize>,
127    /// Maximum number of instances permitted (inclusive). `None` = unbounded above.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub max_count: Option<usize>,
130}
131
132impl ObjectTypeCardinality {
133    /// True if `count` satisfies the `[min_count, max_count]` window.
134    #[must_use]
135    pub fn admits(&self, count: usize) -> bool {
136        let above_min = self.min_count.is_none_or(|m| count >= m);
137        let below_max = self.max_count.is_none_or(|m| count <= m);
138        above_min && below_max
139    }
140}
141
142impl OCEL {
143    // --- OCEDO formal layer:  L = (E, O, eval, oaval) ---------------------
144    //
145    // Paper grounding (Latif et al., "Object-Centric Analysis of XES Event Logs",
146    // OCEDO meta-model, Fig. 1): an event has exactly one `time`, one event-type,
147    // 1..* event-attribute-values, and a qualified `*` reference to objects.
148    // An object has one object-type, 1..* object-attribute-values, and qualified
149    // from/to object-relations. OCPQ Def. 2 adds: every event has >=1 qualified
150    // object ref; objects carry qualified O2O refs; type/objects are time-stable;
151    // attribute values (oaval) vary per timestamp.
152
153    /// `E` — the set of events.
154    #[must_use]
155    pub fn event_set(&self) -> &[OCELEvent] {
156        &self.events
157    }
158
159    /// `O` — the set of objects.
160    #[must_use]
161    pub fn object_set(&self) -> &[OCELObject] {
162        &self.objects
163    }
164
165    /// `eval(e)` — the event-attribute-value map for event `e` (name → value).
166    /// Returns `None` if the event id is unknown.
167    #[must_use]
168    pub fn eval(&self, event_id: &str) -> Option<BTreeMap<&str, &OCELAttributeValue>> {
169        let e = self.events.iter().find(|e| e.id == event_id)?;
170        Some(
171            e.attributes
172                .iter()
173                .map(|a| (a.name.as_str(), &a.value))
174                .collect(),
175        )
176    }
177
178    /// `oaval(o, t)` — object-attribute-value map for object `o` *as of* time `t`.
179    ///
180    /// Time-varying semantics: for each attribute name, returns the latest value
181    /// whose stamp is `<= t`. Attributes first set after `t` are absent. This is
182    /// the temporal projection of the OCED `object attribute value` node.
183    #[must_use]
184    pub fn oaval(
185        &self,
186        object_id: &str,
187        at: DateTime<FixedOffset>,
188    ) -> Option<BTreeMap<&str, &OCELAttributeValue>> {
189        let o = self.objects.iter().find(|o| o.id == object_id)?;
190        // group by name, keep the latest <= at
191        let mut latest: BTreeMap<&str, (&DateTime<FixedOffset>, &OCELAttributeValue)> =
192            BTreeMap::new();
193        for a in &o.attributes {
194            if a.time <= at {
195                latest
196                    .entry(a.name.as_str())
197                    .and_modify(|cur| {
198                        if a.time >= *cur.0 {
199                            *cur = (&a.time, &a.value);
200                        }
201                    })
202                    .or_insert((&a.time, &a.value));
203            }
204        }
205        Some(latest.into_iter().map(|(k, v)| (k, v.1)).collect())
206    }
207
208    /// The distinct timestamps at which object `o`'s attributes change
209    /// (the temporal support of `oaval(o, .)`), sorted ascending.
210    #[must_use]
211    pub fn object_attr_timeline(&self, object_id: &str) -> Vec<DateTime<FixedOffset>> {
212        let mut stamps: BTreeSet<DateTime<FixedOffset>> = BTreeSet::new();
213        if let Some(o) = self.objects.iter().find(|o| o.id == object_id) {
214            for a in &o.attributes {
215                stamps.insert(a.time);
216            }
217        }
218        stamps.into_iter().collect()
219    }
220
221    /// E2O — qualified event→object references for event `e` (object_id, qualifier).
222    /// Mirrors the dotted `C` arc (event — qualifier — object) of the meta-model.
223    #[must_use]
224    pub fn e2o(&self, event_id: &str) -> Vec<(&str, &str)> {
225        self.events
226            .iter()
227            .find(|e| e.id == event_id)
228            .map(|e| {
229                e.relationships
230                    .iter()
231                    .map(|r| (r.object_id.as_str(), r.qualifier.as_str()))
232                    .collect()
233            })
234            .unwrap_or_default()
235    }
236
237    /// O2O — qualified object→object references for object `o` (to_object_id, qualifier).
238    /// Mirrors the `B` `from/to` object-relation with an object-relation-type/qualifier.
239    #[must_use]
240    pub fn o2o(&self, object_id: &str) -> Vec<(&str, &str)> {
241        self.objects
242            .iter()
243            .find(|o| o.id == object_id)
244            .map(|o| {
245                o.relationships
246                    .iter()
247                    .map(|r| (r.object_id.as_str(), r.qualifier.as_str()))
248                    .collect()
249            })
250            .unwrap_or_default()
251    }
252
253    /// Count objects of a given type (`|{o in O : type(o) = ot}|`).
254    #[must_use]
255    pub fn count_objects_of_type(&self, object_type: &str) -> usize {
256        self.objects
257            .iter()
258            .filter(|o| o.object_type == object_type)
259            .count()
260    }
261}
262
263impl OCELEvent {
264    pub fn new(id: String, event_type: &str) -> Self {
265        Self {
266            id,
267            event_type: event_type.to_string(),
268            time: chrono::Utc::now().into(),
269            attributes: Vec::new(),
270            relationships: Vec::new(),
271        }
272    }
273    pub fn with_attribute(mut self, attr: OCELEventAttribute) -> Self {
274        self.attributes.push(attr);
275        self
276    }
277}
278
279impl OCELEventAttribute {
280    pub fn string(name: &str, val: String) -> Self {
281        Self {
282            name: name.to_string(),
283            value: OCELAttributeValue::String(val),
284        }
285    }
286    pub fn integer(name: &str, val: i64) -> Self {
287        Self {
288            name: name.to_string(),
289            value: OCELAttributeValue::Integer(val),
290        }
291    }
292}
293
294impl OCELObject {
295    pub fn new(id: String, object_type: &str) -> Self {
296        Self {
297            id,
298            object_type: object_type.to_string(),
299            attributes: Vec::new(),
300            relationships: Vec::new(),
301        }
302    }
303    pub fn with_attribute(mut self, attr: OCELEventAttribute) -> Self {
304        self.attributes.push(OCELObjectAttribute {
305            name: attr.name,
306            value: attr.value,
307            time: chrono::Utc::now().into(),
308        });
309        self
310    }
311}
312
313impl OCELRelationship {
314    pub fn new(event_id: String, object_id: String) -> Self {
315        Self {
316            object_id,
317            qualifier: "".to_string(),
318        }
319    }
320    pub fn qualified(mut self, qualifier: &str) -> Self {
321        self.qualifier = qualifier.to_string();
322        self
323    }
324}
325
326impl OCEL {
327    pub fn new(events: Vec<OCELEvent>, objects: Vec<OCELObject>) -> Self {
328        Self {
329            events,
330            objects,
331            event_types: Vec::new(),
332            object_types: Vec::new(),
333        }
334    }
335}
336
337// ── OCEL 2.0 object-centric types ─────────────────────────────────────────
338
339/// Type alias: `OcelObject` is the OCEL 2.0 name for [`Object`].
340pub type OcelObject = Object;
341
342/// An OCEL 2.0 object with a typed identity.
343#[derive(Debug, Clone)]
344pub struct Object {
345    id: String,
346    object_type: String,
347}
348
349impl Object {
350    pub fn new(id: &str, object_type: &str) -> Self {
351        Object { id: id.to_owned(), object_type: object_type.to_owned() }
352    }
353    pub fn id(&self) -> &str { &self.id }
354    pub fn object_type(&self) -> &str { &self.object_type }
355}
356
357/// An OCEL 2.0 event (activity + optional timestamp).
358#[derive(Debug, Clone)]
359pub struct OcelEvent {
360    id: String,
361    activity: String,
362    timestamp_ns: u64,
363}
364
365impl OcelEvent {
366    pub fn new(id: &str, activity: &str) -> Self {
367        OcelEvent { id: id.to_owned(), activity: activity.to_owned(), timestamp_ns: 0 }
368    }
369
370    #[must_use]
371    pub fn at_ns(mut self, ns: u64) -> Self { self.timestamp_ns = ns; self }
372
373    pub fn id(&self) -> &str { &self.id }
374    pub fn activity(&self) -> &str { &self.activity }
375}
376
377/// Directed link from an event to an object (OCEL 2.0 E2O relation).
378#[derive(Debug, Clone)]
379pub struct EventObjectLink {
380    event_id: String,
381    object_id: String,
382    qualifier: Option<String>,
383}
384
385impl EventObjectLink {
386    pub fn new(event_id: &str, object_id: &str) -> Self {
387        EventObjectLink { event_id: event_id.to_owned(), object_id: object_id.to_owned(), qualifier: None }
388    }
389
390    #[must_use]
391    pub fn qualified(mut self, q: &str) -> Self { self.qualifier = Some(q.to_owned()); self }
392
393    pub fn event_id(&self) -> &str { &self.event_id }
394    pub fn object_id(&self) -> &str { &self.object_id }
395    pub fn qualifier(&self) -> Option<&str> { self.qualifier.as_deref() }
396}
397
398/// Directed link between two objects (OCEL 2.0 O2O relation).
399#[derive(Debug, Clone)]
400pub struct ObjectObjectLink {
401    from_id: String,
402    to_id: String,
403    qualifier: Option<String>,
404}
405
406impl ObjectObjectLink {
407    pub fn new(from: &str, to: &str) -> Self {
408        ObjectObjectLink { from_id: from.to_owned(), to_id: to.to_owned(), qualifier: None }
409    }
410
411    #[must_use]
412    pub fn qualified(mut self, q: &str) -> Self { self.qualifier = Some(q.to_owned()); self }
413
414    pub fn from_id(&self) -> &str { &self.from_id }
415    pub fn to_id(&self) -> &str { &self.to_id }
416}
417
418/// An attribute change on an object at a point in time.
419#[derive(Debug, Clone)]
420pub struct ObjectChange {
421    object_id: String,
422    attribute: String,
423    value: String,
424}
425
426impl ObjectChange {
427    pub fn new(object_id: &str, attribute: &str, value: &str) -> Self {
428        ObjectChange {
429            object_id: object_id.to_owned(),
430            attribute: attribute.to_owned(),
431            value: value.to_owned(),
432        }
433    }
434}
435
436/// An OCEL 2.0 log — the complete object-centric event log.
437#[derive(Debug, Clone)]
438pub struct OcelLog {
439    objects: Vec<Object>,
440    events: Vec<OcelEvent>,
441    e2o_links: Vec<EventObjectLink>,
442    o2o_links: Vec<ObjectObjectLink>,
443    changes: Vec<ObjectChange>,
444}
445
446impl OcelLog {
447    pub fn new(
448        objects: impl IntoIterator<Item = Object>,
449        events: impl IntoIterator<Item = OcelEvent>,
450        e2o_links: impl IntoIterator<Item = EventObjectLink>,
451        o2o_links: impl IntoIterator<Item = ObjectObjectLink>,
452        changes: impl IntoIterator<Item = ObjectChange>,
453    ) -> Self {
454        OcelLog {
455            objects: objects.into_iter().collect(),
456            events: events.into_iter().collect(),
457            e2o_links: e2o_links.into_iter().collect(),
458            o2o_links: o2o_links.into_iter().collect(),
459            changes: changes.into_iter().collect(),
460        }
461    }
462
463    pub fn objects(&self) -> &[Object] { &self.objects }
464    pub fn events(&self) -> &[OcelEvent] { &self.events }
465    pub fn event_object_links(&self) -> &[EventObjectLink] { &self.e2o_links }
466    pub fn object_object_links(&self) -> &[ObjectObjectLink] { &self.o2o_links }
467    pub fn object_changes(&self) -> &[ObjectChange] { &self.changes }
468
469    #[must_use]
470    pub fn validate(&self) -> Result<(), OcelRefusal> {
471        if self.e2o_links.is_empty() {
472            return Err(OcelRefusal::EmptyEventObjectLinks);
473        }
474        let object_ids: std::collections::HashSet<&str> =
475            self.objects.iter().map(|o| o.id.as_str()).collect();
476        for link in &self.e2o_links {
477            if !object_ids.contains(link.object_id.as_str()) {
478                return Err(OcelRefusal::DanglingEventObjectLink);
479            }
480        }
481        Ok(())
482    }
483}
484
485/// Named refusal variants for OCEL 2.0 log validation laws.
486#[derive(Debug, Clone, PartialEq, Eq)]
487pub enum OcelRefusal {
488    /// An event-to-object link references an object not present in the log.
489    DanglingEventObjectLink,
490    /// The log has no event-to-object links — violates object-centricity law.
491    EmptyEventObjectLinks,
492}
493
494impl std::fmt::Display for OcelRefusal {
495    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496        match self {
497            OcelRefusal::DanglingEventObjectLink => write!(f, "DanglingEventObjectLink"),
498            OcelRefusal::EmptyEventObjectLinks => write!(f, "EmptyEventObjectLinks"),
499        }
500    }
501}
502
503impl std::error::Error for OcelRefusal {}