1use 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#[derive(Debug, Serialize, Deserialize)]
21pub struct Ocel2Log {
22 #[serde(rename = "objectTypes")]
24 pub object_types: Vec<Ocel2ObjectType>,
25 #[serde(rename = "eventTypes")]
27 pub event_types: Vec<Ocel2EventType>,
28 pub objects: Vec<Ocel2Object>,
30 pub events: Vec<Ocel2Event>,
32 #[serde(rename = "ocel:global-log", skip_serializing_if = "Option::is_none")]
34 pub global_log: Option<Ocel2GlobalLog>,
35}
36
37#[derive(Debug, Serialize, Deserialize)]
39pub struct Ocel2GlobalLog {
40 #[serde(
42 rename = "ocel:attribute-names",
43 skip_serializing_if = "Option::is_none"
44 )]
45 pub attribute_names: Option<Vec<String>>,
46 #[serde(rename = "ocel:ordering", skip_serializing_if = "Option::is_none")]
48 pub ordering: Option<String>,
49 #[serde(rename = "ocel:version", skip_serializing_if = "Option::is_none")]
51 pub version: Option<String>,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
56pub struct Ocel2ObjectType {
57 pub name: String,
59 pub attributes: Vec<Ocel2Attribute>,
61}
62
63#[derive(Debug, Serialize, Deserialize)]
65pub struct Ocel2EventType {
66 pub name: String,
68 pub attributes: Vec<Ocel2Attribute>,
70}
71
72#[derive(Debug, Serialize, Deserialize)]
74pub struct Ocel2Attribute {
75 pub name: String,
77 #[serde(rename = "type")]
79 pub attr_type: String,
80}
81
82#[derive(Debug, Serialize, Deserialize)]
84pub struct Ocel2Object {
85 pub id: String,
87 #[serde(rename = "type")]
89 pub object_type: String,
90 #[serde(skip_serializing_if = "HashMap::is_empty")]
92 pub attributes: HashMap<String, Ocel2Value>,
93 #[serde(rename = "relationships", skip_serializing_if = "Vec::is_empty")]
95 pub relationships: Vec<Ocel2ObjectRelationship>,
96}
97
98#[derive(Debug, Serialize, Deserialize)]
100pub struct Ocel2ObjectRelationship {
101 #[serde(rename = "objectId")]
103 pub object_id: String,
104 pub qualifier: String,
106}
107
108#[derive(Debug, Serialize, Deserialize)]
110pub struct Ocel2Event {
111 pub id: String,
113 #[serde(rename = "type")]
115 pub event_type: String,
116 pub time: String,
118 #[serde(skip_serializing_if = "HashMap::is_empty")]
120 pub attributes: HashMap<String, Ocel2Value>,
121 pub relationships: Vec<Ocel2EventObjectRelationship>,
123}
124
125#[derive(Debug, Serialize, Deserialize)]
127pub struct Ocel2EventObjectRelationship {
128 #[serde(rename = "objectId")]
130 pub object_id: String,
131 pub qualifier: String,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(untagged)]
138pub enum Ocel2Value {
139 String(String),
141 Integer(i64),
143 Float(f64),
145 Boolean(bool),
147 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 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
169pub struct Ocel2Exporter {
171 pub include_metadata: bool,
173 pub include_anomalies: bool,
175 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 pub fn new() -> Self {
192 Self::default()
193 }
194
195 pub fn with_metadata(mut self, include: bool) -> Self {
197 self.include_metadata = include;
198 self
199 }
200
201 pub fn with_anomalies(mut self, include: bool) -> Self {
203 self.include_anomalies = include;
204 self
205 }
206
207 pub fn with_pretty_print(mut self, pretty: bool) -> Self {
209 self.pretty_print = pretty;
210 self
211 }
212
213 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 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 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 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 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 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 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 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 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 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 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
477fn 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
489fn 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}