Skip to main content

kevy_resp/
reply_encode_resp3.rs

1//! RESP3 reply encoders — the additive prefixes that ride on top of the
2//! RESP2 wire (`+ - : $ * $-1 *-1`). Sibling of [`crate::reply_encode`]:
3//! everything here is `out: &mut Vec<u8>` + zero-alloc-past-initial-reserve,
4//! same shape as the RESP2 encoders, so a dispatch path can pick a proto
5//! per connection without changing its calling convention.
6//!
7//! Wire format reference: <https://github.com/antirez/RESP3/blob/master/spec.md>
8//!
9//! The two big-ticket helpers are the **header** encoders
10//! ([`encode_map_header`] / [`encode_set_header`] / [`encode_push_header`]).
11//! Like [`encode_array_len`] in RESP2, they emit only the count prefix —
12//! the caller follows up with the right number of sub-replies (2N for maps,
13//! N for sets / push). This keeps the encoders alloc-free and matches how
14//! dispatch already streams replies into the conn's output buffer.
15
16/// `%<count>\r\n` — a map header. Follow with `count` × 2 sub-replies
17/// (key₁ value₁ key₂ value₂ …). The count is the **pair** count, not
18/// the element count.
19///
20/// Used for replies that RESP2 ships as `*2N` array-of-pairs — `HGETALL`,
21/// `CONFIG GET`, `XINFO STREAM`. The map header is a single byte plus the
22/// count digits; vs RESP2's `*2N` it saves zero header bytes but the
23/// payload typically saves 4 B per pair by allowing simple-string keys.
24pub fn encode_map_header(out: &mut Vec<u8>, count: i64) {
25    out.push(b'%');
26    push_int(out, count);
27    out.extend_from_slice(b"\r\n");
28}
29
30/// `~<count>\r\n` — a set header. Follow with `count` sub-replies; the
31/// receiving client treats them as a set (dedup is its job; the wire
32/// doesn't require it).
33///
34/// Used for `SMEMBERS` / `SINTER` / `SUNION` / `SDIFF` / `SRANDMEMBER COUNT`.
35pub fn encode_set_header(out: &mut Vec<u8>, count: i64) {
36    out.push(b'~');
37    push_int(out, count);
38    out.extend_from_slice(b"\r\n");
39}
40
41/// `><count>\r\n` — an out-of-band push frame header. Follow with
42/// `count` sub-replies. The RESP3 client demultiplexes push frames from
43/// regular replies, so this is what `PUBLISH` / pattern-subscribe
44/// delivery uses when the consumer speaks RESP3.
45pub fn encode_push_header(out: &mut Vec<u8>, count: i64) {
46    out.push(b'>');
47    push_int(out, count);
48    out.extend_from_slice(b"\r\n");
49}
50
51/// `,<value>\r\n` — a double. `inf` / `-inf` / `nan` are valid wire
52/// payloads per spec; we forward Rust's standard float formatting which
53/// emits exactly those tokens.
54///
55/// Vs RESP2's `$<len>\r\n<digits>\r\n` shape this saves ~6 B per value
56/// (no length prefix, no trailing CRLF after the digits — the digits
57/// ARE the CRLF-terminated line). Worth it on `ZSCORE` flood or
58/// `ZRANGE WITHSCORES`.
59pub fn encode_double(out: &mut Vec<u8>, v: f64) {
60    out.push(b',');
61    if v.is_nan() {
62        out.extend_from_slice(b"nan");
63    } else if v.is_infinite() {
64        out.extend_from_slice(if v > 0.0 { b"inf" } else { b"-inf" });
65    } else {
66        // Match the wire shape RESP3 clients expect: an integer-valued
67        // double serialises without a decimal point ("3" not "3.0"),
68        // matching what the parse_double_reply round-trip expects.
69        if v == v.trunc() && v.abs() < 1e17 {
70            push_int(out, v as i64);
71        } else {
72            // Rust's default `{}` for f64 emits a shortest round-trippable
73            // representation — same shape Redis emits for ZSCORE. Format
74            // into a stack buffer (no heap alloc) then extend.
75            use std::io::Write as _;
76            let _ = write!(out, "{v}");
77        }
78    }
79    out.extend_from_slice(b"\r\n");
80}
81
82/// `#t\r\n` / `#f\r\n` — boolean.
83pub fn encode_boolean(out: &mut Vec<u8>, v: bool) {
84    out.extend_from_slice(if v { b"#t\r\n" } else { b"#f\r\n" });
85}
86
87/// `_\r\n` — RESP3 true null. RESP2 fallback is the existing
88/// [`crate::encode_null_bulk`] (`$-1\r\n`).
89pub fn encode_null(out: &mut Vec<u8>) {
90    out.extend_from_slice(b"_\r\n");
91}
92
93/// `(<digits>\r\n` — arbitrary-precision integer carried as its string
94/// representation. We don't ship a bignum type (charter: zero deps), so
95/// the caller hands in pre-formatted digit bytes.
96pub fn encode_big_number(out: &mut Vec<u8>, digits: &[u8]) {
97    out.reserve(digits.len() + 4);
98    out.push(b'(');
99    out.extend_from_slice(digits);
100    out.extend_from_slice(b"\r\n");
101}
102
103/// `=<len>\r\n<fmt>:<data>\r\n` — verbatim string. `fmt` MUST be a
104/// 3-byte format tag (`b"txt"`, `b"mkd"`, `b"raw"`, …); `data` is the
105/// payload after the `:` separator. The wire `len` covers the 3-byte
106/// fmt + `:` + payload (so `len = 4 + data.len()`).
107///
108/// Used for `CLIENT INFO` / `DEBUG OBJECT` style replies where a RESP3
109/// client wants to know "this is markdown, render it as markdown" but
110/// a RESP2 client still gets the raw bytes.
111pub fn encode_verbatim(out: &mut Vec<u8>, fmt: [u8; 3], data: &[u8]) {
112    let total_len = 4 + data.len();
113    out.reserve(total_len + 16);
114    out.push(b'=');
115    push_int(out, total_len as i64);
116    out.extend_from_slice(b"\r\n");
117    out.extend_from_slice(&fmt);
118    out.push(b':');
119    out.extend_from_slice(data);
120    out.extend_from_slice(b"\r\n");
121}
122
123/// `!<len>\r\n<error>\r\n` — length-prefixed error. Use when the error
124/// payload contains CRLF (the simple `-...` shape can't encode it).
125pub fn encode_blob_error(out: &mut Vec<u8>, msg: &[u8]) {
126    out.reserve(msg.len() + 16);
127    out.push(b'!');
128    push_int(out, msg.len() as i64);
129    out.extend_from_slice(b"\r\n");
130    out.extend_from_slice(msg);
131    out.extend_from_slice(b"\r\n");
132}
133
134/// Local copy of [`crate::reply_encode::push_int`] — keeps this file
135/// independent of the RESP2 encoder module's private helpers (so they
136/// can evolve separately) without taking a dep on a public re-export.
137fn push_int(out: &mut Vec<u8>, n: i64) {
138    if n == 0 {
139        out.push(b'0');
140        return;
141    }
142    let mut tmp = [0u8; 20];
143    let mut i = tmp.len();
144    let neg = n < 0;
145    let mut v = n;
146    while v != 0 {
147        let digit = (v % 10).unsigned_abs() as u8;
148        i -= 1;
149        tmp[i] = b'0' + digit;
150        v /= 10;
151    }
152    if neg {
153        out.push(b'-');
154    }
155    out.extend_from_slice(&tmp[i..]);
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::{parse_reply, Reply};
162
163    /// Each encoder pairs with its parse_reply round-trip — byte-exact
164    /// out, structurally-exact back in.
165    #[test]
166    fn map_header_round_trip() {
167        let mut out = Vec::new();
168        encode_map_header(&mut out, 2);
169        // Build a full map: 2 pairs, each (Int, Bulk).
170        crate::encode_integer(&mut out, 1);
171        crate::encode_bulk(&mut out, b"a");
172        crate::encode_integer(&mut out, 2);
173        crate::encode_bulk(&mut out, b"b");
174        assert_eq!(out, b"%2\r\n:1\r\n$1\r\na\r\n:2\r\n$1\r\nb\r\n");
175        let (r, used) = parse_reply(&out).unwrap().unwrap();
176        assert_eq!(used, out.len());
177        assert_eq!(
178            r,
179            Reply::Map(vec![
180                (Reply::Int(1), Reply::Bulk(b"a".to_vec())),
181                (Reply::Int(2), Reply::Bulk(b"b".to_vec())),
182            ])
183        );
184    }
185
186    #[test]
187    fn set_header_round_trip() {
188        let mut out = Vec::new();
189        encode_set_header(&mut out, 3);
190        for i in 1..=3 {
191            crate::encode_integer(&mut out, i);
192        }
193        assert_eq!(out, b"~3\r\n:1\r\n:2\r\n:3\r\n");
194        let (r, _) = parse_reply(&out).unwrap().unwrap();
195        assert_eq!(r, Reply::Set(vec![Reply::Int(1), Reply::Int(2), Reply::Int(3)]));
196    }
197
198    #[test]
199    fn push_header_round_trip() {
200        let mut out = Vec::new();
201        encode_push_header(&mut out, 3);
202        crate::encode_simple_string(&mut out, "message");
203        crate::encode_bulk(&mut out, b"news");
204        crate::encode_bulk(&mut out, b"hello");
205        assert_eq!(out, b">3\r\n+message\r\n$4\r\nnews\r\n$5\r\nhello\r\n");
206        let (r, _) = parse_reply(&out).unwrap().unwrap();
207        assert_eq!(
208            r,
209            Reply::Push(vec![
210                Reply::Simple(b"message".to_vec()),
211                Reply::Bulk(b"news".to_vec()),
212                Reply::Bulk(b"hello".to_vec()),
213            ])
214        );
215    }
216
217    #[test]
218    fn double_round_trip() {
219        let mut out = Vec::new();
220        // 1.5 avoids clippy::approx_constant flagging 3.14 as a PI proxy.
221        encode_double(&mut out, 1.5);
222        assert_eq!(out, b",1.5\r\n");
223        let (r, _) = parse_reply(&out).unwrap().unwrap();
224        assert_eq!(r, Reply::Double(1.5));
225
226        out.clear();
227        encode_double(&mut out, 5.0); // integer-valued: no decimal point
228        assert_eq!(out, b",5\r\n");
229        let (r, _) = parse_reply(&out).unwrap().unwrap();
230        assert_eq!(r, Reply::Double(5.0));
231
232        out.clear();
233        encode_double(&mut out, f64::INFINITY);
234        assert_eq!(out, b",inf\r\n");
235
236        out.clear();
237        encode_double(&mut out, f64::NEG_INFINITY);
238        assert_eq!(out, b",-inf\r\n");
239
240        out.clear();
241        encode_double(&mut out, f64::NAN);
242        assert_eq!(out, b",nan\r\n");
243    }
244
245    #[test]
246    fn boolean_and_null_round_trip() {
247        let mut out = Vec::new();
248        encode_boolean(&mut out, true);
249        assert_eq!(out, b"#t\r\n");
250        let (r, _) = parse_reply(&out).unwrap().unwrap();
251        assert_eq!(r, Reply::Boolean(true));
252
253        out.clear();
254        encode_boolean(&mut out, false);
255        assert_eq!(out, b"#f\r\n");
256        let (r, _) = parse_reply(&out).unwrap().unwrap();
257        assert_eq!(r, Reply::Boolean(false));
258
259        out.clear();
260        encode_null(&mut out);
261        assert_eq!(out, b"_\r\n");
262        let (r, _) = parse_reply(&out).unwrap().unwrap();
263        assert_eq!(r, Reply::Null);
264    }
265
266    #[test]
267    fn verbatim_round_trip() {
268        let mut out = Vec::new();
269        encode_verbatim(&mut out, *b"txt", b"Some string");
270        assert_eq!(out, b"=15\r\ntxt:Some string\r\n");
271        let (r, _) = parse_reply(&out).unwrap().unwrap();
272        assert_eq!(r, Reply::Verbatim { fmt: *b"txt", data: b"Some string".to_vec() });
273    }
274
275    #[test]
276    fn big_number_round_trip() {
277        let mut out = Vec::new();
278        encode_big_number(&mut out, b"170141183460469231731687303715884105727");
279        assert_eq!(out, b"(170141183460469231731687303715884105727\r\n");
280        let (r, _) = parse_reply(&out).unwrap().unwrap();
281        assert_eq!(
282            r,
283            Reply::BigNumber(b"170141183460469231731687303715884105727".to_vec())
284        );
285    }
286
287    #[test]
288    fn blob_error_round_trip() {
289        let mut out = Vec::new();
290        encode_blob_error(&mut out, b"ERR bad thing\nwith newline");
291        let (r, _) = parse_reply(&out).unwrap().unwrap();
292        assert_eq!(r, Reply::BlobError(b"ERR bad thing\nwith newline".to_vec()));
293    }
294}