rustvello_proto/status/
machine.rs1use std::fmt;
2
3use crate::identifiers::RunnerId;
4
5use super::{InvocationStatus, InvocationStatusRecord, STATUS_CONFIG};
6
7#[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#[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#[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
76pub 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
100pub 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 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
153pub 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
169pub 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}