Skip to main content

ember_protocol/
serialize.rs

1//! Direct-to-buffer RESP3 serialization.
2//!
3//! Writes frames directly into a `BytesMut` buffer with no intermediate
4//! allocations. Integer-to-string conversion uses `itoa` for fast
5//! stack-based formatting.
6
7use bytes::BufMut;
8use bytes::BytesMut;
9
10use crate::types::Frame;
11
12impl Frame {
13    /// Serializes this frame into the provided buffer.
14    ///
15    /// Writes the full RESP3 wire representation, including type prefix
16    /// and trailing `\r\n` delimiters.
17    pub fn serialize(&self, dst: &mut BytesMut) {
18        match self {
19            Frame::Simple(s) => {
20                dst.put_u8(b'+');
21                dst.put_slice(s.as_bytes());
22                dst.put_slice(b"\r\n");
23            }
24            Frame::Error(msg) => {
25                dst.put_u8(b'-');
26                dst.put_slice(msg.as_bytes());
27                dst.put_slice(b"\r\n");
28            }
29            Frame::Integer(n) => {
30                dst.put_u8(b':');
31                write_i64(*n, dst);
32                dst.put_slice(b"\r\n");
33            }
34            Frame::Bulk(data) => {
35                dst.put_u8(b'$');
36                write_i64(data.len() as i64, dst);
37                dst.put_slice(b"\r\n");
38                dst.put_slice(data);
39                dst.put_slice(b"\r\n");
40            }
41            Frame::Array(items) => {
42                dst.put_u8(b'*');
43                write_i64(items.len() as i64, dst);
44                dst.put_slice(b"\r\n");
45                for item in items {
46                    item.serialize(dst);
47                }
48            }
49            Frame::Null => {
50                dst.put_slice(b"_\r\n");
51            }
52            Frame::Map(pairs) => {
53                dst.put_u8(b'%');
54                write_i64(pairs.len() as i64, dst);
55                dst.put_slice(b"\r\n");
56                for (key, val) in pairs {
57                    key.serialize(dst);
58                    val.serialize(dst);
59                }
60            }
61        }
62    }
63}
64
65/// Writes an i64 as its decimal ASCII representation directly into the buffer.
66fn write_i64(val: i64, dst: &mut BytesMut) {
67    let mut buf = itoa::Buffer::new();
68    dst.put_slice(buf.format(val).as_bytes());
69}
70
71#[cfg(test)]
72mod tests {
73    use bytes::Bytes;
74
75    use super::*;
76
77    fn serialize(frame: &Frame) -> Vec<u8> {
78        let mut buf = BytesMut::new();
79        frame.serialize(&mut buf);
80        buf.to_vec()
81    }
82
83    #[test]
84    fn simple_string() {
85        assert_eq!(serialize(&Frame::Simple("OK".into())), b"+OK\r\n");
86    }
87
88    #[test]
89    fn error() {
90        assert_eq!(serialize(&Frame::Error("ERR bad".into())), b"-ERR bad\r\n");
91    }
92
93    #[test]
94    fn integer() {
95        assert_eq!(serialize(&Frame::Integer(42)), b":42\r\n");
96        assert_eq!(serialize(&Frame::Integer(-1)), b":-1\r\n");
97        assert_eq!(serialize(&Frame::Integer(0)), b":0\r\n");
98    }
99
100    #[test]
101    fn bulk_string() {
102        assert_eq!(
103            serialize(&Frame::Bulk(Bytes::from_static(b"hello"))),
104            b"$5\r\nhello\r\n"
105        );
106    }
107
108    #[test]
109    fn empty_bulk_string() {
110        assert_eq!(
111            serialize(&Frame::Bulk(Bytes::from_static(b""))),
112            b"$0\r\n\r\n"
113        );
114    }
115
116    #[test]
117    fn null() {
118        assert_eq!(serialize(&Frame::Null), b"_\r\n");
119    }
120
121    #[test]
122    fn array() {
123        let frame = Frame::Array(vec![Frame::Simple("hello".into()), Frame::Integer(42)]);
124        assert_eq!(serialize(&frame), b"*2\r\n+hello\r\n:42\r\n");
125    }
126
127    #[test]
128    fn empty_array() {
129        assert_eq!(serialize(&Frame::Array(vec![])), b"*0\r\n");
130    }
131
132    #[test]
133    fn map() {
134        let frame = Frame::Map(vec![(Frame::Simple("key".into()), Frame::Integer(1))]);
135        assert_eq!(serialize(&frame), b"%1\r\n+key\r\n:1\r\n");
136    }
137
138    #[test]
139    fn round_trip() {
140        use crate::parse::parse_frame;
141
142        let frames = vec![
143            Frame::Simple("OK".into()),
144            Frame::Error("ERR nope".into()),
145            Frame::Integer(i64::MAX),
146            Frame::Integer(i64::MIN),
147            Frame::Bulk(Bytes::from_static(b"binary\x00data")),
148            Frame::Bulk(Bytes::from_static(b"")),
149            Frame::Null,
150            Frame::Array(vec![
151                Frame::Integer(1),
152                Frame::Bulk(Bytes::from_static(b"two")),
153                Frame::Null,
154            ]),
155            Frame::Map(vec![
156                (Frame::Simple("a".into()), Frame::Integer(1)),
157                (Frame::Simple("b".into()), Frame::Integer(2)),
158            ]),
159            Frame::Array(vec![
160                Frame::Array(vec![Frame::Integer(1), Frame::Integer(2)]),
161                Frame::Array(vec![Frame::Integer(3)]),
162            ]),
163        ];
164
165        for original in &frames {
166            let mut buf = BytesMut::new();
167            original.serialize(&mut buf);
168
169            let (parsed, consumed) = parse_frame(&buf)
170                .expect("round-trip parse should not error")
171                .expect("round-trip parse should return a frame");
172
173            assert_eq!(&parsed, original, "round-trip failed for {original:?}");
174            assert_eq!(consumed, buf.len(), "should consume entire buffer");
175        }
176    }
177}