printobserver-types 0.2.0

Shared domain types for printobserver.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
//! The history: one closed pair of an event kind and the payload it carries.
//!
//! [`EventKind`] and [`EventPayload`] are one closed pair rather than two
//! fields that happen to agree. Every kind has exactly one payload variant and
//! every payload variant has exactly one kind, so a consumer that matches the
//! kind knows the payload's shape, and a mismatched pair is unrepresentable
//! rather than merely undocumented: an [`EventRecord`] carries the payload, the
//! `kind` on the wire is that payload's own tag, and a serialized record naming
//! one kind while carrying another's payload fails to parse.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::action::{AcknowledgementDisposition, PrintAction};
use crate::adjustable::Adjustable;
use crate::assessment::AgentAssessment;
use crate::ids::{ActionId, EventId, InterventionId, PrintId};
use crate::image::ImageRef;
use crate::intervention::InterventionOutcome;
use crate::policy::PolicyDecision;
use crate::raw::RawBytes;
use crate::timestamp::Timestamp;

/// Where an event came from.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum EventSource {
    /// Obico, over its webhook.
    Obico,
    /// A person.
    Operator,
    /// The supervising agent.
    Agent,
    /// The supervisor itself.
    System,
}

/// The kind of printer notification Obico sent, normalized.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum ObicoNotificationType {
    /// A print started.
    Started,
    /// A print finished.
    Done,
    /// A print was cancelled.
    Cancelled,
    /// A print was paused.
    Paused,
    /// A print was resumed.
    Resumed,
    /// The printer is waiting for a filament change.
    FilamentChange,
    /// A heater cooled down.
    HeaterCooled,
    /// A heater reached its target.
    HeaterTarget,
}

/// Obico reported a print failure.
///
/// The two instants are optional because Obico's own field for each is a Unix
/// timestamp number, an empty string, or absent, and the last two both mean the
/// producer reported no instant. An absent field here is that, never an epoch
/// date standing in for it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ObicoFailureAlertPayload {
    /// Whether Obico called it a warning rather than a failure.
    pub is_warning: bool,
    /// Whether Obico paused the print itself.
    pub print_paused: bool,
    /// Obico's own identifier for the print, when it named one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub obico_print_id: Option<i64>,
    /// The file being printed, when Obico named one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub file_name: Option<String>,
    /// When the print started, when Obico reported an instant for it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub started_at: Option<Timestamp>,
    /// When the print ended, when Obico reported an instant for it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ended_at: Option<Timestamp>,
}

/// Obico sent a printer notification.
///
/// The two instants are optional for the same reason
/// [`ObicoFailureAlertPayload`]'s are, and are absent along with the rest of
/// the print's fields when the notification is about no print at all.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ObicoPrinterNotificationPayload {
    /// Which notification it is.
    pub notification_type: ObicoNotificationType,
    /// Obico's own identifier for the print, when the notification is about one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub obico_print_id: Option<i64>,
    /// The file being printed, when the notification is about one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub file_name: Option<String>,
    /// When the print started, when Obico reported an instant for it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub started_at: Option<Timestamp>,
    /// When the print ended, when Obico reported an instant for it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ended_at: Option<Timestamp>,
}

/// An external body arrived that could not be read.
///
/// This kind always carries its `raw` bytes, and it exists so that an alert
/// this system cannot read is written down rather than dropped.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MalformedExternalEventPayload {
    /// One line saying why the body could not be read.
    pub detail: String,
}

/// Where a port failed while an event was being handled.
///
/// A closed set of exactly the sites at which a failure has nowhere else to be
/// recorded. The printer's action methods record theirs on the
/// [`ActionRecord`](crate::ActionRecord) the request minted, and a restoring
/// call records its own on the [`Intervention`](crate::Intervention) it was
/// expiring; those are not sites here, because a second record of them would be
/// a second version of one fact.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum PortFailureSite {
    /// Reading the printer's own state.
    PrinterSnapshot,
    /// Reading the job the printer reports it is running.
    PrinterJob,
    /// Writing the image the event arrived with.
    ImageWrite,
    /// Running the supervision turn the event prompted.
    SupervisionTurn,
}

