use super::EventLogIssue;
pub fn validate_event_chain(
events: &crate::domain::model::event::EventLog,
current_status: &str,
statuses: &crate::domain::model::status::StatusesConfig,
) -> Vec<EventLogIssue> {
use crate::domain::model::event::EventAction;
let mut v = Vec::new();
let initial_status = if let Some(first) = events.first() {
match &first.action {
EventAction::Created { state: status } => {
if !statuses.contains_name(status.as_str()) {
v.push(EventLogIssue::CreatedStatusUnknown {
status: status.as_str().to_string(),
});
} else if status.as_str() != statuses.initial() {
v.push(EventLogIssue::CreatedStatusMismatch {
status: status.as_str().to_string(),
expected: statuses.initial().to_string(),
});
}
status.as_str()
}
_ => {
v.push(EventLogIssue::FirstActionNotCreated {
found: first.action.to_string(),
});
current_status
}
}
} else {
current_status
};
let status_events: Vec<&crate::domain::model::event::Event> = events
.iter()
.filter(|e| matches!(e.action, EventAction::StatusChanged { .. }))
.collect();
let mut prev_status = initial_status;
for event in &status_events {
let (from, to) = match &event.action {
EventAction::StatusChanged { from, to } => (from.as_str(), to.as_str()),
_ => unreachable!("filter above guarantees StatusChanged"),
};
if !statuses.contains_name(from) {
v.push(EventLogIssue::StatusChangeFromUnknown {
from: from.to_string(),
});
}
if !statuses.contains_name(to) {
v.push(EventLogIssue::StatusChangeToUnknown { to: to.to_string() });
}
if from != prev_status {
v.push(EventLogIssue::EventChainBroken {
from: from.to_string(),
prev_status: prev_status.to_string(),
});
}
let transition_ok = statuses
.resolve(from)
.map(|s| {
statuses
.next_for(&s)
.is_none_or(|next| next.iter().any(|n| n == to))
})
.unwrap_or(true); if !transition_ok {
v.push(EventLogIssue::InvalidTransition {
from: from.to_string(),
to: to.to_string(),
});
}
prev_status = to;
}
if let Some(last) = status_events.last() {
if let EventAction::StatusChanged { to, .. } = &last.action {
if to.as_str() != current_status {
v.push(EventLogIssue::FinalStatusMismatch {
last_to: to.as_str().to_string(),
current_status: current_status.to_string(),
});
}
}
}
v
}
#[cfg(test)]
pub mod strategy {
use crate::domain::model::event::{Event, EventAction, EventLog};
use crate::domain::model::status::StatusesConfig;
use crate::domain::model::temporal::timestamp::strategy::timestamp;
use proptest::prelude::*;
pub fn valid_creation_only_log(cfg: &StatusesConfig) -> impl Strategy<Value = EventLog> + '_ {
let initial = crate::domain::model::event::State::new(cfg.initial())
.expect("config initial must be valid State");
timestamp().prop_map(move |ts| {
EventLog::from_iter(std::iter::once(Event {
timestamp: ts,
action: EventAction::Created {
state: initial.clone(),
},
}))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::status::StatusesConfig;
use proptest::prelude::*;
proptest! {
#[test]
fn creation_only_log_validates_under_initial_status(
log in strategy::valid_creation_only_log(&StatusesConfig::default_issue())
) {
let cfg = StatusesConfig::default_issue();
let initial = cfg.initial().to_string();
let v = validate_event_chain(&log, &initial, &cfg);
prop_assert!(v.is_empty(), "expected clean log to validate, got {v:?}");
}
}
}