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}