Skip to main content

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}