Skip to main content

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}