thunder/wire/value.rs
1//! The 8-variant value model and the `Request`/`Response` frames.
2//!
3//! Externally-tagged encoding (rmp-serde default): unit variants serialize
4//! as a bare string (`"Null"`), payload variants as a single-key map
5//! (`{"Int": 42}`). `Response.result` is a serde `Result`, so a successful
6//! string reply nests two one-key maps: `{"Ok": {"Str": "PONG"}}` — pinned
7//! by the conformance corpus.
8
9use std::sync::Arc;
10
11use serde::{Deserialize, Serialize};
12
13/// The wire value model (WIRE-002) — byte-compatible with the value
14/// models the family shipped before Thunder, by construction.
15///
16/// `Map` is an insertion-ordered pair list because keys may be any value.
17#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
18pub enum Value {
19 /// SQL NULL / nil.
20 Null,
21 Bool(bool),
22 Int(i64),
23 Float(f64),
24 /// Raw bytes, refcounted. Emitted as MessagePack **bin** (WIRE-010); the
25 /// legacy int-array form decodes too (WIRE-011).
26 ///
27 /// The payload is `Arc<[u8]>` rather than `Vec<u8>` so a decoded value can
28 /// move into a product's store, and a stored buffer can reach the encoder,
29 /// as a **refcount bump instead of a memcpy** — in both directions. With an
30 /// owned `Vec`, a server paid one full copy of the payload per read and per
31 /// write, worst exactly where a binary protocol is supposed to win: large
32 /// values and the raw-LE-f32 embeddings this wire exists to carry.
33 ///
34 /// **The wire is unchanged.** The emitted form is still MessagePack `bin`
35 /// and the legacy int-array form is still accepted, so no corpus vector
36 /// moves and no other language lane is affected — only the Rust type.
37 Bytes(#[serde(with = "arc_bytes")] Arc<[u8]>),
38 Str(String),
39 Array(Vec<Value>),
40 Map(Vec<(Value, Value)>),
41}
42
43/// Serde adapter for [`Value::Bytes`]: emits MessagePack **bin** (WIRE-010)
44/// and accepts both `bin` and the legacy int-array form (WIRE-011), exactly
45/// as `serde_bytes` does for `Vec<u8>` — the refcounted payload is an
46/// in-process detail the wire never sees.
47mod arc_bytes {
48 use std::sync::Arc;
49
50 use std::fmt;
51
52 use serde::de::{self, SeqAccess, Visitor};
53 use serde::{Deserializer, Serializer};
54
55 pub(super) fn serialize<S: Serializer>(
56 bytes: &Arc<[u8]>,
57 serializer: S,
58 ) -> Result<S::Ok, S::Error> {
59 serde_bytes::serialize(&**bytes, serializer)
60 }
61
62 pub(super) fn deserialize<'de, D: Deserializer<'de>>(
63 deserializer: D,
64 ) -> Result<Arc<[u8]>, D::Error> {
65 // Builds the shared buffer DIRECTLY from the decoder's bytes.
66 //
67 // The obvious spelling — `Arc::from(serde_bytes::deserialize::<Vec<u8>>(d)?)`
68 // — costs a second allocation and a second full copy, because
69 // `Arc<[u8]>` stores its refcount inline with the data and so cannot
70 // adopt a `Vec`'s buffer. That would have moved a memcpy off the read
71 // path and onto the decode path, which is not a fix. This visitor
72 // allocates once, as the `Vec` path did.
73 deserializer.deserialize_bytes(ArcBytesVisitor)
74 }
75
76 struct ArcBytesVisitor;
77
78 impl<'de> Visitor<'de> for ArcBytesVisitor {
79 type Value = Arc<[u8]>;
80
81 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 f.write_str("bytes (MessagePack bin, or the legacy int array)")
83 }
84
85 /// The canonical path: MessagePack `bin` (WIRE-010). One allocation,
86 /// one copy, straight into the shared buffer.
87 fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
88 Ok(Arc::from(v))
89 }
90
91 fn visit_borrowed_bytes<E: de::Error>(self, v: &'de [u8]) -> Result<Self::Value, E> {
92 Ok(Arc::from(v))
93 }
94
95 /// Some decoders hand over an owned buffer; adopting it still costs
96 /// the copy `Arc<[u8]>` inherently requires, but no more than that.
97 fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
98 Ok(Arc::from(v))
99 }
100
101 /// WIRE-011: the legacy int-array form, decoded forever, never
102 /// emitted. Rare by construction, so the intermediate `Vec` here is
103 /// not on any hot path.
104 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
105 let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0));
106 while let Some(byte) = seq.next_element::<u8>()? {
107 bytes.push(byte);
108 }
109 Ok(Arc::from(bytes))
110 }
111
112 /// A `str` arriving where bytes are expected — tolerated as its UTF-8
113 /// bytes, matching `serde_bytes`.
114 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
115 Ok(Arc::from(v.as_bytes()))
116 }
117 }
118}
119
120impl Value {
121 /// Extract the inner string slice.
122 pub fn as_str(&self) -> Option<&str> {
123 match self {
124 Self::Str(s) => Some(s.as_str()),
125 _ => None,
126 }
127 }
128
129 /// Extract bytes (also accepts `Str` as UTF-8 bytes).
130 pub fn as_bytes(&self) -> Option<&[u8]> {
131 match self {
132 Self::Bytes(b) => Some(b),
133 Self::Str(s) => Some(s.as_bytes()),
134 _ => None,
135 }
136 }
137
138 /// The shared buffer behind [`Value::Bytes`], for the zero-copy path.
139 ///
140 /// `Arc::clone` on the result is a refcount bump: a product can put the
141 /// decoded payload straight into its store without copying it.
142 pub fn as_shared_bytes(&self) -> Option<&Arc<[u8]>> {
143 match self {
144 Self::Bytes(b) => Some(b),
145 _ => None,
146 }
147 }
148
149 /// Consume the value and take its shared buffer — no copy, no refcount
150 /// bump beyond the move itself.
151 pub fn into_shared_bytes(self) -> Option<Arc<[u8]>> {
152 match self {
153 Self::Bytes(b) => Some(b),
154 _ => None,
155 }
156 }
157
158 /// Build a `Bytes` value from anything that can become a shared buffer.
159 pub fn bytes(buffer: impl Into<Arc<[u8]>>) -> Self {
160 Self::Bytes(buffer.into())
161 }
162
163 /// Extract an integer.
164 pub fn as_int(&self) -> Option<i64> {
165 match self {
166 Self::Int(i) => Some(*i),
167 _ => None,
168 }
169 }
170
171 /// Extract a float (accepts `Int` widened to `f64`).
172 pub fn as_float(&self) -> Option<f64> {
173 match self {
174 Self::Float(f) => Some(*f),
175 Self::Int(i) => Some(*i as f64),
176 _ => None,
177 }
178 }
179
180 /// Extract a bool.
181 pub fn as_bool(&self) -> Option<bool> {
182 match self {
183 Self::Bool(b) => Some(*b),
184 _ => None,
185 }
186 }
187
188 /// Extract the array items.
189 pub fn as_array(&self) -> Option<&[Value]> {
190 match self {
191 Self::Array(items) => Some(items.as_slice()),
192 _ => None,
193 }
194 }
195
196 /// Extract the map pairs.
197 pub fn as_map(&self) -> Option<&[(Value, Value)]> {
198 match self {
199 Self::Map(pairs) => Some(pairs.as_slice()),
200 _ => None,
201 }
202 }
203
204 /// Look up a string key in a `Map` value.
205 pub fn map_get(&self, key: &str) -> Option<&Value> {
206 self.as_map()?
207 .iter()
208 .find(|(k, _)| k.as_str() == Some(key))
209 .map(|(_, v)| v)
210 }
211
212 /// True for `Value::Null`.
213 pub fn is_null(&self) -> bool {
214 matches!(self, Self::Null)
215 }
216}
217
218impl From<bool> for Value {
219 fn from(b: bool) -> Self {
220 Self::Bool(b)
221 }
222}
223impl From<i64> for Value {
224 fn from(i: i64) -> Self {
225 Self::Int(i)
226 }
227}
228impl From<f64> for Value {
229 fn from(f: f64) -> Self {
230 Self::Float(f)
231 }
232}
233impl From<String> for Value {
234 fn from(s: String) -> Self {
235 Self::Str(s)
236 }
237}
238impl From<&str> for Value {
239 fn from(s: &str) -> Self {
240 Self::Str(s.to_owned())
241 }
242}
243impl From<Vec<u8>> for Value {
244 /// Copies once, at the boundary — use [`Value::from`] on an `Arc<[u8]>`
245 /// (or [`Value::bytes`]) when the caller already holds a shared buffer.
246 fn from(b: Vec<u8>) -> Self {
247 Self::Bytes(Arc::from(b))
248 }
249}
250impl From<Arc<[u8]>> for Value {
251 /// The zero-copy path: a refcount bump, no payload copy.
252 fn from(b: Arc<[u8]>) -> Self {
253 Self::Bytes(b)
254 }
255}
256impl From<&[u8]> for Value {
257 fn from(b: &[u8]) -> Self {
258 Self::Bytes(Arc::from(b))
259 }
260}
261impl From<Vec<Value>> for Value {
262 fn from(items: Vec<Value>) -> Self {
263 Self::Array(items)
264 }
265}
266
267/// One RPC request (WIRE-001). `id` is client-chosen and echoed back;
268/// many requests multiplex over one connection. Serialized as an array
269/// (WIRE-012); map-shaped requests decode too (WIRE-013).
270#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
271pub struct Request {
272 pub id: u32,
273 pub command: String,
274 pub args: Vec<Value>,
275}
276
277/// One RPC response (WIRE-001). `result` is `Ok(value)` or an error
278/// string; v1 carries no structured error object — conventions are
279/// prefix-based and profile-driven (WIRE-040).
280#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
281pub struct Response {
282 pub id: u32,
283 pub result: Result<Value, String>,
284}
285
286impl Response {
287 /// Success response.
288 pub fn ok(id: u32, value: Value) -> Self {
289 Self {
290 id,
291 result: Ok(value),
292 }
293 }
294
295 /// Error response with the verbatim error string.
296 pub fn err(id: u32, message: impl Into<String>) -> Self {
297 Self {
298 id,
299 result: Err(message.into()),
300 }
301 }
302}
303
304#[cfg(test)]
305#[allow(clippy::unwrap_used, clippy::expect_used)]
306mod bytes_sharing_tests {
307 use super::*;
308
309 /// The property this type change exists for: a decoded payload can be
310 /// shared without copying it. Pinned as a test so a future refactor back
311 /// to an owned `Vec` cannot silently reintroduce the memcpy the Synap
312 /// adoption reported (GH #1).
313 #[test]
314 fn bytes_are_shared_not_copied() {
315 let buffer: Arc<[u8]> = Arc::from(vec![7u8; 4096]);
316 let value = Value::from(Arc::clone(&buffer));
317
318 // Reading the payload out for a store is a refcount bump, not a copy.
319 let taken = value.into_shared_bytes().unwrap();
320 assert_eq!(Arc::strong_count(&buffer), 2, "shared, not cloned");
321 assert!(
322 Arc::ptr_eq(&buffer, &taken),
323 "the very same allocation must come back out"
324 );
325 }
326
327 /// The read direction: a stored buffer reaches the encoder without a copy.
328 #[test]
329 fn a_stored_buffer_reaches_a_value_without_copying() {
330 let stored: Arc<[u8]> = Arc::from(vec![1u8, 2, 3]);
331 let value = Value::bytes(Arc::clone(&stored));
332 let inside = value.as_shared_bytes().unwrap();
333 assert!(Arc::ptr_eq(&stored, inside));
334 }
335
336 /// And the wire is unchanged: the shared payload still round-trips, and
337 /// still emits MessagePack `bin`.
338 #[test]
339 fn sharing_does_not_change_the_wire() {
340 let value = Value::bytes(vec![1u8, 2, 3, 255]);
341 let encoded = rmp_serde::to_vec(&value).unwrap();
342 // 0xc4 is MessagePack bin8. Its position depends on the enum tagging,
343 // so assert it is present rather than guessing an offset — what
344 // matters is that the payload is bin and not an int array (WIRE-010).
345 assert!(
346 encoded.contains(&0xc4),
347 "still emitted as bin, not an int array: {encoded:02x?}"
348 );
349 let decoded: Value = rmp_serde::from_slice(&encoded).unwrap();
350 assert_eq!(decoded, value);
351 }
352}