Skip to main content

rustvello_proto/status/
machine.rs

1use std::fmt;
2
3use crate::identifiers::RunnerId;
4
5use super::{InvocationStatus, InvocationStatusRecord, STATUS_CONFIG};
6
7// ============================================================================
8// State Machine Error Types
9// ============================================================================
10
11/// Error returned when a status transition is invalid.
12#[derive(Debug, Clone)]
13pub struct StatusTransitionError {
14    pub from: Option<InvocationStatus>,
15    pub to: InvocationStatus,
16    pub allowed: Vec<InvocationStatus>,
17}
18
19impl fmt::Display for StatusTransitionError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self.from {
22            Some(from) => write!(f, "invalid status transition: {} -> {}", from, self.to),
23            None => write!(f, "invalid initial status: {}", self.to),
24        }
25    }
26}
27
28/// Error returned when ownership rules are violated.
29#[derive(Debug, Clone)]
30pub struct OwnershipError {
31    pub from_status: InvocationStatus,
32    pub to_status: InvocationStatus,
33    pub current_owner: Option<String>,
34    pub attempted_owner: Option<String>,
35    pub reason: String,
36}
37
38impl fmt::Display for OwnershipError {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        write!(
41            f,
42            "ownership violation ({} -> {}): {}",
43            self.from_status, self.to_status, self.reason
44        )
45    }
46}
47
48/// Combined error type for the state machine.
49#[derive(Debug, Clone)]
50#[non_exhaustive]
51pub enum StatusMachineError {
52    Transition(StatusTransitionError),
53    Ownership(OwnershipError),
54}
55
56impl fmt::Display for StatusMachineError {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            Self::Transition(e) => write!(f, "{e}"),
60            Self::Ownership(e) => write!(f, "{e}"),
61        }
62    }
63}
64
65impl std::error::Error for StatusTransitionError {}
66impl std::error::Error for OwnershipError {}
67impl std::error::Error for StatusMachineError {
68    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69        match self {
70            Self::Transition(e) => Some(e),
71            Self::Ownership(e) => Some(e),
72        }
73    }
74}
75
76// ============================================================================
77// State Machine Functions
78// ============================================================================
79
80/// Validate state transition or return error.
81pub fn validate_transition(
82    from_status: Option<InvocationStatus>,
83    to_status: InvocationStatus,
84) -> Result<(), StatusTransitionError> {
85    let definition = match from_status {
86        Some(s) => STATUS_CONFIG.definition(s),
87        None => &STATUS_CONFIG.initial,
88    };
89    if definition.allowed_transitions.contains(&to_status) {
90        Ok(())
91    } else {
92        Err(StatusTransitionError {
93            from: from_status,
94            to: to_status,
95            allowed: definition.allowed_transitions.clone(),
96        })
97    }
98}
99
100/// Validate ownership requirements for a transition.
101pub fn validate_ownership(
102    current_record: Option<&InvocationStatusRecord>,
103    new_status: InvocationStatus,
104    runner_id: Option<&RunnerId>,
105) -> Result<(), OwnershipError> {
106    let current_record = match current_record {
107        Some(r) => r,
108        None => return Ok(()),
109    };
110
111    let new_def = STATUS_CONFIG.definition(new_status);
112
113    // Allow transitions to statuses that override ownership validation
114    if new_def.overrides_ownership {
115        return Ok(());
116    }
117
118    let current_def = STATUS_CONFIG.definition(current_record.status);
119
120    if current_def.requires_ownership {
121        let current_owner = current_record.runner_id.as_ref().map(RunnerId::as_str);
122        let requester = runner_id.map(RunnerId::as_str);
123        if requester != current_owner {
124            return Err(OwnershipError {
125                from_status: current_record.status,
126                to_status: new_status,
127                current_owner: current_owner.map(String::from),
128                attempted_owner: requester.map(String::from),
129                reason: format!(
130                    "status requires ownership by runner '{}'",
131                    current_owner.unwrap_or("<none>")
132                ),
133            });
134        }
135    }
136
137    if new_def.acquires_ownership && runner_id.is_none() {
138        return Err(OwnershipError {
139            from_status: current_record.status,
140            to_status: new_status,
141            current_owner: current_record
142                .runner_id
143                .as_ref()
144                .map(|r| r.as_str().to_string()),
145            attempted_owner: None,
146            reason: format!("status {new_status} requires a runner_id to acquire ownership"),
147        });
148    }
149
150    Ok(())
151}
152
153/// Compute new owner based on status transition.
154pub fn compute_new_owner(
155    current_record: Option<&InvocationStatusRecord>,
156    new_status: InvocationStatus,
157    runner_id: Option<RunnerId>,
158) -> Option<RunnerId> {
159    let new_def = STATUS_CONFIG.definition(new_status);
160    if new_def.releases_ownership {
161        None
162    } else if new_def.acquires_ownership {
163        runner_id
164    } else {
165        current_record.and_then(|r| r.runner_id.clone())
166    }
167}
168
169/// Execute a status change with full validation (transition + ownership).
170///
171/// Returns a new `InvocationStatusRecord` with the correct owner.
172pub fn status_record_transition(
173    current_record: Option<&InvocationStatusRecord>,
174    new_status: InvocationStatus,
175    runner_id: Option<&RunnerId>,
176) -> Result<InvocationStatusRecord, StatusMachineError> {
177    let from_status = current_record.map(|r| r.status);
178
179    validate_transition(from_status, new_status).map_err(StatusMachineError::Transition)?;
180    validate_ownership(current_record, new_status, runner_id)
181        .map_err(StatusMachineError::Ownership)?;
182
183    let new_owner = compute_new_owner(current_record, new_status, runner_id.cloned());
184    Ok(InvocationStatusRecord::new(new_status, new_owner))
185}