Skip to main content

moq_json/snapshot/
decoder.rs

1//! The track-free half of snapshot consuming: frame payloads in, values out.
2
3use std::cell::RefCell;
4use std::marker::PhantomData;
5
6use serde::de::DeserializeOwned;
7use serde_json::Value;
8
9use super::consumer::Config;
10use crate::{Error, Result};
11
12/// Reconstructs a JSON value from the snapshot and delta frames of a group.
13///
14/// The track-free core of [`Consumer`](super::Consumer), and the mirror of
15/// [`Encoder`](super::Encoder). The caller reads frames from wherever it likes and routes each one
16/// by its position in the group: the first frame of every group is a
17/// [`snapshot`](Self::snapshot), the rest are [`delta`](Self::delta)s.
18///
19/// ```ignore
20/// match frame.keyframe {
21///     true => decoder.snapshot(&frame.payload)?,
22///     false => decoder.delta(&frame.payload)?,
23/// }
24/// let value = decoder.decode()?;
25/// ```
26///
27/// Applying and materializing are separate on purpose. Frames must be applied in order (the merge
28/// patches and the DEFLATE window are both sequential), but a consumer catching up on a backlog only
29/// wants the value at the head, so it applies every frame and calls [`decode`](Self::decode) once.
30/// A caller that wants a value per frame just calls it every time.
31pub struct Decoder<T> {
32	/// Whether frames are DEFLATE-compressed, matching the encoder's config.
33	compression: bool,
34
35	/// The current group's DEFLATE decoder (one window per group), rebuilt at each snapshot.
36	flate: Option<moq_flate::Decoder>,
37
38	/// Reused output for inflated delta frames.
39	plain: Vec<u8>,
40
41	/// Reused key buffers for validating remote patches before changing the baseline.
42	check: RefCell<crate::merge::CheckScratch>,
43
44	/// The reconstructed value, `None` until the first snapshot.
45	current: Option<Value>,
46
47	_marker: PhantomData<fn() -> T>,
48}
49
50impl<T> Decoder<T> {
51	/// Create a decoder with no value, awaiting its first [`snapshot`](Self::snapshot).
52	pub fn new(config: Config) -> Self {
53		Self {
54			compression: config.compression.is_deflate(),
55			flate: None,
56			plain: Vec::new(),
57			check: RefCell::new(crate::merge::CheckScratch::default()),
58			current: None,
59			_marker: PhantomData,
60		}
61	}
62
63	/// Apply a group's first frame: a full snapshot that replaces the current value.
64	///
65	/// Also starts the group's DEFLATE window, so this must be called at every group boundary, not
66	/// only the first.
67	pub fn snapshot(&mut self, payload: &[u8]) -> Result<()> {
68		// Each group is its own compressed stream, so the window starts cold here.
69		self.flate = self.compression.then(moq_flate::Decoder::new);
70		self.current = Some(match self.flate.as_mut() {
71			Some(flate) => serde_json::from_slice(&flate.frame(payload)?)?,
72			None => serde_json::from_slice(payload)?,
73		});
74		Ok(())
75	}
76
77	/// Apply one of a group's later frames: an [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396.html)
78	/// merge patch against the current value.
79	///
80	/// Errors with [`Error::MissingSnapshot`] when no snapshot has been applied yet, since a patch
81	/// has nothing to apply to.
82	pub fn delta(&mut self, payload: &[u8]) -> Result<()> {
83		if self.current.is_none() {
84			return Err(Error::MissingSnapshot);
85		}
86
87		let plain = match self.flate.as_mut() {
88			Some(flate) => {
89				flate.frame_into(payload, &mut self.plain)?;
90				self.plain.as_slice()
91			}
92			None => payload,
93		};
94		crate::merge::apply_bytes(
95			self.current.as_mut().expect("a snapshot precedes any delta"),
96			plain,
97			&self.check,
98		)?;
99		Ok(())
100	}
101
102	/// The reconstructed value as raw JSON, or `None` before the first snapshot.
103	pub fn value(&self) -> Option<&Value> {
104		self.current.as_ref()
105	}
106}
107
108impl<T: DeserializeOwned> Decoder<T> {
109	/// Materialize the reconstructed value as `T`, or `None` before the first snapshot.
110	///
111	/// Deserializing from the reconstructed [`Value`] rather than the frame bytes costs the line and
112	/// column a parse error would carry, so the error is prefixed with the JSON path of the offending
113	/// field instead. Without it a rejected field deep in a document reports only its own complaint,
114	/// with nothing to say where it came from.
115	pub fn decode(&self) -> Result<Option<T>> {
116		let Some(current) = self.current.as_ref() else {
117			return Ok(None);
118		};
119
120		let value = serde_path_to_error::deserialize(current).map_err(|err| {
121			let path = err.path().to_string();
122			match path.as_str() {
123				// The whole document, not a field within it: nothing useful to prefix.
124				"." => Error::Json(err.into_inner().to_string()),
125				_ => Error::Json(format!("{}: {}", path, err.into_inner())),
126			}
127		})?;
128
129		Ok(Some(value))
130	}
131}
132
133#[cfg(test)]
134mod test {
135	use super::super::consumer::Config as ConsumerConfig;
136	use super::super::{Config, Encoder};
137	use super::*;
138	use crate::Compression;
139	use serde_json::{Value, json};
140
141	fn consume(compression: Compression) -> ConsumerConfig {
142		ConsumerConfig { compression }
143	}
144
145	fn deflate() -> Config {
146		Config {
147			compression: Compression::Deflate,
148			..Default::default()
149		}
150	}
151
152	/// Round-trip a sequence of values through an encoder and decoder, yielding the value the
153	/// decoder reconstructs after each frame.
154	fn roundtrip(config: Config, values: &[Value]) -> Vec<Value> {
155		let compression = config.compression;
156		let mut encoder = Encoder::<Value>::new(config);
157		let mut decoder = Decoder::<Value>::new(consume(compression));
158
159		let mut out = Vec::new();
160		for value in values {
161			let Some(frame) = encoder.update(value).unwrap() else {
162				continue;
163			};
164			match frame.keyframe {
165				true => decoder.snapshot(&frame.payload).unwrap(),
166				false => decoder.delta(&frame.payload).unwrap(),
167			}
168			frame.commit();
169			out.push(decoder.decode().unwrap().unwrap());
170		}
171		out
172	}
173
174	#[test]
175	fn plaintext_roundtrip() {
176		let values = vec![
177			json!({ "a": 1, "b": 1 }),
178			json!({ "a": 1, "b": 2 }),
179			json!({ "a": 5, "b": 2 }),
180		];
181		assert_eq!(roundtrip(Config::default(), &values), values);
182	}
183
184	#[test]
185	fn compressed_roundtrip() {
186		let values = vec![
187			json!({ "a": 1, "b": 1 }),
188			json!({ "a": 1, "b": 2 }),
189			json!({ "a": 5, "b": 2 }),
190		];
191		assert_eq!(roundtrip(deflate(), &values), values);
192	}
193
194	/// The window is per group, so a keyframe mid-stream has to restart it on both sides. A decoder
195	/// that kept the old window here would fail to inflate the new group's snapshot.
196	#[test]
197	fn compressed_roundtrip_across_a_group_boundary() {
198		// A tight ratio guarantees at least one roll partway through.
199		let values: Vec<Value> = (0..=40).map(|n| json!({ "n": n })).collect();
200		let config = deflate().with_delta_ratio(2);
201		assert_eq!(roundtrip(config, &values).last().unwrap(), &json!({ "n": 40 }));
202	}
203
204	#[test]
205	fn no_value_before_the_first_snapshot() {
206		let decoder = Decoder::<Value>::new(ConsumerConfig::default());
207		assert_eq!(decoder.value(), None);
208		assert_eq!(decoder.decode().unwrap(), None);
209	}
210
211	#[test]
212	fn a_delta_before_a_snapshot_is_an_error() {
213		let mut decoder = Decoder::<Value>::new(ConsumerConfig::default());
214		assert!(matches!(decoder.delta(br#"{"a":1}"#), Err(Error::MissingSnapshot)));
215	}
216
217	/// A backlog is applied in full but materialized once: the intermediate reconstructions are
218	/// stale, and deserializing each one is exactly the cost the split exists to avoid.
219	#[test]
220	fn frames_apply_without_materializing() {
221		let mut encoder = Encoder::<Value>::new(Config::default().with_delta_ratio(100));
222		let mut decoder = Decoder::<Value>::new(ConsumerConfig::default());
223
224		for n in 0..=20 {
225			let frame = encoder.update(&json!({ "n": n })).unwrap().unwrap();
226			match frame.keyframe {
227				true => decoder.snapshot(&frame.payload).unwrap(),
228				false => decoder.delta(&frame.payload).unwrap(),
229			}
230			frame.commit();
231		}
232
233		assert_eq!(decoder.decode().unwrap(), Some(json!({ "n": 20 })));
234	}
235
236	#[test]
237	fn a_rejected_field_names_its_path() {
238		#[derive(serde::Deserialize, Debug)]
239		#[allow(dead_code)]
240		struct Inner {
241			count: u8,
242		}
243		#[derive(serde::Deserialize, Debug)]
244		#[allow(dead_code)]
245		struct Outer {
246			inner: Inner,
247		}
248
249		let mut decoder = Decoder::<Outer>::new(ConsumerConfig::default());
250		decoder.snapshot(br#"{"inner":{"count":300}}"#).unwrap();
251
252		let err = decoder.decode().unwrap_err();
253		assert!(err.to_string().starts_with("json: inner.count: "), "{err}");
254	}
255}