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) => Repr::List(xs.iter().map(Repr::of).collect::<Result<_, _>>()?),
89 Value::Map(m) => {
90 let mut pairs = Vec::with_capacity(m.len());
91 for (k, val) in m.iter() {
92 pairs.push((Repr::of(k)?, Repr::of(val)?));
93 }
94 Repr::Map(pairs)
95 }
96 Value::Data(d) => Repr::Data {
97 ty: d.ty.to_string(),
98 variant: d.variant.as_ref().map(|v| v.to_string()),
99 fields: d
100 .fields
101 .iter()
102 .map(|(k, val)| Ok((k.to_string(), Repr::of(val)?)))
103 .collect::<Result<_, NotStorable>>()?,
104 },
105 Value::Html(_) => return Err(NotStorable { kind: "view" }),
106 Value::Attr(_) => return Err(NotStorable { kind: "attribute" }),
107 Value::Closure(_) => return Err(NotStorable { kind: "closure" }),
108 })
109 }
110
111 /// Back to a value. Total: every `Repr` denotes a `Value`, which is the point of the type.
112 pub fn to_value(&self) -> Value {
113 match self {
114 Repr::Unit => Value::Unit,
115 Repr::Bool(b) => Value::Bool(*b),
116 Repr::Int(i) => Value::Int(*i),
117 Repr::Float(bits) => Value::Float(*bits),
118 Repr::Str(s) => Value::str_(s),
119 Repr::List(xs) => Value::List(Arc::new(xs.iter().map(Repr::to_value).collect())),
120 Repr::Map(pairs) => {
121 let mut m = PMap::new();
122 for (k, v) in pairs {
123 m = m.insert(k.to_value(), v.to_value());
124 }
125 Value::Map(m)
126 }
127 Repr::Data {
128 ty,
129 variant,
130 fields,
131 } => Value::data(
132 Arc::from(ty.as_str()),
133 variant.as_deref().map(Arc::from),
134 fields
135 .iter()
136 .map(|(k, v)| (Arc::from(k.as_str()), v.to_value()))
137 .collect::<Fields>(),
138 ),
139 }
140 }
141}
142
143/// Encode a value for storage.
144pub fn to_bytes(v: &Value) -> Result<Vec<u8>, NotStorable> {
145 let repr = Repr::of(v)?;
146 // The only failure postcard has for an owned `Vec` sink is allocation, and a `Repr` built from
147 // a `Value` in memory cannot exceed it by construction.
148 Ok(postcard::to_allocvec(&repr).expect("a Repr is encodable"))
149}
150
151/// Decode a value written by [`to_bytes`].
152pub fn from_bytes(bytes: &[u8]) -> Result<Value, postcard::Error> {
153 postcard::from_bytes::<Repr>(bytes).map(|r| r.to_value())
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::core::value_to_repr;
160
161 fn sample() -> Value {
162 let mut m = PMap::new();
163 m = m.insert(Value::str_("a"), Value::Int(1));
164 m = m.insert(Value::str_("b"), Value::Bool(false));
165 Value::data(
166 Arc::from("Todo"),
167 Some(Arc::from("Added")),
168 Fields::from_iter([
169 (Arc::from("id"), Value::str_("t-1")),
170 (Arc::from("text"), Value::str_("milk")),
171 (Arc::from("n"), Value::Int(-7)),
172 (Arc::from("f"), Value::Float(f64::to_bits(1.5))),
173 (
174 Arc::from("xs"),
175 Value::List(Arc::new(vec![Value::Unit, Value::Int(2)])),
176 ),
177 (Arc::from("m"), Value::Map(m)),
178 ]),
179 )
180 }
181
182 #[test]
183 fn every_storable_shape_round_trips_exactly() {
184 let v = sample();
185 let bytes = to_bytes(&v).expect("it is storable");
186 assert_eq!(from_bytes(&bytes).expect("it decodes"), v);
187 }
188
189 #[test]
190 fn a_float_survives_as_its_bit_pattern() {
191 // `Value` stores the bits so that it can be `Ord`; an encoding that went through a decimal
192 // would lose `NaN`'s payload and merge `-0.0` with `0.0`, and both are map keys.
193 for bits in [
194 f64::to_bits(0.0),
195 f64::to_bits(-0.0),
196 f64::to_bits(f64::NAN),
197 f64::to_bits(f64::INFINITY),
198 f64::to_bits(0.1 + 0.2),
199 ] {
200 let v = Value::Float(bits);
201 assert_eq!(from_bytes(&to_bytes(&v).unwrap()).unwrap(), v);
202 }
203 }
204
205 #[test]
206 fn the_three_unstorable_variants_are_refused_at_the_encoder() {
207 // The same refusal `value_to_repr` makes, at the same place, for the same §3.5 reason —
208 // and now unreachable from a program that compiles, because `secure::storable` proves it.
209 let html = Value::Html(Arc::new(crate::html::Html::Text {
210 text: "x".to_string(),
211 hash: 0,
212 }));
213 assert!(to_bytes(&html).is_err());
214 assert_eq!(to_bytes(&html).unwrap_err().kind, "view");
215
216 // …and a value that merely *contains* one is refused too, because the walk is total.
217 let nested = Value::List(Arc::new(vec![Value::Int(1), html]));
218 assert!(to_bytes(&nested).is_err());
219 }
220
221 #[test]
222 fn the_binary_encoding_is_much_smaller_than_the_json_one() {
223 // The reason this module exists, as a number rather than an assertion. The margin is
224 // deliberately loose — this is a regression guard, not a benchmark; `beck bench log` is
225 // where the throughput question is answered.
226 let v = sample();
227 let binary = to_bytes(&v).expect("storable").len();
228 let json = value_to_repr(&v).expect("storable").to_string().len();
229 assert!(
230 binary * 3 < json,
231 "binary {binary} B against JSON {json} B — the encoding change stopped paying"
232 );
233 }
234
235 #[test]
236 fn the_encoding_is_canonical() {
237 // Two equal values encode to the same bytes, whatever order they were built in. The log's
238 // digest depends on it (`beck replay --verify`), and a map that iterated in insertion order
239 // would break it silently.
240 let mut a = PMap::new();
241 a = a.insert(Value::Int(2), Value::str_("b"));
242 a = a.insert(Value::Int(1), Value::str_("a"));
243 let mut b = PMap::new();
244 b = b.insert(Value::Int(1), Value::str_("a"));
245 b = b.insert(Value::Int(2), Value::str_("b"));
246 assert_eq!(
247 to_bytes(&Value::Map(a)).unwrap(),
248 to_bytes(&Value::Map(b)).unwrap()
249 );
250 }
251}