this repo has no description
1//! RESP (REdis Serialization Protocol) types and parsing
2//!
3//! RESP is Redis's wire protocol. This module provides:
4//! - `Value`: Tagged union representing all RESP types
5//! - Error sets categorized by failure origin
6//! - Parser for reading RESP from a byte stream
7//!
8//! ## Protocol Overview
9//!
10//! RESP uses a prefix byte to indicate type:
11//! - `+` Simple string: `+OK\r\n`
12//! - `-` Error: `-ERR message\r\n`
13//! - `:` Integer: `:1000\r\n`
14//! - `$` Bulk string: `$5\r\nhello\r\n` or `$-1\r\n` (null)
15//! - `*` Array: `*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n`
16//!
17//! ## Type System Notes
18//!
19//! The `Value` union demonstrates Zig's sum types. Unlike C enums with void pointers
20//! or C++ variants, Zig's tagged unions:
21//! - Are type-safe: the compiler tracks which variant is active
22//! - Require exhaustive matching: switch must handle all cases
23//! - Have zero overhead: same memory layout as C unions + tag
24//!
25//! Error sets are split by category so callers can handle failures appropriately:
26//! - `ConnectionError`: Network/auth issues, might retry with backoff
27//! - `ProtocolError`: Parsing failures, likely a bug or incompatibility
28//! - `CommandError`: Redis rejected the command, check your usage
29
30const std = @import("std");
31
32// ============================================================================
33// Error Types
34// ============================================================================
35
36/// Errors during connection establishment.
37/// These indicate network or authentication problems.
38pub const ConnectionError = error{
39 /// DNS lookup failed or returned no addresses
40 AddressResolutionFailed,
41 /// TCP connection refused or timed out
42 ConnectionRefused,
43 /// AUTH command failed (wrong password/username)
44 AuthenticationFailed,
45 /// SELECT command failed (invalid database number)
46 InvalidDatabase,
47};
48
49/// Errors in RESP protocol parsing.
50/// These indicate wire-level issues or server bugs.
51pub const ProtocolError = error{
52 /// Response doesn't follow RESP format
53 InvalidResponse,
54 /// Connection closed mid-response
55 ConnectionClosed,
56 /// Response exceeds buffer capacity
57 BufferOverflow,
58 /// Read timeout (if configured)
59 Timeout,
60};
61
62/// Errors returned by Redis commands.
63/// These are application-level errors from Redis itself.
64pub const CommandError = error{
65 /// Generic Redis error (check error message)
66 RedisError,
67 /// Key doesn't exist when required
68 KeyNotFound,
69 /// Operation on wrong type (e.g., INCR on a list)
70 WrongType,
71 /// Command syntax error
72 SyntaxError,
73 /// Command not allowed (permissions, readonly replica, etc.)
74 NotAllowed,
75};
76
77/// Combined error set for all client operations.
78/// Use this for functions that might fail at any level.
79pub const ClientError = ConnectionError || ProtocolError || CommandError || std.mem.Allocator.Error || std.posix.ReadError || std.net.Stream.WriteError;
80
81// ============================================================================
82// RESP Value Type
83// ============================================================================
84
85/// A Redis value as returned by commands.
86///
87/// This tagged union represents all RESP types. The tag field indicates
88/// which variant is active, and Zig's switch ensures exhaustive handling.
89///
90/// ## Memory Ownership
91///
92/// String slices reference the client's read buffer and are valid only
93/// until the next command. Copy data if you need to retain it.
94///
95/// Array elements are heap-allocated and must be freed by the caller
96/// (the client provides a `freeValue` method for this).
97pub const Value = union(enum) {
98 /// Simple string (status reply like "OK", "PONG", "QUEUED")
99 /// These are always short and ASCII-safe.
100 string: []const u8,
101
102 /// Error message from Redis (e.g., "ERR unknown command")
103 /// The message includes the error type prefix.
104 err: []const u8,
105
106 /// 64-bit signed integer
107 integer: i64,
108
109 /// Bulk string (binary-safe, may be null)
110 /// Null is represented as `bulk = null`.
111 bulk: ?[]const u8,
112
113 /// Array of values (recursive, may be nested)
114 /// Empty array is `array = &.{}`.
115 array: []const Value,
116
117 /// Explicit null (RESP3, or null array/bulk in RESP2)
118 nil,
119
120 // ========================================================================
121 // Convenience Methods
122 // ========================================================================
123
124 /// Check if this value represents a Redis error.
125 pub fn isError(self: Value) bool {
126 return self == .err;
127 }
128
129 /// Check if this value is null/nil.
130 /// Handles both explicit nil and null bulk strings.
131 pub fn isNull(self: Value) bool {
132 return switch (self) {
133 .nil => true,
134 .bulk => |b| b == null,
135 else => false,
136 };
137 }
138
139 /// Extract string content from string, bulk, or error values.
140 /// Returns null for other types.
141 pub fn asString(self: Value) ?[]const u8 {
142 return switch (self) {
143 .string => |s| s,
144 .bulk => |b| b,
145 .err => |e| e,
146 else => null,
147 };
148 }
149
150 /// Extract integer value.
151 /// Returns null for non-integer types.
152 pub fn asInt(self: Value) ?i64 {
153 return switch (self) {
154 .integer => |i| i,
155 else => null,
156 };
157 }
158
159 /// Extract array contents.
160 /// Returns null for non-array types, empty slice for nil.
161 pub fn asArray(self: Value) ?[]const Value {
162 return switch (self) {
163 .array => |a| a,
164 .nil => &.{},
165 else => null,
166 };
167 }
168
169 /// Try to interpret as a boolean.
170 /// Integer 1 = true, 0 = false. "OK" = true.
171 pub fn asBool(self: Value) ?bool {
172 return switch (self) {
173 .integer => |i| i != 0,
174 .string => |s| std.mem.eql(u8, s, "OK"),
175 else => null,
176 };
177 }
178
179 /// Format value for debugging/logging
180 pub fn format(
181 self: Value,
182 comptime fmt: []const u8,
183 options: std.fmt.FormatOptions,
184 writer: anytype,
185 ) !void {
186 _ = fmt;
187 _ = options;
188 switch (self) {
189 .string => |s| try writer.print("+{s}", .{s}),
190 .err => |e| try writer.print("-{s}", .{e}),
191 .integer => |i| try writer.print(":{d}", .{i}),
192 .bulk => |b| if (b) |s| {
193 try writer.print("${d}:{s}", .{ s.len, s });
194 } else {
195 try writer.writeAll("$nil");
196 },
197 .array => |a| {
198 try writer.print("*{d}[", .{a.len});
199 for (a, 0..) |v, i| {
200 if (i > 0) try writer.writeAll(", ");
201 try v.format("", .{}, writer);
202 }
203 try writer.writeAll("]");
204 },
205 .nil => try writer.writeAll("(nil)"),
206 }
207 }
208};
209
210// ============================================================================
211// RESP Parser
212// ============================================================================
213
214/// Stateful parser for reading RESP values from a buffer.
215///
216/// The parser maintains position within a buffer and can parse values
217/// incrementally. It handles fragmented reads by tracking how much
218/// data is available.
219///
220/// ## Usage
221///
222/// ```zig
223/// var parser = Parser.init(allocator, buffer[0..len]);
224/// const value = try parser.parseValue();
225/// ```
226pub const Parser = struct {
227 const Self = @This();
228
229 allocator: std.mem.Allocator,
230 buffer: []const u8,
231 pos: usize,
232
233 pub fn init(allocator: std.mem.Allocator, buffer: []const u8) Self {
234 return .{
235 .allocator = allocator,
236 .buffer = buffer,
237 .pos = 0,
238 };
239 }
240
241 /// Parse a single RESP value from the buffer.
242 /// Advances position past the parsed value.
243 pub fn parseValue(self: *Self) ProtocolError!Value {
244 if (self.pos >= self.buffer.len) return ProtocolError.InvalidResponse;
245
246 const type_char = self.buffer[self.pos];
247 self.pos += 1;
248
249 return switch (type_char) {
250 '+' => .{ .string = try self.readLine() },
251 '-' => .{ .err = try self.readLine() },
252 ':' => .{ .integer = try self.readInt() },
253 '$' => try self.readBulk(),
254 '*' => try self.readArray(),
255 else => ProtocolError.InvalidResponse,
256 };
257 }
258
259 /// Read until \r\n, returning content without the terminator.
260 fn readLine(self: *Self) ProtocolError![]const u8 {
261 const start = self.pos;
262 while (self.pos + 1 < self.buffer.len) {
263 if (self.buffer[self.pos] == '\r' and self.buffer[self.pos + 1] == '\n') {
264 const line = self.buffer[start..self.pos];
265 self.pos += 2;
266 return line;
267 }
268 self.pos += 1;
269 }
270 return ProtocolError.InvalidResponse;
271 }
272
273 /// Read an integer terminated by \r\n.
274 fn readInt(self: *Self) ProtocolError!i64 {
275 const line = try self.readLine();
276 return std.fmt.parseInt(i64, line, 10) catch ProtocolError.InvalidResponse;
277 }
278
279 /// Read a bulk string (or null).
280 fn readBulk(self: *Self) ProtocolError!Value {
281 const len = try self.readInt();
282 if (len < 0) return .nil;
283
284 const ulen: usize = @intCast(len);
285 if (self.pos + ulen + 2 > self.buffer.len) return ProtocolError.InvalidResponse;
286
287 const data = self.buffer[self.pos..][0..ulen];
288 self.pos += ulen + 2; // skip data + \r\n
289 return .{ .bulk = data };
290 }
291
292 /// Read an array of values (recursive).
293 fn readArray(self: *Self) ProtocolError!Value {
294 const len = try self.readInt();
295 if (len < 0) return .nil;
296 if (len == 0) return .{ .array = &.{} };
297
298 const ulen: usize = @intCast(len);
299 const values = self.allocator.alloc(Value, ulen) catch return ProtocolError.BufferOverflow;
300 errdefer self.allocator.free(values);
301
302 for (0..ulen) |i| {
303 values[i] = try self.parseValue();
304 }
305
306 return .{ .array = values };
307 }
308
309 /// Check if there's more data to parse.
310 pub fn hasMore(self: *const Self) bool {
311 return self.pos < self.buffer.len;
312 }
313
314 /// Get remaining unparsed bytes.
315 pub fn remaining(self: *const Self) []const u8 {
316 return self.buffer[self.pos..];
317 }
318};
319
320// ============================================================================
321// Command Builder
322// ============================================================================
323
324/// Builds a RESP array command in a buffer.
325///
326/// RESP commands are arrays of bulk strings:
327/// ```
328/// *3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
329/// ```
330///
331/// This builder writes directly to a buffer without allocation
332/// for most commands.
333pub const CommandBuilder = struct {
334 const Self = @This();
335
336 buffer: []u8,
337 pos: usize = 0,
338
339 pub fn init(buffer: []u8) Self {
340 return .{ .buffer = buffer };
341 }
342
343 /// Start a command with the given argument count.
344 pub fn begin(self: *Self, arg_count: usize) ProtocolError!void {
345 const written = std.fmt.bufPrint(self.buffer[self.pos..], "*{d}\r\n", .{arg_count}) catch {
346 return ProtocolError.BufferOverflow;
347 };
348 self.pos += written.len;
349 }
350
351 /// Add a bulk string argument.
352 pub fn arg(self: *Self, value: []const u8) ProtocolError!void {
353 const header = std.fmt.bufPrint(self.buffer[self.pos..], "${d}\r\n", .{value.len}) catch {
354 return ProtocolError.BufferOverflow;
355 };
356 self.pos += header.len;
357
358 if (self.pos + value.len + 2 > self.buffer.len) return ProtocolError.BufferOverflow;
359
360 @memcpy(self.buffer[self.pos..][0..value.len], value);
361 self.pos += value.len;
362 self.buffer[self.pos] = '\r';
363 self.buffer[self.pos + 1] = '\n';
364 self.pos += 2;
365 }
366
367 /// Add an integer argument (formatted as string).
368 pub fn argInt(self: *Self, value: anytype) ProtocolError!void {
369 var buf: [24]u8 = undefined;
370 const str = std.fmt.bufPrint(&buf, "{d}", .{value}) catch unreachable;
371 return self.arg(str);
372 }
373
374 /// Get the built command.
375 pub fn getCommand(self: *const Self) []const u8 {
376 return self.buffer[0..self.pos];
377 }
378};
379
380// ============================================================================
381// Tests
382// ============================================================================
383
384test "Value type properties" {
385 const testing = std.testing;
386
387 const string_val = Value{ .string = "OK" };
388 try testing.expect(!string_val.isNull());
389 try testing.expect(!string_val.isError());
390 try testing.expectEqualStrings("OK", string_val.asString().?);
391 try testing.expect(string_val.asBool().?);
392
393 const err_val = Value{ .err = "ERR unknown command" };
394 try testing.expect(err_val.isError());
395 try testing.expect(!err_val.isNull());
396
397 const nil_val: Value = .nil;
398 try testing.expect(nil_val.isNull());
399 try testing.expect(!nil_val.isError());
400 try testing.expect(nil_val.asString() == null);
401
402 const bulk_nil = Value{ .bulk = null };
403 try testing.expect(bulk_nil.isNull());
404
405 const int_val = Value{ .integer = 42 };
406 try testing.expectEqual(@as(i64, 42), int_val.asInt().?);
407 try testing.expect(int_val.asBool().?);
408
409 const zero_val = Value{ .integer = 0 };
410 try testing.expect(!zero_val.asBool().?);
411}
412
413test "Parser: simple string" {
414 const data = "+OK\r\n";
415 var parser = Parser.init(std.testing.allocator, data);
416 const value = try parser.parseValue();
417 try std.testing.expectEqualStrings("OK", value.asString().?);
418}
419
420test "Parser: error" {
421 const data = "-ERR unknown command\r\n";
422 var parser = Parser.init(std.testing.allocator, data);
423 const value = try parser.parseValue();
424 try std.testing.expect(value.isError());
425 try std.testing.expectEqualStrings("ERR unknown command", value.asString().?);
426}
427
428test "Parser: integer" {
429 const data = ":1000\r\n";
430 var parser = Parser.init(std.testing.allocator, data);
431 const value = try parser.parseValue();
432 try std.testing.expectEqual(@as(i64, 1000), value.asInt().?);
433}
434
435test "Parser: negative integer" {
436 const data = ":-42\r\n";
437 var parser = Parser.init(std.testing.allocator, data);
438 const value = try parser.parseValue();
439 try std.testing.expectEqual(@as(i64, -42), value.asInt().?);
440}
441
442test "Parser: bulk string" {
443 const data = "$5\r\nhello\r\n";
444 var parser = Parser.init(std.testing.allocator, data);
445 const value = try parser.parseValue();
446 try std.testing.expectEqualStrings("hello", value.asString().?);
447}
448
449test "Parser: null bulk" {
450 const data = "$-1\r\n";
451 var parser = Parser.init(std.testing.allocator, data);
452 const value = try parser.parseValue();
453 try std.testing.expect(value.isNull());
454}
455
456test "Parser: array" {
457 const data = "*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n";
458 var parser = Parser.init(std.testing.allocator, data);
459 const value = try parser.parseValue();
460 defer std.testing.allocator.free(value.array);
461
462 try std.testing.expectEqual(@as(usize, 2), value.array.len);
463 try std.testing.expectEqualStrings("foo", value.array[0].asString().?);
464 try std.testing.expectEqualStrings("bar", value.array[1].asString().?);
465}
466
467test "Parser: empty array" {
468 const data = "*0\r\n";
469 var parser = Parser.init(std.testing.allocator, data);
470 const value = try parser.parseValue();
471 try std.testing.expectEqual(@as(usize, 0), value.asArray().?.len);
472}
473
474test "Parser: null array" {
475 const data = "*-1\r\n";
476 var parser = Parser.init(std.testing.allocator, data);
477 const value = try parser.parseValue();
478 try std.testing.expect(value.isNull());
479}
480
481test "CommandBuilder: simple command" {
482 var buf: [256]u8 = undefined;
483 var builder = CommandBuilder.init(&buf);
484
485 try builder.begin(3);
486 try builder.arg("SET");
487 try builder.arg("key");
488 try builder.arg("value");
489
490 const expected = "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n";
491 try std.testing.expectEqualStrings(expected, builder.getCommand());
492}
493
494test "CommandBuilder: with integer" {
495 var buf: [256]u8 = undefined;
496 var builder = CommandBuilder.init(&buf);
497
498 try builder.begin(3);
499 try builder.arg("EXPIRE");
500 try builder.arg("key");
501 try builder.argInt(3600);
502
503 const expected = "*3\r\n$6\r\nEXPIRE\r\n$3\r\nkey\r\n$4\r\n3600\r\n";
504 try std.testing.expectEqualStrings(expected, builder.getCommand());
505}
506
507test "error set categories" {
508 // Verify error sets are distinct
509 const conn_err: ConnectionError = ConnectionError.AuthenticationFailed;
510 const proto_err: ProtocolError = ProtocolError.InvalidResponse;
511 const cmd_err: CommandError = CommandError.WrongType;
512
513 // All can be caught as ClientError
514 const all: [3]ClientError = .{ conn_err, proto_err, cmd_err };
515 try std.testing.expectEqual(@as(usize, 3), all.len);
516}