1use serde::{Deserialize, Serialize};
9
10use crate::address::Address;
11use crate::consts_external::EVENT_LOG_V;
12use crate::error::{AxCode, AxError};
13use crate::locator::B3Hash;
14
15use super::identity::{RunId, Seq, TimeMs};
16use super::kind::EventKind;
17
18const PAYLOAD_DEPTH_MAX: usize = 126;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
28#[serde(transparent)]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30pub struct Payload(serde_json::Map<String, serde_json::Value>);
31
32impl Payload {
33 pub fn new(map: serde_json::Map<String, serde_json::Value>) -> Result<Self, AxError> {
35 reject_floats_and_depth(&map)?;
36 Ok(Payload(map))
37 }
38
39 pub fn empty() -> Self {
40 Payload(serde_json::Map::new())
41 }
42
43 pub fn of(record: &impl Serialize) -> Result<Self, AxError> {
52 let value = serde_json::to_value(record).map_err(|err| {
53 AxError::failure(
54 AxCode::InvalidArgs,
55 "encode a ledger payload",
56 err.to_string(),
57 )
58 .with_recovery("give the record string keys and integer numbers only")
59 })?;
60 let serde_json::Value::Object(map) = value else {
61 return Err(AxError::failure(
62 AxCode::InvalidArgs,
63 "encode a ledger payload",
64 "the record did not encode to a JSON object",
65 )
66 .with_recovery("a ledger payload is an object; record this fact as a struct"));
67 };
68 Payload::new(map)
69 }
70
71 pub fn read<T: serde::de::DeserializeOwned>(&self) -> Result<T, AxError> {
84 T::deserialize(&self.0).map_err(|err| {
85 AxError::failure(
86 AxCode::WireMismatch,
87 "read a ledger payload",
88 err.to_string(),
89 )
90 .with_recovery("replay with the build that wrote this record")
91 })
92 }
93
94 pub fn as_map(&self) -> &serde_json::Map<String, serde_json::Value> {
95 &self.0
96 }
97}
98
99impl<'de> Deserialize<'de> for Payload {
100 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101 let map = serde_json::Map::deserialize(deserializer)?;
102 Payload::new(map).map_err(serde::de::Error::custom)
103 }
104}
105
106fn reject_floats_and_depth(
113 map: &serde_json::Map<String, serde_json::Value>,
114) -> Result<(), AxError> {
115 let mut pending: Vec<(&serde_json::Value, usize)> =
118 map.values().map(|value| (value, 1)).collect();
119 while let Some((value, above)) = pending.pop() {
120 match value {
121 serde_json::Value::Number(n) if !n.is_i64() && !n.is_u64() => {
122 return Err(
123 AxError::failure(AxCode::InvalidArgs, "build payload", n.to_string())
124 .with_recovery("ledger payloads never carry floats; scale to integers"),
125 );
126 }
127 serde_json::Value::Array(items) => {
128 let level = opened(above)?;
129 pending.extend(items.iter().map(|item| (item, level)));
130 }
131 serde_json::Value::Object(inner) => {
132 let level = opened(above)?;
133 pending.extend(inner.values().map(|item| (item, level)));
134 }
135 serde_json::Value::Null
136 | serde_json::Value::Bool(_)
137 | serde_json::Value::Number(_)
138 | serde_json::Value::String(_) => {}
139 }
140 }
141 Ok(())
142}
143
144fn opened(above: usize) -> Result<usize, AxError> {
147 above
148 .checked_add(1)
149 .filter(|level| *level <= PAYLOAD_DEPTH_MAX)
150 .ok_or_else(|| {
151 AxError::failure(
152 AxCode::InvalidArgs,
153 "build payload",
154 format!("nesting deeper than {PAYLOAD_DEPTH_MAX} levels"),
155 )
156 .with_recovery(
157 "store the body in CAS and carry its locator in the payload; the ledger reader cannot parse this depth back",
158 )
159 })
160}
161
162#[derive(Debug, Clone, PartialEq)]
165pub struct EventDraft {
166 pub run: RunId,
167 pub t: TimeMs,
168 pub who: String,
169 pub addr: Option<Address>,
170 pub kind: EventKind,
171 pub data: Payload,
172 pub ig: bool,
175}
176
177fn ig_is_false(ig: &bool) -> bool {
178 !*ig
179}
180
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185#[serde(deny_unknown_fields)]
186#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
187pub struct EventRecord {
188 v: u32,
189 run: RunId,
190 seq: Seq,
191 prev: B3Hash,
192 t: TimeMs,
193 who: String,
194 #[serde(skip_serializing_if = "Option::is_none", default)]
195 addr: Option<Address>,
196 kind: EventKind,
197 data: Payload,
198 #[serde(skip_serializing_if = "ig_is_false", default)]
199 ig: bool,
200}
201
202impl EventRecord {
203 pub fn from_draft(draft: EventDraft, seq: Seq, prev: B3Hash) -> Self {
206 EventRecord {
207 v: EVENT_LOG_V,
208 run: draft.run,
209 seq,
210 prev,
211 t: draft.t,
212 who: draft.who,
213 addr: draft.addr,
214 kind: draft.kind,
215 data: draft.data,
216 ig: draft.ig,
217 }
218 }
219
220 pub fn canonical_line(&self) -> Result<Vec<u8>, AxError> {
224 serde_json::to_vec(self).map_err(|e| {
225 AxError::failure(
226 AxCode::InvalidArgs,
227 "serialize event",
228 format!("{:?} at seq {}", self.kind, self.seq.value()),
229 )
230 .with_recovery(format!(
231 "JSON refused this payload ({e}); give the event's data only string keys and finite numbers"
232 ))
233 })
234 }
235
236 pub fn parse_line(raw: &[u8]) -> Result<Self, AxError> {
240 serde_json::from_slice(raw).map_err(|e| {
241 AxError::failure(AxCode::InvalidArgs, "parse event line", e.to_string())
242 .with_recovery("the line is not a canonical EventRecord")
243 })
244 }
245
246 pub fn to_ref(&self) -> EventRef {
250 EventRef {
251 seq: self.seq,
252 kind: self.kind,
253 }
254 }
255
256 pub fn v(&self) -> u32 {
257 self.v
258 }
259
260 pub fn run(&self) -> RunId {
261 self.run
262 }
263
264 pub fn seq(&self) -> Seq {
265 self.seq
266 }
267
268 pub fn prev(&self) -> B3Hash {
269 self.prev
270 }
271
272 pub fn t(&self) -> TimeMs {
273 self.t
274 }
275
276 pub fn who(&self) -> &str {
277 &self.who
278 }
279
280 pub fn addr(&self) -> Option<&Address> {
281 self.addr.as_ref()
282 }
283
284 pub fn kind(&self) -> EventKind {
285 self.kind
286 }
287
288 pub fn data(&self) -> &Payload {
289 &self.data
290 }
291
292 pub fn ig(&self) -> bool {
293 self.ig
294 }
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
300pub struct EventRef {
301 seq: Seq,
302 kind: EventKind,
303}
304
305impl EventRef {
306 pub fn seq(&self) -> Seq {
307 self.seq
308 }
309
310 pub fn kind(&self) -> EventKind {
311 self.kind
312 }
313}
314
315#[cfg(test)]
316#[allow(
317 clippy::unwrap_used,
318 clippy::expect_used,
319 clippy::panic,
320 clippy::indexing_slicing,
321 reason = "test code"
322)]
323mod tests;