Skip to main content

printobserver_types/
obico.rs

1//! The wire shapes the self-hosted Obico webhook notification plugin sends.
2//!
3//! These are held here as this repository's committed claim about an external
4//! producer, beside the three samples that model what it sends. What
5//! reconciles that claim against the real producer is the scheduled Obico tier,
6//! which drives a live self-hosted Obico and fails naming any field that has
7//! moved.
8//!
9//! The producer sends three shapes: the failure alert, a printer notification
10//! about a print, which carries `print` and `img_url`, and a printer
11//! notification not about a print, which carries neither. One sample per shape
12//! is committed under this crate's own `samples/obico/`, and the third is not
13//! an incomplete copy of the second: it is what the producer sends when there
14//! is no print, and its two absences are the whole reason it is committed.
15
16use std::borrow::Cow;
17
18use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
19use serde::de::{Error as _, Unexpected};
20use serde::{Deserialize, Deserializer, Serialize, Serializer};
21
22/// The one `type` a failure alert's event object carries.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
24pub enum ObicoFailureEventType {
25    /// Obico's own spelling of a print failure.
26    #[serde(rename = "PrintFailure")]
27    PrintFailure,
28}
29
30/// The `type` a printer notification's event object carries.
31///
32/// These are the producer's own spellings; the normalized vocabulary is
33/// [`ObicoNotificationType`](crate::ObicoNotificationType).
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
35pub enum ObicoEventType {
36    /// A print started.
37    PrintStarted,
38    /// A print finished.
39    PrintDone,
40    /// A print was cancelled.
41    PrintCancelled,
42    /// A print was paused.
43    PrintPaused,
44    /// A print was resumed.
45    PrintResumed,
46    /// The printer is waiting for a filament change.
47    FilamentChange,
48    /// A heater cooled down.
49    HeaterCooledDown,
50    /// A heater reached its target.
51    HeaterTargetReached,
52}
53
54/// A Unix timestamp number, or the empty string the producer sends instead.
55#[derive(Debug, Clone, Copy, PartialEq)]
56pub enum ObicoTimestamp {
57    /// Seconds since the Unix epoch.
58    Seconds(f64),
59    /// The producer sent the empty string, meaning it has no instant to send.
60    NotReported,
61}
62
63impl Serialize for ObicoTimestamp {
64    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
65        match self {
66            Self::Seconds(seconds) => serializer.serialize_f64(*seconds),
67            Self::NotReported => serializer.serialize_str(""),
68        }
69    }
70}
71
72/// The two forms the producer sends, before the empty string is given meaning.
73#[derive(Deserialize)]
74#[serde(untagged)]
75enum ObicoTimestampWire {
76    /// A JSON number.
77    Seconds(f64),
78    /// A JSON string, which the producer sends only as the empty one.
79    Text(String),
80}
81
82impl<'de> Deserialize<'de> for ObicoTimestamp {
83    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
84        match ObicoTimestampWire::deserialize(deserializer)? {
85            ObicoTimestampWire::Seconds(seconds) => Ok(Self::Seconds(seconds)),
86            ObicoTimestampWire::Text(text) if text.is_empty() => Ok(Self::NotReported),
87            ObicoTimestampWire::Text(text) => Err(D::Error::invalid_value(
88                Unexpected::Str(&text),
89                &"a Unix timestamp number or an empty string",
90            )),
91        }
92    }
93}
94
95impl JsonSchema for ObicoTimestamp {
96    fn schema_name() -> Cow<'static, str> {
97        Cow::Borrowed("ObicoTimestamp")
98    }
99
100    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
101        json_schema!({
102            "title": "ObicoTimestamp",
103            "description": "A Unix timestamp number, or the empty string the producer sends \
104                            when it has no instant to send.",
105            "anyOf": [
106                { "type": "number" },
107                { "type": "string", "maxLength": 0 }
108            ]
109        })
110    }
111}
112
113/// The `printer` object both shapes carry.
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
115pub struct ObicoPrinterInfo {
116    /// Obico's own identifier for the printer.
117    pub id: i64,
118    /// The name Obico shows the printer under.
119    pub name: String,
120}
121
122/// The `print` object, which the producer sends when there is a print.
123///
124/// Each instant has **three** input states rather than two, and the option and
125/// the enum carry one each: a number is [`ObicoTimestamp::Seconds`], the empty
126/// string the producer sends for a print that has not started or ended is
127/// [`ObicoTimestamp::NotReported`], and a field the producer omits altogether
128/// is `None`. The last two both mean the producer reported no instant, and
129/// neither is refused and neither is an epoch date; they are held apart here
130/// only so that a body round-trips back into exactly the form it arrived in.
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
132pub struct ObicoPrintInfo {
133    /// Obico's own identifier for the print.
134    pub id: i64,
135    /// The file being printed.
136    pub filename: String,
137    /// When the print started, absent when the producer omits the field.
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub started_at: Option<ObicoTimestamp>,
140    /// When the print ended, absent when the producer omits the field.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub ended_at: Option<ObicoTimestamp>,
143}
144
145/// The `event` object a failure alert carries.
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
147pub struct ObicoFailureEvent {
148    /// Always the producer's own `PrintFailure`.
149    #[serde(rename = "type")]
150    pub event_type: ObicoFailureEventType,
151    /// Whether the producer called it a warning rather than a failure.
152    pub is_warning: bool,
153    /// Whether the producer paused the print itself.
154    pub print_paused: bool,
155}
156
157/// The `event` object a printer notification carries.
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
159pub struct ObicoNotificationEvent {
160    /// Which notification it is, in the producer's own spelling.
161    #[serde(rename = "type")]
162    pub event_type: ObicoEventType,
163    /// Whether the producer called it a warning.
164    pub is_warning: bool,
165    /// Whether the producer paused the print itself.
166    pub print_paused: bool,
167}
168
169/// The whole body the producer sends for a print failure.
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
171pub struct ObicoFailureAlert {
172    /// What happened.
173    pub event: ObicoFailureEvent,
174    /// Which printer it happened on.
175    pub printer: ObicoPrinterInfo,
176    /// Which print it happened to.
177    pub print: ObicoPrintInfo,
178    /// Where the snapshot that shows it can be fetched.
179    pub img_url: String,
180}
181
182/// The whole body the producer sends for a printer notification.
183///
184/// `print` and `img_url` are present together when the notification is about a
185/// print and absent together when it is not; the absent form is a shape the
186/// producer sends rather than an incomplete copy of the present one.
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
188pub struct ObicoPrinterNotification {
189    /// What happened.
190    pub event: ObicoNotificationEvent,
191    /// Which printer it happened on.
192    pub printer: ObicoPrinterInfo,
193    /// Which print it is about, when it is about one.
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub print: Option<ObicoPrintInfo>,
196    /// Where the snapshot can be fetched, when the notification carries one.
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub img_url: Option<String>,
199}