Skip to main content

datasynth_ocpm/export/
ocel2.rs

1//! OCEL 2.0 JSON export functionality.
2//!
3//! This module exports OCPM event logs in the OCEL 2.0 JSON format,
4//! which is the standard format for object-centric event logs.
5//!
6//! OCEL 2.0 Specification: https://www.ocel-standard.org/
7
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fs::File;
11use std::io::{BufWriter, Write};
12use std::path::Path;
13
14use crate::models::{
15    EventLifecycle, ObjectAttributeValue, ObjectGraph, ObjectQualifier, OcpmEvent, OcpmEventLog,
16    RelationshipIndex,
17};
18
19/// OCEL 2.0 complete log structure.
20#[derive(Debug, Serialize, Deserialize)]
21pub struct Ocel2Log {
22    /// Object types (metamodel)
23    #[serde(rename = "objectTypes")]
24    pub object_types: Vec<Ocel2ObjectType>,
25    /// Event types (metamodel)
26    #[serde(rename = "eventTypes")]
27    pub event_types: Vec<Ocel2EventType>,
28    /// Object instances
29    pub objects: Vec<Ocel2Object>,
30    /// Event instances
31    pub events: Vec<Ocel2Event>,
32    /// Global log attributes
33    #[serde(rename = "ocel:global-log", skip_serializing_if = "Option::is_none")]
34    pub global_log: Option<Ocel2GlobalLog>,
35}
36
37/// OCEL 2.0 global log metadata.
38#[derive(Debug, Serialize, Deserialize)]
39pub struct Ocel2GlobalLog {
40    /// Log name
41    #[serde(
42        rename = "ocel:attribute-names",
43        skip_serializing_if = "Option::is_none"
44    )]
45    pub attribute_names: Option<Vec<String>>,
46    /// Ordering timestamp attribute
47    #[serde(rename = "ocel:ordering", skip_serializing_if = "Option::is_none")]
48    pub ordering: Option<String>,
49    /// Version
50    #[serde(rename = "ocel:version", skip_serializing_if = "Option::is_none")]
51    pub version: Option<String>,
52}
53
54/// OCEL 2.0 object type definition.
55#[derive(Debug, Serialize, Deserialize)]
56pub struct Ocel2ObjectType {
57    /// Object type name
58    pub name: String,
59    /// Attributes for this object type
60    pub attributes: Vec<Ocel2Attribute>,
61}
62
63/// OCEL 2.0 event type definition.
64#[derive(Debug, Serialize, Deserialize)]
65pub struct Ocel2EventType {
66    /// Event type name (activity)
67    pub name: String,
68    /// Attributes for this event type
69    pub attributes: Vec<Ocel2Attribute>,
70}
71
72/// OCEL 2.0 attribute definition.
73#[derive(Debug, Serialize, Deserialize)]
74pub struct Ocel2Attribute {
75    /// Attribute name
76    pub name: String,
77    /// Attribute type (string, integer, float, boolean, time, etc.)
78    #[serde(rename = "type")]
79    pub attr_type: String,
80}
81
82/// OCEL 2.0 object instance.
83#[derive(Debug, Serialize, Deserialize)]
84pub struct Ocel2Object {
85    /// Object identifier (UUID string)
86    pub id: String,
87    /// Object type name
88    #[serde(rename = "type")]
89    pub object_type: String,
90    /// Object attribute values
91    #[serde(skip_serializing_if = "HashMap::is_empty")]
92    pub attributes: HashMap<String, Ocel2Value>,
93    /// Relationships to other objects
94    #[serde(rename = "relationships", skip_serializing_if = "Vec::is_empty")]
95    pub relationships: Vec<Ocel2ObjectRelationship>,
96}
97
98/// OCEL 2.0 object relationship.
99#[derive(Debug, Serialize, Deserialize)]
100pub struct Ocel2ObjectRelationship {
101    /// Target object ID
102    #[serde(rename = "objectId")]
103    pub object_id: String,
104    /// Relationship qualifier
105    pub qualifier: String,
106}
107
108/// OCEL 2.0 event instance.
109#[derive(Debug, Serialize, Deserialize)]
110pub struct Ocel2Event {
111    /// Event identifier
112    pub id: String,
113    /// Event type (activity)
114    #[serde(rename = "type")]
115    pub event_type: String,
116    /// Event timestamp
117    pub time: String,
118    /// Event attribute values
119    #[serde(skip_serializing_if = "HashMap::is_empty")]
120    pub attributes: HashMap<String, Ocel2Value>,
121    /// Related objects with qualifiers
122    pub relationships: Vec<Ocel2EventObjectRelationship>,
123}
124
125/// OCEL 2.0 event-to-object relationship.
126#[derive(Debug, Serialize, Deserialize)]
127pub struct Ocel2EventObjectRelationship {
128    /// Object ID
129    #[serde(rename = "objectId")]
130    pub object_id: String,
131    /// Qualifier (created, updated, read, etc.)
132    pub qualifier: String,
133}
134
135/// OCEL 2.0 attribute value.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(untagged)]
138pub enum Ocel2Value {
139    /// String value
140    String(String),
141    /// Integer value
142    Integer(i64),
143    /// Floating point value
144    Float(f64),
145    /// Boolean value
146    Boolean(bool),
147    /// Null value
148    Null,
149}
150
151impl From<&ObjectAttributeValue> for Ocel2Value {
152    fn from(value: &ObjectAttributeValue) -> Self {
153        match value {
154            ObjectAttributeValue::String(s) => Ocel2Value::String(s.clone()),
155            ObjectAttributeValue::Integer(i) => Ocel2Value::Integer(*i),
156            ObjectAttributeValue::Decimal(d) => {
157                // Convert Decimal to f64 for JSON compatibility
158                Ocel2Value::Float(d.to_string().parse().unwrap_or(0.0))
159            }
160            ObjectAttributeValue::Date(d) => Ocel2Value::String(d.to_string()),
161            ObjectAttributeValue::DateTime(dt) => Ocel2Value::String(dt.to_rfc3339()),
162            ObjectAttributeValue::Boolean(b) => Ocel2Value::Boolean(*b),
163            ObjectAttributeValue::Reference(id) => Ocel2Value::String(id.to_string()),
164            ObjectAttributeValue::Null => Ocel2Value::Null,
165        }
166    }
167}
168
169/// OCEL 2.0 exporter.
170pub struct Ocel2Exporter {
171    /// Include metadata attributes
172    pub include_metadata: bool,
173    /// Include anomaly markers
174    pub include_anomalies: bool,
175    /// Pretty print JSON
176    pub pretty_print: bool,
177}
178
179impl Default for Ocel2Exporter {
180    fn default() -> Self {
181        Self {
182            include_metadata: true,
183            include_anomalies: true,
184            pretty_print: true,
185        }
186    }
187}
188
189impl Ocel2Exporter {
190    /// Create a new exporter with default settings.
191    pub fn new() -> Self {
192        Self::default()
193    }
194
195    /// Set whether to include metadata attributes.
196    pub fn with_metadata(mut self, include: bool) -> Self {
197        self.include_metadata = include;
198        self
199    }
200
201    /// Set whether to include anomaly markers.
202    pub fn with_anomalies(mut self, include: bool) -> Self {
203        self.include_anomalies = include;
204        self
205    }
206
207    /// Set whether to pretty print the output.
208    pub fn with_pretty_print(mut self, pretty: bool) -> Self {
209        self.pretty_print = pretty;
210        self
211    }
212
213    /// Convert an OCPM event log to OCEL 2.0 format.
214    pub fn convert(&self, log: &OcpmEventLog) -> Ocel2Log {
215        let object_types = self.convert_object_types(log);
216        let event_types = self.convert_event_types(log);
217        let objects = self.convert_objects(&log.objects, &log.object_relationships);
218        let events = self.convert_events(&log.events);
219
220        let global_log = if self.include_metadata {
221            Some(Ocel2GlobalLog {
222                attribute_names: Some(vec![
223                    "company_code".into(),
224                    "resource_id".into(),
225                    "document_ref".into(),
226                ]),
227                ordering: Some("time".into()),
228                version: Some("2.0".into()),
229            })
230        } else {
231            None
232        };
233
234        Ocel2Log {
235            object_types,
236            event_types,
237            objects,
238            events,
239            global_log,
240        }
241    }
242
243    /// Convert object type definitions.
244    fn convert_object_types(&self, log: &OcpmEventLog) -> Vec<Ocel2ObjectType> {
245        log.object_types
246            .values()
247            .map(|ot| {
248                let mut attributes = vec![
249                    Ocel2Attribute {
250                        name: "external_id".into(),
251                        attr_type: "string".into(),
252                    },
253                    Ocel2Attribute {
254                        name: "company_code".into(),
255                        attr_type: "string".into(),
256                    },
257                    Ocel2Attribute {
258                        name: "current_state".into(),
259                        attr_type: "string".into(),
260                    },
261                    Ocel2Attribute {
262                        name: "created_at".into(),
263                        attr_type: "time".into(),
264                    },
265                ];
266
267                if self.include_anomalies {
268                    attributes.push(Ocel2Attribute {
269                        name: "is_anomaly".into(),
270                        attr_type: "boolean".into(),
271                    });
272                }
273
274                Ocel2ObjectType {
275                    name: ot.type_id.clone(),
276                    attributes,
277                }
278            })
279            .collect()
280    }
281
282    /// Convert event type definitions.
283    fn convert_event_types(&self, log: &OcpmEventLog) -> Vec<Ocel2EventType> {
284        log.activity_types
285            .values()
286            .map(|at| {
287                let mut attributes = vec![
288                    Ocel2Attribute {
289                        name: "resource_id".into(),
290                        attr_type: "string".into(),
291                    },
292                    Ocel2Attribute {
293                        name: "company_code".into(),
294                        attr_type: "string".into(),
295                    },
296                    Ocel2Attribute {
297                        name: "lifecycle".into(),
298                        attr_type: "string".into(),
299                    },
300                ];
301
302                if self.include_anomalies {
303                    attributes.push(Ocel2Attribute {
304                        name: "is_anomaly".into(),
305                        attr_type: "boolean".into(),
306                    });
307                }
308
309                Ocel2EventType {
310                    name: at.activity_id.clone(),
311                    attributes,
312                }
313            })
314            .collect()
315    }
316
317    /// Convert object instances.
318    fn convert_objects(
319        &self,
320        graph: &ObjectGraph,
321        relationships: &RelationshipIndex,
322    ) -> Vec<Ocel2Object> {
323        graph
324            .iter()
325            .map(|obj| {
326                let mut attributes: HashMap<String, Ocel2Value> = obj
327                    .attributes
328                    .iter()
329                    .map(|(k, v)| (k.clone(), Ocel2Value::from(v)))
330                    .collect();
331
332                // Add standard attributes
333                attributes.insert(
334                    "external_id".into(),
335                    Ocel2Value::String(obj.external_id.clone()),
336                );
337                attributes.insert(
338                    "company_code".into(),
339                    Ocel2Value::String(obj.company_code.clone()),
340                );
341                attributes.insert(
342                    "current_state".into(),
343                    Ocel2Value::String(obj.current_state.clone()),
344                );
345                attributes.insert(
346                    "created_at".into(),
347                    Ocel2Value::String(obj.created_at.to_rfc3339()),
348                );
349
350                if self.include_anomalies {
351                    attributes.insert("is_anomaly".into(), Ocel2Value::Boolean(obj.is_anomaly));
352                }
353
354                // Get relationships for this object
355                let rels: Vec<Ocel2ObjectRelationship> = relationships
356                    .get_outgoing(obj.object_id)
357                    .into_iter()
358                    .map(|rel| Ocel2ObjectRelationship {
359                        object_id: rel.target_object_id.to_string(),
360                        qualifier: rel.relationship_type.clone(),
361                    })
362                    .collect();
363
364                Ocel2Object {
365                    id: obj.object_id.to_string(),
366                    object_type: obj.object_type_id.clone(),
367                    attributes,
368                    relationships: rels,
369                }
370            })
371            .collect()
372    }
373
374    /// Convert events.
375    fn convert_events(&self, events: &[OcpmEvent]) -> Vec<Ocel2Event> {
376        events
377            .iter()
378            .map(|event| {
379                let mut attributes: HashMap<String, Ocel2Value> = event
380                    .attributes
381                    .iter()
382                    .map(|(k, v)| (k.clone(), Ocel2Value::from(v)))
383                    .collect();
384
385                // Add standard attributes
386                attributes.insert(
387                    "resource_id".into(),
388                    Ocel2Value::String(event.resource_id.clone()),
389                );
390                attributes.insert(
391                    "company_code".into(),
392                    Ocel2Value::String(event.company_code.clone()),
393                );
394                attributes.insert(
395                    "lifecycle".into(),
396                    Ocel2Value::String(lifecycle_to_string(&event.lifecycle)),
397                );
398
399                if let Some(ref doc_ref) = event.document_ref {
400                    attributes.insert("document_ref".into(), Ocel2Value::String(doc_ref.clone()));
401                }
402
403                if let Some(case_id) = event.case_id {
404                    attributes.insert("case_id".into(), Ocel2Value::String(case_id.to_string()));
405                }
406
407                if self.include_anomalies {
408                    attributes.insert("is_anomaly".into(), Ocel2Value::Boolean(event.is_anomaly));
409                }
410
411                let relationships: Vec<Ocel2EventObjectRelationship> = event
412                    .object_refs
413                    .iter()
414                    .map(|obj_ref| Ocel2EventObjectRelationship {
415                        object_id: obj_ref.object_id.to_string(),
416                        qualifier: qualifier_to_string(&obj_ref.qualifier),
417                    })
418                    .collect();
419
420                Ocel2Event {
421                    id: event.event_id.to_string(),
422                    event_type: event.activity_id.clone(),
423                    time: event.timestamp.to_rfc3339(),
424                    attributes,
425                    relationships,
426                }
427            })
428            .collect()
429    }
430
431    /// Export an OCPM event log to a JSON file.
432    pub fn export_to_file<P: AsRef<Path>>(
433        &self,
434        log: &OcpmEventLog,
435        path: P,
436    ) -> std::io::Result<()> {
437        let ocel2_log = self.convert(log);
438        let file = File::create(path)?;
439        let writer = BufWriter::new(file);
440
441        if self.pretty_print {
442            serde_json::to_writer_pretty(writer, &ocel2_log)?;
443        } else {
444            serde_json::to_writer(writer, &ocel2_log)?;
445        }
446
447        Ok(())
448    }
449
450    /// Export an OCPM event log to a JSON string.
451    pub fn export_to_string(&self, log: &OcpmEventLog) -> serde_json::Result<String> {
452        let ocel2_log = self.convert(log);
453
454        if self.pretty_print {
455            serde_json::to_string_pretty(&ocel2_log)
456        } else {
457            serde_json::to_string(&ocel2_log)
458        }
459    }
460
461    /// Export an OCPM event log to a writer.
462    pub fn export_to_writer<W: Write>(
463        &self,
464        log: &OcpmEventLog,
465        writer: W,
466    ) -> serde_json::Result<()> {
467        let ocel2_log = self.convert(log);
468
469        if self.pretty_print {
470            serde_json::to_writer_pretty(writer, &ocel2_log)
471        } else {
472            serde_json::to_writer(writer, &ocel2_log)
473        }
474    }
475}
476
477/// Convert EventLifecycle to OCEL 2.0 string.
478fn lifecycle_to_string(lifecycle: &EventLifecycle) -> String {
479    match lifecycle {
480        EventLifecycle::Start => "start".into(),
481        EventLifecycle::Complete => "complete".into(),
482        EventLifecycle::Abort => "abort".into(),
483        EventLifecycle::Suspend => "suspend".into(),
484        EventLifecycle::Resume => "resume".into(),
485        EventLifecycle::Atomic => "atomic".into(),
486    }
487}
488
489/// Convert ObjectQualifier to OCEL 2.0 string.
490fn qualifier_to_string(qualifier: &ObjectQualifier) -> String {
491    match qualifier {
492        ObjectQualifier::Created => "created".into(),
493        ObjectQualifier::Updated => "updated".into(),
494        ObjectQualifier::Read => "read".into(),
495        ObjectQualifier::Consumed => "consumed".into(),
496        ObjectQualifier::Context => "context".into(),
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn test_ocel2_exporter_creation() {
506        let exporter = Ocel2Exporter::new()
507            .with_metadata(true)
508            .with_anomalies(true)
509            .with_pretty_print(false);
510
511        assert!(exporter.include_metadata);
512        assert!(exporter.include_anomalies);
513        assert!(!exporter.pretty_print);
514    }
515
516    #[test]
517    fn test_ocel2_export_empty_log() {
518        let log = OcpmEventLog::new().with_standard_types();
519        let exporter = Ocel2Exporter::new();
520
521        let ocel2 = exporter.convert(&log);
522
523        assert!(!ocel2.object_types.is_empty());
524        assert!(!ocel2.event_types.is_empty());
525        assert!(ocel2.objects.is_empty());
526        assert!(ocel2.events.is_empty());
527    }
528
529    #[test]
530    fn test_ocel2_export_to_string() {
531        let log = OcpmEventLog::new().with_standard_types();
532        let exporter = Ocel2Exporter::new().with_pretty_print(false);
533
534        let json = exporter.export_to_string(&log);
535        assert!(json.is_ok());
536
537        let json_str = json.unwrap();
538        assert!(json_str.contains("objectTypes"));
539        assert!(json_str.contains("eventTypes"));
540    }
541
542    #[test]
543    fn test_attribute_value_conversion() {
544        let str_val = ObjectAttributeValue::String("test".into());
545        let int_val = ObjectAttributeValue::Integer(42);
546        let bool_val = ObjectAttributeValue::Boolean(true);
547        let null_val = ObjectAttributeValue::Null;
548
549        assert!(matches!(Ocel2Value::from(&str_val), Ocel2Value::String(_)));
550        assert!(matches!(
551            Ocel2Value::from(&int_val),
552            Ocel2Value::Integer(42)
553        ));
554        assert!(matches!(
555            Ocel2Value::from(&bool_val),
556            Ocel2Value::Boolean(true)
557        ));
558        assert!(matches!(Ocel2Value::from(&null_val), Ocel2Value::Null));
559    }
560
561    #[test]
562    fn test_lifecycle_conversion() {
563        assert_eq!(lifecycle_to_string(&EventLifecycle::Start), "start");
564        assert_eq!(lifecycle_to_string(&EventLifecycle::Complete), "complete");
565        assert_eq!(lifecycle_to_string(&EventLifecycle::Abort), "abort");
566        assert_eq!(lifecycle_to_string(&EventLifecycle::Atomic), "atomic");
567    }
568
569    #[test]
570    fn test_qualifier_conversion() {
571        assert_eq!(qualifier_to_string(&ObjectQualifier::Created), "created");
572        assert_eq!(qualifier_to_string(&ObjectQualifier::Updated), "updated");
573        assert_eq!(qualifier_to_string(&ObjectQualifier::Read), "read");
574        assert_eq!(qualifier_to_string(&ObjectQualifier::Consumed), "consumed");
575    }
576}