1use serde::{Deserialize, Serialize};
14
15use crate::{Address, AxCode, AxError, ByteLen, GoalId, Payload, PursuitState, TimeMs, Version};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21pub struct GoalConflict {
22 pub goal: GoalId,
23 pub with: GoalId,
24 pub level: ConflictLevel,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
33pub enum ConflictLevel {
34 Serialize,
35 Arbitrate,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45pub struct PursuitChanged {
46 pub step: PursuitMove,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub goal: Option<String>,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
55pub enum PursuitMove {
56 Set,
57 Pause,
58 Resume,
59 Clear,
60}
61
62impl PursuitChanged {
63 pub fn held(self) -> Result<Option<(String, PursuitState)>, AxError> {
71 let state = match self.step {
72 PursuitMove::Set | PursuitMove::Resume => PursuitState::Running,
73 PursuitMove::Pause => PursuitState::Paused,
74 PursuitMove::Clear => return Ok(None),
75 };
76 let goal = self.goal.ok_or_else(|| {
77 AxError::failure(
78 AxCode::WireMismatch,
79 "read a pursuit_changed line",
80 "a step that leaves a pursuit names no goal",
81 )
82 .with_recovery("replay with the build that wrote this record")
83 })?;
84 Ok(Some((goal, state)))
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct SignalEnqueued {
95 pub id: SignalId,
96 pub kind: SignalKind,
97 pub from: String,
98 pub room: Address,
99 pub room_version: Version,
100 pub payload: Payload,
101 pub at: TimeMs,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub lane: Option<Lane>,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct SignalConsumed {
114 pub id: SignalId,
115 pub by: String,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct SignalWaitStarted {
125 pub on: Address,
126 pub signal: SignalId,
127 pub deadline_ms: u64,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct SignalWaitEnded {
134 pub signal: SignalId,
135 pub by: WaitEnd,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "snake_case", tag = "end")]
142pub enum WaitEnd {
143 Reply { reply: SignalId },
145 Timeout,
147 Left,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct SignalLanded {
157 pub signal: SignalId,
158 pub landing: Landing,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(rename_all = "snake_case")]
165#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
166pub enum Landing {
167 Delivered,
169 Queued,
172 Knocked,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct WorktreeOpened {
182 pub name: String,
183 pub disk_bytes: ByteLen,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "snake_case")]
189pub enum Lane {
190 Urgent,
191 Ordinary,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
200#[serde(try_from = "String", into = "String")]
201pub struct SignalId(String);
202
203impl TryFrom<String> for SignalId {
204 type Error = AxError;
205
206 fn try_from(raw: String) -> Result<SignalId, AxError> {
207 SignalId::parse(&raw)
208 }
209}
210
211impl From<SignalId> for String {
212 fn from(id: SignalId) -> String {
213 id.0
214 }
215}
216
217impl SignalId {
218 pub fn parse(raw: &str) -> Result<SignalId, AxError> {
223 if raw.is_empty() || raw.chars().any(char::is_whitespace) {
224 return Err(AxError::failure(
225 AxCode::InvalidArgs,
226 "read a signal id",
227 format!("{raw:?}"),
228 )
229 .with_recovery(
230 "use a non-empty id with no whitespace, such as a run id and a counter",
231 ));
232 }
233 Ok(SignalId(raw.to_owned()))
234 }
235
236 #[must_use]
237 pub fn as_str(&self) -> &str {
238 &self.0
239 }
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(try_from = "String", into = "String")]
252pub enum SignalKind {
253 Mention,
254 Thread,
255 Broadcast,
256 Steer,
257}
258
259impl SignalKind {
260 #[must_use]
261 pub fn as_str(self) -> &'static str {
262 match self {
263 SignalKind::Mention => "mention",
264 SignalKind::Thread => "thread",
265 SignalKind::Broadcast => "broadcast",
266 SignalKind::Steer => "steer",
267 }
268 }
269
270 pub fn parse(raw: &str) -> Result<SignalKind, AxError> {
273 match raw {
274 "mention" => Ok(SignalKind::Mention),
275 "thread" => Ok(SignalKind::Thread),
276 "broadcast" => Ok(SignalKind::Broadcast),
277 "steer" => Ok(SignalKind::Steer),
278 other => {
279 Err(
280 AxError::failure(AxCode::InvalidArgs, "read a signal kind", other.to_owned())
281 .with_recovery("mention, thread, broadcast or steer"),
282 )
283 }
284 }
285 }
286}
287
288impl TryFrom<String> for SignalKind {
289 type Error = AxError;
290
291 fn try_from(raw: String) -> Result<SignalKind, AxError> {
292 SignalKind::parse(&raw)
293 }
294}
295
296impl From<SignalKind> for String {
297 fn from(kind: SignalKind) -> String {
298 kind.as_str().to_owned()
299 }
300}
301
302#[cfg(test)]
303#[allow(clippy::unwrap_used, clippy::expect_used, reason = "test code")]
304mod tests {
305 use super::*;
306
307 #[test]
310 fn a_pursuit_step_writes_the_keys_the_hand_written_map_wrote() {
311 let hand = |json: serde_json::Value| {
312 serde_json::to_string(&Payload::new(json.as_object().unwrap().clone()).unwrap())
313 .unwrap()
314 };
315 let typed =
316 |record: &PursuitChanged| serde_json::to_string(&Payload::of(record).unwrap()).unwrap();
317 let paused = PursuitChanged {
318 step: PursuitMove::Pause,
319 goal: Some("read the meter".to_owned()),
320 };
321 let cleared = PursuitChanged {
322 step: PursuitMove::Clear,
323 goal: None,
324 };
325 assert_eq!(
326 (typed(&paused), typed(&cleared)),
327 (
328 hand(serde_json::json!({ "step": "pause", "goal": "read the meter" })),
329 hand(serde_json::json!({ "step": "clear" })),
330 )
331 );
332 assert_eq!(
333 (paused.held().unwrap(), cleared.held().unwrap()),
334 (
335 Some(("read the meter".to_owned(), PursuitState::Paused)),
336 None
337 )
338 );
339 }
340
341 #[test]
344 fn a_worktree_line_writes_the_keys_the_hand_written_map_wrote() {
345 let opened = WorktreeOpened {
346 name: "node-1".to_owned(),
347 disk_bytes: ByteLen::new(6),
348 };
349 assert_eq!(
350 serde_json::to_string(&Payload::of(&opened).unwrap()).unwrap(),
351 r#"{"disk_bytes":6,"name":"node-1"}"#
352 );
353 }
354
355 #[test]
358 fn a_goal_line_writes_the_keys_the_hand_written_map_wrote() {
359 let conflict = GoalConflict {
360 goal: GoalId::new("b").unwrap(),
361 with: GoalId::new("a").unwrap(),
362 level: ConflictLevel::Arbitrate,
363 };
364 let entry = crate::GoalEntry {
365 id: GoalId::new("a").unwrap(),
366 owner: "lab/a".to_owned(),
367 resources: vec![crate::GoalResource::External("the printer".to_owned())],
368 statement: "print".to_owned(),
369 standing: true,
370 };
371 let bytes = |payload: Payload| serde_json::to_string(&payload).unwrap();
372 assert_eq!(
373 (
374 bytes(Payload::of(&conflict).unwrap()),
375 bytes(Payload::of(&entry).unwrap()),
376 ),
377 (
378 r#"{"goal":"b","level":"arbitrate","with":"a"}"#.to_owned(),
379 r#"{"id":"a","owner":"lab/a","resources":[{"external":"the printer"}],"standing":true,"statement":"print"}"#.to_owned(),
380 )
381 );
382 assert_eq!(
383 Payload::of(&conflict)
384 .unwrap()
385 .read::<GoalConflict>()
386 .unwrap(),
387 conflict
388 );
389 }
390}