ocel 0.1.5

OCEL 2.0 data model, I/O (JSON/SQLite/XML), and validation library
Documentation
use std::path::PathBuf;

use chrono::{DateTime, Utc};
use ocel::io::{json, sqlite, xml};
use ocel::{
    AttrType, AttributeDefinition, Event, EventType, ObjectType, Ocel, Relationship, Violation,
};

fn ts(secs: i64) -> DateTime<Utc> {
    DateTime::from_timestamp(secs, 0).unwrap()
}

fn fixture(name: &str) -> PathBuf {
    let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    p.push("tests/fixtures");
    p.push(name);
    p
}

#[test]
fn valid_fixture_passes() {
    let ocel = json::read_path(fixture("order_management_small.json")).unwrap();
    assert_eq!(ocel.validate(), Ok(()));
}

#[test]
fn minimal_passes() {
    let ocel = json::read_path(fixture("minimal.json")).unwrap();
    assert_eq!(ocel.validate(), Ok(()));
}

#[test]
fn detects_dangling_e2o_and_undeclared_type() {
    let ocel = Ocel {
        event_types: vec![],
        object_types: vec![],
        events: vec![Event {
            id: "e1".into(),
            event_type: "ghost".into(),
            time: ts(0),
            attributes: vec![],
            relationships: vec![Relationship {
                object_id: "missing".into(),
                qualifier: "q".into(),
            }],
        }],
        objects: vec![],
    };
    let violations = ocel.validate().unwrap_err();
    assert!(violations.contains(&Violation::UndeclaredEventType {
        event: "e1".into(),
        event_type: "ghost".into(),
    }));
    assert!(violations.contains(&Violation::DanglingE2O {
        event: "e1".into(),
        object: "missing".into(),
    }));
}

#[test]
fn detects_undeclared_attribute() {
    let ocel = Ocel {
        event_types: vec![EventType {
            name: "t".into(),
            attributes: vec![],
        }],
        object_types: vec![],
        events: vec![Event {
            id: "e1".into(),
            event_type: "t".into(),
            time: ts(0),
            attributes: vec![ocel::EventAttribute {
                name: "surprise".into(),
                value: ocel::AttrValue::String("x".into()),
            }],
            relationships: vec![],
        }],
        objects: vec![],
    };
    assert!(ocel
        .validate()
        .unwrap_err()
        .contains(&Violation::UndeclaredEventAttribute {
            event: "e1".into(),
            attribute: "surprise".into(),
        }));
}

/// Attribute names shared across types are allowed: the official datasets
/// (e.g. Zenodo Order Management) use names like "price" in multiple types.
#[test]
fn shared_attribute_names_across_types_are_valid() {
    let dup = || AttributeDefinition {
        name: "price".into(),
        value_type: AttrType::Float,
    };
    let ocel = Ocel {
        event_types: vec![],
        object_types: vec![
            ObjectType {
                name: "item".into(),
                attributes: vec![dup()],
            },
            ObjectType {
                name: "product".into(),
                attributes: vec![dup()],
            },
        ],
        events: vec![],
        objects: vec![],
    };
    assert_eq!(ocel.validate(), Ok(()));
}

/// The official `PM4Py` examples validate cleanly across all three formats.
/// Skips when absent; run `sh scripts/fetch-official-fixtures.sh` to enable it.
#[test]
fn official_examples_validate_if_present() {
    let json_path = fixture("official/ocel20_example.jsonocel");
    if !json_path.exists() {
        eprintln!("skipping: official fixtures not present");
        return;
    }
    assert_eq!(json::read_path(&json_path).unwrap().validate(), Ok(()));
    assert_eq!(
        sqlite::read_path(fixture("official/ocel20_example.sqlite"))
            .unwrap()
            .validate(),
        Ok(())
    );
    assert_eq!(
        xml::read_path(fixture("official/ocel20_example.xmlocel"))
            .unwrap()
            .validate(),
        Ok(())
    );
}