beck_core/repr.rs
1//! The storable shape of a [`Value`], and the binary encoding of it.
2//!
3//! # Why this is not `serde_json::Value`
4//!
5//! [`crate::core::value_to_repr`] produces a self-describing JSON tree, and that is the right thing
6//! for anything a person reads. It is the wrong thing for the log, for two reasons that compound:
7//!
8//! 1. **Size.** The JSON repr tags every scalar: `Value::Int(1)` becomes `{"$":"int","v":1}` — 19
9//! bytes carrying eight bits. A `Toggled(id)` event is a few dozen bytes of information and
10//! around two hundred of punctuation.
11//! 2. **Work.** Every append builds a whole `serde_json::Value` tree, then serialises it to text;
12//! every read parses text into a tree, then walks the tree to rebuild the `Value`. Four
13//! traversals and two allocations per event, in the one place §3.7 makes the whole system
14//! serial.
15//!
16//! Phase 0 stored events with `postcard` and measured 7,660 events/s through Postgres
17//! ([`docs/18-phase-0-report.md`](../../../../../docs/18-phase-0-report.md) §18.3.2). Phase 1 rewrote
18//! the log against `beck_core::Value` — which the runtime must not know the shape of — and reached
19//! for JSON because it is self-describing, which is exactly what postcard is not. That was a real
20//! constraint and this module is the answer to it: a **concrete** type postcard can encode, plus
21//! total conversions to and from `Value`.
22//!
23//! # Why a second type rather than `Serialize` on `Value`
24//!
25//! `Value` carries `Arc`s, a persistent map, and three variants that are *not storable at all* —
26//! `Html`, `Attr`, `Closure`. A derived `Serialize` would have to either panic on those or invent
27//! an encoding for them, and inventing one is how a view ends up in the log
28//! ([`crate::secure`], §3.5). Making the storable subset a separate type means **the encoder cannot
29//! be handed something unstorable**: the conversion returns [`NotStorable`], at the boundary, once.
30//!
31//! # Format stability
32//!
33//! This encoding is on disk, so it is a compatibility surface. [`FORMAT`] is stamped into every
34//! store and checked on open, because a log read back under a different encoding does not fail —
35//! it produces plausible nonsense, which is the one outcome an append-only audit trail may never
36//! have.
37
38use std::sync::Arc;
39
40use serde::{Deserialize, Serialize};
41
42use crate::core::{Fields, NotStorable, Value};
43use crate::pmap::PMap;
44
45/// The on-disk format version.
46///
47/// Bump when [`Repr`]'s shape changes in a way that makes old bytes decode differently. A store
48/// stamped with a different version is refused rather than read: replay is the only description of
49/// a program's history, and a misread log is worse than an unreadable one.
50///
51/// * `1` — JSON text (Phase 1, Phase 2).
52/// * `2` — postcard over [`Repr`].
53pub const FORMAT: u32 = 2;
54
55/// A [`Value`] restricted to what may be stored, as a concrete type a non-self-describing codec can
56/// encode.
57///
58/// The variants are exactly [`crate::core::value_to_repr`]'s cases, minus the three it refuses.
59#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
60pub enum Repr {
61 Unit,
62 Bool(bool),
63 Int(i64),
64 /// The bit pattern, as `Value` stores it — so that a round trip is exact for `NaN` and for
65 /// negative zero, which a decimal rendering is not.
66 Float(u64),
67 Str(String),
68 List(Vec<Repr>),
69 /// Pairs rather than a map, because the key is a `Repr` and most map encodings insist on
70 /// strings. Order is the `PMap`'s, which is sorted, so the encoding is canonical.
71 Map(Vec<(Repr, Repr)>),
72 Data {
73 ty: String,
74 variant: Option<String>,
75 fields: Vec<(String, Repr)>,
76 },
77}
78
79impl Repr {
80 /// The storable projection of a value, or the reason there isn't one.
81 pub fn of(v: &Value) -> Result<Repr, NotStorable> {
82 Ok(match v {
83 Value::Unit => Repr::Unit,
84 Value::Bool(b) => Repr::Bool(*b),
85 Value::Int(i) => Repr::Int(*i),
86 Value::Float(bits) => Repr::Float(*bits),
87 Value::Str(s) => Repr::Str(s.to_string()),
88 Value::List(xs) => {
89 let mut out = Vec::with_capacity(xs.len());
90 xs.try_for_each(|x| Repr::of(x).map(|r| out.push(r)))?;
91 Repr::List(out)
92 }
93 Value::Map(m) => {
94 let mut pairs = Vec::with_capacity(m.len());
95 for (k, val) in m.iter() {
96 pairs.push((Repr::of(k)?, Repr::of(val)?));
97 }
98 Repr::Map(pairs)
99 }
100 Value::Data(d) => Repr::Data {
101 ty: d.ty.to_string(),
102 variant: d.variant.as_ref().map(|v| v.to_string()),
103 fields: d
104 .fields
105 .iter()
106 .map(|(k, val)| Ok((k.to_string(), Repr::of(val)?)))
107 .collect::<Result<_, NotStorable>>()?,
108 },
109 Value::Html(_) => return Err(NotStorable { kind: "view" }),
110 Value::Attr(_) => return Err(NotStorable { kind: "attribute" }),
111 Value::Closure(_) => return Err(NotStorable { kind: "closure" }),
112 })
113 }
114
115 /// Back to a value. Total: every `Repr` denotes a `Value`, which is the point of the type.
116 pub fn to_value(&self) -> Value {
117 match self {
118 Repr::Unit => Value::Unit,
119 Repr::Bool(b) => Value::Bool(*b),
120 Repr::Int(i) => Value::Int(*i),
121 Repr::Float(bits) => Value::Float(*bits),
122 Repr::Str(s) => Value::str_(s),
123 Repr::List(xs) => Value::list(xs.iter().map(Repr::to_value).collect()),
124 Repr::Map(pairs) => {
125 let mut m = PMap::new();
126 for (k, v) in pairs {
127 m = m.insert(k.to_value(), v.to_value());
128 }
129 Value::Map(m)
130 }
131 Repr::Data {
132 ty,
133 variant,
134 fields,
135 } => Value::data(
136 Arc::from(ty.as_str()),
137 variant.as_deref().map(Arc::from),
138 fields
139 .iter()
140 .map(|(k, v)| (Arc::from(k.as_str()), v.to_value()))
141 .collect::<Fields>(),
142 ),
143 }
144 }
145}
146
147/// Encode a value for storage.
148pub fn to_bytes(v: &Value) -> Result<Vec<u8>, NotStorable> {
149 let repr = Repr::of(v)?;
150 // The only failure postcard has for an owned `Vec` sink is allocation, and a `Repr` built from
151 // a `Value` in memory cannot exceed it by construction.
152 Ok(postcard::to_allocvec(&repr).expect("a Repr is encodable"))
153}
154
155/// Decode a value written by [`to_bytes`].
156pub fn from_bytes(bytes: &[u8]) -> Result<Value, postcard::Error> {
157 postcard::from_bytes::<Repr>(bytes).map(|r| r.to_value())
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::core::value_to_repr;
164
165 fn sample() -> Value {
166 let mut m = PMap::new();
167 m = m.insert(Value::str_("a"), Value::Int(1));
168 m = m.insert(Value::str_("b"), Value::Bool(false));
169 Value::data(
170 Arc::from("Todo"),
171 Some(Arc::from("Added")),
172 Fields::from_iter([
173 (Arc::from("id"), Value::str_("t-1")),
174 (Arc::from("text"), Value::str_("milk")),
175 (Arc::from("n"), Value::Int(-7)),
176 (Arc::from("f"), Value::Float(f64::to_bits(1.5))),
177 (
178 Arc::from("xs"),
179 Value::list(vec![Value::Unit, Value::Int(2)]),
180 ),
181 (Arc::from("m"), Value::Map(m)),
182 ]),
183 )
184 }
185
186 #[test]
187 fn every_storable_shape_round_trips_exactly() {
188 let v = sample();
189 let bytes = to_bytes(&v).expect("it is storable");
190 assert_eq!(from_bytes(&bytes).expect("it decodes"), v);
191 }
192
193 #[test]
194 fn a_float_survives_as_its_bit_pattern() {
195 // `Value` stores the bits so that it can be `Ord`; an encoding that went through a decimal
196 // would lose `NaN`'s payload and merge `-0.0` with `0.0`, and both are map keys.
197 for bits in [
198 f64::to_bits(0.0),
199 f64::to_bits(-0.0),
200 f64::to_bits(f64::NAN),
201 f64::to_bits(f64::INFINITY),
202 f64::to_bits(0.1 + 0.2),
203 ] {
204 let v = Value::Float(bits);
205 assert_eq!(from_bytes(&to_bytes(&v).unwrap()).unwrap(), v);
206 }
207 }
208
209 #[test]
210 fn the_three_unstorable_variants_are_refused_at_the_encoder() {
211 // The same refusal `value_to_repr` makes, at the same place, for the same §3.5 reason —
212 // and now unreachable from a program that compiles, because `secure::storable` proves it.
213 let html = Value::Html(Arc::new(crate::html::Html::Text {
214 text: "x".to_string(),
215 hash: 0,
216 }));
217 assert!(to_bytes(&html).is_err());
218 assert_eq!(to_bytes(&html).unwrap_err().kind, "view");
219
220 // …and a value that merely *contains* one is refused too, because the walk is total.
221 let nested = Value::list(vec![Value::Int(1), html]);
222 assert!(to_bytes(&nested).is_err());
223 }
224
225 #[test]
226 fn the_binary_encoding_is_much_smaller_than_the_json_one() {
227 // The reason this module exists, as a number rather than an assertion. The margin is
228 // deliberately loose — this is a regression guard, not a benchmark; `beck bench log` is
229 // where the throughput question is answered.
230 let v = sample();
231 let binary = to_bytes(&v).expect("storable").len();
232 let json = value_to_repr(&v).expect("storable").to_string().len();
233 assert!(
234 binary * 3 < json,
235 "binary {binary} B against JSON {json} B — the encoding change stopped paying"
236 );
237 }
238
239 #[test]
240 fn the_encoding_is_canonical() {
241 // Two equal values encode to the same bytes, whatever order they were built in. The log's
242 // digest depends on it (`beck replay --verify`), and a map that iterated in insertion order
243 // would break it silently.
244 let mut a = PMap::new();
245 a = a.insert(Value::Int(2), Value::str_("b"));
246 a = a.insert(Value::Int(1), Value::str_("a"));
247 let mut b = PMap::new();
248 b = b.insert(Value::Int(1), Value::str_("a"));
249 b = b.insert(Value::Int(2), Value::str_("b"));
250 assert_eq!(
251 to_bytes(&Value::Map(a)).unwrap(),
252 to_bytes(&Value::Map(b)).unwrap()
253 );
254 }
255}