/// A port failed while one event was being handled.
///
/// The event is named rather than implied, so that a reader holding an event's
/// identifier reaches every failure recorded while that event was being
/// handled. A failure recorded here is one the handling survived: the event is
/// already in the history by the time any of these sites is reached, and the
/// loop goes on to handle the next event.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PortFailurePayload {
    /// The event whose handling reached the failing call.
    pub event_id: EventId,
    /// Where it failed.
    pub site: PortFailureSite,
    /// What the port said about it, in the port's own words.
    pub detail: String,
}

/// An actor asked for an action.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ActionRequestedPayload {
    /// The action's identifier.
    pub action_id: ActionId,
    /// What was asked for.
    pub action: PrintAction,
    /// Who asked.
    pub actor: crate::action::Actor,
}

/// An accepted action reached the printer.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ActionExecutedPayload {
    /// The action's identifier.
    pub action_id: ActionId,
    /// The bounded intervention it opened, when it opened one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub intervention_id: Option<InterventionId>,
}

/// Policy refused an action.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ActionRejectedPayload {
    /// The action's identifier.
    pub action_id: ActionId,
    /// The whole decision, carrying which rejection it was.
    pub decision: PolicyDecision,
}

/// A bounded intervention expired.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct InterventionExpiredPayload {
    /// The intervention's identifier.
    pub intervention_id: InterventionId,
    /// What it had changed.
    pub adjustable: Adjustable,
    /// What became of it.
    pub outcome: InterventionOutcome,
}

/// A supervision session was opened.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SupervisionSessionOpenedPayload {
    /// The session's own name in the harness.
    pub session_name: String,
    /// The identity the harness ran it under.
    pub harness_identity: String,
}

/// A supervision session was closed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SupervisionSessionClosedPayload {
    /// The session's own name in the harness.
    pub session_name: String,
    /// Why it was closed.
    pub close_reason: String,
}

/// The agent wrote down what it made of a turn.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AgentAssessmentPayload {
    /// The session the turn ran in.
    pub session_name: String,
    /// What the agent answered with.
    pub assessment: AgentAssessment,
}

/// An operator acknowledged an event.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct OperatorAcknowledgementPayload {
    /// The event being acknowledged.
    pub acknowledged_event_id: EventId,
    /// What the operator asked for next.
    pub disposition: AcknowledgementDisposition,
}

/// What one restart put back the way it found it.
///
/// A supervisor that has been restarted adopts whatever the store holds rather
/// than starting empty, and each of these is one of those adoptions. They are
/// recorded rather than merely done, because a print that carried on across a
/// restart and one that was started again look identical afterwards unless the
/// history says which happened.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum StartupOutcome {
    /// A print left open was adopted as the print this supervisor is watching.
    PrintAdopted,
    /// A session left open was resumed rather than replaced.
    SessionResumed {
        /// The session's own name in the harness.
        session_name: String,
    },
    /// An intervention already past its expiry was expired on start.
    InterventionExpired {
        /// The intervention that had outlived its bound.
        intervention_id: InterventionId,
        /// What it had changed.
        adjustable: Adjustable,
        /// What became of putting the prior value back.
        outcome: InterventionOutcome,
    },
}

/// A supervisor reconciled one thing the store held when it started.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct StartupReconciliationPayload {
    /// The print it is about.
    pub print_id: PrintId,
    /// What was reconciled.
    pub outcome: StartupOutcome,
}

/// Which event this is, without its payload.
///
/// Every kind here has exactly one [`EventPayload`] variant, and the spellings
/// are the same on the wire.
#[derive(
    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum EventKind {
    /// Obico reported a print failure.
    ObicoFailureAlert,
    /// Obico sent a printer notification.
    ObicoPrinterNotification,
    /// An external body arrived that could not be read.
    MalformedExternalEvent,
    /// An actor asked for an action.
    ActionRequested,
    /// An accepted action reached the printer.
    ActionExecuted,
    /// Policy refused an action.
    ActionRejected,
    /// A bounded intervention expired.
    InterventionExpired,
    /// A supervision session was opened.
    SupervisionSessionOpened,
    /// A supervision session was closed.
    SupervisionSessionClosed,
    /// The agent wrote down what it made of a turn.
    AgentAssessment,
    /// An operator acknowledged an event.
    OperatorAcknowledgement,
    /// A port failed while an event was being handled.
    PortFailure,
    /// A supervisor reconciled one thing the store held when it started.
    StartupReconciliation,
}

