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}