use crate::ocel::{OCELEvent, OCELObject, OCELType, OCEL};
use hashbrown::HashSet;
use serde_json;
pub fn import_ocel_json(ocel_json: &str) -> Result<OCEL, serde_json::Error> {
serde_json::from_str(ocel_json)
}
pub fn import_ocel_json_slice(slice: &[u8]) -> Result<OCEL, serde_json::Error> {
serde_json::from_slice(slice)
}
pub fn import_ocel_ndjson(ndjson: &str) -> Result<OCEL, String> {
let mut events = Vec::new();
let mut objects: Vec<OCELObject> = Vec::new();
let mut event_type_names = HashSet::new();
let mut object_type_names = HashSet::new();
for line in ndjson.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Ok(mut val) = serde_json::from_str::<serde_json::Value>(line) {
let mut is_event = false;
if let Some(val_map) = val.as_object_mut() {
if val_map.contains_key("timestamp") || val_map.contains_key("time") {
is_event = true;
}
}
if is_event {
if let Some(val_map) = val.as_object_mut() {
if !val_map.contains_key("id") {
if let Some(event_id) = val_map.get("event_id").cloned() {
val_map.insert("id".to_string(), event_id);
}
}
if !val_map.contains_key("type") {
if let Some(activity) = val_map.get("activity").cloned() {
val_map.insert("type".to_string(), activity);
}
}
if !val_map.contains_key("time") {
if let Some(timestamp) = val_map.get("timestamp").cloned() {
val_map.insert("time".to_string(), timestamp);
}
}
if let Some(time_val) = val_map.get_mut("time") {
if let Some(time_str) = time_val.as_str() {
let mut s = time_str.to_string();
if s.contains(' ') {
s = s.replacen(' ', "T", 1);
}
let has_offset = s.ends_with('Z')
|| (s.len() > 10
&& (s[10..].contains('+') || s[10..].contains('-')));
if !has_offset {
s.push('Z');
}
*time_val = serde_json::Value::String(s);
}
}
let mut relationships = if let Some(serde_json::Value::Array(rels)) =
val_map.get("relationships")
{
rels.clone()
} else {
Vec::new()
};
if let Some(serde_json::Value::Array(objects_arr)) = val_map.get("objects") {
for obj_val in objects_arr {
if let Some(obj_map) = obj_val.as_object() {
let obj_id = obj_map
.get("id")
.or_else(|| obj_map.get("objectId"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let obj_type = obj_map
.get("type")
.or_else(|| obj_map.get("qualifier"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let (Some(oid), Some(otype)) = (obj_id, obj_type) {
relationships.push(serde_json::json!({
"objectId": oid,
"qualifier": otype
}));
if !objects.iter().any(|o| o.id == oid) {
objects.push(OCELObject {
id: oid.clone(),
object_type: otype.clone(),
attributes: vec![],
relationships: vec![],
});
object_type_names.insert(otype);
}
}
}
}
}
val_map.insert(
"relationships".to_string(),
serde_json::Value::Array(relationships),
);
let mut attributes = Vec::new();
if let Some(serde_json::Value::Array(attrs)) = val_map.get("attributes") {
attributes.extend(attrs.clone());
} else if let Some(serde_json::Value::Object(attrs_map)) =
val_map.get("attributes")
{
for (k, v) in attrs_map {
attributes.push(serde_json::json!({
"name": k,
"value": v.clone()
}));
}
}
let standard_event_keys: HashSet<&str> = [
"id",
"event_id",
"type",
"activity",
"time",
"timestamp",
"attributes",
"relationships",
"objects",
]
.iter()
.cloned()
.collect();
let keys_to_remove: Vec<String> = val_map
.keys()
.filter(|k| !standard_event_keys.contains(k.as_str()))
.cloned()
.collect();
for k in keys_to_remove {
if let Some(v) = val_map.remove(&k) {
attributes.push(serde_json::json!({
"name": k,
"value": v
}));
}
}
val_map.insert(
"attributes".to_string(),
serde_json::Value::Array(attributes),
);
}
if let Ok(event) = serde_json::from_value::<OCELEvent>(val) {
event_type_names.insert(event.event_type.clone());
events.push(event);
}
} else {
if let Some(val_map) = val.as_object_mut() {
if !val_map.contains_key("type") {
if let Some(otype) = val_map.get("object_type").cloned() {
val_map.insert("type".to_string(), otype);
}
}
let mut attributes = Vec::new();
if let Some(serde_json::Value::Array(attrs)) = val_map.get("attributes") {
attributes.extend(attrs.clone());
} else if let Some(serde_json::Value::Object(attrs_map)) =
val_map.get("attributes")
{
for (k, v) in attrs_map {
attributes.push(serde_json::json!({
"name": k,
"value": v.clone(),
"time": "1970-01-01T00:00:00Z"
}));
}
}
let standard_obj_keys: HashSet<&str> =
["id", "type", "object_type", "attributes", "relationships"]
.iter()
.cloned()
.collect();
let keys_to_remove: Vec<String> = val_map
.keys()
.filter(|k| !standard_obj_keys.contains(k.as_str()))
.cloned()
.collect();
for k in keys_to_remove {
if let Some(v) = val_map.remove(&k) {
attributes.push(serde_json::json!({
"name": k,
"value": v,
"time": "1970-01-01T00:00:00Z"
}));
}
}
val_map.insert(
"attributes".to_string(),
serde_json::Value::Array(attributes),
);
}
if let Ok(object) = serde_json::from_value::<OCELObject>(val) {
if let Some(existing) = objects.iter_mut().find(|o| o.id == object.id) {
existing.object_type = object.object_type.clone();
existing.attributes = object.attributes.clone();
existing.relationships = object.relationships.clone();
} else {
objects.push(object.clone());
}
object_type_names.insert(object.object_type);
}
}
}
}
let event_types = event_type_names
.into_iter()
.map(|name| OCELType {
name,
attributes: vec![],
})
.collect();
let object_types = object_type_names
.into_iter()
.map(|name| OCELType {
name,
attributes: vec![],
})
.collect();
Ok(OCEL {
event_types,
object_types,
events,
objects,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_import_ocel_ndjson_basic() {
let ndjson = r#"
{"id":"e1","type":"DiagnosticRaised","time":"2026-05-30T12:00:00Z","attributes":[],"relationships":[{"objectId":"o1","qualifier":"subject"}]}
{"id":"o1","type":"File","attributes":[],"relationships":[]}
{"id":"e2","type":"RouteSelected","time":"2026-05-30T12:05:00Z","attributes":[],"relationships":[{"objectId":"o1","qualifier":"subject"}]}
{"id":"e3","type":"RouteSelected","time":"2026-05-30T12:05:00Z"#;
let ocel = import_ocel_ndjson(ndjson).unwrap();
assert_eq!(ocel.events.len(), 2);
assert_eq!(ocel.objects.len(), 1);
assert_eq!(ocel.event_types.len(), 2); assert_eq!(ocel.object_types.len(), 1); }
#[test]
fn test_import_ocel_ndjson_preprocessing() {
let ndjson = r#"
{"event_id":"e1","activity":"DiagnosticRaised","timestamp":"2026-05-30 12:00:00","custom_event_prop":"hello","objects":[{"objectId":"o1","type":"File"}]}
{"id":"o1","object_type":"File","custom_obj_prop":"world"}
"#;
let ocel = import_ocel_ndjson(ndjson).unwrap();
assert_eq!(ocel.events.len(), 1);
assert_eq!(ocel.objects.len(), 1);
let e = &ocel.events[0];
assert_eq!(e.id, "e1");
assert_eq!(e.event_type, "DiagnosticRaised");
assert_eq!(e.time.to_rfc3339(), "2026-05-30T12:00:00+00:00");
assert_eq!(e.relationships.len(), 1);
assert_eq!(e.relationships[0].object_id, "o1");
assert_eq!(e.relationships[0].qualifier, "File");
assert_eq!(e.attributes.len(), 1);
assert_eq!(e.attributes[0].name, "custom_event_prop");
assert_eq!(
e.attributes[0].value,
crate::ocel::OCELAttributeValue::String("hello".to_string())
);
let o = &ocel.objects[0];
assert_eq!(o.id, "o1");
assert_eq!(o.object_type, "File");
assert_eq!(o.attributes.len(), 1);
assert_eq!(o.attributes[0].name, "custom_obj_prop");
assert_eq!(
o.attributes[0].value,
crate::ocel::OCELAttributeValue::String("world".to_string())
);
assert_eq!(
o.attributes[0].time.to_rfc3339(),
"1970-01-01T00:00:00+00:00"
);
}
#[test]
fn test_import_ocel_ndjson_map_attributes() {
let ndjson = r#"
{"id":"e1","type":"DiagnosticRaised","time":"2026-05-30T12:00:00Z","attributes":{"custom_event_prop":"hello"},"relationships":[]}
{"id":"o1","type":"File","attributes":{"custom_obj_prop":"world"},"relationships":[]}
"#;
let ocel = import_ocel_ndjson(ndjson).unwrap();
assert_eq!(ocel.events.len(), 1);
assert_eq!(ocel.objects.len(), 1);
let e = &ocel.events[0];
assert_eq!(e.attributes.len(), 1);
assert_eq!(e.attributes[0].name, "custom_event_prop");
assert_eq!(
e.attributes[0].value,
crate::ocel::OCELAttributeValue::String("hello".to_string())
);
let o = &ocel.objects[0];
assert_eq!(o.attributes.len(), 1);
assert_eq!(o.attributes[0].name, "custom_obj_prop");
assert_eq!(
o.attributes[0].value,
crate::ocel::OCELAttributeValue::String("world".to_string())
);
}
}