use std::collections::{HashMap, HashSet};
use super::{ObjectTypeCardinality, OCEL};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ValidationError {
pub code: String,
pub message: String,
}
impl ValidationError {
fn new(code: &str, message: impl Into<String>) -> Self {
Self {
code: code.to_string(),
message: message.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ValidationReport {
pub valid: bool,
pub errors: Vec<ValidationError>,
}
impl ValidationReport {
fn from_errors(errors: Vec<ValidationError>) -> Self {
Self {
valid: errors.is_empty(),
errors,
}
}
}
#[must_use]
pub fn validate(
ocel: &OCEL,
cardinality: &HashMap<String, ObjectTypeCardinality>,
) -> ValidationReport {
let mut errors = Vec::new();
let declared_event_types: HashSet<&str> =
ocel.event_types.iter().map(|t| t.name.as_str()).collect();
let declared_object_types: HashSet<&str> =
ocel.object_types.iter().map(|t| t.name.as_str()).collect();
let mut object_ids: HashSet<&str> = HashSet::new();
for o in &ocel.objects {
if !object_ids.insert(o.id.as_str()) {
errors.push(ValidationError::new(
"DUPLICATE_OBJECT_ID",
format!("object id '{}' declared more than once", o.id),
));
}
if !declared_object_types.contains(o.object_type.as_str()) {
errors.push(ValidationError::new(
"UNDECLARED_OBJECT_TYPE",
format!(
"object '{}' has type '{}' not in objectTypes",
o.id, o.object_type
),
));
}
}
let mut event_ids: HashSet<&str> = HashSet::new();
for e in &ocel.events {
if !event_ids.insert(e.id.as_str()) {
errors.push(ValidationError::new(
"DUPLICATE_EVENT_ID",
format!("event id '{}' declared more than once", e.id),
));
}
if !declared_event_types.contains(e.event_type.as_str()) {
errors.push(ValidationError::new(
"UNDECLARED_EVENT_TYPE",
format!(
"event '{}' has type '{}' not in eventTypes",
e.id, e.event_type
),
));
}
if e.relationships.is_empty() {
errors.push(ValidationError::new(
"E2O_EMPTY",
format!(
"event '{}' has no qualified object reference (OCPQ Def. 2)",
e.id
),
));
}
for r in &e.relationships {
if !object_ids.contains(r.object_id.as_str()) {
errors.push(ValidationError::new(
"DANGLING_E2O",
format!(
"event '{}' references unknown object '{}' (qualifier '{}')",
e.id, r.object_id, r.qualifier
),
));
}
}
}
for o in &ocel.objects {
for r in &o.relationships {
if !object_ids.contains(r.object_id.as_str()) {
errors.push(ValidationError::new(
"DANGLING_O2O",
format!(
"object '{}' references unknown object '{}' (qualifier '{}')",
o.id, r.object_id, r.qualifier
),
));
}
}
}
for (type_name, card) in cardinality {
let count = ocel.count_objects_of_type(type_name);
if let Some(min) = card.min_count {
if count < min {
errors.push(ValidationError::new(
"CARDINALITY_MIN",
format!(
"object type '{type_name}' has {count} instances, below min_count {min}"
),
));
}
}
if let Some(max) = card.max_count {
if count > max {
errors.push(ValidationError::new(
"CARDINALITY_MAX",
format!(
"object type '{type_name}' has {count} instances, above max_count {max}"
),
));
}
}
}
ValidationReport::from_errors(errors)
}