Skip to main content

onemessagebus/
envelope.rs

1//! The one envelope: one JSON object per NDJSON line.
2//!
3//! Nothing here emits, orders, bounds, or redacts anything — this is the wire
4//! shape and the types that fill it. `docs/wire.md` states the shape; the
5//! contract tests hold these types to it.
6
7use std::fmt;
8
9use schemars::JsonSchema;
10use serde::de::{self, DeserializeOwned, Deserializer};
11use serde::{Deserialize, Serialize};
12use serde_json::{Map, Value};
13
14use crate::vocabulary::Vocabulary;
15
16/// One event, as a producing process writes it and as a consumer reads it.
17///
18/// Merge order across streams is `(ts, stream, seq)`. A consumer detects loss
19/// as a per-stream [`seq`](Self::seq) gap; there is no cross-stream promise
20/// beyond the timestamps.
21///
22/// The type parameter is the [`Vocabulary`] the envelope is written over: it
23/// decides the source words, the dimensions carried between `kind` and
24/// `labels`, and the label set. Deserialization refuses an unknown top-level
25/// field, a `seq` that is not an unsigned integer, a source word the vocabulary
26/// does not admit, and a missing required field — each by name.
27#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
28#[serde(bound = "")]
29#[schemars(
30    bound = "V::Source: JsonSchema, V::Dimensions: JsonSchema, V::Labels: JsonSchema",
31    rename = "Envelope"
32)]
33pub struct Envelope<V: Vocabulary> {
34    /// The envelope schema version the producer wrote against.
35    pub v: u32,
36    /// RFC 3339, millisecond precision, UTC.
37    // llmlint: ignore[invalid_states_unrepresentable] the contract holds every envelope to the bytes its producer wrote and the recorded streams round-trip with no byte changed; a parsed timestamp re-renders the stamp in its own spelling, and `(ts, stream, seq)` orders the merge over the string as written.
38    pub ts: String,
39    /// Unique id of the producing process.
40    // llmlint: ignore[invalid_states_unrepresentable] the contract gives a stream id no grammar beyond a unique id per producing process and refuses none on read; a relay carries a sibling's stream ids untouched, so a narrower type would refuse lines the recorded streams hold.
41    pub stream: String,
42    /// Monotonic per [`stream`](Self::stream).
43    pub seq: u64,
44    /// What produced the event, in the vocabulary's words.
45    pub source: V::Source,
46    /// What happened, as its producer named it.
47    pub kind: Kind,
48    /// The vocabulary's reserved top-level dimensions, carried as named fields
49    /// here — between `kind` and `labels` on the wire — and omitted when
50    /// absent. [`NoDimensions`] writes nothing.
51    #[serde(flatten)]
52    pub dimensions: V::Dimensions,
53    /// The reserved keys the vocabulary declares plus free-form extras.
54    /// Producers stamp what they know; enrichers never rewrite.
55    #[serde(default)]
56    pub labels: V::Labels,
57    /// Kind-specific detail. Text fields are bounded by
58    /// [`MAX_PAYLOAD_TEXT_BYTES`](crate::MAX_PAYLOAD_TEXT_BYTES); larger
59    /// evidence is an [`ArtifactRef`].
60    #[serde(default)]
61    pub payload: Map<String, Value>,
62    /// Evidence stored by the producing library and referenced by id.
63    #[serde(default)]
64    pub artifacts: Vec<ArtifactRef>,
65}
66
67/// Read by hand rather than derived, because a derive with a flattened field
68/// hands the flattened type only the keys it declares and drops the rest — so
69/// an unknown top-level field would vanish instead of being refused by name.
70/// Every key the envelope names is taken here; whatever remains is the
71/// vocabulary's dimensions, which refuse what they do not admit.
72impl<'de, V: Vocabulary> Deserialize<'de> for Envelope<V> {
73    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
74        let mut fields: Map<String, Value> = Map::deserialize(deserializer)?;
75        let v = take(&mut fields, "v")?;
76        // llmlint: ignore[boundary_inputs_validated] Contract W lists what a read refuses — an unknown top-level field, a non-u64 seq, an unknown source, a missing required field, as each producer's serde does today — and names no timestamp refusal; no producer refuses one, the recorded streams round-trip byte for byte, and the merge orders `(ts, stream, seq)` over the string as written.
77        let ts = take(&mut fields, "ts")?;
78        let stream = take(&mut fields, "stream")?;
79        let seq = take(&mut fields, "seq")?;
80        let source = take(&mut fields, "source")?;
81        let kind = take(&mut fields, "kind")?;
82        let labels = take_or_default(&mut fields, "labels")?;
83        let payload = take_or_default(&mut fields, "payload")?;
84        let artifacts = take_or_default(&mut fields, "artifacts")?;
85        let dimensions =
86            serde_json::from_value(Value::Object(fields)).map_err(de::Error::custom)?;
87        Ok(Self {
88            v,
89            ts,
90            stream,
91            seq,
92            source,
93            kind,
94            dimensions,
95            labels,
96            payload,
97            artifacts,
98        })
99    }
100}
101
102/// One required key of a document being read by hand, as its type.
103pub(crate) fn take<T: DeserializeOwned, E: de::Error>(
104    fields: &mut Map<String, Value>,
105    key: &'static str,
106) -> Result<T, E> {
107    let value = fields.remove(key).ok_or_else(|| E::missing_field(key))?;
108    serde_json::from_value(value).map_err(|failure| E::custom(format!("{key}: {failure}")))
109}
110
111/// One optional key of a document being read by hand, defaulted when absent.
112pub(crate) fn take_or_default<T: DeserializeOwned + Default, E: de::Error>(
113    fields: &mut Map<String, Value>,
114    key: &'static str,
115) -> Result<T, E> {
116    match fields.remove(key) {
117        Some(value) => {
118            serde_json::from_value(value).map_err(|failure| E::custom(format!("{key}: {failure}")))
119        }
120        None => Ok(T::default()),
121    }
122}
123
124impl<V: Vocabulary> Envelope<V> {
125    /// The key three streams merge in: `(ts, stream, seq)`.
126    #[must_use]
127    pub fn order_key(&self) -> (&str, &str, u64) {
128        (&self.ts, &self.stream, self.seq)
129    }
130}
131
132/// What happened, as the kebab-case wire string.
133///
134/// Open on the wire and a string here, because a relay carries a sibling's
135/// kinds without interpreting them; a producing library keeps its own closed
136/// enum and converts into this with `From`.
137#[derive(
138    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
139)]
140#[serde(transparent)]
141// llmlint: ignore[invalid_states_unrepresentable] the contract makes `kind` open on the wire and a string newtype in the core because a relay carries a sibling's kinds without interpreting them; kebab-case is refused where a kind is authored (`events emit`), never where one is read.
142pub struct Kind(pub String);
143
144impl Kind {
145    /// The wire spelling, which is what a filter's `kind` glob matches.
146    #[must_use]
147    pub fn as_str(&self) -> &str {
148        &self.0
149    }
150}
151
152impl From<&str> for Kind {
153    fn from(kind: &str) -> Self {
154        Self(kind.to_owned())
155    }
156}
157
158impl From<String> for Kind {
159    fn from(kind: String) -> Self {
160        Self(kind)
161    }
162}
163
164impl fmt::Display for Kind {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        f.write_str(&self.0)
167    }
168}
169
170/// An open source word: whatever produced the event, by name.
171///
172/// The [`Open`](crate::Open) vocabulary's source. A vocabulary that closes the
173/// set declares an enum instead, and serde is what refuses a word outside it.
174#[derive(
175    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
176)]
177#[serde(transparent)]
178pub struct Source(pub String);
179
180impl Source {
181    /// The word as it travels.
182    #[must_use]
183    pub fn as_str(&self) -> &str {
184        &self.0
185    }
186}
187
188impl From<&str> for Source {
189    fn from(source: &str) -> Self {
190        Self(source.to_owned())
191    }
192}
193
194impl From<String> for Source {
195    fn from(source: String) -> Self {
196        Self(source)
197    }
198}
199
200impl fmt::Display for Source {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        f.write_str(&self.0)
203    }
204}
205
206/// An open label set: an ordered map of whatever the producer stamped.
207///
208/// The [`Open`](crate::Open) vocabulary's labels. A vocabulary that reserves
209/// keys declares a struct with a field per key and a flattened map for the
210/// rest, which serializes to the same bytes when the same keys are stamped.
211#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
212#[serde(transparent)]
213pub struct Labels(pub Map<String, Value>);
214
215impl Labels {
216    /// No labels at all.
217    #[must_use]
218    pub fn new() -> Self {
219        Self::default()
220    }
221
222    /// What is stamped under `key`, when it is text.
223    #[must_use]
224    pub fn get_str(&self, key: &str) -> Option<&str> {
225        self.0.get(key).and_then(Value::as_str)
226    }
227
228    /// Stamp `value` under `key`, replacing what was there.
229    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<Value>) -> &mut Self {
230        self.0.insert(key.into(), value.into());
231        self
232    }
233
234    /// The same labels with `value` stamped under `key`.
235    #[must_use]
236    pub fn with(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
237        self.insert(key, value);
238        self
239    }
240}
241
242/// No top-level dimensions: writes nothing, and refuses any field it is handed.
243///
244/// The type an envelope's `dimensions` has under a vocabulary that declares
245/// none. Refusing unknown fields is what makes an envelope over such a
246/// vocabulary reject an unknown top-level key by name.
247#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
248#[serde(deny_unknown_fields)]
249pub struct NoDimensions {}
250
251/// Evidence too large for a payload: stored by the producing library and
252/// referenced by id, to be read back through that library.
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
254#[serde(deny_unknown_fields)]
255pub struct ArtifactRef {
256    /// Identifier, unique within the producing library's store.
257    pub id: String,
258    /// What the artifact is — a gate log, a check log, a transcript, a report.
259    pub kind: String,
260    /// Size of the stored artifact.
261    pub bytes: u64,
262}