this repo has no description
at main 516 lines 17 kB view raw
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}