Skip to main content

printobserver_types/
event.rs

1//! The event log's envelope: an open kind name and an opaque payload.
2//!
3//! An [`EventRecord`] is what the store holds and the server serves, and the
4//! one thing every domain and every client agree on about it is its shape:
5//! the fields every event carries, a `kind` naming which event it is, and a
6//! `payload` whose form is the kind's own. Nothing here lists the kinds. Each
7//! is declared by the domain that owns the event — a payload type implementing
8//! [`EventPayload`] under a [`KIND`](EventPayload::KIND) of its own — so a
9//! domain that adds an event edits its own crate and nothing central.
10//!
11//! # Why the log is open
12//!
13//! A closed pair — one enum of kinds and one enum of payloads, a variant per
14//! domain's events — made the crate every other crate builds against the place
15//! every domain's payload had to be written, and let a reader match on another
16//! domain's kind by name. The envelope makes neither possible: a reader of the
17//! log carries a record through, filters by name, or reads a typed payload out
18//! of it with [`EventBody::read`] and is answered nothing when the kind is
19//! another's. A record under a kind no crate of this workspace declares — a
20//! row written by a newer server, say — survives a round trip unchanged.
21//!
22//! # The wire form, which is unchanged
23//!
24//! [`EventBody`] is flattened into the record, so the wire form of a record is
25//! still the fields `id`, `print_id`, `source`, `received_at`, `image`, `kind`,
26//! `payload` and `raw`, and `{"kind": .., "payload": ..}` is the text the
27//! closed pair's tagged form serialized to.
28
29use core::fmt;
30use core::str::FromStr;
31use std::borrow::Cow;
32
33use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
34use serde::de::{DeserializeOwned, Error as _};
35use serde::{Deserialize, Deserializer, Serialize};
36use serde_json::Value;
37
38use crate::ids::{EventId, PrintId};
39use crate::image::ImageRef;
40use crate::raw::RawBytes;
41use crate::timestamp::Timestamp;
42
43/// The pattern every kind name matches: lowercase `snake_case`.
44///
45/// [`EventKind::new`] refuses a name outside it, and the envelope's schema
46/// declares it, so a kind read off the wire or out of the store is held to the
47/// same rule as one a domain declares.
48pub const KIND_PATTERN: &str = "^[a-z][a-z0-9_]*$";
49
50/// Why a string is not a kind name.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct EventKindError {
53    /// The string that was refused.
54    name: String,
55}
56
57impl EventKindError {
58    /// The string that was refused.
59    #[must_use]
60    pub fn name(&self) -> &str {
61        &self.name
62    }
63}
64
65impl fmt::Display for EventKindError {
66    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67        write!(
68            formatter,
69            "{:?} is not a kind name: a kind is lowercase snake_case, matching {KIND_PATTERN}",
70            self.name
71        )
72    }
73}
74
75impl core::error::Error for EventKindError {}
76
77/// Whether a string is a kind name: what [`KIND_PATTERN`] admits, spelled as
78/// the check rather than as the pattern so that no regex engine is needed.
79fn is_kind_name(text: &str) -> bool {
80    let mut characters = text.chars();
81    characters
82        .next()
83        .is_some_and(|first| first.is_ascii_lowercase())
84        && characters.all(|rest| rest.is_ascii_lowercase() || rest.is_ascii_digit() || rest == '_')
85}
86
87/// The name one kind of event is written down under.
88///
89/// Lowercase `snake_case`, declared by the domain that owns the event as the
90/// [`KIND`](EventPayload::KIND) of its payload type. It serializes as the bare
91/// string, is refused on construction and on parse in any other spelling, and
92/// is ordered and hashable so that a filter can hold a set of them.
93#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
94#[serde(transparent)]
95pub struct EventKind(String);
96
97impl EventKind {
98    /// The kind this name spells.
99    ///
100    /// # Errors
101    ///
102    /// Returns [`EventKindError`] when the name is not lowercase `snake_case`.
103    pub fn new(name: impl Into<String>) -> Result<Self, EventKindError> {
104        let name = name.into();
105        if is_kind_name(&name) {
106            Ok(Self(name))
107        } else {
108            Err(EventKindError { name })
109        }
110    }
111
112    /// The name, as it is written down.
113    #[must_use]
114    pub fn as_str(&self) -> &str {
115        &self.0
116    }
117}
118
119impl FromStr for EventKind {
120    type Err = EventKindError;
121
122    fn from_str(text: &str) -> Result<Self, Self::Err> {
123        Self::new(text)
124    }
125}
126
127impl<'de> Deserialize<'de> for EventKind {
128    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
129        let text = String::deserialize(deserializer)?;
130        Self::new(text).map_err(D::Error::custom)
131    }
132}
133
134impl fmt::Display for EventKind {
135    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136        formatter.write_str(&self.0)
137    }
138}
139
140impl JsonSchema for EventKind {
141    fn schema_name() -> Cow<'static, str> {
142        Cow::Borrowed("EventKind")
143    }
144
145    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
146        json_schema!({
147            "type": "string",
148            "title": "EventKind",
149            "description": "The name one kind of event is written down under: lowercase `snake_case`, declared by the domain that owns the event.",
150            "pattern": KIND_PATTERN
151        })
152    }
153}
154
155/// Where an event came from, as the bare string each domain declares for itself.
156///
157/// Nothing here lists the sources; each domain declares its own name as a
158/// constant beside the events it raises.
159#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
160#[serde(transparent)]
161pub struct EventSource(String);
162
163impl EventSource {
164    /// The source this name spells.
165    #[must_use]
166    pub fn new(name: impl Into<String>) -> Self {
167        Self(name.into())
168    }
169
170    /// The name, as it is written down.
171    #[must_use]
172    pub fn as_str(&self) -> &str {
173        &self.0
174    }
175}
176
177impl fmt::Display for EventSource {
178    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179        formatter.write_str(&self.0)
180    }
181}
182
183impl JsonSchema for EventSource {
184    fn schema_name() -> Cow<'static, str> {
185        Cow::Borrowed("EventSource")
186    }
187
188    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
189        json_schema!({
190            "type": "string",
191            "title": "EventSource",
192            "description": "Where an event came from, as the bare string the domain that raised it declares for itself."
193        })
194    }
195}
196
197/// One typed payload declared under a kind name, by the domain that owns it.
198///
199/// [`KIND`](Self::KIND) is the one authoritative source of the kind's name:
200/// the schema marker is written from it by [`event_schema_of`], the store's
201/// kind column is written from it, and the clients' tables are generated from
202/// the marker.
203pub trait EventPayload: Serialize + DeserializeOwned + JsonSchema {
204    /// The name this payload's events are written down under: lowercase
205    /// `snake_case`, as [`KIND_PATTERN`] states.
206    const KIND: &'static str;
207
208    /// The kind this payload belongs to.
209    ///
210    /// # Panics
211    ///
212    /// Panics when [`KIND`](Self::KIND) is not a kind name, which is a
213    /// declaring crate's own defect: its schema test drives this for every
214    /// kind it declares, and the client generator refuses a marker outside the
215    /// pattern, so a misspelt name never reaches a running system.
216    #[must_use]
217    fn kind() -> EventKind {
218        EventKind::new(Self::KIND).expect("a declared kind name is lowercase snake_case")
219    }
220}
221
222/// The kind and the payload, as the log holds them.
223///
224/// The pair `{"kind": .., "payload": ..}`: the kind is the name a payload type
225/// declares, and the payload is that type's value, held as JSON so that a body
226/// under a kind this crate has never heard of is carried rather than refused.
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
228pub struct EventBody {
229    /// Which event this is.
230    pub kind: EventKind,
231    /// What it carries, in the form its kind declares.
232    pub payload: Value,
233}
234
235impl EventBody {
236    /// The body of one typed payload, under the kind its type declares.
237    ///
238    /// # Errors
239    ///
240    /// Returns the serialization failure when the payload cannot be rendered
241    /// as JSON, which a payload type declared with `serde` never is.
242    pub fn of<P: EventPayload>(payload: &P) -> Result<Self, serde_json::Error> {
243        Ok(Self {
244            kind: P::kind(),
245            payload: serde_json::to_value(payload)?,
246        })
247    }
248
249    /// Whether this body is under the kind `P` declares.
250    #[must_use]
251    pub fn is<P: EventPayload>(&self) -> bool {
252        self.kind.as_str() == P::KIND
253    }
254
255    /// The payload as `P`, when this body is under the kind `P` declares.
256    ///
257    /// Answers nothing for another kind's body, and the parse failure for a
258    /// body under the right kind whose payload is not of the type.
259    #[must_use]
260    pub fn read<P: EventPayload>(&self) -> Option<Result<P, serde_json::Error>> {
261        self.is::<P>()
262            .then(|| serde_json::from_value(self.payload.clone()))
263    }
264}
265
266/// One event, as the store holds it and the server serves it.
267///
268/// `raw` holds the bytes exactly as received for an externally sourced event
269/// and is absent for an internally raised one — it is what makes the history
270/// auditable when a normalization turns out to be wrong. `print_id` is
271/// optional, because an externally sourced event may name no print this system
272/// knows.
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
274pub struct EventRecord {
275    /// This event's identifier, minted by the store.
276    pub id: EventId,
277    /// The print it belongs to, when it belongs to one.
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub print_id: Option<PrintId>,
280    /// Where it came from.
281    pub source: EventSource,
282    /// When it was received.
283    pub received_at: Timestamp,
284    /// The image it arrived with, when it arrived with one.
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub image: Option<ImageRef>,
287    /// The kind and the payload, flattened into the record's own fields.
288    #[serde(flatten)]
289    pub body: EventBody,
290    /// The bytes exactly as received, for an externally sourced event.
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    pub raw: Option<RawBytes>,
293}
294
295impl EventRecord {
296    /// Which event this is.
297    #[must_use]
298    pub const fn kind(&self) -> &EventKind {
299        &self.body.kind
300    }
301
302    /// The payload as `P`, when this record is under the kind `P` declares.
303    ///
304    /// Answers nothing for a record of another kind; see [`EventBody::read`].
305    #[must_use]
306    pub fn payload_as<P: EventPayload>(&self) -> Option<Result<P, serde_json::Error>> {
307        self.body.read::<P>()
308    }
309}