├── .github ├── CODEOWNERS └── workflows │ └── zig-test.yml ├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── build.zig ├── build.zig.zon └── src ├── main.zig ├── redis.zig └── tests.zig /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ralphvw -------------------------------------------------------------------------------- /.github/workflows/zig-test.yml: -------------------------------------------------------------------------------- 1 | name: Zig Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | name: Run zig tests with Redis 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | redis: 16 | image: redis:7 17 | ports: 18 | - 6379:6379 19 | options: >- 20 | --health-cmd "redis-cli ping || exit 1" 21 | --health-interval 5s 22 | --health-timeout 5s 23 | --health-retries 5 24 | 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v3 28 | 29 | - name: Set up Zig 30 | uses: goto-bus-stop/setup-zig@v2 31 | with: 32 | version: 0.13.0 33 | 34 | - name: Run zig test 35 | run: zig test src/tests.zig 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Zig build cache 2 | .zig-cache/ 3 | 4 | # Ignore binaries and build artifacts 5 | zig-out/ 6 | 7 | # Ignore temporary files created by editors or OS 8 | *.swp 9 | *.swo 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Ignore logs and debug files 14 | *.log 15 | 16 | # Ignore system-specific files 17 | .idea/ # IntelliJ IDEA or CLion project files 18 | .vscode/ # VS Code settings 19 | *.iml # IntelliJ module files 20 | 21 | # Ignore compiled library files (if any) 22 | *.o 23 | *.a 24 | *.so 25 | *.dll 26 | *.dylib 27 | 28 | # Ignore user-specific configuration files 29 | *.user 30 | 31 | # Ignore test coverage or analysis tools output 32 | coverage/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Rediz 2 | 3 | Thank you for your interest in contributing to **Rediz**, the Redis client for Zig! 4 | 5 | We welcome contributions of all kinds — whether it's fixing a bug, adding a feature, improving performance, or enhancing documentation. 6 | 7 | --- 8 | 9 | ## Getting Started 10 | 11 | 1. **Fork** the repository 12 | 2. **Clone** your fork locally 13 | 3. Create a new **feature branch**: 14 | ```sh 15 | git checkout -b feature/my-awesome-thing 16 | ``` 17 | 4. Make your changes 18 | 5. Run the tests: 19 | ```sh 20 | zig test src/tests.zig 21 | ``` 22 | 6. Commit and push: 23 | ```sh 24 | git push origin feature/my-awesome-thing 25 | ``` 26 | 7. Open a **Pull Request** on GitHub 27 | 28 | --- 29 | 30 | ## Code Guidelines 31 | 32 | - Use idiomatic Zig (`zig fmt` will help) 33 | - Keep functions focused and low-level if possible 34 | - Prefer clear naming over comments 35 | - Avoid unnecessary allocations unless justified 36 | - Use `std.testing` for test coverage 37 | 38 | --- 39 | 40 | ## Contributions We Love 41 | 42 | - Adding new Redis command support (e.g. `DEL`, `HGETALL`, `INCR`) 43 | - Improving protocol parsing (RESP arrays, errors, etc.) 44 | - Memory usage improvements 45 | - Useful test cases and example apps 46 | 47 | --- 48 | 49 | ## Questions? 50 | 51 | Feel free to open an issue or start a GitHub discussion if you’re unsure about anything before submitting a PR. 52 | 53 | Thanks again! 🎉 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REDIZ 2 | 3 | A Zig library for interacting with Redis. 4 | 5 | ## Features 6 | 7 | - Connect to Redis 8 | - `SET` and `GET` commands 9 | - `HSET` and `HGET` commands 10 | 11 | ## Installation 12 | 13 | `zig fetch --save git+https://github.com/ralphvw/rediz#main` 14 | 15 | ## Usage 16 | 17 | ```zig 18 | const std = @import("std"); 19 | const RedisClient = @import("Rediz").RedisClient; 20 | 21 | pub fn main() !void { 22 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 23 | const allocator = gpa.allocator(); 24 | defer _ = gpa.deinit(); 25 | 26 | // Connect to Redis (example: redis://password@localhost:6379/0) 27 | var client = try RedisClient.connect(allocator, "redis://127.0.0.1:6379"); 28 | defer client.disconnect(); 29 | 30 | // Set a key-value pair 31 | try client.set("zig_test", ""); 32 | 33 | // Get value into a heap-allocated buffer (caller must free) 34 | const value = try client.get("zig_test"); 35 | 36 | if (value) |v| { 37 | std.debug.print("Got value: {s}\n", .{v}); 38 | allocator.free(v); // Caller must free 39 | } 40 | 41 | // Get value into a stack allocated buffer 42 | var buffer: [100]u8 = undefined; 43 | var response: []const u8 = undefined; 44 | if (try client.getInto("some_key", buffer[0..])) |v| { 45 | response = v; 46 | } 47 | } 48 | ``` 49 | 50 | ## Adding Rediz to Your Project 51 | 52 | You can include Rediz in your project by adding the following to your `build.zig`: 53 | 54 | ```zig 55 | const std = @import("std"); 56 | 57 | pub fn build(b: *std.build.Builder) void { 58 | /// ... build script 59 | 60 | const rediz = b.dependency("Rediz", .{ 61 | .target = target, 62 | .optimize = optimize, 63 | }); 64 | 65 | // the executable from your call to b.addExecutable(...) 66 | exe.root_module.addImport("rediz", rediz.module("rediz")); 67 | } 68 | ``` 69 | 70 | ## 🤝 Contributing 71 | 72 | See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on how to contribute, set up your environment, and submit pull requests. 73 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | // Standard target options allows the person running `zig build` to choose 5 | // what target to build for. Here we do not override the defaults, which 6 | // means any target is allowed, and the default is native. Other options 7 | // for restricting supported target set are available. 8 | const target = b.standardTargetOptions(.{}); 9 | 10 | // Standard optimization options allow the person running `zig build` to select 11 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 12 | // set a preferred release mode, allowing the user to decide how to optimize. 13 | const optimize = b.standardOptimizeOption(.{}); 14 | 15 | const lib = b.addStaticLibrary(.{ 16 | .name = "rediz", 17 | // In this case the main source file is merely a path, however, in more 18 | // complicated build scripts, this could be a generated file. 19 | .root_source_file = b.path("src/redis.zig"), 20 | .target = target, 21 | .optimize = optimize, 22 | }); 23 | 24 | // This declares intent for the library to be installed into the standard 25 | // location when the user invokes the "install" step (the default step when 26 | // running `zig build`). 27 | b.installArtifact(lib); 28 | 29 | const rediz_module = b.addModule("rediz", .{ 30 | .root_source_file = b.path("src/redis.zig"), 31 | .target = target, 32 | .optimize = optimize, 33 | }); 34 | 35 | // Link module to both library and executable (optional) 36 | lib.root_module.addImport("rediz", rediz_module); 37 | 38 | const exe = b.addExecutable(.{ 39 | .name = "rediz-example", 40 | .root_source_file = b.path("src/main.zig"), 41 | .target = target, 42 | .optimize = optimize, 43 | }); 44 | 45 | // This declares intent for the executable to be installed into the 46 | // standard location when the user invokes the "install" step (the default 47 | // step when running `zig build`). 48 | b.installArtifact(exe); 49 | 50 | // This *creates* a Run step in the build graph, to be executed when another 51 | // step is evaluated that depends on it. The next line below will establish 52 | // such a dependency. 53 | const run_cmd = b.addRunArtifact(exe); 54 | 55 | // By making the run step depend on the install step, it will be run from the 56 | // installation directory rather than directly from within the cache directory. 57 | // This is not necessary, however, if the application depends on other installed 58 | // files, this ensures they will be present and in the expected location. 59 | run_cmd.step.dependOn(b.getInstallStep()); 60 | 61 | // This allows the user to pass arguments to the application in the build 62 | // command itself, like this: `zig build run -- arg1 arg2 etc` 63 | if (b.args) |args| { 64 | run_cmd.addArgs(args); 65 | } 66 | 67 | // This creates a build step. It will be visible in the `zig build --help` menu, 68 | // and can be selected like this: `zig build run` 69 | // This will evaluate the `run` step rather than the default, which is "install". 70 | const run_step = b.step("run", "Run the app"); 71 | run_step.dependOn(&run_cmd.step); 72 | } 73 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | // This is the default name used by packages depending on this one. For 3 | // example, when a user runs `zig fetch --save `, this field is used 4 | // as the key in the `dependencies` table. Although the user can choose a 5 | // different name, most users will stick with this provided value. 6 | // 7 | // It is redundant to include "zig" in this name because it is already 8 | // within the Zig package namespace. 9 | .name = "rediz", 10 | 11 | // This is a [Semantic Version](https://semver.org/). 12 | // In a future version of Zig it will be used for package deduplication. 13 | .version = "0.0.0", 14 | 15 | // This field is optional. 16 | // This is currently advisory only; Zig does not yet do anything 17 | // with this value. 18 | //.minimum_zig_version = "0.11.0", 19 | 20 | // This field is optional. 21 | // Each dependency must either provide a `url` and `hash`, or a `path`. 22 | // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. 23 | // Once all dependencies are fetched, `zig build` no longer requires 24 | // internet connectivity. 25 | .dependencies = .{ 26 | // See `zig fetch --save ` for a command-line interface for adding dependencies. 27 | //.example = .{ 28 | // // When updating this field to a new URL, be sure to delete the corresponding 29 | // // `hash`, otherwise you are communicating that you expect to find the old hash at 30 | // // the new URL. 31 | // .url = "https://example.com/foo.tar.gz", 32 | // 33 | // // This is computed from the file contents of the directory of files that is 34 | // // obtained after fetching `url` and applying the inclusion rules given by 35 | // // `paths`. 36 | // // 37 | // // This field is the source of truth; packages do not come from a `url`; they 38 | // // come from a `hash`. `url` is just one of many possible mirrors for how to 39 | // // obtain a package matching this `hash`. 40 | // // 41 | // // Uses the [multihash](https://multiformats.io/multihash/) format. 42 | // .hash = "...", 43 | // 44 | // // When this is provided, the package is found in a directory relative to the 45 | // // build root. In this case the package's hash is irrelevant and therefore not 46 | // // computed. This field and `url` are mutually exclusive. 47 | // .path = "foo", 48 | 49 | // // When this is set to `true`, a package is declared to be lazily 50 | // // fetched. This makes the dependency only get fetched if it is 51 | // // actually used. 52 | // .lazy = false, 53 | //}, 54 | }, 55 | 56 | // Specifies the set of files and directories that are included in this package. 57 | // Only files and directories listed here are included in the `hash` that 58 | // is computed for this package. Only files listed here will remain on disk 59 | // when using the zig package manager. As a rule of thumb, one should list 60 | // files required for compilation plus any license(s). 61 | // Paths are relative to the build root. Use the empty string (`""`) to refer to 62 | // the build root itself. 63 | // A directory listed here means that all files within, recursively, are included. 64 | .paths = .{ 65 | "build.zig", 66 | "build.zig.zon", 67 | "src", 68 | // For example... 69 | //"LICENSE", 70 | //"README.md", 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const RedisClient = @import("redis.zig").RedisClient; 3 | 4 | pub fn main() !void { 5 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 6 | const allocator = gpa.allocator(); 7 | defer _ = gpa.deinit(); 8 | 9 | // Connect to Redis (example: redis://password@localhost:6379/0) 10 | var client = try RedisClient.connect(allocator, "redis://127.0.0.1:6379"); 11 | defer client.disconnect(); 12 | 13 | // Set and get value 14 | try client.set("zig_test", ""); 15 | const value = try client.get("somestation"); 16 | 17 | if (value) |v| { 18 | std.debug.print("Got value: {s}\n", .{v}); 19 | allocator.free(v); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/redis.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const net = std.net; 3 | const mem = std.mem; 4 | const Uri = std.Uri; 5 | 6 | pub const RedisClient = struct { 7 | stream: net.Stream, 8 | allocator: mem.Allocator, 9 | 10 | const Self = @This(); 11 | 12 | /// Connect to a Redis server using the provided URI. 13 | /// The URI should be in the format: redis://[username:password@]host[:port][/db] 14 | /// If the port is not specified, it defaults to 6379. 15 | /// If the database is not specified, it defaults to 0. 16 | /// If the username is not specified, it defaults to an empty string. 17 | /// If the password is not specified, it defaults to an empty string. 18 | /// The function returns a RedisClient instance on success or an error on failure. 19 | pub fn connect(allocator: mem.Allocator, uri: []const u8) !Self { 20 | const parsed_uri = try Uri.parse(uri); 21 | const port = parsed_uri.port orelse 6379; 22 | const host = parsed_uri.host.?; 23 | const address = try net.Address.resolveIp(host.percent_encoded, port); 24 | 25 | const stream = try net.tcpConnectToAddress(address); 26 | 27 | var client = Self{ 28 | .stream = stream, 29 | .allocator = allocator, 30 | }; 31 | 32 | if (parsed_uri.password) |password| { 33 | try client.auth(password.percent_encoded); 34 | } 35 | 36 | const path = parsed_uri.path; 37 | if (path.percent_encoded.len > 1 and path.percent_encoded[0] == '/') { 38 | const db = try std.fmt.parseInt(u8, path.percent_encoded[1..], 10); 39 | try client.select(db); 40 | } 41 | 42 | return client; 43 | } 44 | 45 | /// Disconnect from the Redis server. 46 | pub fn disconnect(self: *Self) void { 47 | self.stream.close(); 48 | } 49 | 50 | /// Send a command to the Redis server. 51 | pub fn sendCommand(self: *Self, comptime N: usize, args: [N][]const u8) !void { 52 | var writer = self.stream.writer(); 53 | try writer.print("*{d}\r\n", .{N}); 54 | inline for (args) |arg| { 55 | try writer.print("${d}\r\n", .{arg.len}); 56 | try writer.writeAll(arg); 57 | try writer.writeAll("\r\n"); 58 | } 59 | } 60 | 61 | /// Read a simple string response from the Redis server. 62 | pub fn readSimpleString(self: *Self) ![]const u8 { 63 | var reader = self.stream.reader(); 64 | const line = try reader.readUntilDelimiterAlloc(self.allocator, '\r', 1024); 65 | // skip bytes if line string starts with '\n' 66 | if (line.len > 0 and line[0] == '\n') { 67 | const new_line = try self.allocator.alloc(u8, line.len - 1); 68 | std.mem.copyForwards(u8, new_line, line[1..]); 69 | self.allocator.free(line); 70 | return new_line; 71 | } 72 | 73 | return line; 74 | } 75 | 76 | /// Read a bulk string response from the Redis server. 77 | fn readBulkString(self: *Self) !?[]const u8 { 78 | var reader = self.stream.reader(); 79 | const len = try reader.readUntilDelimiterAlloc(self.allocator, '\r', 1024); 80 | defer self.allocator.free(len); 81 | if (containsChar(len, '-')) { 82 | return null; 83 | } 84 | 85 | const length = std.fmt.parseInt(usize, len[2..], 10) catch return null; 86 | if (length == -1) return null; 87 | 88 | var data = try self.allocator.alloc(u8, length + 1); 89 | errdefer self.allocator.free(data); 90 | try reader.readNoEof(data); 91 | try reader.skipBytes(2, .{}); 92 | if (data.len > 0 and data[0] == '\n') { 93 | const new_data = try self.allocator.alloc(u8, data.len - 1); 94 | std.mem.copyForwards(u8, new_data, data[1..]); 95 | self.allocator.free(data); 96 | return new_data; 97 | } 98 | 99 | return data; 100 | } 101 | 102 | /// Sets a key-value pair in Redis. 103 | pub fn set(self: *Self, key: []const u8, value: []const u8) !void { 104 | try self.sendCommand(3, .{ "SET", key, value }); 105 | const response = try self.readSimpleString(); 106 | defer self.allocator.free(response); 107 | if (!mem.eql(u8, response, "+OK")) { 108 | return error.RedisError; 109 | } 110 | } 111 | 112 | /// Gets the value of a key from Redis. 113 | /// Retuns an allocated string. Remember to free it after use. 114 | /// Returns null if the key does not exist. 115 | pub fn get(self: *Self, key: []const u8) !?[]const u8 { 116 | try self.sendCommand(2, .{ "GET", key }); 117 | return try self.readBulkString(); 118 | } 119 | 120 | /// Gets the value of a key from Redis and copies it into the provided buffer. 121 | /// Returns an error if the buffer is too small. 122 | /// Returns null if the key does not exist. 123 | pub fn getInto(self: *Self, key: []const u8, buffer: []u8) !?[]const u8 { 124 | try self.sendCommand(2, .{ "GET", key }); 125 | 126 | const result = try self.readBulkString(); 127 | if (result == null) return null; 128 | 129 | const value = result.?; 130 | 131 | if (buffer.len < value.len) { 132 | self.allocator.free(value); 133 | return error.BufferTooSmall; 134 | } 135 | 136 | std.mem.copyForwards(u8, buffer[0..value.len], value); 137 | self.allocator.free(value); 138 | 139 | return buffer[0..value.len]; 140 | } 141 | 142 | /// Sets a field in a Redis hash. 143 | /// Equivalent to: HSET key field value 144 | pub fn hset(self: *Self, key: []const u8, field: []const u8, value: []const u8) !void { 145 | try self.sendCommand(4, .{ "HSET", key, field, value }); 146 | const response = try self.readSimpleString(); 147 | defer self.allocator.free(response); 148 | if (!std.mem.startsWith(u8, response, ":")) { 149 | return error.RedisError; 150 | } 151 | } 152 | 153 | /// Gets the value of a field in a Redis hash. 154 | /// Returns null if field or key doesn't exist. 155 | /// Caller must free the returned value. 156 | pub fn hget(self: *Self, key: []const u8, field: []const u8) !?[]const u8 { 157 | try self.sendCommand(3, .{ "HGET", key, field }); 158 | return try self.readBulkString(); 159 | } 160 | 161 | /// Gets the value of a field in a Redis hash and copies it into the provided buffer. 162 | /// Returns null if the field or key doesn't exist. 163 | /// Returns an error if the buffer is too small. 164 | pub fn hgetInto(self: *Self, key: []const u8, field: []const u8, buffer: []u8) !?[]const u8 { 165 | try self.sendCommand(3, .{ "HGET", key, field }); 166 | 167 | const result = try self.readBulkString(); 168 | if (result == null) return null; 169 | 170 | const value = result.?; 171 | 172 | if (buffer.len < value.len) { 173 | self.allocator.free(value); 174 | return error.BufferTooSmall; 175 | } 176 | 177 | std.mem.copyForwards(u8, buffer[0..value.len], value); 178 | self.allocator.free(value); 179 | 180 | return buffer[0..value.len]; 181 | } 182 | 183 | /// Authenticates with the Redis server using the provided password. 184 | fn auth(self: *Self, password: []const u8) !void { 185 | try self.sendCommand(2, .{ "AUTH", password }); 186 | const response = try self.readSimpleString(); 187 | defer self.allocator.free(response); 188 | if (!mem.eql(u8, response, "+OK")) { 189 | return error.AuthFailed; 190 | } 191 | } 192 | 193 | /// Selects a Redis database. 194 | pub fn select(self: *Self, db: u8) !void { 195 | var buf: [16]u8 = undefined; 196 | const db_str = try std.fmt.bufPrint(&buf, "{}", .{db}); 197 | try self.sendCommand(2, .{ "SELECT", db_str }); 198 | const response = try self.readSimpleString(); 199 | defer self.allocator.free(response); 200 | if (!mem.eql(u8, response, "+OK")) { 201 | return error.SelectFailed; 202 | } 203 | } 204 | 205 | /// Helper function to check if a byte array contains a specific character. 206 | fn containsChar(input: []const u8, target: u8) bool { 207 | for (input) |char| { 208 | if (char == target) { 209 | return true; 210 | } 211 | } 212 | return false; 213 | } 214 | }; 215 | -------------------------------------------------------------------------------- /src/tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | const RedisClient = @import("redis.zig").RedisClient; 4 | 5 | test "RedisClient can connect and disconnect" { 6 | var client = try RedisClient.connect(std.testing.allocator, "redis://127.0.0.1:6379"); 7 | defer client.disconnect(); 8 | 9 | // Send a PING to confirm connection 10 | try client.sendCommand(1, .{"PING"}); 11 | const response = try client.readSimpleString(); 12 | defer std.testing.allocator.free(response); 13 | 14 | try testing.expect(std.mem.eql(u8, response, "+PONG")); 15 | } 16 | 17 | test "RedisClient can set and get a key" { 18 | var client = try RedisClient.connect(std.testing.allocator, "redis://127.0.0.1:6379"); 19 | defer client.disconnect(); 20 | 21 | try client.set("test_key", "test_value"); 22 | 23 | const value = try client.get("test_key"); 24 | 25 | defer std.testing.allocator.free(value.?); 26 | try testing.expect(value != null); 27 | try testing.expect(std.mem.eql(u8, value.?, "test_value")); 28 | } 29 | 30 | test "RedisClient handles missing keys" { 31 | var client = try RedisClient.connect(std.testing.allocator, "redis://127.0.0.1:6379"); 32 | defer client.disconnect(); 33 | 34 | const value = try client.get("non_existent_key"); 35 | try testing.expect(value == null); 36 | } 37 | 38 | test "RedisClient fails authentication with wrong password" { 39 | const result = RedisClient.connect(std.testing.allocator, "redis://:wrongpass@127.0.0.1:6379"); 40 | try testing.expectError(error.AuthFailed, result); 41 | } 42 | 43 | test "RedisClient can select a database" { 44 | var client = try RedisClient.connect(std.testing.allocator, "redis://127.0.0.1:6379/2"); 45 | defer client.disconnect(); 46 | 47 | try client.set("db_test_key", "db_test_value"); 48 | const value = try client.get("db_test_key"); 49 | 50 | defer std.testing.allocator.free(value.?); 51 | 52 | try testing.expect(value != null); 53 | try testing.expect(std.mem.eql(u8, value.?, "db_test_value")); 54 | } 55 | 56 | test "Redis client can get value into a stack allocated buffer" { 57 | var client = try RedisClient.connect(std.testing.allocator, "redis://127.0.0.1:6379"); 58 | defer client.disconnect(); 59 | 60 | try client.set("db_test_key", "db_test_value"); 61 | var buffer: [100]u8 = undefined; 62 | const response = try client.getInto("db_test_key", buffer[0..]); 63 | try testing.expect(std.mem.eql(u8, response.?, "db_test_value")); 64 | } 65 | 66 | test "Redis client fails to get value into stack allocated buffer because size is too small" { 67 | var client = try RedisClient.connect(std.testing.allocator, "redis://127.0.0.1:6379"); 68 | defer client.disconnect(); 69 | 70 | try client.set("db_test_key", "db_test_value"); 71 | 72 | var buffer: [1]u8 = undefined; 73 | 74 | const result = client.getInto("db_test_key", buffer[0..]); 75 | 76 | try testing.expectError(error.BufferTooSmall, result); 77 | } 78 | 79 | test "Redis client can set and get from a hashset" { 80 | const allocator = std.testing.allocator; 81 | var client = try RedisClient.connect(allocator, "redis://127.0.0.1:6379"); 82 | defer client.disconnect(); 83 | 84 | try client.hset("lumon_employees", "emp_1", "Mark S."); 85 | const response = try client.hget("lumon_employees", "emp_1"); 86 | if (response) |v| { 87 | try testing.expect(std.mem.eql(u8, v, "Mark S.")); 88 | allocator.free(v); 89 | } else { 90 | try testing.expect(false); 91 | } 92 | } 93 | 94 | test "Redis client can set and get from a hashset into a stack allocated buffer" { 95 | const allocator = std.testing.allocator; 96 | var client = try RedisClient.connect(allocator, "redis://127.0.0.1:6379"); 97 | defer client.disconnect(); 98 | 99 | var buffer: [100]u8 = undefined; 100 | 101 | try client.hset("lumon_employees", "emp_1", "Mark S."); 102 | 103 | const response = try client.hgetInto("lumon_employees", "emp_1", &buffer); 104 | if (response) |v| { 105 | try testing.expect(std.mem.eql(u8, v, "Mark S.")); 106 | } else { 107 | try testing.expect(false); 108 | } 109 | } 110 | 111 | test "Redis client fails to get from a hashset into a stack allocated buffer" { 112 | const allocator = std.testing.allocator; 113 | var client = try RedisClient.connect(allocator, "redis://127.0.0.1:6379"); 114 | defer client.disconnect(); 115 | 116 | var buffer: [1]u8 = undefined; 117 | 118 | try client.hset("lumon_employees", "emp_1", "Mark S."); 119 | 120 | const response = client.hgetInto("lumon_employees", "emp_1", &buffer); 121 | 122 | try testing.expectError(error.BufferTooSmall, response); 123 | } 124 | --------------------------------------------------------------------------------