Skip to main content

dpm_engine/run/
link.rs

1//! Deciding whether a committed project operation belongs to a run.
2
3use super::{LinkRefusal, RunError};
4use crate::{Command, Operation};
5use chrono::{DateTime, Utc};
6use dpm_model::{ActorId, LineageId, OperationId, RunRecord, WorkItemId, WorkspaceId};
7
8/// What a committed operation records about itself, as the project store returned it.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct OperationFacts {
11    /// The operation's identity.
12    pub id: OperationId,
13    /// The principal that made it.
14    pub actor: ActorId,
15    /// When it committed.
16    pub timestamp: DateTime<Utc>,
17    /// The single task it acts on, if its command acts on one.
18    pub work: Option<WorkItemId>,
19    /// The workspace it changed.
20    pub workspace: WorkspaceId,
21    /// The lineage of the store that committed it.
22    pub lineage: LineageId,
23}
24
25impl OperationFacts {
26    /// Read the facts from a recorded operation.
27    #[must_use]
28    pub fn of(operation: &Operation, workspace: WorkspaceId, lineage: LineageId) -> Self {
29        Self {
30            id: operation.id,
31            actor: operation.actor.clone(),
32            timestamp: operation.timestamp,
33            work: command_work(&operation.command),
34            workspace,
35            lineage,
36        }
37    }
38}
39
40/// The one task a command acts on; commands that act on none, or on many, name no task.
41///
42/// Every command is listed so that a new one must decide whether a run can perform it.
43fn command_work(command: &Command) -> Option<WorkItemId> {
44    match command {
45        Command::RatifyContract { work }
46        | Command::Reject { work, .. }
47        | Command::Claim { work }
48        | Command::Release { work, .. }
49        | Command::Handoff { work, .. }
50        | Command::Start { work, .. }
51        | Command::Block { work, .. }
52        | Command::Unblock { work }
53        | Command::ReportProgress { work, .. }
54        | Command::Submit { work, .. }
55        | Command::Verify { work, .. }
56        | Command::AttachArtifact { work, .. }
57        | Command::UnlinkExternal { work, .. }
58        | Command::RevalidateBasis { work, .. } => Some(*work),
59        Command::LinkExternal(request) => Some(request.work),
60        Command::ApplyChange { .. }
61        | Command::RevertChange { .. }
62        | Command::WaiveDependency { .. }
63        | Command::RestoreDependency { .. }
64        | Command::Decide { .. } => None,
65    }
66}
67
68/// Check that attributing `operation` to `record` cannot be wrong.
69///
70/// The operation must have been committed in the run's workspace and lineage, on the run's task, by
71/// the run's executor, no earlier than the run started and, if the run ended at `ended_at`, no
72/// later than that. A link is therefore evidence about what the run did, never a way to move
73/// someone else's operation onto it after the fact.
74pub fn check_link(
75    record: &RunRecord,
76    ended_at: Option<DateTime<Utc>>,
77    operation: &OperationFacts,
78) -> Result<(), RunError> {
79    let refuse = |reason| {
80        Err(RunError::LinkRefused {
81            run: record.id,
82            operation: operation.id,
83            reason,
84        })
85    };
86    if operation.workspace != record.contract.workspace_id {
87        return refuse(LinkRefusal::OtherWorkspace);
88    }
89    if operation.lineage != record.contract.lineage_id {
90        return refuse(LinkRefusal::OtherLineage);
91    }
92    if operation.work != Some(record.work) {
93        return refuse(LinkRefusal::OtherWork);
94    }
95    if operation.actor != record.executor {
96        return refuse(LinkRefusal::OtherActor);
97    }
98    if operation.timestamp < record.started_at {
99        return refuse(LinkRefusal::BeforeStart);
100    }
101    if ended_at.is_some_and(|ended| operation.timestamp > ended) {
102        return refuse(LinkRefusal::AfterEnd);
103    }
104    Ok(())
105}