impl EventKind {
    /// Every kind this vocabulary declares.
    ///
    /// The whole vocabulary, so that a test walking the kinds reads them off
    /// this type rather than off a list of its own.
    pub const ALL: [Self; 13] = [
        Self::ObicoFailureAlert,
        Self::ObicoPrinterNotification,
        Self::MalformedExternalEvent,
        Self::ActionRequested,
        Self::ActionExecuted,
        Self::ActionRejected,
        Self::InterventionExpired,
        Self::SupervisionSessionOpened,
        Self::SupervisionSessionClosed,
        Self::AgentAssessment,
        Self::OperatorAcknowledgement,
        Self::PortFailure,
        Self::StartupReconciliation,
    ];
}

/// What an event carries, tagged by the kind it belongs to.
///
/// The wire shape is the pair `kind` and `payload`, which is why an
/// [`EventRecord`] carries this one value rather than two fields that could
/// disagree.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", content = "payload", rename_all = "snake_case")]
pub enum EventPayload {
    /// Obico reported a print failure.
    ObicoFailureAlert(ObicoFailureAlertPayload),
    /// Obico sent a printer notification.
    ObicoPrinterNotification(ObicoPrinterNotificationPayload),
    /// An external body arrived that could not be read.
    MalformedExternalEvent(MalformedExternalEventPayload),
    /// An actor asked for an action.
    ActionRequested(ActionRequestedPayload),
    /// An accepted action reached the printer.
    ActionExecuted(ActionExecutedPayload),
    /// Policy refused an action.
    ActionRejected(ActionRejectedPayload),
    /// A bounded intervention expired.
    InterventionExpired(InterventionExpiredPayload),
    /// A supervision session was opened.
    SupervisionSessionOpened(SupervisionSessionOpenedPayload),
    /// A supervision session was closed.
    SupervisionSessionClosed(SupervisionSessionClosedPayload),
    /// The agent wrote down what it made of a turn.
    AgentAssessment(AgentAssessmentPayload),
    /// An operator acknowledged an event.
    OperatorAcknowledgement(OperatorAcknowledgementPayload),
    /// A port failed while an event was being handled.
    PortFailure(PortFailurePayload),
    /// A supervisor reconciled one thing the store held when it started.
    StartupReconciliation(StartupReconciliationPayload),
}

impl EventPayload {
    /// The kind this payload belongs to, and there is exactly one.
    #[must_use]
    pub const fn kind(&self) -> EventKind {
        match self {
            Self::ObicoFailureAlert(_) => EventKind::ObicoFailureAlert,
            Self::ObicoPrinterNotification(_) => EventKind::ObicoPrinterNotification,
            Self::MalformedExternalEvent(_) => EventKind::MalformedExternalEvent,
            Self::ActionRequested(_) => EventKind::ActionRequested,
            Self::ActionExecuted(_) => EventKind::ActionExecuted,
            Self::ActionRejected(_) => EventKind::ActionRejected,
            Self::InterventionExpired(_) => EventKind::InterventionExpired,
            Self::SupervisionSessionOpened(_) => EventKind::SupervisionSessionOpened,
            Self::SupervisionSessionClosed(_) => EventKind::SupervisionSessionClosed,
            Self::AgentAssessment(_) => EventKind::AgentAssessment,
            Self::OperatorAcknowledgement(_) => EventKind::OperatorAcknowledgement,
            Self::PortFailure(_) => EventKind::PortFailure,
            Self::StartupReconciliation(_) => EventKind::StartupReconciliation,
        }
    }
}

/// One event, as the store holds it.
///
/// `raw` holds the bytes exactly as received for an externally sourced event
/// and is absent for an internally raised one — it is what makes the history
/// auditable when a normalization turns out to be wrong. `print_id` is
/// optional, because an externally sourced event may name no print this system
/// knows.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct EventRecord {
    /// This event's identifier, minted by the store.
    pub id: EventId,
    /// The print it belongs to, when it belongs to one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub print_id: Option<PrintId>,
    /// Where it came from.
    pub source: EventSource,
    /// When it was received.
    pub received_at: Timestamp,
    /// The image it arrived with, when it arrived with one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub image: Option<ImageRef>,
    /// The kind and the payload, which are one closed pair.
    #[serde(flatten)]
    pub payload: EventPayload,
    /// The bytes exactly as received, for an externally sourced event.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub raw: Option<RawBytes>,
}

impl EventRecord {
    /// Which event this is, read off the payload it carries.
    #[must_use]
    pub const fn kind(&self) -> EventKind {
        self.payload.kind()
    }
}