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