use std::collections::HashMap;
use super::event::{Actor, AuditEvent, AuditEventBuilder, EntityType, EventContext, ops};
use super::materialize::EntityKey;
pub type FileState = HashMap<EntityKey, String>;
#[derive(Debug, Clone, PartialEq)]
pub enum Drift {
Missing {
key: EntityKey,
file_status: String,
},
Diverged {
key: EntityKey,
log_status: String,
file_status: String,
},
Extra {
key: EntityKey,
log_status: String,
},
}
impl std::fmt::Display for Drift {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Drift::Missing { key, file_status } => write!(
f,
"Missing: {}/{} (scope: {:?}) has file status '{}' but no audit events",
key.entity, key.entity_id, key.scope, file_status
),
Drift::Diverged {
key,
log_status,
file_status,
} => write!(
f,
"Diverged: {}/{} (scope: {:?}) audit='{}' file='{}'",
key.entity, key.entity_id, key.scope, log_status, file_status
),
Drift::Extra { key, log_status } => write!(
f,
"Extra: {}/{} (scope: {:?}) has audit status '{}' but no file entry",
key.entity, key.entity_id, key.scope, log_status
),
}
}
}
pub fn compute_drift(
audit_entities: &HashMap<EntityKey, String>,
file_state: &FileState,
) -> Vec<Drift> {
let mut drifts = Vec::new();
for (key, file_status) in file_state {
match audit_entities.get(key) {
None => {
drifts.push(Drift::Missing {
key: key.clone(),
file_status: file_status.clone(),
});
}
Some(log_status) if log_status != file_status => {
drifts.push(Drift::Diverged {
key: key.clone(),
log_status: log_status.clone(),
file_status: file_status.clone(),
});
}
Some(_) => {
}
}
}
for (key, log_status) in audit_entities {
if key.entity == "task" && !file_state.contains_key(key) {
drifts.push(Drift::Extra {
key: key.clone(),
log_status: log_status.clone(),
});
}
}
drifts.sort_by(|a, b| {
let key_a = match a {
Drift::Missing { key, .. } => key,
Drift::Diverged { key, .. } => key,
Drift::Extra { key, .. } => key,
};
let key_b = match b {
Drift::Missing { key, .. } => key,
Drift::Diverged { key, .. } => key,
Drift::Extra { key, .. } => key,
};
(&key_a.entity, &key_a.entity_id).cmp(&(&key_b.entity, &key_b.entity_id))
});
drifts
}
pub fn generate_compensating_events(
drifts: &[Drift],
scope: Option<&str>,
ctx: &EventContext,
) -> Vec<AuditEvent> {
let mut events = Vec::new();
for drift in drifts {
let event = match drift {
Drift::Missing { key, file_status } => AuditEventBuilder::new()
.entity(parse_entity_type(&key.entity))
.entity_id(&key.entity_id)
.op(ops::RECONCILED)
.to(file_status)
.actor(Actor::Reconcile)
.by("@reconcile")
.meta(serde_json::json!({
"reason": format!(
"{} '{}' has file status '{}' but no audit events",
key.entity, key.entity_id, file_status
)
}))
.ctx(ctx.clone()),
Drift::Diverged {
key,
log_status,
file_status,
} => AuditEventBuilder::new()
.entity(parse_entity_type(&key.entity))
.entity_id(&key.entity_id)
.op(ops::RECONCILED)
.from(log_status)
.to(file_status)
.actor(Actor::Reconcile)
.by("@reconcile")
.meta(serde_json::json!({
"reason": format!(
"{} '{}' audit status '{}' differs from file status '{}'",
key.entity, key.entity_id, log_status, file_status
)
}))
.ctx(ctx.clone()),
Drift::Extra { key, log_status } => AuditEventBuilder::new()
.entity(parse_entity_type(&key.entity))
.entity_id(&key.entity_id)
.op(ops::RECONCILED)
.from(log_status)
.actor(Actor::Reconcile)
.by("@reconcile")
.meta(serde_json::json!({
"reason": format!(
"{} '{}' has audit status '{}' but no file entry",
key.entity, key.entity_id, log_status
)
}))
.ctx(ctx.clone()),
};
let event = if let Some(s) = scope {
event.scope(s)
} else if let Some(s) = match drift {
Drift::Missing { key, .. } => key.scope.as_deref(),
Drift::Diverged { key, .. } => key.scope.as_deref(),
Drift::Extra { key, .. } => key.scope.as_deref(),
} {
event.scope(s)
} else {
event
};
if let Some(built) = event.build() {
events.push(built);
}
}
events
}
fn parse_entity_type(s: &str) -> EntityType {
match s {
"task" => EntityType::Task,
"change" => EntityType::Change,
"module" => EntityType::Module,
"wave" => EntityType::Wave,
"planning" => EntityType::Planning,
"config" => EntityType::Config,
_ => EntityType::Task,
}
}
#[cfg(test)]
#[path = "reconcile_tests.rs"]
mod reconcile_tests;