use super::occurrence;
use super::{nonempty, owns, task_mut};
use crate::{EngineError, GateReport, Transition, gates};
use chrono::{DateTime, Utc};
use dpm_model::{
ActorId, AttemptOutcome, BasisSource, DependencyBasis, Plan, SubmissionAttempt, Timeline,
WorkItemId, WorkStatus,
};
fn permit(
plan: &Plan,
work: WorkItemId,
transition: Transition,
at: DateTime<Utc>,
) -> Result<GateReport, EngineError> {
let item = plan
.work_items
.get(&work)
.ok_or(EngineError::MissingWorkItem(work))?;
let report = gates::evaluate(plan, item, transition, &Timeline::at(plan, at));
if report.ready {
return Ok(report);
}
if let Some(reason) = &item.execution.block_reason {
return Err(EngineError::Blocked {
work,
reason: reason.clone(),
});
}
Err(EngineError::NotReady {
work,
transition,
unmet: report.unmet,
})
}
fn require_status(
plan: &mut Plan,
actor: &ActorId,
work: WorkItemId,
expected: WorkStatus,
) -> Result<(), EngineError> {
let item = task_mut(plan, work)?;
owns(item, actor)?;
if item.execution.status == WorkStatus::Claimed && expected == WorkStatus::InProgress {
return Err(EngineError::NotStarted(work));
}
if item.execution.status != expected || item.execution.owner.as_ref() != Some(actor) {
return Err(EngineError::InvalidTransition {
work,
status: item.execution.status,
});
}
Ok(())
}
pub(super) fn claim(
plan: &mut Plan,
actor: &ActorId,
work: WorkItemId,
at: DateTime<Utc>,
) -> Result<(), EngineError> {
task_mut(plan, work)?;
permit(plan, work, Transition::Claim, at)?;
let item = task_mut(plan, work)?;
item.execution.owner = Some(actor.clone());
item.execution.status = WorkStatus::Claimed;
item.execution.events.claimed_at = Some(at);
Ok(())
}
pub(super) fn start(
plan: &mut Plan,
actor: &ActorId,
work: WorkItemId,
occurred_at: Option<DateTime<Utc>>,
committed_at: DateTime<Utc>,
) -> Result<(), EngineError> {
require_status(plan, actor, work, WorkStatus::Claimed)?;
let at = occurrence::resolve(plan, work, Transition::Start, occurred_at, committed_at)?;
let report = permit(plan, work, Transition::Start, at)?;
let item = task_mut(plan, work)?;
item.execution.status = WorkStatus::InProgress;
item.execution.events.started_at = Some(at);
item.execution.events.claimed_at = None;
item.execution.basis.extend(
report
.provisional
.into_iter()
.map(|release| DependencyBasis {
dependency: release.dependency,
predecessor: release.predecessor,
attempt: release.attempt,
recorded_at: at,
source: BasisSource::Start,
}),
);
Ok(())
}
pub(super) fn block(
plan: &mut Plan,
actor: &ActorId,
work: WorkItemId,
reason: &str,
) -> Result<(), EngineError> {
nonempty(&work.to_string(), "block reason", reason)?;
let item = task_mut(plan, work)?;
owns(item, actor)?;
if !matches!(
item.execution.status,
WorkStatus::Planned | WorkStatus::Claimed | WorkStatus::InProgress
) {
return Err(EngineError::InvalidTransition {
work: item.id,
status: item.execution.status,
});
}
if item.execution.status == WorkStatus::InProgress && item.execution.events.started_at.is_none()
{
item.execution.events.start_unrecorded = true;
}
item.execution.block_reason = Some(reason.into());
item.execution.status = WorkStatus::Blocked;
Ok(())
}
pub(super) fn unblock(
plan: &mut Plan,
actor: &ActorId,
work: WorkItemId,
) -> Result<(), EngineError> {
let item = task_mut(plan, work)?;
owns(item, actor)?;
if item.execution.status != WorkStatus::Blocked {
return Err(EngineError::InvalidTransition {
work: item.id,
status: item.execution.status,
});
}
item.execution.block_reason = None;
item.execution.status = if item.start_event().is_some() {
WorkStatus::InProgress
} else if item.execution.owner.is_some() {
WorkStatus::Claimed
} else {
WorkStatus::Planned
};
Ok(())
}
pub(super) fn report_progress(
plan: &mut Plan,
actor: &ActorId,
work: WorkItemId,
percent: u8,
) -> Result<(), EngineError> {
let item = task_mut(plan, work)?;
owns(item, actor)?;
let started_blocked =
item.execution.status == WorkStatus::Blocked && item.start_event().is_some();
if !started_blocked {
require_status(plan, actor, work, WorkStatus::InProgress)?;
}
let item = task_mut(plan, work)?;
if item.execution.owner.as_ref() != Some(actor) {
return Err(EngineError::InvalidTransition {
work,
status: item.execution.status,
});
}
if percent > 100 {
return Err(EngineError::InvalidCommand {
entity: work.to_string(),
reason: "progress must be between 0 and 100".into(),
});
}
item.execution.reported_progress_percent = percent;
Ok(())
}
pub(super) fn submit(
plan: &mut Plan,
actor: &ActorId,
work: WorkItemId,
occurred_at: Option<DateTime<Utc>>,
committed_at: DateTime<Utc>,
) -> Result<(), EngineError> {
require_status(plan, actor, work, WorkStatus::InProgress)?;
let at = occurrence::resolve(plan, work, Transition::Submit, occurred_at, committed_at)?;
permit(plan, work, Transition::Submit, at)?;
let item = task_mut(plan, work)?;
item.execution.status = WorkStatus::Submitted;
item.execution.events.submitted_at = Some(at);
let number = item
.execution
.attempts
.last()
.map_or(1, |a| a.number.wrapping_add(1));
item.execution.attempts.push(SubmissionAttempt {
number,
submitted_at: at,
outcome: AttemptOutcome::Pending,
});
Ok(())
}
pub(super) fn verify(
plan: &mut Plan,
actor: &ActorId,
work: WorkItemId,
occurred_at: Option<DateTime<Utc>>,
committed_at: DateTime<Utc>,
) -> Result<(), EngineError> {
let item = task_mut(plan, work)?;
if item.execution.status != WorkStatus::Submitted {
return Err(EngineError::NotSubmitted(work));
}
if item.execution.owner.as_ref() == Some(actor) {
return Err(EngineError::SelfVerification(work));
}
if item.held_by(actor) {
return Err(EngineError::ActorNotAllowed {
actor: actor.clone(),
action: "verify work it held before a handoff or release",
});
}
super::refuse_evidence_author(plan, actor, work, "verify work whose evidence it authored")?;
let at = occurrence::resolve(plan, work, Transition::Verify, occurred_at, committed_at)?;
permit(plan, work, Transition::Verify, at)?;
let item = task_mut(plan, work)?;
item.execution.status = WorkStatus::Verified;
item.execution.events.verified_at = Some(at);
if let Some(attempt) = item
.execution
.attempts
.last_mut()
.filter(|a| a.outcome == AttemptOutcome::Pending)
{
attempt.outcome = AttemptOutcome::Verified {
actor: actor.clone(),
at,
};
}
Ok(())
}
#[cfg(test)]
mod tests;