printobserver_types/event.rs
1//! The history: one closed pair of an event kind and the payload it carries.
2//!
3//! [`EventKind`] and [`EventPayload`] are one closed pair rather than two
4//! fields that happen to agree. Every kind has exactly one payload variant and
5//! every payload variant has exactly one kind, so a consumer that matches the
6//! kind knows the payload's shape, and a mismatched pair is unrepresentable
7//! rather than merely undocumented: an [`EventRecord`] carries the payload, the
8//! `kind` on the wire is that payload's own tag, and a serialized record naming
9//! one kind while carrying another's payload fails to parse.
10
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use crate::action::{AcknowledgementDisposition, PrintAction};
15use crate::adjustable::Adjustable;
16use crate::assessment::AgentAssessment;
17use crate::ids::{ActionId, EventId, InterventionId, PrintId};
18use crate::image::ImageRef;
19use crate::intervention::InterventionOutcome;
20use crate::policy::PolicyDecision;
21use crate::raw::RawBytes;
22use crate::timestamp::Timestamp;
23
24/// Where an event came from.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
26#[serde(rename_all = "snake_case", deny_unknown_fields)]
27pub enum EventSource {
28 /// Obico, over its webhook.
29 Obico,
30 /// A person.
31 Operator,
32 /// The supervising agent.
33 Agent,
34 /// The supervisor itself.
35 System,
36}
37
38/// The kind of printer notification Obico sent, normalized.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
40#[serde(rename_all = "snake_case", deny_unknown_fields)]
41pub enum ObicoNotificationType {
42 /// A print started.
43 Started,
44 /// A print finished.
45 Done,
46 /// A print was cancelled.
47 Cancelled,
48 /// A print was paused.
49 Paused,
50 /// A print was resumed.
51 Resumed,
52 /// The printer is waiting for a filament change.
53 FilamentChange,
54 /// A heater cooled down.
55 HeaterCooled,
56 /// A heater reached its target.
57 HeaterTarget,
58}
59
60/// Obico reported a print failure.
61///
62/// The two instants are optional because Obico's own field for each is a Unix
63/// timestamp number, an empty string, or absent, and the last two both mean the
64/// producer reported no instant. An absent field here is that, never an epoch
65/// date standing in for it.
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
67#[serde(deny_unknown_fields)]
68pub struct ObicoFailureAlertPayload {
69 /// Whether Obico called it a warning rather than a failure.
70 pub is_warning: bool,
71 /// Whether Obico paused the print itself.
72 pub print_paused: bool,
73 /// Obico's own identifier for the print, when it named one.
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub obico_print_id: Option<i64>,
76 /// The file being printed, when Obico named one.
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub file_name: Option<String>,
79 /// When the print started, when Obico reported an instant for it.
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub started_at: Option<Timestamp>,
82 /// When the print ended, when Obico reported an instant for it.
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub ended_at: Option<Timestamp>,
85}
86
87/// Obico sent a printer notification.
88///
89/// The two instants are optional for the same reason
90/// [`ObicoFailureAlertPayload`]'s are, and are absent along with the rest of
91/// the print's fields when the notification is about no print at all.
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
93#[serde(deny_unknown_fields)]
94pub struct ObicoPrinterNotificationPayload {
95 /// Which notification it is.
96 pub notification_type: ObicoNotificationType,
97 /// Obico's own identifier for the print, when the notification is about one.
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub obico_print_id: Option<i64>,
100 /// The file being printed, when the notification is about one.
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub file_name: Option<String>,
103 /// When the print started, when Obico reported an instant for it.
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub started_at: Option<Timestamp>,
106 /// When the print ended, when Obico reported an instant for it.
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub ended_at: Option<Timestamp>,
109}
110
111/// An external body arrived that could not be read.
112///
113/// This kind always carries its `raw` bytes, and it exists so that an alert
114/// this system cannot read is written down rather than dropped.
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
116#[serde(deny_unknown_fields)]
117pub struct MalformedExternalEventPayload {
118 /// One line saying why the body could not be read.
119 pub detail: String,
120}
121
122/// Where a port failed while an event was being handled.
123///
124/// A closed set of exactly the sites at which a failure has nowhere else to be
125/// recorded. The printer's action methods record theirs on the
126/// [`ActionRecord`](crate::ActionRecord) the request minted, and a restoring
127/// call records its own on the [`Intervention`](crate::Intervention) it was
128/// expiring; those are not sites here, because a second record of them would be
129/// a second version of one fact.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
131#[serde(rename_all = "snake_case", deny_unknown_fields)]
132pub enum PortFailureSite {
133 /// Reading the printer's own state.
134 PrinterSnapshot,
135 /// Reading the job the printer reports it is running.
136 PrinterJob,
137 /// Writing the image the event arrived with.
138 ImageWrite,
139 /// Running the supervision turn the event prompted.
140 SupervisionTurn,
141}
142
143/// A port failed while one event was being handled.
144///
145/// The event is named rather than implied, so that a reader holding an event's
146/// identifier reaches every failure recorded while that event was being
147/// handled. A failure recorded here is one the handling survived: the event is
148/// already in the history by the time any of these sites is reached, and the
149/// loop goes on to handle the next event.
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
151#[serde(deny_unknown_fields)]
152pub struct PortFailurePayload {
153 /// The event whose handling reached the failing call.
154 pub event_id: EventId,
155 /// Where it failed.
156 pub site: PortFailureSite,
157 /// What the port said about it, in the port's own words.
158 pub detail: String,
159}
160
161/// An actor asked for an action.
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
163#[serde(deny_unknown_fields)]
164pub struct ActionRequestedPayload {
165 /// The action's identifier.
166 pub action_id: ActionId,
167 /// What was asked for.
168 pub action: PrintAction,
169 /// Who asked.
170 pub actor: crate::action::Actor,
171}
172
173/// An accepted action reached the printer.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
175#[serde(deny_unknown_fields)]
176pub struct ActionExecutedPayload {
177 /// The action's identifier.
178 pub action_id: ActionId,
179 /// The bounded intervention it opened, when it opened one.
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub intervention_id: Option<InterventionId>,
182}
183
184/// Policy refused an action.
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
186#[serde(deny_unknown_fields)]
187pub struct ActionRejectedPayload {
188 /// The action's identifier.
189 pub action_id: ActionId,
190 /// The whole decision, carrying which rejection it was.
191 pub decision: PolicyDecision,
192}
193
194/// A bounded intervention expired.
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
196#[serde(deny_unknown_fields)]
197pub struct InterventionExpiredPayload {
198 /// The intervention's identifier.
199 pub intervention_id: InterventionId,
200 /// What it had changed.
201 pub adjustable: Adjustable,
202 /// What became of it.
203 pub outcome: InterventionOutcome,
204}
205
206/// A supervision session was opened.
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
208#[serde(deny_unknown_fields)]
209pub struct SupervisionSessionOpenedPayload {
210 /// The session's own name in the harness.
211 pub session_name: String,
212 /// The identity the harness ran it under.
213 pub harness_identity: String,
214}
215
216/// A supervision session was closed.
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
218#[serde(deny_unknown_fields)]
219pub struct SupervisionSessionClosedPayload {
220 /// The session's own name in the harness.
221 pub session_name: String,
222 /// Why it was closed.
223 pub close_reason: String,
224}
225
226/// The agent wrote down what it made of a turn.
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
228#[serde(deny_unknown_fields)]
229pub struct AgentAssessmentPayload {
230 /// The session the turn ran in.
231 pub session_name: String,
232 /// What the agent answered with.
233 pub assessment: AgentAssessment,
234}
235
236/// An operator acknowledged an event.
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
238#[serde(deny_unknown_fields)]
239pub struct OperatorAcknowledgementPayload {
240 /// The event being acknowledged.
241 pub acknowledged_event_id: EventId,
242 /// What the operator asked for next.
243 pub disposition: AcknowledgementDisposition,
244}
245
246/// What one restart put back the way it found it.
247///
248/// A supervisor that has been restarted adopts whatever the store holds rather
249/// than starting empty, and each of these is one of those adoptions. They are
250/// recorded rather than merely done, because a print that carried on across a
251/// restart and one that was started again look identical afterwards unless the
252/// history says which happened.
253#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
254#[serde(rename_all = "snake_case", deny_unknown_fields)]
255pub enum StartupOutcome {
256 /// A print left open was adopted as the print this supervisor is watching.
257 PrintAdopted,
258 /// A session left open was resumed rather than replaced.
259 SessionResumed {
260 /// The session's own name in the harness.
261 session_name: String,
262 },
263 /// An intervention already past its expiry was expired on start.
264 InterventionExpired {
265 /// The intervention that had outlived its bound.
266 intervention_id: InterventionId,
267 /// What it had changed.
268 adjustable: Adjustable,
269 /// What became of putting the prior value back.
270 outcome: InterventionOutcome,
271 },
272}
273
274/// A supervisor reconciled one thing the store held when it started.
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
276#[serde(deny_unknown_fields)]
277pub struct StartupReconciliationPayload {
278 /// The print it is about.
279 pub print_id: PrintId,
280 /// What was reconciled.
281 pub outcome: StartupOutcome,
282}
283
284/// Which event this is, without its payload.
285///
286/// Every kind here has exactly one [`EventPayload`] variant, and the spellings
287/// are the same on the wire.
288#[derive(
289 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
290)]
291#[serde(rename_all = "snake_case", deny_unknown_fields)]
292pub enum EventKind {
293 /// Obico reported a print failure.
294 ObicoFailureAlert,
295 /// Obico sent a printer notification.
296 ObicoPrinterNotification,
297 /// An external body arrived that could not be read.
298 MalformedExternalEvent,
299 /// An actor asked for an action.
300 ActionRequested,
301 /// An accepted action reached the printer.
302 ActionExecuted,
303 /// Policy refused an action.
304 ActionRejected,
305 /// A bounded intervention expired.
306 InterventionExpired,
307 /// A supervision session was opened.
308 SupervisionSessionOpened,
309 /// A supervision session was closed.
310 SupervisionSessionClosed,
311 /// The agent wrote down what it made of a turn.
312 AgentAssessment,
313 /// An operator acknowledged an event.
314 OperatorAcknowledgement,
315 /// A port failed while an event was being handled.
316 PortFailure,
317 /// A supervisor reconciled one thing the store held when it started.
318 StartupReconciliation,
319}
320
321impl EventKind {
322 /// Every kind this vocabulary declares.
323 ///
324 /// The whole vocabulary, so that a test walking the kinds reads them off
325 /// this type rather than off a list of its own.
326 pub const ALL: [Self; 13] = [
327 Self::ObicoFailureAlert,
328 Self::ObicoPrinterNotification,
329 Self::MalformedExternalEvent,
330 Self::ActionRequested,
331 Self::ActionExecuted,
332 Self::ActionRejected,
333 Self::InterventionExpired,
334 Self::SupervisionSessionOpened,
335 Self::SupervisionSessionClosed,
336 Self::AgentAssessment,
337 Self::OperatorAcknowledgement,
338 Self::PortFailure,
339 Self::StartupReconciliation,
340 ];
341}
342
343/// What an event carries, tagged by the kind it belongs to.
344///
345/// The wire shape is the pair `kind` and `payload`, which is why an
346/// [`EventRecord`] carries this one value rather than two fields that could
347/// disagree.
348#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
349#[serde(tag = "kind", content = "payload", rename_all = "snake_case")]
350pub enum EventPayload {
351 /// Obico reported a print failure.
352 ObicoFailureAlert(ObicoFailureAlertPayload),
353 /// Obico sent a printer notification.
354 ObicoPrinterNotification(ObicoPrinterNotificationPayload),
355 /// An external body arrived that could not be read.
356 MalformedExternalEvent(MalformedExternalEventPayload),
357 /// An actor asked for an action.
358 ActionRequested(ActionRequestedPayload),
359 /// An accepted action reached the printer.
360 ActionExecuted(ActionExecutedPayload),
361 /// Policy refused an action.
362 ActionRejected(ActionRejectedPayload),
363 /// A bounded intervention expired.
364 InterventionExpired(InterventionExpiredPayload),
365 /// A supervision session was opened.
366 SupervisionSessionOpened(SupervisionSessionOpenedPayload),
367 /// A supervision session was closed.
368 SupervisionSessionClosed(SupervisionSessionClosedPayload),
369 /// The agent wrote down what it made of a turn.
370 AgentAssessment(AgentAssessmentPayload),
371 /// An operator acknowledged an event.
372 OperatorAcknowledgement(OperatorAcknowledgementPayload),
373 /// A port failed while an event was being handled.
374 PortFailure(PortFailurePayload),
375 /// A supervisor reconciled one thing the store held when it started.
376 StartupReconciliation(StartupReconciliationPayload),
377}
378
379impl EventPayload {
380 /// The kind this payload belongs to, and there is exactly one.
381 #[must_use]
382 pub const fn kind(&self) -> EventKind {
383 match self {
384 Self::ObicoFailureAlert(_) => EventKind::ObicoFailureAlert,
385 Self::ObicoPrinterNotification(_) => EventKind::ObicoPrinterNotification,
386 Self::MalformedExternalEvent(_) => EventKind::MalformedExternalEvent,
387 Self::ActionRequested(_) => EventKind::ActionRequested,
388 Self::ActionExecuted(_) => EventKind::ActionExecuted,
389 Self::ActionRejected(_) => EventKind::ActionRejected,
390 Self::InterventionExpired(_) => EventKind::InterventionExpired,
391 Self::SupervisionSessionOpened(_) => EventKind::SupervisionSessionOpened,
392 Self::SupervisionSessionClosed(_) => EventKind::SupervisionSessionClosed,
393 Self::AgentAssessment(_) => EventKind::AgentAssessment,
394 Self::OperatorAcknowledgement(_) => EventKind::OperatorAcknowledgement,
395 Self::PortFailure(_) => EventKind::PortFailure,
396 Self::StartupReconciliation(_) => EventKind::StartupReconciliation,
397 }
398 }
399}
400
401/// One event, as the store holds it.
402///
403/// `raw` holds the bytes exactly as received for an externally sourced event
404/// and is absent for an internally raised one — it is what makes the history
405/// auditable when a normalization turns out to be wrong. `print_id` is
406/// optional, because an externally sourced event may name no print this system
407/// knows.
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
409pub struct EventRecord {
410 /// This event's identifier, minted by the store.
411 pub id: EventId,
412 /// The print it belongs to, when it belongs to one.
413 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub print_id: Option<PrintId>,
415 /// Where it came from.
416 pub source: EventSource,
417 /// When it was received.
418 pub received_at: Timestamp,
419 /// The image it arrived with, when it arrived with one.
420 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub image: Option<ImageRef>,
422 /// The kind and the payload, which are one closed pair.
423 #[serde(flatten)]
424 pub payload: EventPayload,
425 /// The bytes exactly as received, for an externally sourced event.
426 #[serde(default, skip_serializing_if = "Option::is_none")]
427 pub raw: Option<RawBytes>,
428}
429
430impl EventRecord {
431 /// Which event this is, read off the payload it carries.
432 #[must_use]
433 pub const fn kind(&self) -> EventKind {
434 self.payload.kind()
435 }
436}