├── src ├── commands │ ├── strings │ │ ├── msetnx.zig │ │ ├── psetex.zig │ │ ├── bitfield.zig │ │ ├── decr.zig │ │ ├── mget.zig │ │ ├── mset.zig │ │ ├── decrby.zig │ │ ├── getset.zig │ │ ├── incr.zig │ │ ├── get.zig │ │ ├── incrby.zig │ │ ├── append.zig │ │ ├── incrbyfloat.zig │ │ ├── getbit.zig │ │ ├── getrange.zig │ │ ├── setbit.zig │ │ ├── bitop.zig │ │ ├── bitpos.zig │ │ ├── bitcount.zig │ │ └── set.zig │ ├── keys.zig │ ├── hyperloglog.zig │ ├── keys │ │ └── del.zig │ ├── streams.zig │ ├── hashes.zig │ ├── geo │ │ ├── _utils.zig │ │ ├── geopos.zig │ │ ├── geohash.zig │ │ ├── geodist.zig │ │ ├── geoadd.zig │ │ ├── georadiusbymember.zig │ │ └── georadius.zig │ ├── geo.zig │ ├── hyperloglog │ │ ├── pfadd.zig │ │ ├── pfcount.zig │ │ └── pfmerge.zig │ ├── sets │ │ ├── scard.zig │ │ ├── smembers.zig │ │ ├── sunion.zig │ │ ├── sinter.zig │ │ ├── sismember.zig │ │ ├── sdiff.zig │ │ ├── sunionstore.zig │ │ ├── sdiffstore.zig │ │ ├── sadd.zig │ │ ├── srem.zig │ │ ├── smismember.zig │ │ ├── sinterstore.zig │ │ ├── smove.zig │ │ ├── srandmember.zig │ │ ├── spop.zig │ │ └── sscan.zig │ ├── transactions.zig │ ├── _common_utils.zig │ ├── README.md │ ├── sets.zig │ ├── hashes │ │ ├── hincrby.zig │ │ ├── hmget.zig │ │ └── hset.zig │ ├── strings.zig │ └── streams │ │ ├── xtrim.zig │ │ ├── _utils.zig │ │ └── xread.zig ├── keys │ ├── key.zig │ ├── string.zig │ ├── stream.zig │ └── README.md ├── root.zig ├── types.zig ├── parser │ ├── t_double.zig │ ├── t_number.zig │ ├── t_bool.zig │ ├── t_bignum.zig │ ├── t_string_simple.zig │ ├── void.zig │ ├── t_set.zig │ ├── t_string_blob.zig │ └── t_list.zig ├── commands.zig ├── types │ ├── fixbuf.zig │ ├── attributes.zig │ └── verbatim.zig ├── lib │ └── float.zig └── traits.zig ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── test └── async.zig ├── CLIENT.md ├── README.md └── COMMANDS.md /src/commands/strings/msetnx.zig: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /src/commands/strings/psetex.zig: -------------------------------------------------------------------------------- 1 | // PSETEX key milliseconds value 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | .DS_Store 4 | dump.rdb 5 | *.swp -------------------------------------------------------------------------------- /src/commands/strings/bitfield.zig: -------------------------------------------------------------------------------- 1 | // BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL] 2 | 3 | // Well... this one is tricky, leaving it for later :) 4 | -------------------------------------------------------------------------------- /src/commands/keys.zig: -------------------------------------------------------------------------------- 1 | pub const DEL = @import("./keys/del.zig").DEL; 2 | 3 | test "keys" { 4 | _ = @import("./keys/del.zig"); 5 | } 6 | 7 | test "docs" { 8 | const std = @import("std"); 9 | std.testing.refAllDecls(@This()); 10 | } 11 | -------------------------------------------------------------------------------- /src/keys/key.zig: -------------------------------------------------------------------------------- 1 | pub const Key = struct { 2 | name: []const u8, 3 | 4 | pub fn init(key_name: []const u8) Key { 5 | return Key{key_name}; 6 | } 7 | 8 | pub fn del(self: Key) DEL { 9 | return DEL.init(self.name); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup Zig Compiler 19 | uses: mlugg/setup-zig@v2 20 | 21 | - name: Build and test Zig Project 22 | run: zig build test 23 | -------------------------------------------------------------------------------- /src/keys/string.zig: -------------------------------------------------------------------------------- 1 | const Key = @import("./key.zig"); 2 | const cmds = @import("../commands.zig"); 3 | 4 | const String = struct { 5 | key: Key, 6 | 7 | pub fn init(key_name: []u8) String { 8 | return String{Key{key_name}}; 9 | } 10 | 11 | pub fn set(self: String, newVal: []u8) cmds.SET { 12 | return SET.init(self.key.name, newVal); 13 | } 14 | 15 | pub fn get(self: String) cmds.GET { 16 | return GET.init(self.key.name); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/commands/hyperloglog.zig: -------------------------------------------------------------------------------- 1 | pub const PFADD = @import("./hyperloglog/pfadd.zig").PFADD; 2 | pub const PFCOUNT = @import("./hyperloglog/pfcount.zig").PFCOUNT; 3 | pub const PFMERGE = @import("./hyperloglog/pfmerge.zig").PFMERGE; 4 | 5 | test "hyperloglog" { 6 | _ = @import("./hyperloglog/pfadd.zig"); 7 | _ = @import("./hyperloglog/pfcount.zig"); 8 | _ = @import("./hyperloglog/pfmerge.zig"); 9 | } 10 | 11 | test "docs" { 12 | const std = @import("std"); 13 | std.testing.refAllDecls(@This()); 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/keys/del.zig: -------------------------------------------------------------------------------- 1 | pub const DEL = struct { 2 | keys: []const []const u8, 3 | 4 | const Self = @This(); 5 | pub fn init(keys: []const []const u8) Self { 6 | return .{ .keys = keys }; 7 | } 8 | 9 | const RedisCommand = struct { 10 | pub fn serialize(self: Self, rootSerializer: type, msg: anytype) !void { 11 | return rootSerializer.serialize(msg, .{ "DEL", self.keys }); 12 | } 13 | }; 14 | }; 15 | 16 | test "basic usage" { 17 | _ = DEL.init(&[_][]const u8{ "lol", "123", "test" }); 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/streams.zig: -------------------------------------------------------------------------------- 1 | pub const XADD = @import("./streams/xadd.zig").XADD; 2 | pub const XREAD = @import("./streams/xread.zig").XREAD; 3 | pub const XTRIM = @import("./streams/xtrim.zig").XTRIM; 4 | pub const utils = struct { 5 | pub const FV = @import("./_common_utils.zig").FV; 6 | }; 7 | 8 | test "streams" { 9 | _ = @import("./streams/xadd.zig"); 10 | _ = @import("./streams/xread.zig"); 11 | _ = @import("./streams/xtrim.zig"); 12 | } 13 | 14 | test "docs" { 15 | const std = @import("std"); 16 | std.testing.refAllDecls(@This()); 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/hashes.zig: -------------------------------------------------------------------------------- 1 | pub const HMGET = @import("./hashes/hmget.zig").HMGET; 2 | pub const HSET = @import("./hashes/hset.zig").HSET; 3 | pub const HINCRBY = @import("./hashes/hincrby.zig").HINCRBY; 4 | 5 | pub const utils = struct { 6 | pub const FV = @import("./_common_utils.zig").FV; 7 | }; 8 | 9 | test "hashes" { 10 | _ = @import("./hashes/hmget.zig"); 11 | _ = @import("./hashes/hset.zig"); 12 | _ = @import("./hashes/hincrby.zig"); 13 | } 14 | 15 | test "docs" { 16 | const std = @import("std"); 17 | std.testing.refAllDecls(@This()); 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/geo/_utils.zig: -------------------------------------------------------------------------------- 1 | pub const Unit = enum { 2 | meters, 3 | kilometers, 4 | feet, 5 | miles, 6 | 7 | pub const RedisArguments = struct { 8 | pub fn count(_: Unit) usize { 9 | return 1; 10 | } 11 | 12 | pub fn serialize(self: Unit, comptime rootSerializer: type, msg: anytype) !void { 13 | const symbol = switch (self) { 14 | .meters => "m", 15 | .kilometers => "km", 16 | .feet => "ft", 17 | .miles => "mi", 18 | }; 19 | 20 | try rootSerializer.serializeArgument(msg, []const u8, symbol); 21 | } 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/commands/strings/decr.zig: -------------------------------------------------------------------------------- 1 | // DECR key 2 | 3 | pub const DECR = struct { 4 | key: []const u8, 5 | 6 | const Self = @This(); 7 | 8 | pub fn init(key: []const u8) DECR { 9 | return .{ .key = key }; 10 | } 11 | 12 | pub fn validate(self: Self) !void { 13 | if (self.key.len == 0) return error.EmptyKeyName; 14 | } 15 | 16 | pub const RedisCommand = struct { 17 | pub fn serialize(self: DECR, comptime rootSerializer: type, msg: anytype) !void { 18 | return rootSerializer.serializeCommand(msg, .{ "DECR", self.key }); 19 | } 20 | }; 21 | }; 22 | 23 | test "basic usage" { 24 | _ = DECR.init("lol"); 25 | } 26 | -------------------------------------------------------------------------------- /src/root.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const Client = @import("./client.zig"); 4 | pub const commands = @import("./commands.zig"); 5 | const parser = @import("./parser.zig"); 6 | pub const freeReply = parser.RESP3Parser.freeReply; 7 | const serializer = @import("./serializer.zig"); 8 | pub const traits = @import("./traits.zig"); 9 | pub const types = @import("./types.zig"); 10 | 11 | test "okredis" { 12 | _ = @import("./client.zig"); 13 | _ = @import("./parser.zig"); 14 | _ = @import("./types.zig"); 15 | _ = @import("./serializer.zig"); 16 | _ = @import("./commands.zig"); 17 | } 18 | 19 | test "docs" { 20 | std.testing.refAllDecls(@This()); 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/geo.zig: -------------------------------------------------------------------------------- 1 | pub const GEOADD = @import("./geo/geoadd.zig").GEOADD; 2 | pub const GEODIST = @import("./geo/geodist.zig").GEODIST; 3 | pub const GEOHASH = @import("./geo/geohash.zig").GEOHASH; 4 | pub const GEOPOS = @import("./geo/geopos.zig").GEOPOS; 5 | pub const GEORADIUS = @import("./geo/georadius.zig").GEORADIUS; 6 | pub const GEORADIUSBYMEMBER = @import("./geo/georadiusbymember.zig").GEORADIUSBYMEMBER; 7 | 8 | test "geo" { 9 | _ = @import("./geo/geoadd.zig"); 10 | _ = @import("./geo/geodist.zig"); 11 | _ = @import("./geo/geohash.zig"); 12 | _ = @import("./geo/geopos.zig"); 13 | _ = @import("./geo/georadius.zig"); 14 | _ = @import("./geo/georadiusbymember.zig"); 15 | } 16 | 17 | test "docs" { 18 | const std = @import("std"); 19 | std.testing.refAllDecls(@This()); 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/strings/mget.zig: -------------------------------------------------------------------------------- 1 | // MGET key [key ...] 2 | 3 | pub const MGET = struct { 4 | keys: []const []const u8, 5 | 6 | pub fn init(keys: []const []const u8) MGET { 7 | return .{ 8 | .keys = keys, 9 | }; 10 | } 11 | 12 | pub fn validate(self: MGET) !void { 13 | if (self.keys.len == 0) return error.KeysArrayIsEmpty; 14 | for (self.keys) |k| { 15 | if (k.len == 0) return error.EmptyKeyName; 16 | } 17 | } 18 | 19 | const RedisCommand = struct { 20 | pub fn serialize(self: MGET, rootSerializer: type, msg: anytype) !void { 21 | return rootSerializer.serialize(msg, .{ "MGET", self.keys }); 22 | } 23 | }; 24 | }; 25 | 26 | test "basic usage" { 27 | _ = MGET.init(&[_][]const u8{ "lol", "key1", "key2" }); 28 | } 29 | -------------------------------------------------------------------------------- /src/types.zig: -------------------------------------------------------------------------------- 1 | const attributes = @import("./types/attributes.zig"); 2 | const verbatim = @import("./types/verbatim.zig"); 3 | const fixbuf = @import("./types/fixbuf.zig"); 4 | const reply = @import("./types/reply.zig"); 5 | const err = @import("./types/error.zig"); 6 | 7 | pub const WithAttribs = attributes.WithAttribs; 8 | pub const Verbatim = verbatim.Verbatim; 9 | pub const FixBuf = fixbuf.FixBuf; 10 | pub const DynamicReply = reply.DynamicReply; 11 | pub const OrFullErr = err.OrFullErr; 12 | pub const OrErr = err.OrErr; 13 | 14 | test "types" { 15 | _ = @import("./types/attributes.zig"); 16 | _ = @import("./types/verbatim.zig"); 17 | _ = @import("./types/fixbuf.zig"); 18 | _ = @import("./types/reply.zig"); 19 | _ = @import("./types/error.zig"); 20 | } 21 | 22 | test "docs" { 23 | @import("std").testing.refAllDecls(@This()); 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/hyperloglog/pfadd.zig: -------------------------------------------------------------------------------- 1 | // PFADD key element [element ...] 2 | 3 | pub const PFADD = struct { 4 | key: []const u8, 5 | elements: []const []const u8, 6 | 7 | /// Instantiates a new PFADD command. 8 | pub fn init(key: []const u8, elements: []const []const u8) PFADD { 9 | return .{ .key = key, .elements = elements }; 10 | } 11 | 12 | /// Validates if the command is syntactically correct. 13 | pub fn validate(_: PFADD) !void {} 14 | 15 | pub const RedisCommand = struct { 16 | pub fn serialize(self: PFADD, comptime rootSerializer: type, msg: anytype) !void { 17 | return rootSerializer.serializeCommand(msg, .{ "PFADD", self.key, self.elements }); 18 | } 19 | }; 20 | }; 21 | 22 | test "basic usage" { 23 | const cmd = PFADD.init("counter", &[_][]const u8{ "m1", "m2", "m3" }); 24 | try cmd.validate(); 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/hyperloglog/pfcount.zig: -------------------------------------------------------------------------------- 1 | // PFCOUNT key [key ...] 2 | 3 | pub const PFCOUNT = struct { 4 | keys: []const []const u8, 5 | 6 | /// Instantiates a new PFCOUNT command. 7 | pub fn init(keys: []const []const u8) PFCOUNT { 8 | return .{ .keys = keys }; 9 | } 10 | 11 | /// Validates if the command is syntactically correct. 12 | pub fn validate(self: PFCOUNT) !void { 13 | if (self.keys.len == 0) { 14 | return error.EmptyKeySLice; 15 | } 16 | } 17 | 18 | pub const RedisCommand = struct { 19 | pub fn serialize(self: PFCOUNT, comptime rootSerializer: type, msg: anytype) !void { 20 | return rootSerializer.serializeCommand(msg, .{ "PFCOUNT", self.keys }); 21 | } 22 | }; 23 | }; 24 | 25 | test "basic usage" { 26 | const cmd = PFCOUNT.init(&[_][]const u8{ "counter1", "counter2", "counter3" }); 27 | try cmd.validate(); 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/strings/mset.zig: -------------------------------------------------------------------------------- 1 | // // MSET key value [key value ...] 2 | 3 | // pub const MSET = struct { 4 | // kvs: []const KV, 5 | 6 | // const Self = @This(); 7 | // pub fn init(kvs: []const KV) Self { 8 | // return .{ .keys = keys }; 9 | // } 10 | 11 | // pub fn validate(self: Self) !void { 12 | // if (self.kvs.len == 0) return error.KVsArrayIsEmpty; 13 | // for (self.kvs) |kv| { 14 | // if (kv.key.len == 0) return error.EmptyKeyName; 15 | // } 16 | // } 17 | 18 | // const Redis = struct { 19 | // const Command = struct { 20 | // pub fn serialize(self: Self, rootSerializer: type, msg: var) !void { 21 | // return rootSerializer.serialize(msg, .{ "MSET", self.kvs }); 22 | // } 23 | // }; 24 | // }; 25 | // }; 26 | 27 | // test "basic usage" { 28 | // const cmd = MSET.init(.{ "lol", "123", "test" }); 29 | // } 30 | -------------------------------------------------------------------------------- /src/commands/strings/decrby.zig: -------------------------------------------------------------------------------- 1 | // DECRBY key decrement 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const DECRBY = struct { 7 | key: []const u8, 8 | decrement: i64, 9 | 10 | const Self = @This(); 11 | 12 | pub fn init(key: []const u8, decrement: i64) DECRBY { 13 | return .{ .key = key, .decrement = decrement }; 14 | } 15 | 16 | pub fn validate(self: Self) !void { 17 | if (self.key.len == 0) return error.EmptyKeyName; 18 | } 19 | 20 | pub const RedisCommand = struct { 21 | pub fn serialize( 22 | self: DECRBY, 23 | comptime root: type, 24 | w: *Writer, 25 | ) !void { 26 | return root.serializeCommand( 27 | w, 28 | .{ "DECRBY", self.key, self.decrement }, 29 | ); 30 | } 31 | }; 32 | }; 33 | 34 | test "basic usage" { 35 | _ = DECRBY.init("lol", 42); 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/geo/geopos.zig: -------------------------------------------------------------------------------- 1 | // GEOPOS key member [member ...] 2 | 3 | pub const GEOPOS = struct { 4 | key: []const u8, 5 | members: []const []const u8, 6 | 7 | /// Instantiates a new GEOPOS command. 8 | pub fn init(key: []const u8, members: []const []const u8) GEOPOS { 9 | return .{ .key = key, .members = members }; 10 | } 11 | 12 | /// Validates if the command is syntactically correct. 13 | pub fn validate(self: GEOPOS) !void { 14 | if (self.members.len == 0) return error.MembersArrayIsEmpty; 15 | } 16 | 17 | pub const RedisCommand = struct { 18 | pub fn serialize(self: GEOPOS, comptime rootSerializer: type, msg: anytype) !void { 19 | return rootSerializer.serializeCommand(msg, .{ "GEOPOS", self.key, self.members }); 20 | } 21 | }; 22 | }; 23 | 24 | test "basic usage" { 25 | const cmd = GEOPOS.init("mykey", &[_][]const u8{ "member1", "member2" }); 26 | try cmd.validate(); 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/hyperloglog/pfmerge.zig: -------------------------------------------------------------------------------- 1 | // PFMERGE destkey sourcekey [sourcekey ...] 2 | 3 | pub const PFMERGE = struct { 4 | destkey: []const u8, 5 | sourcekeys: []const []const u8, 6 | 7 | /// Instantiates a new PFMERGE command. 8 | pub fn init(destkey: []const u8, sourcekeys: []const []const u8) PFMERGE { 9 | return .{ .destkey = destkey, .sourcekeys = sourcekeys }; 10 | } 11 | 12 | /// Validates if the command is syntactically correct. 13 | pub fn validate(_: PFMERGE) !void {} 14 | 15 | pub const RedisCommand = struct { 16 | pub fn serialize(self: PFMERGE, comptime rootSerializer: type, msg: anytype) !void { 17 | return rootSerializer.serializeCommand(msg, .{ "PFMERGE", self.destkey, self.sourcekeys }); 18 | } 19 | }; 20 | }; 21 | 22 | test "basic usage" { 23 | const cmd = PFMERGE.init("finalcounter", &[_][]const u8{ "counter1", "counter2", "counter3" }); 24 | try cmd.validate(); 25 | } 26 | -------------------------------------------------------------------------------- /src/commands/geo/geohash.zig: -------------------------------------------------------------------------------- 1 | // GEOHASH key member [member ...] 2 | 3 | pub const GEOHASH = struct { 4 | key: []const u8, 5 | members: []const []const u8, 6 | 7 | /// Instantiates a new GEOHASH command. 8 | pub fn init(key: []const u8, members: []const []const u8) GEOHASH { 9 | return .{ .key = key, .members = members }; 10 | } 11 | 12 | /// Validates if the command is syntactically correct. 13 | pub fn validate(self: GEOHASH) !void { 14 | if (self.members.len == 0) return error.MembersArrayIsEmpty; 15 | } 16 | 17 | pub const RedisCommand = struct { 18 | pub fn serialize(self: GEOHASH, comptime rootSerializer: type, msg: anytype) !void { 19 | return rootSerializer.serializeCommand(msg, .{ "GEOHASH", self.key, self.members }); 20 | } 21 | }; 22 | }; 23 | 24 | test "basic usage" { 25 | const cmd = GEOHASH.init("mykey", &[_][]const u8{ "member1", "member2" }); 26 | try cmd.validate(); 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/geo/geodist.zig: -------------------------------------------------------------------------------- 1 | // GEODIST key member1 member2 [m|km|ft|mi] 2 | 3 | const Unit = @import("./_utils.zig").Unit; 4 | 5 | pub const GEODIST = struct { 6 | key: []const u8, 7 | member1: []const u8, 8 | member2: []const u8, 9 | unit: Unit = .meters, 10 | 11 | pub fn init(key: []const u8, member1: []const u8, member2: []const u8, unit: Unit) GEODIST { 12 | return .{ .key = key, .member1 = member1, .member2 = member2, .unit = unit }; 13 | } 14 | 15 | pub fn validate(_: GEODIST) !void {} 16 | 17 | pub const RedisCommand = struct { 18 | pub fn serialize(self: GEODIST, comptime rootSerializer: type, msg: anytype) !void { 19 | return rootSerializer.serializeCommand(msg, .{ 20 | "GEODIST", 21 | self.key, 22 | self.member1, 23 | self.member2, 24 | self.unit, 25 | }); 26 | } 27 | }; 28 | }; 29 | 30 | test "basic usage" { 31 | _ = GEODIST.init("cities", "rome", "paris", .meters); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Loris Cro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/commands/strings/getset.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Writer = std.Io.Writer; 3 | const Value = @import("../_common_utils.zig").Value; 4 | 5 | // GETSET key value 6 | // TODO: check if this is correct 7 | pub const GETSET = struct { 8 | //! ``` 9 | //! const cmd1 = GETSET.init("lol", 42); 10 | //! const cmd2 = GETSET.init("lol", "banana"); 11 | //! ``` 12 | key: []const u8, 13 | value: Value, 14 | 15 | const Self = @This(); 16 | 17 | pub fn init(key: []const u8, value: anytype) GETSET { 18 | return .{ 19 | .key = key, 20 | .value = Value.fromVar(value), 21 | }; 22 | } 23 | 24 | pub fn validate(self: Self) !void { 25 | if (self.key.len == 0) return error.EmptyKeyName; 26 | } 27 | 28 | const Redis = struct { 29 | const Command = struct { 30 | pub fn serialize(self: GETSET, root: type, w: *Writer) !void { 31 | return root.command(w, .{ "GETSET", self.key, self.value }); 32 | } 33 | }; 34 | }; 35 | }; 36 | 37 | test "example" { 38 | _ = GETSET.init("lol", "banana"); 39 | } 40 | -------------------------------------------------------------------------------- /src/keys/stream.zig: -------------------------------------------------------------------------------- 1 | const Key = @import("./key.zig"); 2 | const cmds = @import("../commands.zig"); 3 | 4 | const Stream = struct { 5 | key: Key, 6 | 7 | pub fn init(key_name: []const u8) Stream { 8 | return Stream{Key{key_name}}; 9 | } 10 | 11 | pub fn xadd(self: Stream, id: u8, fvs: []cmds.streams.utils.FV) XADD {} 12 | pub fn xaddStruct(comptime T: type, self: Stream, id: u8, data: T) XADD.forStruct(T) {} // maybe var? 13 | 14 | pub fn xread(count: Count, block: Block, id: []const u8) void {} 15 | pub fn xreadStruct(comptime T: type, count: Count, block: Block, id: []const u8) XREAD.forStruct(T) {} 16 | 17 | pub fn xtrim() void {} 18 | }; 19 | 20 | test "usage" { 21 | const Stream = okredis.keys.Stream; 22 | 23 | temperatures = Stream.init("temps"); 24 | last_hour = temperatures.xread(30, .NoBlock, "123"); 25 | 26 | const MyTemp = struct { 27 | temperature: float64, 28 | humidity: float64, 29 | }; 30 | 31 | last_hour = temperatures.xreadStruct(MyTemp, 30, .NoBlock, "123"); 32 | } 33 | 34 | const StreamConsumer = struct { 35 | keys: []const u8, 36 | 37 | pub fn ensure() void {} 38 | }; 39 | -------------------------------------------------------------------------------- /src/parser/t_double.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const fmt = std.fmt; 4 | const InStream = std.io.InStream; 5 | const builtin = @import("builtin"); 6 | 7 | /// Parses RedisDouble values (e.g. ,123.45) 8 | pub const DoubleParser = struct { 9 | pub fn isSupported(comptime T: type) bool { 10 | return switch (@typeInfo(T)) { 11 | .float => true, 12 | else => false, 13 | }; 14 | } 15 | 16 | pub fn parse(comptime T: type, comptime _: type, r: *Reader) !T { 17 | const digits = try r.takeSentinel('\r'); 18 | const result = switch (@typeInfo(T)) { 19 | else => unreachable, 20 | .float => try fmt.parseFloat(T, digits), 21 | }; 22 | try r.discardAll(1); 23 | return result; 24 | } 25 | 26 | pub fn isSupportedAlloc(comptime T: type) bool { 27 | return isSupported(T); 28 | } 29 | 30 | pub fn parseAlloc( 31 | comptime T: type, 32 | comptime rootParser: type, 33 | allocator: std.mem.Allocator, 34 | r: *Reader, 35 | ) !T { 36 | _ = allocator; 37 | return parse(T, rootParser, r); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/parser/t_number.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const fmt = std.fmt; 4 | const InStream = std.io.InStream; 5 | const builtin = @import("builtin"); 6 | 7 | /// Parses RedisNumber values 8 | pub const NumberParser = struct { 9 | pub fn isSupported(comptime T: type) bool { 10 | return switch (@typeInfo(T)) { 11 | .float, .int => true, 12 | else => false, 13 | }; 14 | } 15 | 16 | pub fn parse(comptime T: type, comptime _: type, r: *Reader) !T { 17 | const digits = try r.takeSentinel('\r'); 18 | const result = switch (@typeInfo(T)) { 19 | else => unreachable, 20 | .int => try fmt.parseInt(T, digits, 10), 21 | .float => try fmt.parseFloat(T, digits), 22 | }; 23 | try r.discardAll(1); 24 | return result; 25 | } 26 | 27 | pub fn isSupportedAlloc(comptime T: type) bool { 28 | return isSupported(T); 29 | } 30 | 31 | pub fn parseAlloc( 32 | comptime T: type, 33 | comptime rootParser: type, 34 | _: std.mem.Allocator, 35 | r: *Reader, 36 | ) !T { 37 | return parse(T, rootParser, r); // TODO: before I passed down an empty struct type. Was I insane? Did I have a plan? 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/commands.zig: -------------------------------------------------------------------------------- 1 | pub const hyperloglog = @import("./commands/hyperloglog.zig"); 2 | pub const strings = @import("./commands/strings.zig"); 3 | pub const streams = @import("./commands/streams.zig"); 4 | pub const hashes = @import("./commands/hashes.zig"); 5 | pub const keys = @import("./commands/keys.zig"); 6 | pub const sets = @import("./commands/sets.zig"); 7 | pub const geo = @import("./commands/geo.zig"); 8 | 9 | // These are all command builders than can be used interchangeably with the main syntax: 10 | // ``` 11 | // try client.send(void, .{"SET", "key", 42}); 12 | // try client.send(void, SET.init("key", 42, .NoExpire, .NoConditions)); 13 | // ``` 14 | // Command builders offer more comptime safety through their `.init` functions and 15 | // most of them also feature a `.validate()` method that performs semantic validation. 16 | // 17 | // The `.validate()` method can be run at comptime for command instances that don't 18 | // depend on runtime data, ensuring correctness without impacting runtime performance. 19 | 20 | test "commands" { 21 | _ = @import("./commands/hyperloglog.zig"); 22 | _ = @import("./commands/strings.zig"); 23 | _ = @import("./commands/streams.zig"); 24 | _ = @import("./commands/hashes.zig"); 25 | _ = @import("./commands/keys.zig"); 26 | _ = @import("./commands/sets.zig"); 27 | _ = @import("./commands/geo.zig"); 28 | } 29 | 30 | test "docs" { 31 | const std = @import("std"); 32 | std.testing.refAllDecls(@This()); 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/strings/incr.zig: -------------------------------------------------------------------------------- 1 | // INCR key 2 | 3 | pub const INCR = struct { 4 | key: []const u8, 5 | 6 | const Self = @This(); 7 | 8 | pub fn init(key: []const u8) INCR { 9 | return .{ .key = key }; 10 | } 11 | 12 | pub fn validate(self: Self) !void { 13 | if (self.key.len == 0) return error.EmptyKeyName; 14 | } 15 | 16 | pub const RedisCommand = struct { 17 | pub fn serialize(self: INCR, comptime rootSerializer: type, msg: anytype) !void { 18 | return rootSerializer.serializeCommand(msg, .{ "INCR", self.key }); 19 | } 20 | }; 21 | }; 22 | 23 | test "example" { 24 | _ = INCR.init("lol"); 25 | } 26 | 27 | test "serializer" { 28 | const std = @import("std"); 29 | const serializer = @import("../../serializer.zig").CommandSerializer; 30 | 31 | var correctBuf: [1000]u8 = undefined; 32 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 33 | 34 | var testBuf: [1000]u8 = undefined; 35 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 36 | 37 | { 38 | correctMsg.end = 0; 39 | testMsg.end = 0; 40 | 41 | try serializer.serializeCommand( 42 | &testMsg, 43 | INCR.init("mykey"), 44 | ); 45 | try serializer.serializeCommand( 46 | &correctMsg, 47 | .{ "INCR", "mykey" }, 48 | ); 49 | 50 | try std.testing.expectEqualSlices( 51 | u8, 52 | correctMsg.buffered(), 53 | testMsg.buffered(), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/strings/get.zig: -------------------------------------------------------------------------------- 1 | // GET key 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const GET = struct { 7 | key: []const u8, 8 | 9 | /// Instantiates a new GET command. 10 | pub fn init(key: []const u8) GET { 11 | return .{ .key = key }; 12 | } 13 | 14 | pub fn validate(self: GET) !void { 15 | if (self.key.len == 0) return error.EmptyKeyName; 16 | } 17 | 18 | pub const RedisCommand = struct { 19 | pub fn serialize(self: GET, comptime root: type, w: *Writer) !void { 20 | return root.serializeCommand(w, .{ "GET", self.key }); 21 | } 22 | }; 23 | }; 24 | 25 | test "example" { 26 | const cmd = GET.init("lol"); 27 | try cmd.validate(); 28 | } 29 | 30 | test "serializer" { 31 | const serializer = @import("../../serializer.zig").CommandSerializer; 32 | 33 | var correctBuf: [1000]u8 = undefined; 34 | var correctMsg: Writer = .fixed(correctBuf[0..]); 35 | 36 | var testBuf: [1000]u8 = undefined; 37 | var testMsg: Writer = .fixed(testBuf[0..]); 38 | { 39 | correctMsg.end = 0; 40 | testMsg.end = 0; 41 | 42 | try serializer.serializeCommand( 43 | &testMsg, 44 | GET.init("mykey"), 45 | ); 46 | try serializer.serializeCommand( 47 | &correctMsg, 48 | .{ "GET", "mykey" }, 49 | ); 50 | 51 | try std.testing.expectEqualSlices( 52 | u8, 53 | correctMsg.buffered(), 54 | testMsg.buffered(), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/commands/sets/scard.zig: -------------------------------------------------------------------------------- 1 | // SCARD key 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SCARD = struct { 7 | key: []const u8, 8 | 9 | /// Instantiates a new SCARD command. 10 | pub fn init(key: []const u8) SCARD { 11 | return .{ .key = key }; 12 | } 13 | 14 | pub fn validate(self: SCARD) !void { 15 | if (self.key.len == 0) return error.EmptyKeyName; 16 | } 17 | 18 | pub const RedisCommand = struct { 19 | pub fn serialize( 20 | self: SCARD, 21 | comptime root_serializer: type, 22 | w: *Writer, 23 | ) !void { 24 | return root_serializer.serializeCommand(w, .{ "SCARD", self.key }); 25 | } 26 | }; 27 | }; 28 | 29 | test "example" { 30 | const cmd = SCARD.init("lol"); 31 | try cmd.validate(); 32 | } 33 | 34 | test "serializer" { 35 | const serializer = @import("../../serializer.zig").CommandSerializer; 36 | 37 | var correctBuf: [1000]u8 = undefined; 38 | var correctMsg: Writer = .fixed(correctBuf[0..]); 39 | 40 | var testBuf: [1000]u8 = undefined; 41 | var testMsg: Writer = .fixed(testBuf[0..]); 42 | { 43 | correctMsg.end = 0; 44 | testMsg.end = 0; 45 | 46 | try serializer.serializeCommand( 47 | &testMsg, 48 | SCARD.init("myset"), 49 | ); 50 | try serializer.serializeCommand( 51 | &correctMsg, 52 | .{ "SCARD", "myset" }, 53 | ); 54 | 55 | try std.testing.expectEqualSlices( 56 | u8, 57 | correctMsg.buffered(), 58 | testMsg.buffered(), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/keys/README.md: -------------------------------------------------------------------------------- 1 | # Keys 2 | This is an OOP interface to commands. 3 | Instead of the command itself being the focus, the key type is the core, and 4 | commands are behaviour attached to it. 5 | 6 | This interface is still data-driven, meaning that the various methods produce 7 | command instances and don't actually do any side-effect. The reason for this 8 | choice is the same as for the rest of this client design: I want to make it 9 | obvious when network communication is happening and I don't want to make it 10 | ambiguous when pipelining is happening or not. 11 | 12 | ## Usage example 13 | 14 | 15 | ```zig 16 | const Stream = okredis.keys.Stream; 17 | 18 | const temps = Stream.init("temperatures"); 19 | 20 | _ = try client.pipeAlloc(OrErr([]u8), allocator, .{ 21 | temps.xadd("*", ["temperature", "123", "humidity": "10"]), 22 | temps.xadd("*", ["temperature", "321", "humidity": "1"]), 23 | }); 24 | 25 | const MyTemps = struct { 26 | temperature: float64, 27 | humidity: float64, 28 | }; 29 | 30 | const cmd = temps.xreadStruct(MyTemps, 10, .NoBlock, "123-123"); 31 | _ = try client.sendAlloc(cmd.Reply, cmd); 32 | 33 | // Even better 34 | _ = try client.sendAlloc(OrErr(cmd.Reply), cmd); 35 | 36 | ``` 37 | 38 | ## Development notes 39 | 40 | Keys depend on commands as they are basically only syntax sugar built on top of 41 | them. 42 | 43 | Internally, all key types depend on the `Key` type in `key.zig` as a way to 44 | offer generic key operations. 45 | 46 | ```zig 47 | const Stream = okredis.keys.Stream; 48 | 49 | // Creates a stream key instance and then creates a command to delete it. 50 | const temps = Stream.init("temperatures"); 51 | const cmd = temps.key.del(); 52 | ``` 53 | -------------------------------------------------------------------------------- /src/commands/strings/incrby.zig: -------------------------------------------------------------------------------- 1 | // INCRBY key increment 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const INCRBY = struct { 7 | key: []const u8, 8 | increment: i64, 9 | 10 | const Self = @This(); 11 | 12 | pub fn init(key: []const u8, increment: i64) INCRBY { 13 | return .{ .key = key, .increment = increment }; 14 | } 15 | 16 | pub fn validate(self: Self) !void { 17 | if (self.key.len == 0) return error.EmptyKeyName; 18 | } 19 | 20 | pub const RedisCommand = struct { 21 | pub fn serialize(self: INCRBY, comptime rootSerializer: type, msg: anytype) !void { 22 | return rootSerializer.serializeCommand(msg, .{ "INCRBY", self.key, self.increment }); 23 | } 24 | }; 25 | }; 26 | 27 | test "basic usage" { 28 | _ = INCRBY.init("lol", 42); 29 | } 30 | 31 | test "serializer" { 32 | const serializer = @import("../../serializer.zig").CommandSerializer; 33 | 34 | var correctBuf: [1000]u8 = undefined; 35 | var correctMsg: Writer = .fixed(correctBuf[0..]); 36 | 37 | var testBuf: [1000]u8 = undefined; 38 | var testMsg: Writer = .fixed(testBuf[0..]); 39 | 40 | { 41 | correctMsg.end = 0; 42 | testMsg.end = 0; 43 | 44 | try serializer.serializeCommand( 45 | &testMsg, 46 | INCRBY.init("mykey", 42), 47 | ); 48 | try serializer.serializeCommand( 49 | &correctMsg, 50 | .{ "INCRBY", "mykey", 42 }, 51 | ); 52 | 53 | try std.testing.expectEqualSlices( 54 | u8, 55 | correctMsg.buffered(), 56 | testMsg.buffered(), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/transactions.zig: -------------------------------------------------------------------------------- 1 | pub const WATCH = struct { 2 | keys: []const []const u8, 3 | 4 | pub fn init(keys: []const []const u8) WATCH { 5 | return .{ 6 | .keys = keys, 7 | }; 8 | } 9 | 10 | pub const RedisCommand = struct { 11 | pub fn serialize(self: WATCH, comptime rootSerializer: type, msg: anytype) !void { 12 | return rootSerializer.serializeCommand(msg, .{ "WATCH", self.keys }); 13 | } 14 | }; 15 | }; 16 | 17 | pub const UNWATCH = struct { 18 | pub const RedisCommand = struct { 19 | pub fn serialize(self: UNWATCH, comptime rootSerializer: type, msg: anytype) !void { 20 | _ = self; 21 | return rootSerializer.serializeCommand(msg, .{"UNWATCH"}); 22 | } 23 | }; 24 | }; 25 | 26 | pub const MULTI = struct { 27 | pub const RedisCommand = struct { 28 | pub fn serialize(self: MULTI, comptime rootSerializer: type, msg: anytype) !void { 29 | _ = self; 30 | return rootSerializer.serializeCommand(msg, .{"MULTI"}); 31 | } 32 | }; 33 | }; 34 | 35 | pub const EXEC = struct { 36 | pub const RedisCommand = struct { 37 | pub fn serialize(self: EXEC, comptime rootSerializer: type, msg: anytype) !void { 38 | _ = self; 39 | return rootSerializer.serializeCommand(msg, .{"EXEC"}); 40 | } 41 | }; 42 | }; 43 | 44 | pub const DISCARD = struct { 45 | pub const RedisCommand = struct { 46 | pub fn serialize(self: DISCARD, comptime rootSerializer: type, msg: anytype) !void { 47 | _ = self; 48 | return rootSerializer.serializeCommand(msg, .{"DISCARD"}); 49 | } 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/commands/strings/append.zig: -------------------------------------------------------------------------------- 1 | // APPEND key value 2 | pub const APPEND = struct { 3 | key: []const u8, 4 | value: []const u8, 5 | 6 | const Self = @This(); 7 | 8 | pub fn init(key: []const u8, value: []const u8) APPEND { 9 | return .{ .key = key, .value = value }; 10 | } 11 | 12 | pub fn validate(self: Self) !void { 13 | if (self.key.len == 0) return error.EmptyKeyName; 14 | if (self.value.len == 0) return error.EmptyValue; 15 | } 16 | 17 | pub const RedisCommand = struct { 18 | pub fn serialize(self: APPEND, comptime rootSerializer: type, msg: anytype) !void { 19 | return rootSerializer.serializeCommand(msg, .{ "APPEND", self.key, self.value }); 20 | } 21 | }; 22 | }; 23 | 24 | test "example" { 25 | _ = APPEND.init("noun", "ism"); 26 | } 27 | 28 | test "serializer" { 29 | const std = @import("std"); 30 | const serializer = @import("../../serializer.zig").CommandSerializer; 31 | 32 | var correctBuf: [1000]u8 = undefined; 33 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 34 | 35 | var testBuf: [1000]u8 = undefined; 36 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 37 | 38 | { 39 | correctMsg.end = 0; 40 | testMsg.end = 0; 41 | 42 | try serializer.serializeCommand( 43 | &testMsg, 44 | APPEND.init("mykey", "42"), 45 | ); 46 | try serializer.serializeCommand( 47 | &correctMsg, 48 | .{ "APPEND", "mykey", "42" }, 49 | ); 50 | 51 | try std.testing.expectEqualSlices( 52 | u8, 53 | correctMsg.buffered(), 54 | testMsg.buffered(), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/commands/strings/incrbyfloat.zig: -------------------------------------------------------------------------------- 1 | // INCRBYFLOAT key increment 2 | pub const INCRBYFLOAT = struct { 3 | key: []const u8, 4 | increment: f64, 5 | 6 | const Self = @This(); 7 | 8 | pub fn init(key: []const u8, increment: f64) INCRBYFLOAT { 9 | return .{ .key = key, .increment = increment }; 10 | } 11 | 12 | pub fn validate(self: Self) !void { 13 | if (self.key.len == 0) return error.EmptyKeyName; 14 | } 15 | 16 | pub const RedisCommand = struct { 17 | pub fn serialize(self: INCRBYFLOAT, comptime rootSerializer: type, msg: anytype) !void { 18 | return rootSerializer.serializeCommand(msg, .{ "INCRBYFLOAT", self.key, self.increment }); 19 | } 20 | }; 21 | }; 22 | 23 | test "basic usage" { 24 | _ = INCRBYFLOAT.init("lol", 42); 25 | } 26 | 27 | test "serializer" { 28 | const std = @import("std"); 29 | const serializer = @import("../../serializer.zig").CommandSerializer; 30 | 31 | var correctBuf: [1000]u8 = undefined; 32 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 33 | 34 | var testBuf: [1000]u8 = undefined; 35 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 36 | 37 | { 38 | correctMsg.end = 0; 39 | testMsg.end = 0; 40 | 41 | try serializer.serializeCommand( 42 | &testMsg, 43 | INCRBYFLOAT.init("mykey", 42.1337), 44 | ); 45 | try serializer.serializeCommand( 46 | &correctMsg, 47 | .{ "INCRBYFLOAT", "mykey", 42.1337 }, 48 | ); 49 | 50 | try std.testing.expectEqualSlices( 51 | u8, 52 | correctMsg.buffered(), 53 | testMsg.buffered(), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/strings/getbit.zig: -------------------------------------------------------------------------------- 1 | // GETBIT key offset 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const GETBIT = struct { 7 | key: []const u8, 8 | offset: usize, 9 | 10 | const Self = @This(); 11 | 12 | pub fn init(key: []const u8, offset: usize) GETBIT { 13 | return .{ .key = key, .offset = offset }; 14 | } 15 | 16 | pub fn validate(self: Self) !void { 17 | if (self.key.len == 0) return error.EmptyKeyName; 18 | } 19 | 20 | pub const RedisCommand = struct { 21 | pub fn serialize(self: GETBIT, comptime root: type, w: *Writer) !void { 22 | return root.serializeCommand( 23 | w, 24 | .{ "GETBIT", self.key, self.offset }, 25 | ); 26 | } 27 | }; 28 | }; 29 | 30 | test "basic usage" { 31 | _ = GETBIT.init("lol", 100); 32 | } 33 | 34 | test "serializer" { 35 | const serializer = @import("../../serializer.zig").CommandSerializer; 36 | 37 | var correctBuf: [1000]u8 = undefined; 38 | var correctMsg: Writer = .fixed(correctBuf[0..]); 39 | 40 | var testBuf: [1000]u8 = undefined; 41 | var testMsg: Writer = .fixed(testBuf[0..]); 42 | 43 | { 44 | correctMsg.end = 0; 45 | testMsg.end = 0; 46 | 47 | try serializer.serializeCommand( 48 | &testMsg, 49 | GETBIT.init("mykey", 100), 50 | ); 51 | try serializer.serializeCommand( 52 | &correctMsg, 53 | .{ "GETBIT", "mykey", 100 }, 54 | ); 55 | 56 | try std.testing.expectEqualSlices( 57 | u8, 58 | correctMsg.buffered(), 59 | testMsg.buffered(), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/strings/getrange.zig: -------------------------------------------------------------------------------- 1 | // GETRANGE key start end 2 | 3 | pub const GETRANGE = struct { 4 | key: []const u8, 5 | start: isize, 6 | end: isize, 7 | 8 | const Self = @This(); 9 | 10 | pub fn init(key: []const u8, start: isize, end: isize) GETRANGE { 11 | return .{ .key = key, .start = start, .end = end }; 12 | } 13 | 14 | pub fn validate(self: Self) !void { 15 | if (self.key.len == 0) return error.EmptyKeyName; 16 | } 17 | 18 | pub const RedisCommand = struct { 19 | pub fn serialize(self: GETRANGE, comptime rootSerializer: type, msg: anytype) !void { 20 | return rootSerializer.serializeCommand(msg, .{ "GETRANGE", self.key, self.start, self.end }); 21 | } 22 | }; 23 | }; 24 | 25 | test "basic usage" { 26 | _ = GETRANGE.init("lol", 5, 100); 27 | } 28 | 29 | test "serializer" { 30 | const std = @import("std"); 31 | const serializer = @import("../../serializer.zig").CommandSerializer; 32 | 33 | var correctBuf: [1000]u8 = undefined; 34 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 35 | 36 | var testBuf: [1000]u8 = undefined; 37 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 38 | 39 | { 40 | correctMsg.end = 0; 41 | testMsg.end = 0; 42 | 43 | try serializer.serializeCommand( 44 | &testMsg, 45 | GETRANGE.init("mykey", 1, 99), 46 | ); 47 | try serializer.serializeCommand( 48 | &correctMsg, 49 | .{ "GETRANGE", "mykey", 1, 99 }, 50 | ); 51 | 52 | try std.testing.expectEqualSlices( 53 | u8, 54 | correctMsg.buffered(), 55 | testMsg.buffered(), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/commands/_common_utils.zig: -------------------------------------------------------------------------------- 1 | pub const FV = struct { 2 | field: []const u8, 3 | value: []const u8, 4 | 5 | pub const RedisArguments = struct { 6 | pub fn count(_: FV) usize { 7 | return 2; 8 | } 9 | 10 | pub fn serialize(self: FV, comptime rootSerializer: type, msg: anytype) !void { 11 | try rootSerializer.serializeArgument(msg, []const u8, self.field); 12 | try rootSerializer.serializeArgument(msg, []const u8, self.value); 13 | } 14 | }; 15 | }; 16 | 17 | /// Union used to allow users to pass numbers transparently to SET-like commands. 18 | pub const Value = union(enum) { 19 | String: []const u8, 20 | int: i64, 21 | float: f64, 22 | 23 | /// Wraps either a string or a number. 24 | pub fn fromVar(value: anytype) Value { 25 | return switch (@typeInfo(@TypeOf(value))) { 26 | .int, .comptime_int => Value{ .int = value }, 27 | .float, .comptime_float => Value{ .float = value }, 28 | .array => Value{ .String = value[0..] }, 29 | .pointer => Value{ .String = value }, 30 | else => @compileError("Unsupported type."), 31 | }; 32 | } 33 | 34 | pub const RedisArguments = struct { 35 | pub fn count(_: Value) usize { 36 | return 1; 37 | } 38 | 39 | pub fn serialize(self: Value, comptime rootSerializer: type, msg: anytype) !void { 40 | switch (self) { 41 | .String => |s| try rootSerializer.serializeArgument(msg, []const u8, s), 42 | .int => |i| try rootSerializer.serializeArgument(msg, i64, i), 43 | .float => |f| try rootSerializer.serializeArgument(msg, f64, f), 44 | } 45 | } 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/commands/README.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | Commands are a type-checked inteface to send commands to Redis. Conceptually 3 | they are equivalent to a corresponding sequence of strings that represents an 4 | equivalent of the same command. 5 | 6 | Commands also offer an optional validation step that allows the user to get 7 | extra safety either at comptime or at runtime, depending on what information 8 | the command depends on. 9 | 10 | Commands must implement an interface consumed by `serializer.zig`. 11 | 12 | ## Development notes 13 | This directory contains implementations for all the various commands supported 14 | by Redis. 15 | 16 | Each sub-directory represents a command group just like they are strucuted in 17 | [redis.io](https://redis.io/commands). 18 | 19 | Elements shared by more than one command of the same group are declared in the 20 | respective `_utils.zig`. 21 | 22 | Elements shared by multiple groups are declared in `_common_utils.zig`. 23 | 24 | Shared elements are made available to the user through each group that uses 25 | them, regardless whether they are unique to the group or in `_common_utils.zig`. 26 | 27 | ```zig 28 | const Value = okredis.commands.strings.utils.Value; 29 | const Value2 = okredis.commands.hashes.utils.Value; 30 | ``` 31 | 32 | Integration tests with `serializer.zig` are scattered across the various files. 33 | This is the only extraneous dependency outside of this sub-tree and I decided 34 | that adding it was better than the alternatives. Long story short, I like having 35 | those tests close to where the command implementation is, they really do depend 36 | on how the command serializer works, and I know that `serializer.zig` is never 37 | going to depend on any of the content in `commands`, so it will never cause a 38 | circular dependency. -------------------------------------------------------------------------------- /src/commands/geo/geoadd.zig: -------------------------------------------------------------------------------- 1 | // GEOADD key longitude latitude member [longitude latitude member ...] 2 | 3 | const std = @import("std"); 4 | 5 | pub const GEOADD = struct { 6 | key: []const u8, 7 | points: []const GeoPoint, 8 | 9 | pub const GeoPoint = struct { 10 | long: f64, 11 | lat: f64, 12 | member: []const u8, 13 | 14 | pub const RedisArguments = struct { 15 | pub fn count(_: GeoPoint) usize { 16 | return 3; 17 | } 18 | 19 | pub fn serialize(self: GeoPoint, comptime rootSerializer: type, msg: anytype) !void { 20 | try rootSerializer.serializeArgument(msg, f64, self.long); 21 | try rootSerializer.serializeArgument(msg, f64, self.lat); 22 | try rootSerializer.serializeArgument(msg, []const u8, self.member); 23 | } 24 | }; 25 | }; 26 | 27 | /// Instantiates a new GEOADD command. 28 | pub fn init(key: []const u8, points: []const GeoPoint) GEOADD { 29 | return .{ .key = key, .points = points }; 30 | } 31 | 32 | /// Validates if the command is syntactically correct. 33 | pub fn validate(self: GEOADD) !void { 34 | if (self.points.len == 0) return error.PointsArrayIsEmpty; 35 | } 36 | 37 | pub const RedisCommand = struct { 38 | pub fn serialize(self: GEOADD, comptime rootSerializer: type, msg: anytype) !void { 39 | return rootSerializer.serializeCommand(msg, .{ "GEOADD", self.key, self.points }); 40 | } 41 | }; 42 | }; 43 | 44 | test "basic usage" { 45 | const cmd = GEOADD.init("mykey", &[_]GEOADD.GeoPoint{ 46 | .{ .long = 80.05, .lat = 80.05, .member = "place1" }, 47 | .{ .long = 81.05, .lat = 81.05, .member = "place2" }, 48 | }); 49 | 50 | try cmd.validate(); 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/sets.zig: -------------------------------------------------------------------------------- 1 | pub const SADD = @import("./sets/sadd.zig").SADD; 2 | pub const SINTER = @import("./sets/sinter.zig").SINTER; 3 | pub const SMISMEMBER = @import("./sets/smismember.zig").SMISMEMBER; 4 | pub const SREM = @import("./sets/srem.zig").SREM; 5 | pub const SCARD = @import("./sets/scard.zig").SCARD; 6 | pub const SINTERSTORE = @import("./sets/sinterstore.zig").SINTERSTORE; 7 | pub const SMOVE = @import("./sets/smove.zig").SMOVE; 8 | pub const SSCAN = @import("./sets/sscan.zig").SSCAN; 9 | pub const SDIFF = @import("./sets/sdiff.zig").SDIFF; 10 | pub const SISMEMBER = @import("./sets/sismember.zig").SISMEMBER; 11 | pub const SPOP = @import("./sets/spop.zig").SPOP; 12 | pub const SUNION = @import("./sets/sunion.zig").SUNION; 13 | pub const SDIFFSTORE = @import("./sets/sdiffstore.zig").SDIFFSTORE; 14 | pub const SMEMBERS = @import("./sets/smembers.zig").SMEMBERS; 15 | pub const SRANDMEMBER = @import("./sets/srandmember.zig").SRANDMEMBER; 16 | pub const SUNIONSTORE = @import("./sets/sunionstore.zig").SUNIONSTORE; 17 | 18 | test "sets" { 19 | _ = @import("./sets/sadd.zig"); 20 | _ = @import("./sets/sinter.zig"); 21 | _ = @import("./sets/smismember.zig"); 22 | _ = @import("./sets/srem.zig"); 23 | _ = @import("./sets/scard.zig"); 24 | _ = @import("./sets/sinterstore.zig"); 25 | _ = @import("./sets/smove.zig"); 26 | _ = @import("./sets/sscan.zig"); 27 | _ = @import("./sets/sdiff.zig"); 28 | _ = @import("./sets/sismember.zig"); 29 | _ = @import("./sets/spop.zig"); 30 | _ = @import("./sets/sunion.zig"); 31 | _ = @import("./sets/sdiffstore.zig"); 32 | _ = @import("./sets/smembers.zig"); 33 | _ = @import("./sets/srandmember.zig"); 34 | _ = @import("./sets/sunionstore.zig"); 35 | } 36 | 37 | test "docs" { 38 | const std = @import("std"); 39 | std.testing.refAllDecls(@This()); 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/hashes/hincrby.zig: -------------------------------------------------------------------------------- 1 | // HINCRBY key field increment 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const HINCRBY = struct { 7 | key: []const u8, 8 | field: []const u8, 9 | increment: i64, 10 | 11 | pub fn init(key: []const u8, field: []const u8, increment: i64) HINCRBY { 12 | return .{ .key = key, .field = field, .increment = increment }; 13 | } 14 | 15 | pub fn validate(_: HINCRBY) !void {} 16 | 17 | pub const RedisCommand = struct { 18 | pub fn serialize( 19 | self: HINCRBY, 20 | comptime rootSerializer: type, 21 | w: *Writer, 22 | ) !void { 23 | return rootSerializer.serializeCommand(w, .{ 24 | "HINCRBY", 25 | self.key, 26 | self.field, 27 | self.increment, 28 | }); 29 | } 30 | }; 31 | }; 32 | 33 | test "basic usage" { 34 | _ = HINCRBY.init("hashname", "fieldname", 42); 35 | } 36 | 37 | test "serializer" { 38 | const serializer = @import("../../serializer.zig").CommandSerializer; 39 | 40 | var correctBuf: [1000]u8 = undefined; 41 | var correctMsg: Writer = .fixed(correctBuf[0..]); 42 | 43 | var testBuf: [1000]u8 = undefined; 44 | var testMsg: Writer = .fixed(testBuf[0..]); 45 | 46 | { 47 | correctMsg.end = 0; 48 | testMsg.end = 0; 49 | 50 | try serializer.serializeCommand( 51 | &testMsg, 52 | HINCRBY.init("mykey", "myfield", 42), 53 | ); 54 | try serializer.serializeCommand( 55 | &correctMsg, 56 | .{ "HINCRBY", "mykey", "myfield", 42 }, 57 | ); 58 | 59 | try std.testing.expectEqualSlices( 60 | u8, 61 | correctMsg.buffered(), 62 | testMsg.buffered(), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/commands/sets/smembers.zig: -------------------------------------------------------------------------------- 1 | // SMEMBERS key 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SMEMBERS = struct { 7 | key: []const u8, 8 | 9 | /// Instantiates a new SMEMBERS command. 10 | pub fn init(key: []const u8) SMEMBERS { 11 | // TODO: support std.hashmap used as a set! 12 | return .{ .key = key }; 13 | } 14 | 15 | /// Validates if the command is syntactically correct. 16 | pub fn validate(_: SMEMBERS) !void {} 17 | 18 | pub const RedisCommand = struct { 19 | pub fn serialize(self: SMEMBERS, comptime rootSerializer: type, msg: anytype) !void { 20 | return rootSerializer.serializeCommand(msg, .{ "SMEMBERS", self.key }); 21 | } 22 | }; 23 | }; 24 | 25 | test "basic usage" { 26 | const cmd = SMEMBERS.init("myset"); 27 | try cmd.validate(); 28 | } 29 | 30 | test "serializer" { 31 | const serializer = @import("../../serializer.zig").CommandSerializer; 32 | 33 | var correctBuf: [1000]u8 = undefined; 34 | var correctMsg: Writer = .fixed(correctBuf[0..]); 35 | 36 | var testBuf: [1000]u8 = undefined; 37 | var testMsg: Writer = .fixed(testBuf[0..]); 38 | 39 | { 40 | { 41 | correctMsg.end = 0; 42 | testMsg.end = 0; 43 | 44 | try serializer.serializeCommand( 45 | &testMsg, 46 | SMEMBERS.init("set1"), 47 | ); 48 | try serializer.serializeCommand( 49 | &correctMsg, 50 | .{ "SMEMBERS", "set1" }, 51 | ); 52 | 53 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.getWritten(), testMsg.getWritten() }); 54 | try std.testing.expectEqualSlices( 55 | u8, 56 | correctMsg.buffered(), 57 | testMsg.buffered(), 58 | ); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/commands/strings/setbit.zig: -------------------------------------------------------------------------------- 1 | // SETBIT key offset value 2 | const Value = @import("../_common_utils.zig").Value; 3 | 4 | pub const SETBIT = struct { 5 | //! ``` 6 | //! const cmd1 = SETBIT.init("lol", 100, 42); 7 | //! const cmd2 = SETBIT.init("lol", 100, "banana"); 8 | //! ``` 9 | 10 | key: []const u8, 11 | offset: usize, 12 | value: Value, 13 | 14 | pub fn init(key: []const u8, offset: usize, value: anytype) SETBIT { 15 | return .{ .key = key, .offset = offset, .value = Value.fromVar(value) }; 16 | } 17 | 18 | pub fn validate(self: SETBIT) !void { 19 | if (self.key.len == 0) return error.EmptyKeyName; 20 | } 21 | 22 | pub const RedisCommand = struct { 23 | pub fn serialize(self: SETBIT, comptime rootSerializer: type, msg: anytype) !void { 24 | return rootSerializer.serializeCommand(msg, .{ "SETBIT", self.key, self.offset, self.value }); 25 | } 26 | }; 27 | }; 28 | 29 | test "basic usage" { 30 | _ = SETBIT.init("lol", 100, "banana"); 31 | } 32 | 33 | test "serializer" { 34 | const std = @import("std"); 35 | const serializer = @import("../../serializer.zig").CommandSerializer; 36 | 37 | var correctBuf: [1000]u8 = undefined; 38 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 39 | 40 | var testBuf: [1000]u8 = undefined; 41 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 42 | 43 | { 44 | correctMsg.end = 0; 45 | testMsg.end = 0; 46 | 47 | try serializer.serializeCommand( 48 | &testMsg, 49 | SETBIT.init("mykey", 1, 99), 50 | ); 51 | try serializer.serializeCommand( 52 | &correctMsg, 53 | .{ "SETBIT", "mykey", 1, 99 }, 54 | ); 55 | 56 | try std.testing.expectEqualSlices( 57 | u8, 58 | correctMsg.buffered(), 59 | testMsg.buffered(), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/parser/t_bool.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const fmt = std.fmt; 4 | const testing = std.testing; 5 | const builtin = @import("builtin"); 6 | 7 | pub const BoolParser = struct { 8 | pub fn isSupported(comptime T: type) bool { 9 | return switch (@typeInfo(T)) { 10 | .bool, .int, .float => true, 11 | else => false, 12 | }; 13 | } 14 | 15 | pub fn parse(comptime T: type, comptime _: type, r: *Reader) !T { 16 | const ch = try r.takeByte(); 17 | try r.discardAll(2); 18 | return switch (@typeInfo(T)) { 19 | else => unreachable, 20 | .bool => ch == 't', 21 | .int, .float => if (ch == 't') @as(T, 1) else @as(T, 0), 22 | }; 23 | } 24 | 25 | pub fn isSupportedAlloc(comptime T: type) bool { 26 | return isSupported(T); 27 | } 28 | 29 | pub fn parseAlloc(comptime T: type, comptime rootParser: type, allocator: std.mem.Allocator, r: *Reader) !T { 30 | _ = allocator; 31 | 32 | return parse(T, rootParser, r); 33 | } 34 | }; 35 | 36 | test "parses bools" { 37 | var r_true = Truer(); 38 | try testing.expect(true == try BoolParser.parse(bool, struct {}, &r_true)); 39 | var r_false = Falser(); 40 | try testing.expect(false == try BoolParser.parse(bool, struct {}, &r_false)); 41 | var r_true2 = Truer(); 42 | try testing.expect(1 == try BoolParser.parse(i64, struct {}, &r_true2)); 43 | var r_false2 = Falser(); 44 | try testing.expect(0 == try BoolParser.parse(u32, struct {}, &r_false2)); 45 | var r_true3 = Truer(); 46 | try testing.expect(1.0 == try BoolParser.parse(f32, struct {}, &r_true3)); 47 | var r_false3 = Falser(); 48 | try testing.expect(0.0 == try BoolParser.parse(f64, struct {}, &r_false3)); 49 | } 50 | 51 | // TODO: get rid of this! 52 | fn Truer() Reader { 53 | return std.Io.Reader.fixed("#t\r\n"[1..]); 54 | } 55 | 56 | fn Falser() Reader { 57 | return std.Io.Reader.fixed("#f\r\n"[1..]); 58 | } 59 | -------------------------------------------------------------------------------- /src/commands/sets/sunion.zig: -------------------------------------------------------------------------------- 1 | // SUNION key [key ...] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SUNION = struct { 7 | keys: []const []const u8, 8 | 9 | /// Instantiates a new SUNION command. 10 | pub fn init(keys: []const []const u8) SUNION { 11 | return .{ .keys = keys }; 12 | } 13 | 14 | /// Validates if the command is syntactically correct. 15 | pub fn validate(self: SUNION) !void { 16 | if (self.keys.len == 0) return error.KeysArrayIsEmpty; 17 | // TODO: should we check for duplicated keys? if so, we need an allocator, methinks. 18 | } 19 | 20 | pub const RedisCommand = struct { 21 | pub fn serialize(self: SUNION, comptime root: type, w: *Writer) !void { 22 | return root.serializeCommand(w, .{ "SUNION", self.keys }); 23 | } 24 | }; 25 | }; 26 | 27 | test "basic usage" { 28 | const cmd = SUNION.init(&[_][]const u8{ "set1", "set2" }); 29 | try cmd.validate(); 30 | } 31 | 32 | test "serializer" { 33 | const serializer = @import("../../serializer.zig").CommandSerializer; 34 | 35 | var correctBuf: [1000]u8 = undefined; 36 | var correctMsg: Writer = .fixed(correctBuf[0..]); 37 | 38 | var testBuf: [1000]u8 = undefined; 39 | var testMsg: Writer = .fixed(testBuf[0..]); 40 | 41 | { 42 | { 43 | correctMsg.end = 0; 44 | testMsg.end = 0; 45 | 46 | try serializer.serializeCommand( 47 | &testMsg, 48 | SUNION.init(&[_][]const u8{ "set1", "set2" }), 49 | ); 50 | try serializer.serializeCommand( 51 | &correctMsg, 52 | .{ "SUNION", "set1", "set2" }, 53 | ); 54 | 55 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.getWritten(), testMsg.getWritten() }); 56 | try std.testing.expectEqualSlices( 57 | u8, 58 | correctMsg.buffered(), 59 | testMsg.buffered(), 60 | ); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/commands/sets/sinter.zig: -------------------------------------------------------------------------------- 1 | // SINTER key [key ...] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SINTER = struct { 7 | keys: []const []const u8, 8 | 9 | /// Instantiates a new SINTER command. 10 | pub fn init(keys: []const []const u8) SINTER { 11 | return .{ .keys = keys }; 12 | } 13 | 14 | /// Validates if the command is syntactically correct. 15 | pub fn validate(self: SINTER) !void { 16 | if (self.keys.len == 0) return error.KeysArrayIsEmpty; 17 | // TODO: should we check for duplicated keys? if so, we need an allocator, methinks. 18 | } 19 | 20 | pub const RedisCommand = struct { 21 | pub fn serialize( 22 | self: SINTER, 23 | comptime root: type, 24 | w: *Writer, 25 | ) !void { 26 | return root.serializeCommand(w, .{ "SINTER", self.keys }); 27 | } 28 | }; 29 | }; 30 | 31 | test "basic usage" { 32 | const cmd = SINTER.init(&[_][]const u8{ "set1", "set2" }); 33 | try cmd.validate(); 34 | } 35 | 36 | test "serializer" { 37 | const serializer = @import("../../serializer.zig").CommandSerializer; 38 | 39 | var correctBuf: [1000]u8 = undefined; 40 | var correctMsg: Writer = .fixed(correctBuf[0..]); 41 | 42 | var testBuf: [1000]u8 = undefined; 43 | var testMsg: Writer = .fixed(testBuf[0..]); 44 | 45 | { 46 | { 47 | correctMsg.end = 0; 48 | testMsg.end = 0; 49 | 50 | try serializer.serializeCommand( 51 | &testMsg, 52 | SINTER.init(&[_][]const u8{ "set1", "set2" }), 53 | ); 54 | try serializer.serializeCommand( 55 | &correctMsg, 56 | .{ "SINTER", "set1", "set2" }, 57 | ); 58 | 59 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.getWritten(), testMsg.getWritten() }); 60 | try std.testing.expectEqualSlices( 61 | u8, 62 | correctMsg.buffered(), 63 | testMsg.buffered(), 64 | ); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/commands/sets/sismember.zig: -------------------------------------------------------------------------------- 1 | // SISMEMBER key member 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SISMEMBER = struct { 7 | key: []const u8, 8 | member: []const u8, 9 | 10 | /// Instantiates a new SISMEMBER command. 11 | pub fn init(key: []const u8, member: []const u8) SISMEMBER { 12 | // TODO: support std.hashmap used as a set! 13 | return .{ .key = key, .member = member }; 14 | } 15 | 16 | /// Validates if the command is syntactically correct. 17 | pub fn validate(_: SISMEMBER) !void {} 18 | 19 | pub const RedisCommand = struct { 20 | pub fn serialize( 21 | self: SISMEMBER, 22 | comptime root_serializer: type, 23 | w: *Writer, 24 | ) !void { 25 | return root_serializer.serializeCommand(w, .{ 26 | "SISMEMBER", 27 | self.key, 28 | self.member, 29 | }); 30 | } 31 | }; 32 | }; 33 | 34 | test "basic usage" { 35 | const cmd = SISMEMBER.init("myset", "mymember"); 36 | try cmd.validate(); 37 | } 38 | 39 | test "serializer" { 40 | const serializer = @import("../../serializer.zig").CommandSerializer; 41 | 42 | var correctBuf: [1000]u8 = undefined; 43 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 44 | 45 | var testBuf: [1000]u8 = undefined; 46 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 47 | 48 | { 49 | { 50 | correctMsg.end = 0; 51 | testMsg.end = 0; 52 | 53 | try serializer.serializeCommand( 54 | &testMsg, 55 | SISMEMBER.init("set1", "alice"), 56 | ); 57 | try serializer.serializeCommand( 58 | &correctMsg, 59 | .{ "SISMEMBER", "set1", "alice" }, 60 | ); 61 | 62 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.getWritten(), testMsg.getWritten() }); 63 | try std.testing.expectEqualSlices( 64 | u8, 65 | correctMsg.buffered(), 66 | testMsg.buffered(), 67 | ); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/commands/sets/sdiff.zig: -------------------------------------------------------------------------------- 1 | // SDIFF key [key ...] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SDIFF = struct { 7 | keys: []const []const u8, 8 | 9 | /// Instantiates a new SDIFF command. 10 | pub fn init(keys: []const []const u8) SDIFF { 11 | return .{ .keys = keys }; 12 | } 13 | 14 | /// Validates if the command is syntactically correct. 15 | pub fn validate(self: SDIFF) !void { 16 | if (self.keys.len == 0) return error.KeysArrayIsEmpty; 17 | // TODO: should we check for duplicated keys? if so, we need an allocator, methinks. 18 | } 19 | 20 | pub const RedisCommand = struct { 21 | pub fn serialize( 22 | self: SDIFF, 23 | comptime root_serializer: type, 24 | w: *Writer, 25 | ) !void { 26 | return root_serializer.serializeCommand(w, .{ 27 | "SDIFF", 28 | self.keys, 29 | }); 30 | } 31 | }; 32 | }; 33 | 34 | test "basic usage" { 35 | const cmd = SDIFF.init(&[_][]const u8{ "set1", "set2" }); 36 | try cmd.validate(); 37 | } 38 | 39 | test "serializer" { 40 | const serializer = @import("../../serializer.zig").CommandSerializer; 41 | 42 | var correctBuf: [1000]u8 = undefined; 43 | var correctMsg: Writer = .fixed(correctBuf[0..]); 44 | 45 | var testBuf: [1000]u8 = undefined; 46 | var testMsg: Writer = .fixed(testBuf[0..]); 47 | 48 | { 49 | { 50 | correctMsg.end = 0; 51 | testMsg.end = 0; 52 | 53 | try serializer.serializeCommand( 54 | &testMsg, 55 | SDIFF.init(&[_][]const u8{ "set1", "set2" }), 56 | ); 57 | try serializer.serializeCommand( 58 | &correctMsg, 59 | .{ "SDIFF", "set1", "set2" }, 60 | ); 61 | 62 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 63 | try std.testing.expectEqualSlices( 64 | u8, 65 | correctMsg.buffered(), 66 | testMsg.buffered(), 67 | ); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/commands/sets/sunionstore.zig: -------------------------------------------------------------------------------- 1 | // SUNIONSTORE destination key [key ...] 2 | 3 | const std = @import("std"); 4 | 5 | pub const SUNIONSTORE = struct { 6 | destination: []const u8, 7 | keys: []const []const u8, 8 | 9 | /// Instantiates a new SUNIONSTORE command. 10 | pub fn init(destination: []const u8, keys: []const []const u8) SUNIONSTORE { 11 | // TODO: support std.hashmap used as a set! 12 | return .{ .destination = destination, .keys = keys }; 13 | } 14 | 15 | /// Validates if the command is syntactically correct. 16 | pub fn validate(self: SUNIONSTORE) !void { 17 | if (self.keys.len == 0) return error.KeysArrayIsEmpty; 18 | // TODO: should we check for duplicated members? if so, we need an allocator, methinks. 19 | } 20 | 21 | pub const RedisCommand = struct { 22 | pub fn serialize(self: SUNIONSTORE, comptime rootSerializer: type, msg: anytype) !void { 23 | return rootSerializer.serializeCommand(msg, .{ "SUNIONSTORE", self.destination, self.keys }); 24 | } 25 | }; 26 | }; 27 | 28 | test "basic usage" { 29 | const cmd = SUNIONSTORE.init("finalSet", &[_][]const u8{ "set1", "set2" }); 30 | try cmd.validate(); 31 | } 32 | 33 | test "serializer" { 34 | const serializer = @import("../../serializer.zig").CommandSerializer; 35 | 36 | var correctBuf: [1000]u8 = undefined; 37 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 38 | 39 | var testBuf: [1000]u8 = undefined; 40 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 41 | 42 | { 43 | { 44 | correctMsg.end = 0; 45 | testMsg.end = 0; 46 | 47 | try serializer.serializeCommand( 48 | &testMsg, 49 | SUNIONSTORE.init("destination", &[_][]const u8{ "set1", "set2" }), 50 | ); 51 | try serializer.serializeCommand( 52 | &correctMsg, 53 | .{ "SUNIONSTORE", "destination", "set1", "set2" }, 54 | ); 55 | 56 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.getWritten(), testMsg.getWritten() }); 57 | try std.testing.expectEqualSlices( 58 | u8, 59 | correctMsg.buffered(), 60 | testMsg.buffered(), 61 | ); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/commands/sets/sdiffstore.zig: -------------------------------------------------------------------------------- 1 | // SDIFFSTORE destination key [key ...] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SDIFFSTORE = struct { 7 | destination: []const u8, 8 | keys: []const []const u8, 9 | 10 | /// Instantiates a new SDIFFSTORE command. 11 | pub fn init(destination: []const u8, keys: []const []const u8) SDIFFSTORE { 12 | // TODO: support std.hashmap used as a set! 13 | return .{ .destination = destination, .keys = keys }; 14 | } 15 | 16 | /// Validates if the command is syntactically correct. 17 | pub fn validate(self: SDIFFSTORE) !void { 18 | if (self.keys.len == 0) return error.KeysArrayIsEmpty; 19 | // TODO: should we check for duplicated members? if so, we need an allocator, methinks. 20 | } 21 | 22 | pub const RedisCommand = struct { 23 | pub fn serialize(self: SDIFFSTORE, comptime rootSerializer: type, msg: anytype) !void { 24 | return rootSerializer.serializeCommand(msg, .{ "SDIFFSTORE", self.destination, self.keys }); 25 | } 26 | }; 27 | }; 28 | 29 | test "basic usage" { 30 | const cmd = SDIFFSTORE.init("finalSet", &[_][]const u8{ "set1", "set2" }); 31 | try cmd.validate(); 32 | } 33 | 34 | test "serializer" { 35 | const serializer = @import("../../serializer.zig").CommandSerializer; 36 | 37 | var correctBuf: [1000]u8 = undefined; 38 | var correctMsg: Writer = .fixed(correctBuf[0..]); 39 | 40 | var testBuf: [1000]u8 = undefined; 41 | var testMsg: Writer = .fixed(testBuf[0..]); 42 | 43 | { 44 | { 45 | correctMsg.end = 0; 46 | testMsg.end = 0; 47 | 48 | try serializer.serializeCommand( 49 | &testMsg, 50 | SDIFFSTORE.init("destination", &[_][]const u8{ "set1", "set2" }), 51 | ); 52 | try serializer.serializeCommand( 53 | &correctMsg, 54 | .{ "SDIFFSTORE", "destination", "set1", "set2" }, 55 | ); 56 | 57 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 58 | try std.testing.expectEqualSlices( 59 | u8, 60 | correctMsg.buffered(), 61 | testMsg.buffered(), 62 | ); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/commands/sets/sadd.zig: -------------------------------------------------------------------------------- 1 | // SADD key member [member ...] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SADD = struct { 7 | key: []const u8, 8 | members: []const []const u8, 9 | 10 | /// Instantiates a new SADD command. 11 | pub fn init(key: []const u8, members: []const []const u8) SADD { 12 | // TODO: support std.hashmap used as a set! 13 | return .{ .key = key, .members = members }; 14 | } 15 | 16 | /// Validates if the command is syntactically correct. 17 | pub fn validate(self: SADD) !void { 18 | if (self.members.len == 0) return error.MembersArrayIsEmpty; 19 | // TODO: should we check for duplicated members? if so, we need an allocator, methinks. 20 | } 21 | 22 | pub const RedisCommand = struct { 23 | pub fn serialize( 24 | self: SADD, 25 | comptime root_serializer: type, 26 | r: *Writer, 27 | ) !void { 28 | return root_serializer.serializeCommand( 29 | r, 30 | .{ "SADD", self.key, self.members }, 31 | ); 32 | } 33 | }; 34 | }; 35 | 36 | test "basic usage" { 37 | const cmd = SADD.init("myset", &[_][]const u8{ "alice", "bob" }); 38 | try cmd.validate(); 39 | } 40 | 41 | test "serializer" { 42 | const serializer = @import("../../serializer.zig").CommandSerializer; 43 | 44 | var correctBuf: [1000]u8 = undefined; 45 | var correctMsg: Writer = .fixed(correctBuf[0..]); 46 | 47 | var testBuf: [1000]u8 = undefined; 48 | var testMsg: Writer = .fixed(testBuf[0..]); 49 | 50 | { 51 | { 52 | correctMsg.end = 0; 53 | testMsg.end = 0; 54 | 55 | try serializer.serializeCommand( 56 | &testMsg, 57 | SADD.init("set1", &[_][]const u8{ "alice", "bob" }), 58 | ); 59 | try serializer.serializeCommand( 60 | &correctMsg, 61 | .{ "SADD", "set1", "alice", "bob" }, 62 | ); 63 | 64 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.getWritten(), testMsg.getWritten() }); 65 | try std.testing.expectEqualSlices( 66 | u8, 67 | correctMsg.buffered(), 68 | testMsg.buffered(), 69 | ); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/commands/sets/srem.zig: -------------------------------------------------------------------------------- 1 | // SREM key member [member ...] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SREM = struct { 7 | key: []const u8, 8 | members: []const []const u8, 9 | 10 | /// Instantiates a new SREM command. 11 | pub fn init(key: []const u8, members: []const []const u8) SREM { 12 | // TODO: support std.hashmap used as a set! 13 | return .{ .key = key, .members = members }; 14 | } 15 | 16 | /// Validates if the command is syntactically correct. 17 | pub fn validate(self: SREM) !void { 18 | if (self.members.len == 0) return error.MembersArrayIsEmpty; 19 | // TODO: should we check for duplicated members? if so, we need an allocator, methinks. 20 | } 21 | 22 | pub const RedisCommand = struct { 23 | pub fn serialize( 24 | self: SREM, 25 | comptime root_serializer: type, 26 | w: *Writer, 27 | ) !void { 28 | return root_serializer.serializeCommand( 29 | w, 30 | .{ "SREM", self.key, self.members }, 31 | ); 32 | } 33 | }; 34 | }; 35 | 36 | test "basic usage" { 37 | const cmd = SREM.init("myset", &[_][]const u8{ "alice", "bob" }); 38 | try cmd.validate(); 39 | } 40 | 41 | test "serializer" { 42 | const serializer = @import("../../serializer.zig").CommandSerializer; 43 | 44 | var correctBuf: [1000]u8 = undefined; 45 | var correctMsg: Writer = .fixed(correctBuf[0..]); 46 | 47 | var testBuf: [1000]u8 = undefined; 48 | var testMsg: Writer = .fixed(testBuf[0..]); 49 | 50 | { 51 | { 52 | correctMsg.end = 0; 53 | testMsg.end = 0; 54 | 55 | try serializer.serializeCommand( 56 | &testMsg, 57 | SREM.init("set1", &[_][]const u8{ "alice", "bob" }), 58 | ); 59 | try serializer.serializeCommand( 60 | &correctMsg, 61 | .{ "SREM", "set1", "alice", "bob" }, 62 | ); 63 | 64 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.getWritten(), testMsg.getWritten() }); 65 | try std.testing.expectEqualSlices( 66 | u8, 67 | correctMsg.buffered(), 68 | testMsg.buffered(), 69 | ); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/parser/t_bignum.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const testing = std.testing; 4 | 5 | /// Parses RedisNumber values 6 | pub const BigNumParser = struct { 7 | pub fn isSupported(comptime _: type) bool { 8 | return false; 9 | } 10 | 11 | pub fn parse(comptime T: type, comptime _: type, r: *Reader) !T { 12 | _ = r; 13 | 14 | @compileError("The BigNum parser handles a type that needs an allocator."); 15 | } 16 | 17 | // TODO: add support for strings 18 | pub fn isSupportedAlloc(comptime T: type) bool { 19 | return T == std.math.big.int.Managed or T == []u8; 20 | } 21 | 22 | pub fn parseAlloc( 23 | comptime T: type, 24 | comptime _: type, 25 | allocator: std.mem.Allocator, 26 | r: *Reader, 27 | ) !T { 28 | var w: std.Io.Writer.Allocating = .init(allocator); 29 | errdefer w.deinit(); 30 | 31 | _ = try r.streamDelimiter(&w.writer, '\r'); 32 | try r.discardAll(2); 33 | 34 | if (T == []u8 or T == []const u8) { 35 | return w.toOwnedSlice(); 36 | } 37 | 38 | // TODO: check that the type is correct! 39 | // T has to be `std.math.big.int` 40 | var res: T = try T.init(allocator); 41 | try res.setString(10, try w.toOwnedSlice()); 42 | return res; 43 | } 44 | }; 45 | 46 | test "bignum" { 47 | const allocator = std.heap.page_allocator; 48 | var r_bignum = MakeBigNum(); 49 | var bgn = try BigNumParser.parseAlloc( 50 | std.math.big.int.Managed, 51 | void, 52 | allocator, 53 | &r_bignum, 54 | ); 55 | defer bgn.deinit(); 56 | 57 | const bgnStr = try bgn.toString(allocator, 10, .lower); 58 | defer allocator.free(bgnStr); 59 | try testing.expectEqualSlices(u8, "1234567899990000009999876543211234567890", bgnStr); 60 | 61 | var r_bignum2 = MakeBigNum(); 62 | const str = try BigNumParser.parseAlloc( 63 | []u8, 64 | void, 65 | allocator, 66 | &r_bignum2, 67 | ); 68 | defer allocator.free(str); 69 | 70 | try testing.expectEqualSlices(u8, bgnStr, str); 71 | } 72 | 73 | // TODO: get rid of this 74 | fn MakeBigNum() Reader { 75 | return std.Io.Reader.fixed( 76 | "(1234567899990000009999876543211234567890\r\n"[1..], 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/commands/strings.zig: -------------------------------------------------------------------------------- 1 | pub const APPEND = @import("./strings/append.zig").APPEND; 2 | pub const BITCOUNT = @import("./strings/bitcount.zig").BITCOUNT; 3 | // pub const BITFIELD = @import("./strings/bitfield.zig").BITFIELD; 4 | pub const BITOP = @import("./strings/bitop.zig").BITOP; 5 | pub const BITPOS = @import("./strings/bitpos.zig").BITPOS; 6 | pub const DECR = @import("./strings/decr.zig").DECR; 7 | pub const DECRBY = @import("./strings/decrby.zig").DECRBY; 8 | pub const GET = @import("./strings/get.zig").GET; 9 | pub const GETBIT = @import("./strings/getbit.zig").GETBIT; 10 | pub const GETRANGE = @import("./strings/getrange.zig").GETRANGE; 11 | pub const GETSET = @import("./strings/getset.zig").GETSET; 12 | pub const INCR = @import("./strings/incr.zig").INCR; 13 | pub const INCRBY = @import("./strings/incrby.zig").INCRBY; 14 | pub const INCRBYFLOAT = @import("./strings/incrbyfloat.zig").INCRBYFLOAT; 15 | pub const MGET = @import("./strings/mget.zig").MGET; 16 | // pub const MSET = @import("./strings/mset.zig").MSET; 17 | // pub const MSETNX = @import("./strings/msetnx.zig").MSETNX; 18 | // pub const PSETEX = @import("./strings/psetex.zig").PSETEX; 19 | pub const SET = @import("./strings/set.zig").SET; 20 | pub const SETBIT = @import("./strings/setbit.zig").SETBIT; 21 | pub const utils = struct { 22 | pub const Value = @import("./_common_utils.zig").Value; 23 | }; 24 | 25 | test "strings" { 26 | _ = @import("./strings/append.zig"); 27 | _ = @import("./strings/bitcount.zig"); 28 | _ = @import("./strings/bitfield.zig"); 29 | _ = @import("./strings/bitop.zig"); 30 | _ = @import("./strings/bitpos.zig"); 31 | _ = @import("./strings/decr.zig"); 32 | _ = @import("./strings/decrby.zig"); 33 | _ = @import("./strings/get.zig"); 34 | _ = @import("./strings/getbit.zig"); 35 | _ = @import("./strings/getrange.zig"); 36 | _ = @import("./strings/getset.zig"); 37 | _ = @import("./strings/incr.zig"); 38 | _ = @import("./strings/incrby.zig"); 39 | _ = @import("./strings/incrbyfloat.zig"); 40 | _ = @import("./strings/mget.zig"); 41 | _ = @import("./strings/mset.zig"); 42 | _ = @import("./strings/msetnx.zig"); 43 | _ = @import("./strings/psetex.zig"); 44 | _ = @import("./strings/set.zig"); 45 | _ = @import("./strings/setbit.zig"); 46 | } 47 | 48 | test "docs" { 49 | const std = @import("std"); 50 | std.testing.refAllDecls(@This()); 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/strings/bitop.zig: -------------------------------------------------------------------------------- 1 | // BITOP operation destkey key [key ...] 2 | 3 | // TODO: implement Op as a Redis.Arguments? 4 | 5 | pub const BITOP = struct { 6 | //! ``` 7 | //! const cmd = BITOP.init(.AND, "result", &[_][]const u8{ "key1", "key2" }); 8 | //! ``` 9 | 10 | operation: Op, 11 | destKey: []const u8, 12 | sourceKeys: []const []const u8, 13 | 14 | pub fn init(operation: Op, destKey: []const u8, sourceKeys: []const []const u8) BITOP { 15 | return .{ .operation = operation, .destKey = destKey, .sourceKeys = sourceKeys }; 16 | } 17 | 18 | pub fn validate(self: BITOP) !void { 19 | if (self.key.len == 0) return error.EmptyKeyName; 20 | if (self.value.len == 0) return error.EmptyValue; 21 | } 22 | 23 | pub const RedisCommand = struct { 24 | pub fn serialize(self: BITOP, comptime rootSerializer: type, msg: anytype) !void { 25 | const op = switch (self.operation) { 26 | .AND => "AND", 27 | .OR => "OR", 28 | .XOR => "XOR", 29 | .NOT => "NOT", 30 | }; 31 | return rootSerializer.serializeCommand(msg, .{ "BITOP", op, self.destKey, self.sourceKeys }); 32 | } 33 | }; 34 | 35 | pub const Op = enum { 36 | AND, 37 | OR, 38 | XOR, 39 | NOT, 40 | }; 41 | }; 42 | 43 | test "basic usage" { 44 | _ = BITOP.init(.AND, "result", &[_][]const u8{ "key1", "key2" }); 45 | } 46 | 47 | test "serializer" { 48 | const std = @import("std"); 49 | const serializer = @import("../../serializer.zig").CommandSerializer; 50 | 51 | var correctBuf: [1000]u8 = undefined; 52 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 53 | 54 | var testBuf: [1000]u8 = undefined; 55 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 56 | 57 | { 58 | correctMsg.end = 0; 59 | testMsg.end = 0; 60 | 61 | try serializer.serializeCommand( 62 | &testMsg, 63 | BITOP.init(.AND, "mykey", &[_][]const u8{ "key1", "key2" }), 64 | ); 65 | try serializer.serializeCommand( 66 | &correctMsg, 67 | .{ "BITOP", "AND", "mykey", "key1", "key2" }, 68 | ); 69 | try std.testing.expectEqualSlices( 70 | u8, 71 | correctMsg.buffered(), 72 | testMsg.buffered(), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/async.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Io = std.Io; 3 | 4 | const okredis = @import("src/root.zig"); 5 | const Client = okredis.Client; 6 | 7 | const gpa = std.heap.smp_allocator; 8 | pub fn main() !void { 9 | 10 | // Pick your preferred Io implementation. 11 | var threaded: Io.Threaded = .init(gpa); 12 | defer threaded.deinit(); 13 | const io = threaded.io(); 14 | 15 | // Open a TCP connection. 16 | // NOTE: managing the connection is your responsibility. 17 | const addr: Io.net.IpAddress = try .parseIp4("127.0.0.1", 6379); 18 | const connection = try addr.connect(io, .{ .mode = .stream }); 19 | defer connection.close(io); 20 | 21 | var rbuf: [1024]u8 = undefined; 22 | var wbuf: [1024]u8 = undefined; 23 | var reader = connection.reader(io, &rbuf); 24 | var writer = connection.writer(io, &wbuf); 25 | 26 | // The last argument are auth credentials. 27 | var client = try Client.init(io, &reader.interface, &writer.interface, null); 28 | 29 | var rand: std.Random.DefaultPrng = .init(6379); 30 | var g: Io.Group = .init; 31 | // for (0..1) |i| { 32 | for (0..try std.Thread.getCpuCount()) |i| { 33 | g.async(io, countdown, .{ &client, i, rand.random().int(u16) }); 34 | } 35 | 36 | g.wait(io); 37 | } 38 | 39 | fn countdown(client: *Client, i: usize, num: u32) void { 40 | countdownFallible(client, i, num) catch |err| { 41 | std.debug.print("[{}] error: {t}\n", .{ i, err }); 42 | }; 43 | } 44 | 45 | fn countdownFallible(client: *Client, i: usize, n: u32) !void { 46 | var num = n; 47 | const key = try std.fmt.allocPrint(gpa, "coro-{}", .{i}); 48 | try client.send(void, .{ "SET", key, num }); 49 | while (num > 0) { 50 | const value = try client.send(u32, .{ "INCRBY", key, -1 }); 51 | // std.debug.print("({})[{}] {}\n", .{ 52 | // std.Thread.getCurrentId(), 53 | // i, 54 | // value, 55 | // }); 56 | num -= 1; 57 | if (value != num) { 58 | std.debug.print("({})[{}] {} != {}\n", .{ 59 | std.Thread.getCurrentId(), 60 | i, 61 | value, 62 | num, 63 | }); 64 | return error.Mismatch; 65 | } 66 | // if (value != num) @panic("mismatch!"); 67 | } 68 | std.log.info("[{}] correct countdown from {}", .{ i, n }); 69 | } 70 | -------------------------------------------------------------------------------- /src/commands/sets/smismember.zig: -------------------------------------------------------------------------------- 1 | // SMISMEMBER key member [member ...] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SMISMEMBER = struct { 7 | key: []const u8, 8 | members: []const []const u8, 9 | 10 | /// Instantiates a new SMISMEMBER command. 11 | pub fn init(key: []const u8, members: []const []const u8) SMISMEMBER { 12 | // TODO: support std.hashmap used as a set! 13 | return .{ .key = key, .members = members }; 14 | } 15 | 16 | /// Validates if the command is syntactically correct. 17 | pub fn validate(self: SMISMEMBER) !void { 18 | if (self.members.len == 0) return error.MembersArrayIsEmpty; 19 | // TODO: should we check for duplicated members? if so, we need an allocator, methinks. 20 | } 21 | 22 | pub const RedisCommand = struct { 23 | pub fn serialize( 24 | self: SMISMEMBER, 25 | comptime root_serializer: type, 26 | w: *Writer, 27 | ) !void { 28 | return root_serializer.serializeCommand(w, .{ 29 | "SMISMEMBER", 30 | self.key, 31 | self.members, 32 | }); 33 | } 34 | }; 35 | }; 36 | 37 | test "basic usage" { 38 | const cmd = SMISMEMBER.init("myset", &[_][]const u8{ "alice", "bob" }); 39 | try cmd.validate(); 40 | } 41 | 42 | test "serializer" { 43 | const serializer = @import("../../serializer.zig").CommandSerializer; 44 | 45 | var correctBuf: [1000]u8 = undefined; 46 | var correctMsg = Writer.fixed(correctBuf[0..]); 47 | 48 | var testBuf: [1000]u8 = undefined; 49 | var testMsg = Writer.fixed(testBuf[0..]); 50 | 51 | { 52 | { 53 | correctMsg.end = 0; 54 | testMsg.end = 0; 55 | 56 | try serializer.serializeCommand( 57 | &testMsg, 58 | SMISMEMBER.init("set1", &[_][]const u8{ "alice", "bob" }), 59 | ); 60 | try serializer.serializeCommand( 61 | &correctMsg, 62 | .{ "SMISMEMBER", "set1", "alice", "bob" }, 63 | ); 64 | 65 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.getWritten(), testMsg.getWritten() }); 66 | try std.testing.expectEqualSlices( 67 | u8, 68 | correctMsg.buffered(), 69 | testMsg.buffered(), 70 | ); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/commands/sets/sinterstore.zig: -------------------------------------------------------------------------------- 1 | // SINTERSTORE destination key [key ...] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SINTERSTORE = struct { 7 | destination: []const u8, 8 | keys: []const []const u8, 9 | 10 | /// Instantiates a new SINTERSTORE command. 11 | pub fn init(destination: []const u8, keys: []const []const u8) SINTERSTORE { 12 | // TODO: support std.hashmap used as a set! 13 | return .{ .destination = destination, .keys = keys }; 14 | } 15 | 16 | /// Validates if the command is syntactically correct. 17 | pub fn validate(self: SINTERSTORE) !void { 18 | if (self.keys.len == 0) return error.KeysArrayIsEmpty; 19 | // TODO: should we check for duplicated members? if so, we need an allocator, methinks. 20 | } 21 | 22 | pub const RedisCommand = struct { 23 | pub fn serialize( 24 | self: SINTERSTORE, 25 | comptime rootSerializer: type, 26 | w: *Writer, 27 | ) !void { 28 | return rootSerializer.serializeCommand(w, .{ 29 | "SINTERSTORE", 30 | self.destination, 31 | self.keys, 32 | }); 33 | } 34 | }; 35 | }; 36 | 37 | test "basic usage" { 38 | const cmd = SINTERSTORE.init("finalSet", &[_][]const u8{ "set1", "set2" }); 39 | try cmd.validate(); 40 | } 41 | 42 | test "serializer" { 43 | const serializer = @import("../../serializer.zig").CommandSerializer; 44 | 45 | var correctBuf: [1000]u8 = undefined; 46 | var correctMsg: Writer = .fixed(correctBuf[0..]); 47 | 48 | var testBuf: [1000]u8 = undefined; 49 | var testMsg: Writer = .fixed(testBuf[0..]); 50 | 51 | { 52 | { 53 | correctMsg.end = 0; 54 | testMsg.end = 0; 55 | 56 | try serializer.serializeCommand( 57 | &testMsg, 58 | SINTERSTORE.init("destination", &[_][]const u8{ "set1", "set2" }), 59 | ); 60 | try serializer.serializeCommand( 61 | &correctMsg, 62 | .{ "SINTERSTORE", "destination", "set1", "set2" }, 63 | ); 64 | 65 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 66 | try std.testing.expectEqualSlices( 67 | u8, 68 | correctMsg.buffered(), 69 | testMsg.buffered(), 70 | ); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/commands/sets/smove.zig: -------------------------------------------------------------------------------- 1 | // SMOVE source destination member 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SMOVE = struct { 7 | source: []const u8, 8 | destination: []const u8, 9 | member: []const u8, 10 | 11 | /// Instantiates a new SMOVE command. 12 | pub fn init( 13 | source: []const u8, 14 | destination: []const u8, 15 | member: []const u8, 16 | ) SMOVE { 17 | // TODO: support std.hashmap used as a set! 18 | return .{ 19 | .source = source, 20 | .destination = destination, 21 | .member = member, 22 | }; 23 | } 24 | 25 | /// Validates if the command is syntactically correct. 26 | pub fn validate(self: SMOVE) !void { 27 | // TODO: maybe this check is dumb and we shouldn't have it 28 | if (std.mem.eql(u8, self.source, self.destination)) { 29 | return error.SameSourceAndDestination; 30 | } 31 | } 32 | 33 | pub const RedisCommand = struct { 34 | pub fn serialize( 35 | self: SMOVE, 36 | comptime rootSerializer: type, 37 | w: *Writer, 38 | ) !void { 39 | return rootSerializer.serializeCommand(w, .{ 40 | "SMOVE", 41 | self.source, 42 | self.destination, 43 | self.member, 44 | }); 45 | } 46 | }; 47 | }; 48 | 49 | test "basic usage" { 50 | const cmd = SMOVE.init("source", "destination", "element"); 51 | try cmd.validate(); 52 | 53 | const cmd1 = SMOVE.init("source", "source", "element"); 54 | try std.testing.expectError(error.SameSourceAndDestination, cmd1.validate()); 55 | } 56 | 57 | test "serializer" { 58 | const serializer = @import("../../serializer.zig").CommandSerializer; 59 | 60 | var correctBuf: [1000]u8 = undefined; 61 | var correctMsg: Writer = .fixed(correctBuf[0..]); 62 | 63 | var testBuf: [1000]u8 = undefined; 64 | var testMsg: Writer = .fixed(testBuf[0..]); 65 | 66 | { 67 | { 68 | correctMsg.end = 0; 69 | testMsg.end = 0; 70 | 71 | try serializer.serializeCommand( 72 | &testMsg, 73 | SMOVE.init("s", "d", "m"), 74 | ); 75 | try serializer.serializeCommand( 76 | &correctMsg, 77 | .{ "SMOVE", "s", "d", "m" }, 78 | ); 79 | 80 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 81 | try std.testing.expectEqualSlices( 82 | u8, 83 | correctMsg.buffered(), 84 | testMsg.buffered(), 85 | ); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/commands/strings/bitpos.zig: -------------------------------------------------------------------------------- 1 | // BITPOS key bit [start] [end] 2 | 3 | pub const Bit = enum { 4 | Zero, 5 | One, 6 | }; 7 | 8 | pub const BITPOS = struct { 9 | //! ``` 10 | //! const cmd = BITPOS.init("test", .Zero, -3, null); 11 | //! ``` 12 | 13 | key: []const u8, 14 | bit: Bit, 15 | bounds: Bounds, 16 | 17 | pub fn init(key: []const u8, bit: Bit, start: ?isize, end: ?isize) BITPOS { 18 | return .{ 19 | .key = key, 20 | .bit = bit, 21 | .bounds = Bounds{ .start = start, .end = end }, 22 | }; 23 | } 24 | 25 | pub fn validate(self: BITPOS) !void { 26 | if (self.key.len == 0) return error.EmptyKeyName; 27 | } 28 | 29 | pub const RedisCommand = struct { 30 | pub fn serialize(self: BITPOS, comptime rootSerializer: type, msg: anytype) !void { 31 | const bit = switch (self.bit) { 32 | .Zero => "0", 33 | .One => "1", 34 | }; 35 | return rootSerializer.serializeCommand(msg, .{ "BITPOS", self.key, bit, self.bounds }); 36 | } 37 | }; 38 | }; 39 | 40 | const Bounds = struct { 41 | start: ?isize, 42 | end: ?isize, 43 | 44 | pub const RedisArguments = struct { 45 | pub fn count(self: Bounds) usize { 46 | const one: usize = 1; 47 | const zero: usize = 0; 48 | return (if (self.start) |_| one else zero) + (if (self.end) |_| one else zero); 49 | } 50 | 51 | pub fn serialize(self: Bounds, comptime rootSerializer: type, msg: anytype) !void { 52 | if (self.start) |s| { 53 | try rootSerializer.serializeArgument(msg, isize, s); 54 | } 55 | if (self.end) |e| { 56 | try rootSerializer.serializeArgument(msg, isize, e); 57 | } 58 | } 59 | }; 60 | }; 61 | 62 | test "basic usage" { 63 | _ = BITPOS.init("test", .Zero, -3, null); 64 | } 65 | 66 | test "serializer" { 67 | const std = @import("std"); 68 | const serializer = @import("../../serializer.zig").CommandSerializer; 69 | 70 | var correctBuf: [1000]u8 = undefined; 71 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 72 | 73 | var testBuf: [1000]u8 = undefined; 74 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 75 | 76 | { 77 | correctMsg.end = 0; 78 | testMsg.end = 0; 79 | 80 | const cmd = BITPOS.init("test", .Zero, -3, null); 81 | try serializer.serializeCommand( 82 | &testMsg, 83 | cmd, 84 | ); 85 | try serializer.serializeCommand( 86 | &correctMsg, 87 | .{ "BITPOS", "test", "0", "-3" }, 88 | ); 89 | 90 | try std.testing.expectEqualSlices( 91 | u8, 92 | correctMsg.buffered(), 93 | testMsg.buffered(), 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/commands/streams/xtrim.zig: -------------------------------------------------------------------------------- 1 | // XTRIM key MAXLEN [~] count 2 | 3 | pub const XTRIM = struct { 4 | key: []const u8, 5 | strategy: Strategy, 6 | 7 | /// Instantiates a new XTRIM command. 8 | pub fn init(key: []const u8, strategy: Strategy) XTRIM { 9 | return .{ .key = key, .strategy = strategy }; 10 | } 11 | 12 | /// Validates if the command is syntactically correct. 13 | pub fn validate(self: XTRIM) !void { 14 | if (self.key.len == 0) return error.EmptyKeyName; 15 | } 16 | 17 | pub const RedisCommand = struct { 18 | pub fn serialize(self: XTRIM, comptime rootSerializer: type, msg: anytype) !void { 19 | return rootSerializer.serializeCommand(msg, .{ "XTRIM", self.key, self.strategy }); 20 | } 21 | }; 22 | 23 | pub const Strategy = union(enum) { 24 | MaxLen: struct { 25 | precise: bool = false, 26 | count: u64, 27 | }, 28 | 29 | pub const RedisArguments = struct { 30 | pub fn count(self: Strategy) usize { 31 | switch (self) { 32 | .MaxLen => |m| if (!m.precise) { 33 | return 3; 34 | } else { 35 | return 2; 36 | }, 37 | } 38 | } 39 | 40 | pub fn serialize(self: Strategy, comptime rootSerializer: type, msg: anytype) !void { 41 | switch (self) { 42 | .MaxLen => |m| { 43 | try rootSerializer.serializeArgument(msg, []const u8, "MAXLEN"); 44 | if (!m.precise) try rootSerializer.serializeArgument(msg, []const u8, "~"); 45 | try rootSerializer.serializeArgument(msg, u64, m.count); 46 | }, 47 | } 48 | } 49 | }; 50 | }; 51 | }; 52 | 53 | test "basic usage" { 54 | _ = XTRIM.init("mykey", XTRIM.Strategy{ .MaxLen = .{ .count = 10 } }); 55 | } 56 | 57 | test "serializer" { 58 | const std = @import("std"); 59 | const serializer = @import("../../serializer.zig").CommandSerializer; 60 | 61 | var correctBuf: [1000]u8 = undefined; 62 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 63 | 64 | var testBuf: [1000]u8 = undefined; 65 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 66 | 67 | { 68 | correctMsg.end = 0; 69 | testMsg.end = 0; 70 | 71 | try serializer.serializeCommand( 72 | &testMsg, 73 | XTRIM.init("mykey", XTRIM.Strategy{ .MaxLen = .{ .count = 30 } }), 74 | ); 75 | try serializer.serializeCommand( 76 | &correctMsg, 77 | .{ "XTRIM", "mykey", "MAXLEN", "~", 30 }, 78 | ); 79 | 80 | try std.testing.expectEqualSlices( 81 | u8, 82 | correctMsg.buffered(), 83 | testMsg.buffered(), 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/commands/strings/bitcount.zig: -------------------------------------------------------------------------------- 1 | // BITCOUNT key [start end] 2 | 3 | pub const BITCOUNT = struct { 4 | //! ``` 5 | //! const cmd = BITCOUNT.init("test", BITCOUNT.Bounds{ .slice = .{ .start = -2, .end = -1 } }); 6 | //! ``` 7 | 8 | key: []const u8, 9 | bounds: Bounds = .FullString, 10 | 11 | const Self = @This(); 12 | 13 | pub fn init(key: []const u8, bounds: Bounds) BITCOUNT { 14 | return .{ .key = key, .bounds = bounds }; 15 | } 16 | 17 | pub fn validate(self: Self) !void { 18 | if (self.key.len == 0) return error.EmptyKeyName; 19 | } 20 | 21 | pub const RedisCommand = struct { 22 | pub fn serialize(self: BITCOUNT, comptime rootSerializer: type, msg: anytype) !void { 23 | return rootSerializer.serializeCommand(msg, .{ "BITCOUNT", self.key, self.bounds }); 24 | } 25 | }; 26 | 27 | pub const Bounds = union(enum) { 28 | FullString, 29 | slice: struct { 30 | start: isize, 31 | end: isize, 32 | }, 33 | 34 | pub const RedisArguments = struct { 35 | pub fn count(self: Bounds) usize { 36 | return switch (self) { 37 | .FullString => 0, 38 | .slice => 2, 39 | }; 40 | } 41 | 42 | pub fn serialize(self: Bounds, comptime rootSerializer: type, msg: anytype) !void { 43 | switch (self) { 44 | .FullString => {}, 45 | .slice => |slice| { 46 | try rootSerializer.serializeArgument(msg, isize, slice.start); 47 | try rootSerializer.serializeArgument(msg, isize, slice.end); 48 | }, 49 | } 50 | } 51 | }; 52 | }; 53 | }; 54 | 55 | test "example" { 56 | _ = BITCOUNT.init("test", BITCOUNT.Bounds{ .slice = .{ .start = -2, .end = -1 } }); 57 | } 58 | 59 | test "serializer" { 60 | const std = @import("std"); 61 | const serializer = @import("../../serializer.zig").CommandSerializer; 62 | 63 | var correctBuf: [1000]u8 = undefined; 64 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 65 | 66 | var testBuf: [1000]u8 = undefined; 67 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 68 | 69 | { 70 | correctMsg.end = 0; 71 | testMsg.end = 0; 72 | 73 | try serializer.serializeCommand( 74 | &testMsg, 75 | BITCOUNT.init("mykey", BITCOUNT.Bounds{ 76 | .slice = .{ 77 | .start = 1, 78 | .end = 10, 79 | }, 80 | }), 81 | ); 82 | try serializer.serializeCommand( 83 | &correctMsg, 84 | .{ "BITCOUNT", "mykey", 1, 10 }, 85 | ); 86 | 87 | try std.testing.expectEqualSlices( 88 | u8, 89 | correctMsg.buffered(), 90 | testMsg.buffered(), 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/types/fixbuf.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const fmt = std.fmt; 4 | 5 | /// It's a fixed length buffer, useful for parsing strings 6 | /// without requiring an allocator. 7 | pub fn FixBuf(comptime size: usize) type { 8 | return struct { 9 | buf: [size]u8, 10 | len: usize, 11 | 12 | const Self = @This(); 13 | 14 | /// Returns a slice pointing to the contents in the buffer. 15 | pub fn toSlice(self: *const Self) []const u8 { 16 | return self.buf[0..self.len]; 17 | } 18 | 19 | pub const Redis = struct { 20 | pub const Parser = struct { 21 | pub fn parse( 22 | tag: u8, 23 | comptime rootParser: type, 24 | r: *Reader, 25 | ) !Self { 26 | switch (tag) { 27 | else => return error.UnsupportedConversion, 28 | '-', '!' => { 29 | try rootParser.parseFromTag(void, tag, r); 30 | return error.GotErrorReply; 31 | }, 32 | '+', '(' => { 33 | var res: Self = undefined; 34 | var ch = try r.takeByte(); 35 | for (&res.buf, 0..) |*elem, i| { 36 | if (ch == '\r') { 37 | res.len = i; 38 | try r.discardAll(1); 39 | return res; 40 | } 41 | elem.* = ch; 42 | ch = try r.takeByte(); 43 | } 44 | if (ch != '\r') return error.BufTooSmall; 45 | try r.discardAll(1); 46 | return res; 47 | }, 48 | '$' => { 49 | const digits = try r.takeSentinel('\r'); 50 | const respSize = try fmt.parseInt(usize, digits, 10); 51 | try r.discardAll(1); 52 | 53 | if (respSize > size) return error.BufTooSmall; 54 | 55 | var res: Self = undefined; 56 | res.len = respSize; 57 | try r.readSliceAll(res.buf[0..respSize]); 58 | try r.discardAll(2); 59 | 60 | return res; 61 | }, 62 | } 63 | } 64 | 65 | pub fn destroy(_: Self, comptime _: type, _: std.mem.Allocator) void {} 66 | 67 | pub fn parseAlloc(tag: u8, comptime rootParser: type, msg: anytype) !Self { 68 | return parse(tag, rootParser, msg); 69 | } 70 | }; 71 | }; 72 | }; 73 | } 74 | 75 | test "docs" { 76 | @import("std").testing.refAllDecls(@This()); 77 | @import("std").testing.refAllDecls(FixBuf(42)); 78 | } 79 | -------------------------------------------------------------------------------- /src/commands/streams/_utils.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const StreamFns = enum { 4 | XADD, 5 | XREAD, 6 | XREADGROUP, 7 | XRANGE, 8 | XREVRANGE, 9 | }; 10 | 11 | pub const SpecialIDs = struct { 12 | pub const NEW_MESSAGES = "$"; 13 | pub const ASSIGN_NEW_MESSAGES = "<"; 14 | pub const MIN = "-"; 15 | pub const MAX = "+"; 16 | pub const AUTO_ID = "*"; 17 | pub const BEGINNING = "0-0"; 18 | }; 19 | 20 | pub fn isValidStreamID(cmd: StreamFns, id: []const u8) bool { 21 | return switch (cmd) { 22 | .XREAD => isNumericStreamID(id) or isAny(id, .{SpecialIDs.NEW_MESSAGES}), 23 | .XREADGROUP => isNumericStreamID(id) or isAny(id, .{ SpecialIDs.NEW_MESSAGES, SpecialIDs.ASSIGN_NEW_MESSAGES }), 24 | .XADD => !std.mem.eql(u8, id, SpecialIDs.BEGINNING) and (isNumericStreamID(id) or isAny(id, .{SpecialIDs.AUTO_ID})), 25 | .XRANGE, .XREVRANGE => isNumericStreamID(id) or isAny(id, .{ SpecialIDs.MIN, SpecialIDs.MAX }), 26 | }; 27 | } 28 | 29 | fn isAny(arg: []const u8, strings: anytype) bool { 30 | inline for (std.meta.fields(@TypeOf(strings))) |field| { 31 | const str = @field(strings, field.name); 32 | if (std.mem.eql(u8, arg, str)) return true; 33 | } 34 | return false; 35 | } 36 | 37 | pub fn isNumericStreamID(id: []const u8) bool { 38 | if (id.len > 41) return false; 39 | 40 | var hyphenPosition: isize = -1; 41 | var i: usize = 0; 42 | while (i < id.len) : (i += 1) { 43 | switch (id[i]) { 44 | '0'...'9' => {}, 45 | '-' => { 46 | if (hyphenPosition != -1) return false; 47 | hyphenPosition = @bitCast(i); 48 | const first_part = id[0..i]; 49 | if (first_part.len == 0) return false; 50 | _ = std.fmt.parseInt(u64, first_part, 10) catch return false; 51 | }, 52 | else => return false, 53 | } 54 | } 55 | const second_part = id[@bitCast(hyphenPosition + 1)..]; 56 | if (second_part.len == 0) return false; 57 | _ = std.fmt.parseInt(u64, second_part, 10) catch return false; 58 | return true; 59 | } 60 | 61 | test "numeric stream ids" { 62 | try std.testing.expectEqual(false, isNumericStreamID("")); 63 | try std.testing.expectEqual(false, isNumericStreamID(" ")); 64 | try std.testing.expectEqual(false, isNumericStreamID("-")); 65 | try std.testing.expectEqual(false, isNumericStreamID("-0")); 66 | try std.testing.expectEqual(false, isNumericStreamID("-1234")); 67 | try std.testing.expectEqual(false, isNumericStreamID("0-")); 68 | try std.testing.expectEqual(false, isNumericStreamID("123-")); 69 | try std.testing.expectEqual(true, isNumericStreamID("0")); 70 | try std.testing.expectEqual(true, isNumericStreamID("123")); 71 | try std.testing.expectEqual(true, isNumericStreamID("0-0")); 72 | try std.testing.expectEqual(true, isNumericStreamID("0-123")); 73 | try std.testing.expectEqual(true, isNumericStreamID("123123123-123123123")); 74 | try std.testing.expectEqual(true, isNumericStreamID("18446744073709551615-18446744073709551615")); 75 | try std.testing.expectEqual(false, isNumericStreamID("18446744073709551616-18446744073709551615")); 76 | try std.testing.expectEqual(false, isNumericStreamID("18446744073709551615-18446744073709551616")); 77 | try std.testing.expectEqual(false, isNumericStreamID("922337203685412312377580123123112317-922337212312312312312036854775808")); 78 | } 79 | -------------------------------------------------------------------------------- /src/parser/t_string_simple.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const fmt = std.fmt; 4 | const mem = std.mem; 5 | const testing = std.testing; 6 | const builtin = @import("builtin"); 7 | 8 | /// Parses RedisSimpleString values 9 | pub const SimpleStringParser = struct { 10 | pub fn isSupported(comptime T: type) bool { 11 | return switch (@typeInfo(T)) { 12 | .int, .float, .array => true, 13 | else => false, 14 | }; 15 | } 16 | 17 | pub fn parse(comptime T: type, comptime _: type, r: *Reader) !T { 18 | switch (@typeInfo(T)) { 19 | else => unreachable, 20 | .int => { 21 | const digits = try r.takeSentinel('\r'); 22 | const result = fmt.parseInt(T, digits, 10); 23 | try r.discardAll(1); 24 | return result; 25 | }, 26 | .float => { 27 | const digits = try r.takeSentinel('\r'); 28 | const result = fmt.parseFloat(T, digits); 29 | try r.discardAll(1); 30 | return result; 31 | }, 32 | .array => |arr| { 33 | var res: [arr.len]arr.child = undefined; 34 | const bytesSlice = mem.sliceAsBytes(res[0..]); 35 | var ch = try r.takeByte(); 36 | for (bytesSlice) |*elem| { 37 | if (ch == '\r') { 38 | return error.LengthMismatch; 39 | } 40 | elem.* = ch; 41 | ch = try r.takeByte(); 42 | } 43 | if (ch != '\r') return error.LengthMismatch; 44 | 45 | try r.discardAll(1); 46 | return res; 47 | }, 48 | } 49 | } 50 | 51 | pub fn isSupportedAlloc(comptime T: type) bool { 52 | return switch (@typeInfo(T)) { 53 | .pointer => |ptr| switch (ptr.size) { 54 | .slice, .c => ptr.child == u8, // TODO: relax constraint 55 | .one, .many => false, 56 | }, 57 | else => isSupported(T), 58 | }; 59 | } 60 | 61 | pub fn parseAlloc( 62 | comptime T: type, 63 | comptime _: type, 64 | allocator: std.mem.Allocator, 65 | r: *Reader, 66 | ) !T { 67 | switch (@typeInfo(T)) { 68 | .pointer => |ptr| { 69 | switch (ptr.size) { 70 | .one, .many => @compileError("Only Slices and C pointers should reach sub-parsers"), 71 | .slice => { 72 | var w: std.Io.Writer.Allocating = .init(allocator); 73 | errdefer w.deinit(); 74 | _ = try r.streamDelimiter(&w.writer, '\r'); 75 | const bytes = try w.toOwnedSlice(); 76 | 77 | _ = std.math.divExact(usize, bytes.len, @sizeOf(ptr.child)) catch return error.LengthMismatch; 78 | try r.discardAll(2); 79 | return bytes; 80 | }, 81 | .c => { 82 | // var bytes = try r.readUntilDelimiterAlloc(allocator, '\n', 4096); 83 | // res[res.len - 1] = 0; 84 | // return res; 85 | // TODO implement this 86 | return error.Unimplemented; 87 | }, 88 | } 89 | }, 90 | else => return parse(T, struct {}, r), 91 | } 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /src/lib/float.zig: -------------------------------------------------------------------------------- 1 | /// 2 | /// A simple and very fast float parser in Zig. 3 | /// 4 | /// Written by Tetralux@teknik.io, 2019-09-06. 5 | /// 6 | /// 7 | /// Will error if you have too many decimal places, 8 | /// invalid characters within the number, or an 9 | /// empty string. 10 | /// 11 | /// Is also subject to rounding errors. 12 | /// 13 | const std = @import("std"); 14 | const mem = std.mem; 15 | const time = std.time; 16 | const math = std.math; 17 | 18 | const inf = math.inf; 19 | const nan = math.nan; 20 | const warn = std.debug.warn; 21 | 22 | fn toDigit(ch: u8) callconv(.Inline) !u8 { 23 | if (ch >= '0' and ch <= '9') return ch - '0'; 24 | return error.InvalidCharacter; 25 | } 26 | 27 | fn parseFloat(comptime T: type, slice: []const u8) error{ Empty, InvalidCharacter, TooManyDigits }!T { 28 | var s = mem.separate(slice, " ").next() orelse return error.Empty; 29 | if (s.len == 0) return error.Empty; 30 | 31 | var is_neg = s[0] == '-'; 32 | if (is_neg) { 33 | if (s.len == 1) return error.Empty; 34 | s = s[1..]; 35 | } 36 | 37 | if (mem.eql(u8, s[0..3], "inf")) return if (is_neg) -inf(T) else inf(T); 38 | if (mem.eql(u8, s[0..3], "nan")) return nan(T); // -nan makes no sense. 39 | 40 | // Read the digits into an integer and note 41 | // where the decimal point is. 42 | var n: u64 = 0; 43 | var decimal_point_index: isize = -1; 44 | var decimal_places: usize = 0; 45 | var numeral_places: usize = 0; 46 | for (s) |ch, i| { 47 | if (ch == '.') { 48 | decimal_point_index = @intCast(isize, i); 49 | continue; 50 | } 51 | if (decimal_point_index == -1) 52 | numeral_places += 1 53 | else 54 | decimal_places += 1; 55 | n += try toDigit(ch); 56 | n *= 10; 57 | } 58 | if (decimal_places + numeral_places > 18) return error.TooManyDigits; // f64 has 18 s.f. 59 | 60 | // Shift the decimal point into the right place. 61 | var n_as_float = @intToFloat(f64, n) / 10; 62 | 63 | if (decimal_point_index != -1) { 64 | // We counted from the front, we'll insert the decimal point from the back. 65 | const decimal_point_index_from_back = @intCast(isize, s.len) - decimal_point_index - 1; 66 | 67 | { 68 | var i: isize = 0; 69 | while (i < decimal_point_index_from_back) : (i += 1) { 70 | n_as_float /= 10; 71 | } 72 | } 73 | } 74 | 75 | var res = @floatCast(T, n_as_float); 76 | if (is_neg) res *= -1; 77 | return res; 78 | } 79 | 80 | pub fn main() !void { 81 | { 82 | var total: u64 = 0; 83 | const its = 10240000; 84 | var i: u64 = 0; 85 | while (i < its) : (i += 1) { 86 | var t = try time.Timer.start(); 87 | var f = try parseFloat(f64, "4.77777777777777777"); 88 | var took = t.read(); 89 | // warn("f is {d}\n", f); 90 | // break; 91 | total += took; 92 | } 93 | warn("average time: {d} ns\n", @intToFloat(f64, total) / @intToFloat(f64, its)); 94 | } 95 | 96 | { 97 | var total: u64 = 0; 98 | const its = 10240000; 99 | var i: u64 = 0; 100 | while (i < its) : (i += 1) { 101 | var t = try time.Timer.start(); 102 | var f = try std.fmt.parseFloat(f64, "4.77777777777777778"); 103 | var took = t.read(); 104 | total += took; 105 | } 106 | warn("average time: {d} ns\n", @intToFloat(f64, total) / @intToFloat(f64, its)); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/commands/sets/srandmember.zig: -------------------------------------------------------------------------------- 1 | // SRANDMEMBER key [count] 2 | 3 | const std = @import("std"); 4 | 5 | pub const SRANDMEMBER = struct { 6 | key: []const u8, 7 | count: Count, 8 | 9 | pub const Count = union(enum) { 10 | one, 11 | Count: usize, 12 | 13 | pub const RedisArguments = struct { 14 | pub fn count(self: Count) usize { 15 | return switch (self) { 16 | .one => 0, 17 | .Count => 1, 18 | }; 19 | } 20 | 21 | pub fn serialize(self: Count, comptime rootSerializer: type, msg: anytype) !void { 22 | switch (self) { 23 | .one => {}, 24 | .Count => |c| { 25 | try rootSerializer.serializeArgument(msg, usize, c); 26 | }, 27 | } 28 | } 29 | }; 30 | }; 31 | 32 | /// Instantiates a new SPOP command. 33 | pub fn init(key: []const u8, count: Count) SRANDMEMBER { 34 | // TODO: support std.hashmap used as a set! 35 | return .{ .key = key, .count = count }; 36 | } 37 | 38 | /// Validates if the command is syntactically correct. 39 | pub fn validate(_: SRANDMEMBER) !void {} 40 | 41 | pub const RedisCommand = struct { 42 | pub fn serialize(self: SRANDMEMBER, comptime rootSerializer: type, msg: anytype) !void { 43 | return rootSerializer.serializeCommand(msg, .{ 44 | "SRANDMEMBER", 45 | self.key, 46 | self.count, 47 | }); 48 | } 49 | }; 50 | }; 51 | 52 | test "basic usage" { 53 | const cmd = SRANDMEMBER.init("myset", .one); 54 | try cmd.validate(); 55 | 56 | const cmd1 = SRANDMEMBER.init("myset", SRANDMEMBER.Count{ .Count = 5 }); 57 | try cmd1.validate(); 58 | } 59 | 60 | test "serializer" { 61 | const serializer = @import("../../serializer.zig").CommandSerializer; 62 | 63 | var correctBuf: [1000]u8 = undefined; 64 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 65 | 66 | var testBuf: [1000]u8 = undefined; 67 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 68 | 69 | { 70 | { 71 | correctMsg.end = 0; 72 | testMsg.end = 0; 73 | 74 | try serializer.serializeCommand( 75 | &testMsg, 76 | SRANDMEMBER.init("s", .one), 77 | ); 78 | try serializer.serializeCommand( 79 | &correctMsg, 80 | .{ "SRANDMEMBER", "s" }, 81 | ); 82 | 83 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 84 | try std.testing.expectEqualSlices( 85 | u8, 86 | correctMsg.buffered(), 87 | testMsg.buffered(), 88 | ); 89 | } 90 | 91 | { 92 | correctMsg.end = 0; 93 | testMsg.end = 0; 94 | 95 | try serializer.serializeCommand( 96 | &testMsg, 97 | SRANDMEMBER.init("s", SRANDMEMBER.Count{ .Count = 5 }), 98 | ); 99 | try serializer.serializeCommand( 100 | &correctMsg, 101 | .{ "SRANDMEMBER", "s", 5 }, 102 | ); 103 | 104 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 105 | try std.testing.expectEqualSlices( 106 | u8, 107 | correctMsg.buffered(), 108 | testMsg.buffered(), 109 | ); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/parser/void.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const fmt = std.fmt; 4 | 5 | /// A parser that consumes one full reply and discards it. It's written as a 6 | /// dedicated parser because it doesn't require recursion to consume the right 7 | /// amount of input and, given the fact that the type doesn't "peel away", 8 | /// recursion would look unbounded to the type system. 9 | /// It can also be used to consume just one attribute element by claiming to 10 | /// have found a map instead. This trick is used by the root parser in the 11 | /// initial setup of both `parse` and `parseAlloc`. 12 | pub const VoidParser = struct { 13 | pub fn discardOne(tag: u8, r: *Reader) !void { 14 | // When we start, we have one item to consume. 15 | // As we inspect it, we might discover that it's a container, requiring 16 | // us to increase our items count. 17 | var err_found = false; 18 | 19 | var current_tag = tag; 20 | var items_left: usize = 1; 21 | while (items_left > 0) { 22 | items_left -= 1; 23 | switch (current_tag) { 24 | else => std.debug.panic("Found `{c}` in the *VOID* parser's switch." ++ 25 | " Probably a bug in a type that implements `Redis.Parser`.", .{current_tag}), 26 | '_' => try r.discardAll(2), // `_\r\n` 27 | '#' => try r.discardAll(3), // `#t\r\n`, `#t\r\n` 28 | '$', '=', '!' => { 29 | // Lenght-prefixed string 30 | if (current_tag == '!') { 31 | err_found = true; 32 | } 33 | 34 | const digits = try r.takeSentinel('\r'); 35 | const size = try fmt.parseInt(usize, digits, 10); 36 | try r.discardAll(1 + size + 2); // \n, item, \r\n 37 | }, 38 | ':', ',', '+', '-' => { 39 | // Simple element with final `\r\n` 40 | if (current_tag == '-') { 41 | err_found = true; 42 | } 43 | _ = try r.discardDelimiterInclusive('\n'); 44 | }, 45 | '|' => { 46 | // Attributes are metadata that precedes a proper reply 47 | // item and do not count towards the original 48 | // `itemsToConsume` count. Consume the attribute element 49 | // without counting the current item as consumed. 50 | 51 | const digits = try r.takeSentinel('\r'); 52 | var size = try fmt.parseInt(usize, digits, 10); 53 | try r.discardAll(1); 54 | size *= 2; 55 | 56 | // Add all the new items to the pile that needs to be 57 | // consumed, plus the one that we did not consume this 58 | // loop. 59 | items_left += size + 1; 60 | }, 61 | '*', '%' => { 62 | // Lists, Maps 63 | const digits = try r.takeSentinel('\r'); 64 | var size = try fmt.parseInt(usize, digits, 10); 65 | try r.discardAll(1); 66 | 67 | // Maps advertize the number of field-value pairs. 68 | if (current_tag == '%') size *= 2; 69 | items_left += size; 70 | }, 71 | } 72 | 73 | // If we still have items to consume, read the next tag. 74 | if (items_left > 0) current_tag = try r.takeByte(); 75 | } 76 | if (err_found) return error.GotErrorReply; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/commands/sets/spop.zig: -------------------------------------------------------------------------------- 1 | // SPOP key [count] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SPOP = struct { 7 | key: []const u8, 8 | count: Count, 9 | 10 | pub const Count = union(enum) { 11 | one, 12 | Count: usize, 13 | 14 | pub const RedisArguments = struct { 15 | pub fn count(self: Count) usize { 16 | return switch (self) { 17 | .one => 0, 18 | .Count => 1, 19 | }; 20 | } 21 | 22 | pub fn serialize( 23 | self: Count, 24 | comptime root_serializer: type, 25 | w: *Writer, 26 | ) !void { 27 | switch (self) { 28 | .one => {}, 29 | .Count => |c| { 30 | try root_serializer.serializeArgument(w, usize, c); 31 | }, 32 | } 33 | } 34 | }; 35 | }; 36 | 37 | /// Instantiates a new SPOP command. 38 | pub fn init(key: []const u8, count: Count) SPOP { 39 | // TODO: support std.hashmap used as a set! 40 | return .{ .key = key, .count = count }; 41 | } 42 | 43 | /// Validates if the command is syntactically correct. 44 | pub fn validate(_: SPOP) !void {} 45 | 46 | pub const RedisCommand = struct { 47 | pub fn serialize( 48 | self: SPOP, 49 | comptime root_serializer: type, 50 | w: *Writer, 51 | ) !void { 52 | return root_serializer.serializeCommand( 53 | w, 54 | .{ "SPOP", self.key, self.count }, 55 | ); 56 | } 57 | }; 58 | }; 59 | 60 | test "basic usage" { 61 | const cmd = SPOP.init("myset", .one); 62 | try cmd.validate(); 63 | 64 | const cmd1 = SPOP.init("myset", SPOP.Count{ .Count = 5 }); 65 | try cmd1.validate(); 66 | } 67 | 68 | test "serializer" { 69 | const serializer = @import("../../serializer.zig").CommandSerializer; 70 | 71 | var correctBuf: [1000]u8 = undefined; 72 | var correctMsg: Writer = .fixed(correctBuf[0..]); 73 | 74 | var testBuf: [1000]u8 = undefined; 75 | var testMsg: std.Io.Writer = .fixed(testBuf[0..]); 76 | 77 | { 78 | { 79 | correctMsg.end = 0; 80 | testMsg.end = 0; 81 | 82 | try serializer.serializeCommand( 83 | &testMsg, 84 | SPOP.init("s", .one), 85 | ); 86 | try serializer.serializeCommand( 87 | &correctMsg, 88 | .{ "SPOP", "s" }, 89 | ); 90 | 91 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 92 | try std.testing.expectEqualSlices( 93 | u8, 94 | correctMsg.buffered(), 95 | testMsg.buffered(), 96 | ); 97 | } 98 | 99 | { 100 | correctMsg.end = 0; 101 | testMsg.end = 0; 102 | 103 | try serializer.serializeCommand( 104 | &testMsg, 105 | SPOP.init("s", SPOP.Count{ .Count = 5 }), 106 | ); 107 | try serializer.serializeCommand( 108 | &correctMsg, 109 | .{ "SPOP", "s", 5 }, 110 | ); 111 | 112 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 113 | try std.testing.expectEqualSlices( 114 | u8, 115 | correctMsg.buffered(), 116 | testMsg.buffered(), 117 | ); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/commands/geo/georadiusbymember.zig: -------------------------------------------------------------------------------- 1 | // GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key] 2 | 3 | const Unit = @import("./_utils.zig").Unit; 4 | 5 | pub const GEORADIUSBYMEMBER = struct { 6 | key: []const u8, 7 | member: []const u8, 8 | radius: f64, 9 | unit: Unit, 10 | withcoord: bool, 11 | withdist: bool, 12 | withhash: bool, 13 | 14 | count: ?u64, 15 | ordering: ?Ordering, 16 | store: ?[]const u8, 17 | storedist: ?[]const u8, 18 | 19 | pub const Ordering = enum { 20 | Asc, 21 | Desc, 22 | }; 23 | 24 | pub fn init( 25 | key: []const u8, 26 | member: []const u8, 27 | radius: f64, 28 | unit: Unit, 29 | withcoord: bool, 30 | withdist: bool, 31 | withhash: bool, 32 | count: ?u64, 33 | ordering: ?Ordering, 34 | store: ?[]const u8, 35 | storedist: ?[]const u8, 36 | ) GEORADIUSBYMEMBER { 37 | return .{ 38 | .key = key, 39 | .member = member, 40 | .radius = radius, 41 | .unit = unit, 42 | .withcoord = withcoord, 43 | .withdist = withdist, 44 | .withhash = withhash, 45 | .count = count, 46 | .ordering = ordering, 47 | .store = store, 48 | .storedist = storedist, 49 | }; 50 | } 51 | 52 | pub fn validate(_: GEORADIUSBYMEMBER) !void {} 53 | 54 | pub const RedisCommand = struct { 55 | pub fn serialize(self: GEORADIUSBYMEMBER, comptime rootSerializer: type, msg: anytype) !void { 56 | return rootSerializer.serializeCommand(msg, .{ 57 | "GEORADIUSBYMEMBER", 58 | self.key, 59 | self.member, 60 | self.radius, 61 | self.unit, 62 | self.withcoord, 63 | self.withdist, 64 | self.withhash, 65 | self, 66 | }); 67 | } 68 | }; 69 | 70 | pub const RedisArguments = struct { 71 | pub fn count(self: GEORADIUSBYMEMBER) usize { 72 | var total = 0; 73 | if (self.count) |_| total += 2; 74 | if (self.ordering) |_| total += 1; 75 | if (self.store) |_| total += 2; 76 | if (self.storedist) |_| total += 2; 77 | return total; 78 | } 79 | 80 | pub fn serialize(self: GEORADIUSBYMEMBER, comptime rootSerializer: type, msg: anytype) !void { 81 | if (self.count) |c| { 82 | try rootSerializer.serializeArgument(msg, []const u8, "COUNT"); 83 | try rootSerializer.serializeArgument(msg, u64, c); 84 | } 85 | 86 | if (self.ordering) |o| { 87 | const ord = switch (o) { 88 | .Asc => "ASC", 89 | .Desc => "DESC", 90 | }; 91 | try rootSerializer.serializeArgument(msg, []const u8, ord); 92 | } 93 | 94 | if (self.store) |s| { 95 | try rootSerializer.serializeArgument(msg, []const u8, "STORE"); 96 | try rootSerializer.serializeArgument(msg, []const u8, s); 97 | } 98 | 99 | if (self.storedist) |sd| { 100 | try rootSerializer.serializeArgument(msg, []const u8, "STOREDIST"); 101 | try rootSerializer.serializeArgument(msg, []const u8, sd); 102 | } 103 | } 104 | }; 105 | }; 106 | 107 | test "basic usage" { 108 | const cmd = GEORADIUSBYMEMBER.init("mykey", "mymember", 20, .meters, false, false, false, 0, .Asc, null, null); 109 | try cmd.validate(); 110 | } 111 | -------------------------------------------------------------------------------- /src/commands/geo/georadius.zig: -------------------------------------------------------------------------------- 1 | // GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key] 2 | 3 | const Unit = @import("./_utils.zig").Unit; 4 | 5 | pub const GEORADIUS = struct { 6 | key: []const u8, 7 | longitude: f64, 8 | latitude: f64, 9 | radius: f64, 10 | unit: Unit, 11 | withcoord: bool, 12 | withdist: bool, 13 | withhash: bool, 14 | 15 | count: ?u64, 16 | ordering: ?Ordering, 17 | store: ?[]const u8, 18 | storedist: ?[]const u8, 19 | 20 | pub const Ordering = enum { 21 | Asc, 22 | Desc, 23 | }; 24 | 25 | pub fn init( 26 | key: []const u8, 27 | longitude: f64, 28 | latitude: f64, 29 | radius: f64, 30 | unit: Unit, 31 | withcoord: bool, 32 | withdist: bool, 33 | withhash: bool, 34 | count: ?u64, 35 | ordering: ?Ordering, 36 | store: ?[]const u8, 37 | storedist: ?[]const u8, 38 | ) GEORADIUS { 39 | return .{ 40 | .key = key, 41 | .longitude = longitude, 42 | .latitude = latitude, 43 | .radius = radius, 44 | .unit = unit, 45 | .withcoord = withcoord, 46 | .withdist = withdist, 47 | .withhash = withhash, 48 | .count = count, 49 | .ordering = ordering, 50 | .store = store, 51 | .storedist = storedist, 52 | }; 53 | } 54 | 55 | pub fn validate(_: GEORADIUS) !void {} 56 | 57 | pub const RedisCommand = struct { 58 | pub fn serialize(self: GEORADIUS, comptime rootSerializer: type, msg: anytype) !void { 59 | return rootSerializer.serializeCommand(msg, .{ 60 | "GEORADIUS", 61 | self.key, 62 | self.longitude, 63 | self.latitude, 64 | self.radius, 65 | self.unit, 66 | self.withcoord, 67 | self.withdist, 68 | self.withhash, 69 | self, 70 | }); 71 | } 72 | }; 73 | 74 | pub const RedisArguments = struct { 75 | pub fn count(self: GEORADIUS) usize { 76 | var total = 0; 77 | if (self.count) |_| total += 2; 78 | if (self.ordering) |_| total += 1; 79 | if (self.store) |_| total += 2; 80 | if (self.storedist) |_| total += 2; 81 | return total; 82 | } 83 | 84 | pub fn serialize(self: GEORADIUS, comptime rootSerializer: type, msg: anytype) !void { 85 | if (self.count) |c| { 86 | try rootSerializer.serializeArgument(msg, []const u8, "COUNT"); 87 | try rootSerializer.serializeArgument(msg, u64, c); 88 | } 89 | 90 | if (self.ordering) |o| { 91 | const ord = switch (o) { 92 | .Asc => "ASC", 93 | .Desc => "DESC", 94 | }; 95 | try rootSerializer.serializeArgument(msg, []const u8, ord); 96 | } 97 | 98 | if (self.store) |s| { 99 | try rootSerializer.serializeArgument(msg, []const u8, "STORE"); 100 | try rootSerializer.serializeArgument(msg, []const u8, s); 101 | } 102 | 103 | if (self.storedist) |sd| { 104 | try rootSerializer.serializeArgument(msg, []const u8, "STOREDIST"); 105 | try rootSerializer.serializeArgument(msg, []const u8, sd); 106 | } 107 | } 108 | }; 109 | }; 110 | 111 | test "basic usage" { 112 | const cmd = GEORADIUS.init("mykey", 0.1, 0.2, 20, .meters, true, true, true, 10, null, null, null); 113 | try cmd.validate(); 114 | } 115 | -------------------------------------------------------------------------------- /src/traits.zig: -------------------------------------------------------------------------------- 1 | /// A type that knows how to decode itself form a RESP3 stream. 2 | /// It's expected to implement three functions: 3 | /// ``` 4 | /// fn parse(tag: u8, comptime rootParser: type, msg: var) !Self 5 | /// fn parseAlloc(tag: u8, comptime rootParser: type, allocator: Allocator, msg: var) !Self 6 | /// fn destroy(self: Self, comptime rootParser: type, allocator: Allocator) void 7 | /// ``` 8 | /// `rootParser` is a reference to the RESP3Parser, which contains the main 9 | /// parsing logic. It's passed to the type in order to allow it to recursively 10 | /// reuse the logic already implemented. For example, the KV type uses it to 11 | /// parse both `key` and `value` fields. 12 | /// 13 | /// `msg` is an InStream attached to a Redis connection. 14 | /// 15 | /// In case of failure the parsing function is NOT required to consume the 16 | /// proper amount of stream data. It's expected that decoding errors always 17 | /// result in a broken connection state. 18 | pub fn isParserType(comptime T: type) bool { 19 | const tid = @typeInfo(T); 20 | if ((tid == .@"struct" or tid == .@"enum" or tid == .@"union") and 21 | @hasDecl(T, "Redis") and @hasDecl(T.Redis, "Parser")) 22 | { 23 | if (!@hasDecl(T.Redis.Parser, "parse")) 24 | @compileError( 25 | \\`Redis.Parser` trait requires implementing: 26 | \\ fn parse(tag: u8, comptime rootParser: type, msg: var) !Self 27 | \\ 28 | ); 29 | 30 | if (!@hasDecl(T.Redis.Parser, "parseAlloc")) 31 | @compileError( 32 | \\`Redis.Parser` trait requires implementing: 33 | \\ fn parseAlloc(tag: u8, comptime rootParser: type, allocator: Allocator, msg: var) !Self 34 | \\ 35 | ); 36 | 37 | if (!@hasDecl(T.Redis.Parser, "destroy")) 38 | @compileError( 39 | \\`Redis.Parser` trait requires implementing: 40 | \\ fn destroy(self: *Self, comptime rootParser: type, allocator: Allocator) void 41 | \\ 42 | ); 43 | 44 | return true; 45 | } 46 | return false; 47 | } 48 | 49 | /// A type that wants access to attributes because intends to decode them. 50 | /// When the declaration is missing or returns false, attributes are discarded 51 | /// from the stream automatically by the main parser. 52 | pub fn handlesAttributes(comptime T: type) bool { 53 | if (comptime isParserType(T)) { 54 | if (@hasDecl(T.Redis.Parser, "HandlesAttributes")) { 55 | return T.Redis.Parser.HandlesAttributes; 56 | } 57 | } 58 | return false; 59 | } 60 | 61 | /// A type that doesn't want to be wrapped directly in an optional because 62 | /// it would have ill-formed / unclear semantics. An example of this are 63 | /// types that read attributes. For those types this trait defaults to `true` 64 | pub fn noOptionalWrapper(comptime T: type) bool { 65 | if (comptime isParserType(T)) { 66 | if (@hasDecl(T.Redis.Parser, "NoOptionalWrapper")) { 67 | return T.Redis.Parser.NoOptionalWrapper; 68 | } else { 69 | if (@hasDecl(T.Redis.Parser, "HandlesAttributes")) { 70 | return T.Redis.Parser.HandlesAttributes; 71 | } 72 | } 73 | } 74 | return false; 75 | } 76 | 77 | /// A type that knows how to serialize itself as one or more arguments to a 78 | /// Redis command. The RESP3 protocol is used in a asymmetrical way by Redis, 79 | /// so this is NOT the inverse operation of parsing. As an example, a struct 80 | /// might implement decoding from a RESP Map, but the correct way of 81 | /// serializing itself would be as a FLAT sequence of field-value pairs, to be 82 | /// used with XADD or HMSET: 83 | /// HMSET mystruct field1 val1 field2 val2 ... 84 | pub fn isArguments(comptime T: type) bool { 85 | const tid = @typeInfo(T); 86 | return (tid == .@"struct" or tid == .@"enum" or tid == .@"union") and @hasDecl(T, "RedisArguments"); 87 | } 88 | 89 | pub fn isCommand(comptime T: type) bool { 90 | const tid = @typeInfo(T); 91 | return (tid == .@"struct" or tid == .@"enum" or tid == .@"union") and @hasDecl(T, "RedisCommand"); 92 | } 93 | // test "trait error message" { 94 | // const T = struct { 95 | // pub const Redis = struct { 96 | // pub const Parser = struct {}; 97 | // }; 98 | // }; 99 | 100 | // _ = isParserType(T); 101 | // } 102 | 103 | test "docs" { 104 | @import("std").testing.refAllDecls(@This()); 105 | } 106 | -------------------------------------------------------------------------------- /src/commands/sets/sscan.zig: -------------------------------------------------------------------------------- 1 | // SSCAN key cursor [MATCH pattern] [COUNT count] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | pub const SSCAN = struct { 7 | key: []const u8, 8 | cursor: []const u8, 9 | pattern: Pattern, 10 | count: Count, 11 | 12 | pub const Pattern = union(enum) { 13 | NoPattern, 14 | Pattern: []const u8, 15 | 16 | pub const RedisArguments = struct { 17 | pub fn count(self: Pattern) usize { 18 | return switch (self) { 19 | .NoPattern => 0, 20 | .Pattern => 2, 21 | }; 22 | } 23 | 24 | pub fn serialize( 25 | self: Pattern, 26 | comptime root: type, 27 | w: *Writer, 28 | ) !void { 29 | switch (self) { 30 | .NoPattern => {}, 31 | .Pattern => |p| { 32 | try root.serializeArgument(w, []const u8, "MATCH"); 33 | try root.serializeArgument(w, []const u8, p); 34 | }, 35 | } 36 | } 37 | }; 38 | }; 39 | 40 | pub const Count = union(enum) { 41 | NoCount, 42 | Count: usize, 43 | 44 | pub const RedisArguments = struct { 45 | pub fn count(self: Count) usize { 46 | return switch (self) { 47 | .NoCount => 0, 48 | .Count => 2, 49 | }; 50 | } 51 | 52 | pub fn serialize( 53 | self: Count, 54 | comptime root: type, 55 | w: *Writer, 56 | ) !void { 57 | switch (self) { 58 | .NoCount => {}, 59 | .Count => |c| { 60 | try root.serializeArgument(w, []const u8, "COUNT"); 61 | try root.serializeArgument(w, usize, c); 62 | }, 63 | } 64 | } 65 | }; 66 | }; 67 | 68 | /// Instantiates a new SPOP command. 69 | pub fn init(key: []const u8, cursor: []const u8, pattern: Pattern, count: Count) SSCAN { 70 | // TODO: support std.hashmap used as a set! 71 | return .{ .key = key, .cursor = cursor, .pattern = pattern, .count = count }; 72 | } 73 | 74 | /// Validates if the command is syntactically correct. 75 | pub fn validate(_: SSCAN) !void {} 76 | 77 | pub const RedisCommand = struct { 78 | pub fn serialize( 79 | self: SSCAN, 80 | comptime root: type, 81 | w: *Writer, 82 | ) !void { 83 | return root.serializeCommand(w, .{ 84 | "SSCAN", 85 | self.key, 86 | self.cursor, 87 | self.pattern, 88 | self.count, 89 | }); 90 | } 91 | }; 92 | }; 93 | 94 | test "basic usage" { 95 | const cmd = SSCAN.init("myset", "0", .NoPattern, .NoCount); 96 | try cmd.validate(); 97 | 98 | const cmd1 = SSCAN.init("myset", "0", SSCAN.Pattern{ .Pattern = "zig_*" }, SSCAN.Count{ .Count = 5 }); 99 | try cmd1.validate(); 100 | } 101 | 102 | test "serializer" { 103 | const serializer = @import("../../serializer.zig").CommandSerializer; 104 | 105 | var correctBuf: [1000]u8 = undefined; 106 | var correctMsg: Writer = .fixed(correctBuf[0..]); 107 | 108 | var testBuf: [1000]u8 = undefined; 109 | var testMsg: Writer = .fixed(testBuf[0..]); 110 | 111 | { 112 | { 113 | correctMsg.end = 0; 114 | testMsg.end = 0; 115 | 116 | try serializer.serializeCommand( 117 | &testMsg, 118 | SSCAN.init("myset", "0", .NoPattern, .NoCount), 119 | ); 120 | try serializer.serializeCommand( 121 | &correctMsg, 122 | .{ "SSCAN", "myset", "0" }, 123 | ); 124 | 125 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 126 | try std.testing.expectEqualSlices(u8, correctMsg.buffered(), testMsg.buffered()); 127 | } 128 | 129 | { 130 | correctMsg.end = 0; 131 | testMsg.end = 0; 132 | 133 | try serializer.serializeCommand( 134 | &testMsg, 135 | SSCAN.init("myset", "0", SSCAN.Pattern{ .Pattern = "zig_*" }, SSCAN.Count{ .Count = 5 }), 136 | ); 137 | try serializer.serializeCommand( 138 | &correctMsg, 139 | .{ "SSCAN", "myset", 0, "MATCH", "zig_*", "COUNT", 5 }, 140 | ); 141 | 142 | // std.debug.warn("\n{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 143 | try std.testing.expectEqualSlices( 144 | u8, 145 | correctMsg.buffered(), 146 | testMsg.buffered(), 147 | ); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/commands/streams/xread.zig: -------------------------------------------------------------------------------- 1 | const utils = @import("./_utils.zig"); 2 | 3 | /// XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [id ...] 4 | pub const XREAD = struct { 5 | count: Count = .NoCount, 6 | block: Block = .NoBlock, 7 | streams: []const []const u8, 8 | ids: []const []const u8, 9 | 10 | /// Instantiates a new XREAD command. 11 | pub fn init(count: Count, block: Block, streams: []const []const u8, ids: []const []const u8) XREAD { 12 | return .{ 13 | .count = count, 14 | .block = block, 15 | .streams = streams, 16 | .ids = ids, 17 | }; 18 | } 19 | 20 | /// Validates if the command is syntactically correct. 21 | pub fn validate(self: XREAD) !void { 22 | 23 | // Zero means blocking forever, use `.Forever` in such case. 24 | switch (self.block) { 25 | else => {}, 26 | .Milliseconds => |m| if (m == 0) return error.ZeroMeansBlockingForever, 27 | } 28 | 29 | // Check if the number of parameters is correct 30 | if (self.streams.len == 0) return error.StreamsArrayIsEmpty; 31 | if (self.streams.len != self.ids.len) return error.StreamsAndIDsLenMismatch; 32 | 33 | // Check the individual stream/id entries 34 | var i: usize = 0; 35 | while (i < self.streams.len) : (i += 1) { 36 | if (self.streams[i].len == 0) return error.EmptyKeyName; 37 | if (!utils.isValidStreamID(.XREAD, self.ids[i])) return error.InvalidID; 38 | } 39 | } 40 | 41 | pub const RedisCommand = struct { 42 | pub fn serialize(self: XREAD, comptime rootSerializer: type, msg: anytype) !void { 43 | return rootSerializer.serializeCommand(msg, .{ 44 | "XREAD", 45 | self.count, 46 | self.block, 47 | "STREAMS", 48 | self.streams, 49 | self.ids, 50 | }); 51 | } 52 | }; 53 | 54 | pub const Count = union(enum) { 55 | NoCount, 56 | Count: usize, 57 | 58 | pub const RedisArguments = struct { 59 | pub fn count(self: Count) usize { 60 | return switch (self) { 61 | .NoCount => 0, 62 | .Count => 2, 63 | }; 64 | } 65 | 66 | pub fn serialize(self: Count, comptime rootSerializer: type, msg: anytype) !void { 67 | switch (self) { 68 | .NoCount => {}, 69 | .Count => |c| { 70 | try rootSerializer.serializeArgument(msg, []const u8, "COUNT"); 71 | try rootSerializer.serializeArgument(msg, u64, c); 72 | }, 73 | } 74 | } 75 | }; 76 | }; 77 | 78 | pub const Block = union(enum) { 79 | NoBlock, 80 | Forever, 81 | Milliseconds: usize, 82 | 83 | pub const RedisArguments = struct { 84 | pub fn count(self: Block) usize { 85 | return switch (self) { 86 | .NoBlock => 0, 87 | else => 2, 88 | }; 89 | } 90 | 91 | pub fn serialize(self: Block, comptime rootSerializer: type, msg: anytype) !void { 92 | switch (self) { 93 | .NoBlock => {}, 94 | .Forever => { 95 | try rootSerializer.serializeArgument(msg, []const u8, "BLOCK"); 96 | try rootSerializer.serializeArgument(msg, u64, 0); 97 | }, 98 | .Milliseconds => |m| { 99 | try rootSerializer.serializeArgument(msg, []const u8, "BLOCK"); 100 | try rootSerializer.serializeArgument(msg, u64, m); 101 | }, 102 | } 103 | } 104 | }; 105 | }; 106 | }; 107 | 108 | test "basic usage" { 109 | const cmd = XREAD.init( 110 | .NoCount, 111 | .NoBlock, 112 | &[_][]const u8{ "stream1", "stream2" }, 113 | &[_][]const u8{ "123-123", "$" }, 114 | ); 115 | 116 | try cmd.validate(); 117 | } 118 | 119 | test "serializer" { 120 | const std = @import("std"); 121 | const serializer = @import("../../serializer.zig").CommandSerializer; 122 | 123 | var correctBuf: [1000]u8 = undefined; 124 | var correctMsg = std.Io.Writer.fixed(correctBuf[0..]); 125 | 126 | var testBuf: [1000]u8 = undefined; 127 | var testMsg = std.Io.Writer.fixed(testBuf[0..]); 128 | 129 | { 130 | correctMsg.end = 0; 131 | testMsg.end = 0; 132 | 133 | try serializer.serializeCommand( 134 | &testMsg, 135 | XREAD.init( 136 | .NoCount, 137 | .NoBlock, 138 | &[_][]const u8{ "key1", "key2" }, 139 | &[_][]const u8{ "$", "$" }, 140 | ), 141 | ); 142 | try serializer.serializeCommand( 143 | &correctMsg, 144 | .{ "XREAD", "STREAMS", "key1", "key2", "$", "$" }, 145 | ); 146 | 147 | try std.testing.expectEqualSlices( 148 | u8, 149 | correctMsg.buffered(), 150 | testMsg.buffered(), 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/commands/hashes/hmget.zig: -------------------------------------------------------------------------------- 1 | // HMGET key field [field ...] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | const common = @import("../_common_utils.zig"); 7 | const FV = common.FV; 8 | 9 | pub const HMGET = struct { 10 | key: []const u8, 11 | fields: []const []const u8, 12 | 13 | /// Instantiates a new HMGET command. 14 | pub fn init(key: []const u8, fields: []const []const u8) HMGET { 15 | return .{ .key = key, .fields = fields }; 16 | } 17 | 18 | /// Validates if the command is syntactically correct. 19 | pub fn validate(self: HMGET) !void { 20 | if (self.key.len == 0) return error.EmptyKeyName; 21 | if (self.fields.len == 0) return error.FieldsArrayIsEmpty; 22 | 23 | // TODO: how the hell do I check for dups without an allocator? 24 | var i: usize = 0; 25 | while (i < self.fields.len) : (i += 1) { 26 | if (self.fields[i].len == 0) return error.EmptyFieldName; 27 | } 28 | } 29 | 30 | // This reassignment is necessary to avoid having two definitions of 31 | // RedisCommand in the same scope (it causes a shadowing error). 32 | pub const forStruct = _forStruct; 33 | 34 | pub const RedisCommand = struct { 35 | pub fn serialize(self: HMGET, comptime rootSerializer: type, msg: anytype) !void { 36 | return rootSerializer.serializeCommand(msg, .{ "HMGET", self.key, self.fields }); 37 | } 38 | }; 39 | }; 40 | 41 | fn _forStruct(comptime T: type) type { 42 | // TODO: there is some duplicated code with xread. Values should be a dedicated generic type. 43 | if (@typeInfo(T) != .@"struct") @compileError("Only Struct types allowed."); 44 | return struct { 45 | key: []const u8, 46 | 47 | const Self = @This(); 48 | pub fn init(key: []const u8) Self { 49 | return .{ .key = key }; 50 | } 51 | 52 | /// Validates if the command is syntactically correct. 53 | pub fn validate(self: Self) !void { 54 | if (self.key.len == 0) return error.EmptyKeyName; 55 | } 56 | 57 | pub const RedisCommand = struct { 58 | pub fn serialize(self: Self, comptime rootSerializer: type, msg: anytype) !void { 59 | return rootSerializer.serializeCommand(msg, .{ 60 | "HMGET", 61 | self.key, 62 | 63 | // Dirty trick to control struct serialization :3 64 | self, 65 | }); 66 | } 67 | }; 68 | 69 | // We are marking ouserlves also as an argument to manage struct serialization. 70 | pub const RedisArguments = struct { 71 | pub fn count(_: Self) usize { 72 | return comptime std.meta.fields(T).len; 73 | } 74 | 75 | pub fn serialize(_: Self, comptime rootSerializer: type, msg: anytype) !void { 76 | inline for (std.meta.fields(T)) |field| { 77 | try rootSerializer.serializeArgument(msg, []const u8, field.name); 78 | } 79 | } 80 | }; 81 | }; 82 | } 83 | 84 | test "basic usage" { 85 | const cmd = HMGET.init("mykey", &[_][]const u8{ "field1", "field2" }); 86 | try cmd.validate(); 87 | 88 | const ExampleStruct = struct { 89 | banana: usize, 90 | id: []const u8, 91 | }; 92 | 93 | const cmd1 = HMGET.forStruct(ExampleStruct).init("mykey"); 94 | try cmd1.validate(); 95 | } 96 | 97 | test "serializer" { 98 | const serializer = @import("../../serializer.zig").CommandSerializer; 99 | 100 | var correctBuf: [1000]u8 = undefined; 101 | var correctMsg: Writer = .fixed(correctBuf[0..]); 102 | 103 | var testBuf: [1000]u8 = undefined; 104 | var testMsg: Writer = .fixed(testBuf[0..]); 105 | 106 | { 107 | { 108 | correctMsg.end = 0; 109 | testMsg.end = 0; 110 | 111 | try serializer.serializeCommand( 112 | &testMsg, 113 | HMGET.init("k1", &[_][]const u8{"f1"}), 114 | ); 115 | try serializer.serializeCommand( 116 | &correctMsg, 117 | .{ "HMGET", "k1", "f1" }, 118 | ); 119 | 120 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 121 | try std.testing.expectEqualSlices( 122 | u8, 123 | correctMsg.buffered(), 124 | testMsg.buffered(), 125 | ); 126 | } 127 | 128 | { 129 | correctMsg.end = 0; 130 | testMsg.end = 0; 131 | 132 | const MyStruct = struct { 133 | field1: []const u8, 134 | field2: u8, 135 | field3: usize, 136 | }; 137 | 138 | const MyHMGET = HMGET.forStruct(MyStruct); 139 | 140 | try serializer.serializeCommand( 141 | &testMsg, 142 | MyHMGET.init("k1"), 143 | ); 144 | try serializer.serializeCommand( 145 | &correctMsg, 146 | .{ "HMGET", "k1", "field1", "field2", "field3" }, 147 | ); 148 | 149 | try std.testing.expectEqualSlices( 150 | u8, 151 | correctMsg.buffered(), 152 | testMsg.buffered(), 153 | ); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/types/attributes.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const Allocator = std.mem.Allocator; 4 | const testing = std.testing; 5 | 6 | const FixBuf = @import("./fixbuf.zig").FixBuf; 7 | const DynamicReply = @import("./reply.zig").DynamicReply; 8 | 9 | /// A generic type that can capture attributes from a Redis reply. 10 | pub fn WithAttribs(comptime T: type) type { 11 | return struct { 12 | /// Attributes are stored as an array of key-value pairs. 13 | /// Each element of a pair is a DynamicReply. 14 | attribs: [][2]DynamicReply, 15 | data: T, 16 | 17 | const Self = @This(); 18 | pub const Redis = struct { 19 | pub const Parser = struct { 20 | pub const HandlesAttributes = true; 21 | 22 | pub fn parse(_: u8, comptime _: type, _: anytype) !Self { 23 | @compileError("WithAttribs requires an allocator. Use `sendAlloc`."); 24 | } 25 | 26 | pub fn destroy( 27 | self: Self, 28 | comptime rootParser: type, 29 | allocator: Allocator, 30 | ) void { 31 | rootParser.freeReply(self.attribs, allocator); 32 | rootParser.freeReply(self.data, allocator); 33 | } 34 | 35 | pub fn parseAlloc( 36 | tag: u8, 37 | comptime rootParser: type, 38 | allocator: Allocator, 39 | msg: anytype, 40 | ) !Self { 41 | var itemTag = tag; 42 | 43 | var res: Self = undefined; 44 | if (itemTag == '|') { 45 | // Here we lie to the root parser and claim we encountered a map type, 46 | // otherwise the parser would also try to parse the actual reply along 47 | // side the attribute. 48 | 49 | // No error catching is done because DynamicReply parses correctly 50 | // both errors and nil values, and it can't incur in a DecodingError. 51 | res.attribs = try rootParser.parseAllocFromTag( 52 | [][2]DynamicReply, 53 | '%', 54 | allocator, 55 | msg, 56 | ); 57 | itemTag = try msg.takeByte(); 58 | } else { 59 | res.attribs = &[0][2]DynamicReply{}; 60 | } 61 | 62 | res.data = try rootParser.parseAllocFromTag(T, itemTag, allocator, msg); 63 | return res; 64 | } 65 | }; 66 | }; 67 | }; 68 | } 69 | 70 | test "WithAttribs" { 71 | const parser = @import("../parser.zig").RESP3Parser; 72 | const allocator = std.heap.page_allocator; 73 | 74 | var cplx_set = MakeComplexListWithAttributes(); 75 | const res = try parser.parseAlloc( 76 | WithAttribs([2]WithAttribs([]WithAttribs(i64))), 77 | allocator, 78 | &cplx_set, 79 | ); 80 | try testing.expectEqual(@as(usize, 2), res.attribs.len); 81 | try testing.expectEqualSlices(u8, "Ciao", res.attribs[0][0].data.String.string); 82 | try testing.expectEqualSlices(u8, "World", res.attribs[0][1].data.String.string); 83 | try testing.expectEqualSlices(u8, "Peach", res.attribs[1][0].data.String.string); 84 | try testing.expectEqual(@as(f64, 9.99), res.attribs[1][1].data.Double); 85 | 86 | try testing.expectEqual(@as(usize, 0), res.data[0].data[0].attribs.len); 87 | try testing.expectEqual(@as(i64, 20), res.data[0].data[0].data); 88 | 89 | try testing.expectEqual(@as(usize, 1), res.data[0].data[1].attribs.len); 90 | try testing.expectEqualSlices(u8, "ttl", res.data[0].data[1].attribs[0][0].data.String.string); 91 | try testing.expectEqual(@as(i64, 128), res.data[0].data[1].attribs[0][1].data.Number); 92 | try testing.expectEqual(@as(i64, 100), res.data[0].data[1].data); 93 | 94 | try testing.expectEqual(@as(usize, 0), res.data[1].attribs.len); 95 | try testing.expectEqual(@as(usize, 1), res.data[1].data[0].attribs.len); 96 | try testing.expectEqualSlices(u8, "Banana", res.data[1].data[0].attribs[0][0].data.String.string); 97 | try testing.expectEqual(true, res.data[1].data[0].attribs[0][1].data.Bool); 98 | try testing.expectEqual(@as(i64, 123), res.data[1].data[0].data); 99 | 100 | try testing.expectEqual(@as(usize, 0), res.data[1].data[1].attribs.len); 101 | try testing.expectEqual(@as(i64, 99), res.data[1].data[1].data); 102 | } 103 | // zig fmt: off 104 | fn MakeComplexListWithAttributes() Reader { 105 | return std.Io.Reader.fixed(( 106 | "|2\r\n" ++ 107 | "+Ciao\r\n" ++ 108 | "+World\r\n" ++ 109 | "+Peach\r\n" ++ 110 | ",9.99\r\n" ++ 111 | "*2\r\n" ++ 112 | "*2\r\n" ++ 113 | ":20\r\n" ++ 114 | "|1\r\n" ++ 115 | "+ttl\r\n" ++ 116 | ":128\r\n" ++ 117 | ":100\r\n" ++ 118 | "*2\r\n" ++ 119 | "|1\r\n" ++ 120 | "+Banana\r\n" ++ 121 | "#t\r\n" ++ 122 | ":123\r\n" ++ 123 | ":99\r\n" 124 | )[0..]); 125 | } 126 | // zig fmt: on 127 | 128 | test "docs" { 129 | @import("std").testing.refAllDecls(@This()); 130 | @import("std").testing.refAllDecls(WithAttribs(FixBuf(100))); 131 | @import("std").testing.refAllDecls(WithAttribs([]u8)); 132 | @import("std").testing.refAllDecls(WithAttribs(usize)); 133 | } 134 | -------------------------------------------------------------------------------- /src/parser/t_set.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const fmt = std.fmt; 4 | const testing = std.testing; 5 | 6 | /// Parses Redis Set values. 7 | pub const SetParser = struct { 8 | // TODO: prevent users from unmarshaling structs out of strings 9 | pub fn isSupported(comptime T: type) bool { 10 | return switch (@typeInfo(T)) { 11 | .array => true, 12 | else => false, 13 | }; 14 | } 15 | 16 | pub fn isSupportedAlloc(comptime T: type) bool { 17 | // HashMap 18 | if (@typeInfo(T) == .@"struct" and @hasDecl(T, "Entry")) { 19 | return void == std.meta.fieldInfo(T.Entry, .value_ptr).type; 20 | } 21 | 22 | return switch (@typeInfo(T)) { 23 | .pointer => true, 24 | else => isSupported(T), 25 | }; 26 | } 27 | 28 | pub fn parse(comptime T: type, comptime rootParser: type, r: *Reader) !T { 29 | return parseImpl(T, rootParser, .{}, r); 30 | } 31 | pub fn parseAlloc( 32 | comptime T: type, 33 | comptime rootParser: type, 34 | allocator: std.mem.Allocator, 35 | r: *Reader, 36 | ) !T { 37 | // HASHMAP 38 | if (@typeInfo(T) == .@"struct" and @hasDecl(T, "Entry")) { 39 | const isManaged = @typeInfo(@TypeOf(T.deinit)).@"fn".params.len == 1; 40 | 41 | // TODO: write real implementation 42 | var buf: [100]u8 = undefined; 43 | var end: usize = 0; 44 | for (&buf, 0..) |*elem, i| { 45 | const ch = try r.takeByte(); 46 | elem.* = ch; 47 | if (ch == '\r') { 48 | end = i; 49 | break; 50 | } 51 | } 52 | 53 | try r.discardAll(1); 54 | const size = try fmt.parseInt(usize, buf[0..end], 10); 55 | 56 | var hmap = T.init(allocator); 57 | errdefer { 58 | if (isManaged) { 59 | hmap.deinit(); 60 | } else { 61 | hmap.deinit(allocator.ptr); 62 | } 63 | } 64 | 65 | const KeyType = std.meta.fieldInfo(T.Entry, .key_ptr).type; 66 | 67 | var foundNil = false; 68 | var foundErr = false; 69 | var hashMapError = false; 70 | var i: usize = 0; 71 | while (i < size) : (i += 1) { 72 | if (foundNil or foundErr or hashMapError) { 73 | rootParser.parse(void, r) catch |err| switch (err) { 74 | error.GotErrorReply => { 75 | foundErr = true; 76 | }, 77 | else => return err, 78 | }; 79 | } else { 80 | const key = rootParser.parseAlloc(KeyType, allocator, r) catch |err| switch (err) { 81 | error.GotNilReply => { 82 | foundNil = true; 83 | continue; 84 | }, 85 | error.GotErrorReply => { 86 | foundErr = true; 87 | continue; 88 | }, 89 | else => return err, 90 | }; 91 | 92 | // If we got here then no error occurred and we can add the key. 93 | (if (isManaged) hmap.put(key.*, {}) else hmap.put(allocator.ptr, key.*, {})) catch { 94 | hashMapError = true; 95 | continue; 96 | }; 97 | } 98 | } 99 | 100 | if (foundErr) return error.GotErrorReply; 101 | if (foundNil) return error.GotNilReply; 102 | if (hashMapError) return error.DecodeError; // TODO: find a way to save and return the precise error? 103 | return hmap; 104 | } 105 | 106 | return parseImpl(T, rootParser, .{ .ptr = allocator }, r); 107 | } 108 | 109 | pub fn parseImpl(comptime T: type, comptime rootParser: type, allocator: anytype, r: *Reader) !T { 110 | // Indirectly delegate all cases to the list parser. 111 | 112 | // TODO: fix this. Delegating with the same top-level T looks 113 | // like a loop to the compiler. Solution would be to make the 114 | // tag comptime known. 115 | // 116 | // return if (@hasField(@TypeOf(allocator), "ptr")) 117 | // rootParser.parseAllocFromTag(T, '*', allocator.ptr, r) 118 | // else 119 | // rootParser.parseFromTag(T, '*', r); 120 | const ListParser = @import("./t_list.zig").ListParser; 121 | return if (@hasField(@TypeOf(allocator), "ptr")) 122 | ListParser.parseAlloc(T, rootParser, allocator.ptr, r) 123 | else 124 | ListParser.parse(T, rootParser, r); 125 | // return error.DecodeError; 126 | } 127 | }; 128 | 129 | test "set" { 130 | const parser = @import("../parser.zig").RESP3Parser; 131 | const allocator = std.heap.page_allocator; 132 | 133 | var set1 = MakeSet(); 134 | const arr = try SetParser.parse([3]i32, parser, &set1); 135 | try testing.expectEqualSlices(i32, &[3]i32{ 1, 2, 3 }, &arr); 136 | 137 | var set2 = MakeSet(); 138 | const sli = try SetParser.parseAlloc([]i64, parser, allocator, &set2); 139 | defer allocator.free(sli); 140 | try testing.expectEqualSlices(i64, &[3]i64{ 1, 2, 3 }, sli); 141 | 142 | var set3 = MakeSet(); 143 | var hmap = try SetParser.parseAlloc(std.AutoHashMap(i64, void), parser, allocator, &set3); 144 | defer hmap.deinit(); 145 | 146 | if (hmap.remove(1)) {} else unreachable; 147 | if (hmap.remove(2)) {} else unreachable; 148 | if (hmap.remove(3)) {} else unreachable; 149 | 150 | try testing.expectEqual(@as(usize, 0), hmap.count()); 151 | } 152 | 153 | // TODO: get rid of this! 154 | fn MakeSet() Reader { 155 | return std.Io.Reader.fixed("~3\r\n:1\r\n:2\r\n:3\r\n"[1..]); 156 | } 157 | -------------------------------------------------------------------------------- /src/commands/hashes/hset.zig: -------------------------------------------------------------------------------- 1 | // HSET key field value [field value ...] 2 | 3 | const std = @import("std"); 4 | const Writer = std.Io.Writer; 5 | 6 | const common = @import("../_common_utils.zig"); 7 | const FV = common.FV; 8 | 9 | pub const HSET = struct { 10 | key: []const u8, 11 | fvs: []const FV, 12 | 13 | /// Instantiates a new HSET command. 14 | pub fn init(key: []const u8, fvs: []const FV) HSET { 15 | return .{ .key = key, .fvs = fvs }; 16 | } 17 | 18 | /// Validates if the command is syntactically correct. 19 | pub fn validate(self: HSET) !void { 20 | if (self.key.len == 0) return error.EmptyKeyName; 21 | if (self.fvs.len == 0) return error.FVsArrayIsEmpty; 22 | 23 | // Check the individual FV pairs 24 | // TODO: how the hell do I check for dups without an allocator? 25 | var i: usize = 0; 26 | while (i < self.fvs.len) : (i += 1) { 27 | if (self.fvs[i].field.len == 0) return error.EmptyFieldName; 28 | } 29 | } 30 | 31 | // This reassignment is necessary to avoid having two definitions of 32 | // RedisCommand in the same scope (it causes a shadowing error). 33 | pub const forStruct = _forStruct; 34 | 35 | pub const RedisCommand = struct { 36 | pub fn serialize(self: HSET, comptime rootSerializer: type, msg: anytype) !void { 37 | return rootSerializer.serializeCommand(msg, .{ "HSET", self.key, self }); 38 | } 39 | }; 40 | 41 | pub const RedisArguments = struct { 42 | pub fn count(self: HSET) usize { 43 | return self.fvs.len * 2; 44 | } 45 | 46 | pub fn serialize(self: HSET, comptime rootSerializer: type, msg: anytype) !void { 47 | for (self.fvs) |fv| { 48 | try rootSerializer.serializeArgument(msg, []const u8, fv.field); 49 | try rootSerializer.serializeArgument(msg, []const u8, fv.value); 50 | } 51 | } 52 | }; 53 | }; 54 | 55 | fn _forStruct(comptime T: type) type { 56 | // TODO: support pointers to struct, check that the struct is serializable (strings and numbers). 57 | // TODO: there is some duplicated code with xread. Values should be a dedicated generic type. 58 | if (@typeInfo(T) != .@"struct") @compileError("Only Struct types allowed."); 59 | return struct { 60 | key: []const u8, 61 | values: T, 62 | 63 | const Self = @This(); 64 | pub fn init(key: []const u8, values: T) Self { 65 | return .{ .key = key, .values = values }; 66 | } 67 | 68 | /// Validates if the command is syntactically correct. 69 | pub fn validate(self: Self) !void { 70 | if (self.key.len == 0) return error.EmptyKeyName; 71 | } 72 | 73 | pub const RedisCommand = struct { 74 | pub fn serialize(self: Self, comptime rootSerializer: type, msg: anytype) !void { 75 | return rootSerializer.serializeCommand(msg, .{ 76 | "HSET", 77 | self.key, 78 | 79 | // Dirty trick to control struct serialization :3 80 | self, 81 | }); 82 | } 83 | }; 84 | 85 | // We are marking ouserlves also as an argument to manage struct serialization. 86 | pub const RedisArguments = struct { 87 | pub fn count(_: Self) usize { 88 | return comptime std.meta.fields(T).len * 2; 89 | } 90 | 91 | pub fn serialize(self: Self, comptime rootSerializer: type, msg: anytype) !void { 92 | inline for (std.meta.fields(T)) |field| { 93 | const arg = @field(self.values, field.name); 94 | const ArgT = @TypeOf(arg); 95 | try rootSerializer.serializeArgument(msg, []const u8, field.name); 96 | try rootSerializer.serializeArgument(msg, ArgT, arg); 97 | } 98 | } 99 | }; 100 | }; 101 | } 102 | 103 | test "basic usage" { 104 | const cmd = HSET.init("mykey", &[_]FV{ 105 | .{ .field = "field1", .value = "val1" }, 106 | .{ .field = "field2", .value = "val2" }, 107 | }); 108 | try cmd.validate(); 109 | 110 | const ExampleStruct = struct { 111 | banana: usize, 112 | id: []const u8, 113 | }; 114 | 115 | const example = ExampleStruct{ 116 | .banana = 10, 117 | .id = "ok", 118 | }; 119 | 120 | const cmd1 = HSET.forStruct(ExampleStruct).init("mykey", example); 121 | try cmd1.validate(); 122 | } 123 | 124 | test "serializer" { 125 | const serializer = @import("../../serializer.zig").CommandSerializer; 126 | 127 | var correctBuf: [1000]u8 = undefined; 128 | var correctMsg: Writer = .fixed(correctBuf[0..]); 129 | 130 | var testBuf: [1000]u8 = undefined; 131 | var testMsg: Writer = .fixed(testBuf[0..]); 132 | 133 | { 134 | { 135 | correctMsg.end = 0; 136 | testMsg.end = 0; 137 | 138 | try serializer.serializeCommand( 139 | &testMsg, 140 | HSET.init("k1", &[_]FV{.{ .field = "f1", .value = "v1" }}), 141 | ); 142 | try serializer.serializeCommand( 143 | &correctMsg, 144 | .{ "HSET", "k1", "f1", "v1" }, 145 | ); 146 | 147 | // std.debug.warn("{}\n\n\n{}\n", .{ correctMsg.buffered(), testMsg.buffered() }); 148 | try std.testing.expectEqualSlices(u8, correctMsg.buffered(), testMsg.buffered()); 149 | } 150 | 151 | { 152 | correctMsg.end = 0; 153 | testMsg.end = 0; 154 | 155 | const MyStruct = struct { 156 | field1: []const u8, 157 | field2: u8, 158 | field3: usize, 159 | }; 160 | 161 | const MyHSET = HSET.forStruct(MyStruct); 162 | 163 | try serializer.serializeCommand( 164 | &testMsg, 165 | MyHSET.init( 166 | "k1", 167 | .{ .field1 = "nice!", .field2 = 'a', .field3 = 42 }, 168 | ), 169 | ); 170 | try serializer.serializeCommand( 171 | &correctMsg, 172 | .{ 173 | "HSET", "k1", 174 | "field1", "nice!", 175 | "field2", 'a', 176 | "field3", 42, 177 | }, 178 | ); 179 | 180 | try std.testing.expectEqualSlices( 181 | u8, 182 | correctMsg.buffered(), 183 | testMsg.buffered(), 184 | ); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/commands/strings/set.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Writer = std.Io.Writer; 3 | const Value = @import("../_common_utils.zig").Value; 4 | 5 | /// SET key value [EX seconds|PX milliseconds] [NX|XX] 6 | pub const SET = struct { 7 | //! Command builder for SET. 8 | //! 9 | //! Allows you to use both strings and numbers as values. 10 | //! ``` 11 | //! const cmd1 = SET.init("mykey", 42, .NoExpire, .NoConditions); 12 | //! const cmd2 = SET.init("mykey", "banana", .NoExpire, .IfNotExisting); 13 | //! ``` 14 | 15 | key: []const u8, 16 | 17 | /// Users should provide either a string or a number to `.init()`. 18 | value: Value, 19 | 20 | /// Time To Live (TTL) for the key, defaults to `.NoExpire`. 21 | expire: Expire = .NoExpire, 22 | 23 | /// Execution constraints, defaults to `.NoCondition` (executes the command unconditionally). 24 | conditions: Conditions = .NoConditions, 25 | 26 | /// Provide either a number or a string as `value`. 27 | pub fn init(key: []const u8, value: anytype, expire: Expire, conditions: Conditions) SET { 28 | return .{ 29 | .key = key, 30 | .value = Value.fromVar(value), 31 | .expire = expire, 32 | .conditions = conditions, 33 | }; 34 | } 35 | 36 | pub fn validate(self: SET) !void { 37 | if (self.key.len == 0) return error.EmptyKeyName; 38 | } 39 | 40 | pub const RedisCommand = struct { 41 | pub fn serialize(self: SET, comptime rootSerializer: type, msg: anytype) !void { 42 | return rootSerializer.serializeCommand(msg, .{ 43 | "SET", 44 | self.key, 45 | self.value, 46 | self.expire, 47 | self.conditions, 48 | }); 49 | } 50 | }; 51 | 52 | pub const Expire = union(enum) { 53 | NoExpire, 54 | Seconds: u64, 55 | Milliseconds: u64, 56 | 57 | pub const RedisArguments = struct { 58 | pub fn count(self: Expire) usize { 59 | return switch (self) { 60 | .NoExpire => 0, 61 | else => 2, 62 | }; 63 | } 64 | 65 | pub fn serialize(self: Expire, comptime rootSerializer: type, msg: anytype) !void { 66 | switch (self) { 67 | .NoExpire => {}, 68 | .Seconds => |s| { 69 | try rootSerializer.serializeArgument(msg, []const u8, "EX"); 70 | try rootSerializer.serializeArgument(msg, u64, s); 71 | }, 72 | .Milliseconds => |m| { 73 | try rootSerializer.serializeArgument(msg, []const u8, "PX"); 74 | try rootSerializer.serializeArgument(msg, u64, m); 75 | }, 76 | } 77 | } 78 | }; 79 | }; 80 | 81 | pub const Conditions = union(enum) { 82 | /// Creates the key uncontidionally. 83 | NoConditions, 84 | 85 | /// Creates the key only if it does not exist yet. 86 | IfNotExisting, 87 | 88 | /// Only overrides an existing key. 89 | IfAlreadyExisting, 90 | 91 | pub const RedisArguments = struct { 92 | pub fn count(self: Conditions) usize { 93 | return switch (self) { 94 | .NoConditions => 0, 95 | else => 1, 96 | }; 97 | } 98 | 99 | pub fn serialize( 100 | self: Conditions, 101 | comptime root: type, 102 | w: *Writer, 103 | ) !void { 104 | switch (self) { 105 | .NoConditions => {}, 106 | .IfNotExisting => try root.serializeArgument( 107 | w, 108 | []const u8, 109 | "NX", 110 | ), 111 | .IfAlreadyExisting => try root.serializeArgument( 112 | w, 113 | []const u8, 114 | "XX", 115 | ), 116 | } 117 | } 118 | }; 119 | }; 120 | }; 121 | 122 | test "basic usage" { 123 | var cmd = SET.init("mykey", 42, .NoExpire, .NoConditions); 124 | cmd = SET.init("mykey", "banana", .NoExpire, .IfNotExisting); 125 | try cmd.validate(); 126 | } 127 | 128 | test "serializer" { 129 | const serializer = @import("../../serializer.zig").CommandSerializer; 130 | 131 | var correctBuf: [1000]u8 = undefined; 132 | var correctMsg: Writer = .fixed(correctBuf[0..]); 133 | 134 | var testBuf: [1000]u8 = undefined; 135 | var testMsg: Writer = .fixed(testBuf[0..]); 136 | 137 | { 138 | { 139 | correctMsg.end = 0; 140 | testMsg.end = 0; 141 | 142 | try serializer.serializeCommand( 143 | &testMsg, 144 | SET.init("mykey", 42, .NoExpire, .NoConditions), 145 | ); 146 | try serializer.serializeCommand( 147 | &correctMsg, 148 | .{ "SET", "mykey", "42" }, 149 | ); 150 | 151 | try std.testing.expectEqualSlices( 152 | u8, 153 | correctMsg.buffered(), 154 | testMsg.buffered(), 155 | ); 156 | } 157 | 158 | { 159 | correctMsg.end = 0; 160 | testMsg.end = 0; 161 | 162 | try serializer.serializeCommand( 163 | &testMsg, 164 | SET.init("mykey", "banana", .NoExpire, .IfNotExisting), 165 | ); 166 | try serializer.serializeCommand( 167 | &correctMsg, 168 | .{ "SET", "mykey", "banana", "NX" }, 169 | ); 170 | 171 | try std.testing.expectEqualSlices( 172 | u8, 173 | correctMsg.buffered(), 174 | testMsg.buffered(), 175 | ); 176 | } 177 | 178 | { 179 | correctMsg.end = 0; 180 | testMsg.end = 0; 181 | 182 | try serializer.serializeCommand( 183 | &testMsg, 184 | SET.init("mykey", "banana", SET.Expire{ .Seconds = 20 }, .IfAlreadyExisting), 185 | ); 186 | try serializer.serializeCommand( 187 | &correctMsg, 188 | .{ "SET", "mykey", "banana", "EX", "20", "XX" }, 189 | ); 190 | 191 | try std.testing.expectEqualSlices( 192 | u8, 193 | correctMsg.buffered(), 194 | testMsg.buffered(), 195 | ); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /CLIENT.md: -------------------------------------------------------------------------------- 1 | # Using the OkRedis client 2 | 3 | ## Table of contents 4 | * [Connecting](#connecting) 5 | * [Buffering](#buffering) 6 | * [Evented vs blocking I/O](#evented-vs-blocking-io) 7 | * [Pipelining](#pipelining) 8 | * [Transactions](#transactions) 9 | * [Pub/Sub](#pubsub) 10 | 11 | ## Connecting 12 | For now only IPv4 is supported, in the near future I'll add support for the 13 | common options. 14 | 15 | ```zig 16 | const std = @import("std"); 17 | const Io = std.Io; 18 | const okredis = @import("./src/okredis.zig"); 19 | const Client = okredis.Client; 20 | 21 | pub fn main() !void { 22 | const gpa = std.heap.smp_allocator; 23 | 24 | // Pick your preferred Io implementation. 25 | var threaded: Io.Threaded = .init(gpa); 26 | defer threaded.deinit(); 27 | const io = threaded.io(); 28 | 29 | // Open a TCP connection. 30 | // NOTE: managing the connection is your responsibility. 31 | const addr: Io.net.IpAddress = try .parseIp4("127.0.0.1", 6379); 32 | const connection = try addr.connect(io, .{ .mode = .stream }); 33 | defer connection.close(io); 34 | 35 | var rbuf: [1024]u8 = undefined; 36 | var wbuf: [1024]u8 = undefined; 37 | var reader = connection.reader(io, &rbuf); 38 | var writer = connection.writer(io, &wbuf); 39 | 40 | // The last argument are auth credentials (null in our case). 41 | var client = try Client.init(io, &reader.interface, &writer.interface, null); 42 | 43 | try client.send(void, .{ "SET", "key", "42" }); 44 | 45 | const reply = try client.send(i64, .{ "GET", "key" }); 46 | std.debug.warn("key = {}\n", .{reply}); 47 | } 48 | ``` 49 | 50 | ## Buffering 51 | Currently the client uses a 4096 bytes long fixed buffer embedded in the 52 | `Client` struct. 53 | 54 | In the future the option of customizing the buffering strategy will be exposed 55 | to the user, once the I/O stream interface becomes more stable in Zig. 56 | 57 | ## Async I/O 58 | This client supports Zig's new (0.16.0) async I/O. 59 | 60 | No change is necessary on your part, other than using a different allocator 61 | than `smp_allocator` (like the provided examples suggest). You can use 62 | `std.heap.GeneralPurposeAllocator` instead. 63 | 64 | ## Pipelining 65 | Redis supports pipelining, which, in short, consists of sending multiple 66 | commands at once and only reading replies once all the commands are sent. 67 | [You can read more here](https://redis.io/topics/pipelining). 68 | 69 | OkRedis exposes pipelining through `pipe` and `pipeAlloc`. 70 | 71 | ```zig 72 | const reply = try client.pipe(struct { 73 | c1: void, 74 | c2: u64, 75 | c3: OrErr(FixBuf(10)), 76 | }, .{ 77 | .{ "SET", "counter", 0 }, 78 | .{ "INCR", "counter" }, 79 | .{ "ECHO", "banana" }, 80 | }); 81 | 82 | std.debug.print("[INCR => {}]\n", .{reply.c2}); 83 | std.debug.print("[ECHO => {s}]\n", .{reply.c3.toSlice()}); 84 | ``` 85 | 86 | Let's break down the code above. 87 | The first argument to `pipe` is a struct *definition* that contains one field 88 | for each command being sent through the pipeline. It's basically the same as 89 | with `send`, except that, since we're sending multiple commands at once, the 90 | return type must comprehend the return types of all commands. 91 | 92 | You can define whatever field name you want when defining the return types. 93 | In the example above I chose (`c1`, `c2`, `c3`), but whichever is fine. 94 | 95 | The second argument to `pipe` is an argument list that contains all the commands 96 | that we want to send. 97 | 98 | Pipelines are multi-command invocations so each command will succeed or fail 99 | independently. This is a small but big difference with transactions, as we will 100 | see in the next section. 101 | 102 | ## Transactions 103 | Transactions are a way of providing isolation and all-or-nothing semantics to a 104 | group of Redis commands. The concept of transactions is orthogonal to pipelines, 105 | but given the semantics of Redis transactions, it's often advantageous to apply 106 | pipelining to one. 107 | 108 | You can [read more about Redis transactions here](https://redis.io/topics/transactions). 109 | 110 | OkRedis provides `trans` and `transAlloc` to perform transactions with automatic 111 | pipelining. It's mostly for convenience as the same result could be achieved by 112 | making explicit use of `MULTI`, `EXEC` and (optionally) `pipe`/`pipeAlloc`. 113 | 114 | ```zig 115 | const reply = try client.trans(OrErr(struct { 116 | c1: OrErr(FixBuf(10)), 117 | c2: u64, 118 | c3: OrErr(void), 119 | }), .{ 120 | .{ "SET", "banana", "no, thanks" }, 121 | .{ "INCR", "counter" }, 122 | .{ "INCR", "banana" }, 123 | }); 124 | 125 | switch (reply) { 126 | .Err => |e| @panic(e.getCode()), 127 | .Nil => @panic("got nil"), 128 | .Ok => |r| { 129 | std.debug.print("\n[SET = {s}] [INCR = {}] [INCR (error) = {s}]\n", .{ 130 | r.c1.Ok.toSlice(), 131 | r.c2, 132 | r.c3.Err.getCode(), 133 | }); 134 | }, 135 | } 136 | ``` 137 | 138 | At first sight the return value works the same way as with pipelining, but there 139 | is one important difference: the whole transaction can return an error or `nil`. 140 | When the transaction gets committed, the result can be: 141 | 142 | 1. A Redis error, in case an error was already encountered when queueing commands. 143 | 2. `nil`, in case the transaction was preceded by a `WATCH` that triggered. 144 | 3. A list of results, each corresponding to a command in the transaction. 145 | 146 | For this reason it's recommended to wrap a transaction's return type in `OrErr`. 147 | 148 | If the return type of all commands is the same, you can also use arrays or 149 | slices (for slices you'll need `pipeAlloc` or `transAlloc`). 150 | 151 | ```zig 152 | // Maybe not the most important transaction of them all... 153 | const reply = try client.transAlloc(OrErr([][]u8), allocator, .{ 154 | .{ "ECHO", "Do you" }, 155 | .{ "ECHO", "want to" }, 156 | .{ "ECHO", "build a" }, 157 | .{ "ECHO", "client?" }, 158 | }); 159 | 160 | // Don't forget to free the memory! 161 | defer okredis.freeReply(reply); 162 | 163 | // Switch over the result. 164 | switch (reply) { 165 | .Err => |e| @panic(e.getCode()), 166 | .Nil => @panic("got nil"), 167 | .Ok => |r| { 168 | for (r) |msg| { 169 | std.debug.print("{s} ", .{msg}); 170 | } 171 | std.debug.print("\n", .{}); 172 | }, 173 | } 174 | ``` 175 | 176 | This prints, as you might have guessed: 177 | ``` 178 | Do you want to build a client? 179 | ``` 180 | 181 | ## Pub/Sub 182 | Pub/Sub is not implemented yet. 183 | 184 | I'm still trying to figure out how the API should look like in order to 185 | provide an allocation-free interface also for Pub/Sub. 186 | 187 | In case I can't make progress in the near future, I'll add some low-level 188 | APIs (similar to what hiredis provides) to make the functionality available in 189 | the meantime. -------------------------------------------------------------------------------- /src/types/verbatim.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const fmt = std.fmt; 4 | const Allocator = std.mem.Allocator; 5 | const testing = std.testing; 6 | 7 | /// Parses the different types of Redis strings and keeps 8 | /// track of the string metadata information when present. 9 | /// Useful to know when Redis is replying with a verbatim 10 | /// markdown string, for example. 11 | /// 12 | /// Requires an allocator, so it can only be used with `sendAlloc`. 13 | pub const Verbatim = struct { 14 | format: Format, 15 | string: []u8, 16 | 17 | pub const Format = union(enum) { 18 | Simple: void, 19 | Err: void, 20 | Verbatim: [3]u8, 21 | }; 22 | 23 | pub const Redis = struct { 24 | pub const Parser = struct { 25 | pub fn parse(_: u8, comptime _: type, _: anytype) !Verbatim { 26 | @compileError("Verbatim requires an allocator, use `parseAlloc`."); 27 | } 28 | 29 | pub fn destroy( 30 | self: Verbatim, 31 | comptime _: type, 32 | allocator: Allocator, 33 | ) void { 34 | allocator.free(self.string); 35 | } 36 | 37 | pub fn parseAlloc( 38 | tag: u8, 39 | comptime rootParser: type, 40 | allocator: Allocator, 41 | r: *Reader, 42 | ) !Verbatim { 43 | switch (tag) { 44 | else => return error.DecodingError, 45 | '-', '!' => { 46 | try rootParser.parseFromTag(void, tag, r); 47 | return error.GotErrorReply; 48 | }, 49 | '$', '+' => return Verbatim{ 50 | .format = Format{ .Simple = {} }, 51 | .string = try rootParser.parseAllocFromTag( 52 | []u8, 53 | tag, 54 | allocator, 55 | r, 56 | ), 57 | }, 58 | '=' => { 59 | const digits = try r.takeSentinel('\r'); 60 | var size = try fmt.parseInt(usize, digits, 10); 61 | try r.discardAll(1); 62 | 63 | // We must consider the case in which a malformed 64 | // verbatim string is received. By the protocol standard 65 | // a verbatim string must start with a `<3 letter type>:` 66 | // prefix, but since modules will be able to produce 67 | // this kind of data, we should protect ourselves 68 | // from potential errors. 69 | var format: Format = undefined; 70 | if (size >= 4) { 71 | format = Format{ 72 | .Verbatim = (try r.takeArray(3)).*, 73 | }; 74 | 75 | // Skip the `:` character, subtract what we consumed 76 | try r.discardAll(1); 77 | size -= 4; 78 | } else { 79 | format = Format{ .Err = {} }; 80 | } 81 | 82 | const res = try allocator.alloc(u8, size); 83 | errdefer allocator.free(res); 84 | 85 | try r.readSliceAll(res); 86 | try r.discardAll(2); 87 | 88 | return .{ 89 | .format = format, 90 | .string = res, 91 | }; 92 | }, 93 | } 94 | } 95 | }; 96 | }; 97 | }; 98 | 99 | test "verbatim" { 100 | const parser = @import("../parser.zig").RESP3Parser; 101 | const allocator = std.heap.page_allocator; 102 | 103 | { 104 | var simple_string = MakeSimpleString(); 105 | const reply = try Verbatim.Redis.Parser.parseAlloc( 106 | '+', 107 | parser, 108 | allocator, 109 | &simple_string, 110 | ); 111 | try testing.expectEqualSlices(u8, "Yayyyy I'm a string!", reply.string); 112 | switch (reply.format) { 113 | else => unreachable, 114 | .Simple => {}, 115 | } 116 | } 117 | 118 | { 119 | var blob_string = MakeBlobString(); 120 | const reply = try Verbatim.Redis.Parser.parseAlloc('$', parser, allocator, &blob_string); 121 | try testing.expectEqualSlices(u8, "Hello World!", reply.string); 122 | switch (reply.format) { 123 | else => unreachable, 124 | .Simple => {}, 125 | } 126 | } 127 | 128 | { 129 | var verb_string = MakeVerbatimString(); 130 | const reply = try Verbatim.Redis.Parser.parseAlloc( 131 | '=', 132 | parser, 133 | allocator, 134 | &verb_string, 135 | ); 136 | try testing.expectEqualSlices(u8, "Oh hello there!", reply.string); 137 | switch (reply.format) { 138 | else => unreachable, 139 | .Verbatim => |format| try testing.expectEqualSlices(u8, "txt", &format), 140 | } 141 | } 142 | 143 | { 144 | var bad_verb = MakeBadVerbatimString(); 145 | const reply = try Verbatim.Redis.Parser.parseAlloc( 146 | '=', 147 | parser, 148 | allocator, 149 | &bad_verb, 150 | ); 151 | try testing.expectEqualSlices(u8, "t", reply.string); 152 | switch (reply.format) { 153 | else => unreachable, 154 | .Err => {}, 155 | } 156 | } 157 | 158 | { 159 | var bad_verb_2 = MakeBadVerbatimString2(); 160 | const reply = try Verbatim.Redis.Parser.parseAlloc( 161 | '=', 162 | parser, 163 | allocator, 164 | &bad_verb_2, 165 | ); 166 | try testing.expectEqualSlices(u8, "", reply.string); 167 | switch (reply.format) { 168 | else => unreachable, 169 | .Verbatim => |format| try testing.expectEqualSlices(u8, "mkd", &format), 170 | } 171 | } 172 | } 173 | 174 | // TODO: get rid of these!!! 175 | fn MakeSimpleString() Reader { 176 | return std.Io.Reader.fixed("+Yayyyy I'm a string!\r\n"[1..]); 177 | } 178 | fn MakeBlobString() Reader { 179 | return std.Io.Reader.fixed("$12\r\nHello World!\r\n"[1..]); 180 | } 181 | fn MakeVerbatimString() Reader { 182 | return std.Io.Reader.fixed("=19\r\ntxt:Oh hello there!\r\n"[1..]); 183 | } 184 | fn MakeBadVerbatimString() Reader { 185 | return std.Io.Reader.fixed("=1\r\nt\r\n"[1..]); 186 | } 187 | fn MakeBadVerbatimString2() Reader { 188 | return std.Io.Reader.fixed("=4\r\nmkd:\r\n"[1..]); 189 | } 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

OkRedis

3 |

4 | 5 | 6 |

7 | 8 |

9 | OkRedis is a zero-allocation client for Redis 6+ 10 |

11 | 12 | ## Handy and Efficient 13 | OkRedis aims to offer an interface with great ergonomics without 14 | compromising on performance or flexibility. 15 | 16 | ## Project status 17 | OkRedis is currently in alpha. The main features are mostly complete, 18 | but a lot of polishing is still required. 19 | 20 | Requires Zig v0.16.0-dev or above. 21 | 22 | Everything mentioned in the docs is already implemented and you can just 23 | `zig run example.zig` to quickly see what it can do. Remember OkRedis only 24 | supports Redis 6 and above. 25 | 26 | ## Zero dynamic allocations, unless explicitly wanted 27 | The client has two main interfaces to send commands: `send` and `sendAlloc`. 28 | Following Zig's mantra of making dynamic allocations explicit, only `sendAlloc` 29 | can allocate dynamic memory, and only does so by using a user-provided allocator. 30 | 31 | The way this is achieved is by making good use of RESP3's typed responses and 32 | Zig's metaprogramming facilities. 33 | The library uses compile-time reflection to specialize down to the parser level, 34 | allowing OkRedis to decode whenever possible a reply directly into a function 35 | frame, **without any intermediate dynamic allocation**. If you want more 36 | information about Zig's comptime: 37 | - [Official documentation](https://ziglang.org/documentation/master/#comptime) 38 | - [What is Zig's Comptime?](https://kristoff.it/blog/what-is-zig-comptime) (blog post written by me) 39 | 40 | By using `sendAlloc` you can decode replies with arbrirary shape at the cost of 41 | occasionally performing dynamic allocations. The interface takes an allocator 42 | as input, so the user can setup custom allocation schemes such as 43 | [arenas](https://en.wikipedia.org/wiki/Region-based_memory_management). 44 | 45 | ## Quickstart 46 | 47 | ```zig 48 | const std = @import("std"); 49 | const Io = std.Io; 50 | const okredis = @import("./src/okredis.zig"); 51 | const SET = okredis.commands.strings.SET; 52 | const OrErr = okredis.types.OrErr; 53 | const Client = okredis.Client; 54 | 55 | pub fn main() !void { 56 | const gpa = std.heap.smp_allocator; 57 | 58 | // Pick your preferred Io implementation. 59 | var threaded: Io.Threaded = .init(gpa); 60 | defer threaded.deinit(); 61 | const io = threaded.io(); 62 | 63 | // Open a TCP connection. 64 | // NOTE: managing the connection is your responsibility. 65 | const addr: Io.net.IpAddress = try .parseIp4("127.0.0.1", 6379); 66 | const connection = try addr.connect(io, .{ .mode = .stream }); 67 | defer connection.close(io); 68 | 69 | var rbuf: [1024]u8 = undefined; 70 | var wbuf: [1024]u8 = undefined; 71 | var reader = connection.reader(io, &rbuf); 72 | var writer = connection.writer(io, &wbuf); 73 | 74 | // The last argument are auth credentials (null in our case). 75 | var client = try Client.init(io, &reader.interface, &writer.interface, null); 76 | 77 | // Basic interface 78 | try client.send(void, .{ "SET", "key", "42" }); 79 | const reply = try client.send(i64, .{ "GET", "key" }); 80 | if (reply != 42) @panic("out of towels"); 81 | 82 | // Command builder interface 83 | const cmd = SET.init("key", "43", .NoExpire, .IfAlreadyExisting); 84 | const otherReply = try client.send(OrErr(void), cmd); 85 | switch (otherReply) { 86 | .Nil => @panic("command should not have returned nil"), 87 | .Err => @panic("command should not have returned an error"), 88 | .Ok => std.debug.print("success!", .{}), 89 | } 90 | } 91 | ``` 92 | 93 | ## Available Documentation 94 | The reference documentation [is available here](https://kristoff.it/zig-okredis#root). 95 | 96 | * [Using the OkRedis client](CLIENT.md#using-the-okredis-client) 97 | * [Connecting](CLIENT.md#connecting) 98 | * [Buffering](CLIENT.md#buffering) 99 | * [Evented vs blocking I/O](CLIENT.md#evented-vs-blocking-io) 100 | * [Pipelining](CLIENT.md#pipelining) 101 | * [Transactions](CLIENT.md#transactions) 102 | * [Pub/Sub](CLIENT.md#pubsub) 103 | 104 | * [Sending commands](COMMANDS.md#sending-commands) 105 | * [Base interface](COMMANDS.md#base-interface) 106 | * [Command builder interface](COMMANDS.md#command-builder-interface) 107 | * [Validating command syntax](COMMANDS.md#validating-command-syntax) 108 | * [Optimized command builders](COMMANDS.md#optimized-command-builders) 109 | * [Creating new command builders](COMMANDS.md#creating-new-command-builders) 110 | * [An afterword on command builders vs methods](COMMANDS.md#an-afterword-on-command-builders-vs-methods) 111 | 112 | * [Decoding Redis Replies](REPLIES.md#decoding-redis-replies) 113 | * [Introduction](REPLIES.md#introduction) 114 | * [The first and second rule of decoding replies](REPLIES.md#the-first-and-second-rule-of-decoding-replies) 115 | * [Decoding Zig types](REPLIES.md#decoding-zig-types) 116 | * [Void](REPLIES.md#void) 117 | * [Numbers](REPLIES.md#numbers) 118 | * [Optionals](REPLIES.md#optionals) 119 | * [Strings](REPLIES.md#strings) 120 | * [Structs](REPLIES.md#structs) 121 | * [Decoding Redis errors and nil replies as values](REPLIES.md#decoding-redis-errors-and-nil-replies-as-values) 122 | * [Redis OK replies](REPLIES.md#redis-ok-replies) 123 | * [Allocating memory dynamically](REPLIES.md#allocating-memory-dynamically) 124 | * [Allocating strings](REPLIES.md#allocating-strings) 125 | * [Freeing complex replies](REPLIES.md#freeing-complex-replies) 126 | * [Allocating Redis Error messages](REPLIES.md#allocating-redis-error-messages) 127 | * [Allocating structured types](REPLIES.md#allocating-structured-types) 128 | * [Parsing dynamic replies](REPLIES.md#parsing-dynamic-replies) 129 | * [Bundled types](REPLIES.md#bundled-types) 130 | * [Decoding types in the standard library](REPLIES.md#decoding-types-in-the-standard-library) 131 | * [Implementing decodable types](REPLIES.md#implementing-decodable-types) 132 | * [Adding types for custom commands (Lua scripts or Redis modules)](REPLIES.md#adding-types-for-custom-commands-lua-scripts-or-redis-modules) 133 | * [Adding types used by a higher-level language](REPLIES.md#adding-types-used-by-a-higher-level-language) 134 | 135 | ## Extending OkRedis 136 | If you are a Lua script or Redis module author, you might be interested in 137 | reading the final sections of `COMMANDS.md` and `REPLIES.md`. 138 | 139 | ## Embedding OkRedis in a higher level language 140 | Take a look at the final section of `REPLIES.md`. 141 | 142 | ## TODOS 143 | - Implement remaining command builders 144 | - Streamline design of Zig errors 145 | - Refine the Redis traits 146 | - Pub/Sub 147 | -------------------------------------------------------------------------------- /src/parser/t_string_blob.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const fmt = std.fmt; 4 | const mem = std.mem; 5 | const testing = std.testing; 6 | const builtin = @import("builtin"); 7 | 8 | /// Parses RedisBlobString values 9 | pub const BlobStringParser = struct { 10 | pub fn isSupported(comptime T: type) bool { 11 | return switch (@typeInfo(T)) { 12 | .int, .float, .array => true, 13 | else => false, 14 | }; 15 | } 16 | 17 | pub fn parse(comptime T: type, comptime _: type, r: *Reader) !T { 18 | const digits = try r.takeSentinel('\r'); 19 | const size = try fmt.parseInt(usize, digits, 10); 20 | try r.discardAll(1); 21 | 22 | switch (@typeInfo(T)) { 23 | else => unreachable, 24 | .int => { 25 | const str_digits = try r.take(size); 26 | try r.discardAll(2); 27 | const res = try fmt.parseInt(T, str_digits, 10); 28 | return res; 29 | }, 30 | .float => { 31 | const str_digits = try r.take(size); 32 | try r.discardAll(2); 33 | const res = try fmt.parseFloat(T, str_digits); 34 | return res; 35 | }, 36 | .array => |arr| { 37 | var res: [arr.len]arr.child = undefined; 38 | const bytesSlice = mem.sliceAsBytes(res[0..]); 39 | if (bytesSlice.len != size) { 40 | return error.LengthMismatch; 41 | } 42 | 43 | try r.readSliceAll(bytesSlice); 44 | try r.discardAll(2); 45 | 46 | return res; 47 | }, 48 | } 49 | } 50 | 51 | pub fn isSupportedAlloc(comptime T: type) bool { 52 | return switch (@typeInfo(T)) { 53 | .pointer => true, 54 | else => isSupported(T), 55 | }; 56 | } 57 | 58 | pub fn parseAlloc(comptime T: type, comptime _: type, allocator: std.mem.Allocator, r: *Reader) !T { 59 | // @compileLog(@typeInfo(T)); 60 | // std.debug.print("\n\nTYPE={}\n\n", .{@typeInfo(T)}); 61 | switch (@typeInfo(T)) { 62 | .pointer => |ptr| { 63 | const digits = try r.takeSentinel('\r'); 64 | var size = try fmt.parseInt(usize, digits, 10); 65 | try r.discardAll(1); 66 | 67 | if (ptr.size == .c) size += @sizeOf(ptr.child); 68 | 69 | const elemSize = std.math.divExact(usize, size, @sizeOf(ptr.child)) catch return error.LengthMismatch; 70 | const res = try allocator.alignedAlloc( 71 | ptr.child, 72 | .fromByteUnits(ptr.alignment), 73 | elemSize, 74 | ); 75 | errdefer allocator.free(res); 76 | 77 | var bytes = mem.sliceAsBytes(res); 78 | if (ptr.size == .c) { 79 | try r.readSliceAll(bytes[0 .. size - @sizeOf(ptr.child)]); 80 | if (ptr.size == .c) { 81 | // TODO: maybe reword this loop for better performance? 82 | for (bytes[(size - @sizeOf(ptr.child))..]) |*b| b.* = 0; 83 | } 84 | } else { 85 | try r.readSliceAll(bytes[0..]); 86 | } 87 | try r.discardAll(2); 88 | 89 | return switch (ptr.size) { 90 | .one, .many => @compileError("Only Slices and C pointers should reach sub-parsers"), 91 | .slice => res, 92 | .c => @ptrCast(res.ptr), 93 | }; 94 | }, 95 | else => return parse(T, struct {}, r), 96 | } 97 | } 98 | }; 99 | 100 | test "string" { 101 | { 102 | var r_int = MakeInt(); 103 | try testing.expect(1337 == try BlobStringParser.parse( 104 | u32, 105 | struct {}, 106 | &r_int, 107 | )); 108 | var r_str = MakeString(); 109 | try testing.expectError(error.InvalidCharacter, BlobStringParser.parse( 110 | u32, 111 | struct {}, 112 | &r_str, 113 | )); 114 | var r_int2 = MakeInt(); 115 | try testing.expect(1337.0 == try BlobStringParser.parse( 116 | f32, 117 | struct {}, 118 | &r_int2, 119 | )); 120 | var r_flt = MakeFloat(); 121 | try testing.expect(12.34 == try BlobStringParser.parse( 122 | f64, 123 | struct {}, 124 | &r_flt, 125 | )); 126 | 127 | var r_str2 = MakeString(); 128 | try testing.expectEqualSlices(u8, "Hello World!", &try BlobStringParser.parse( 129 | [12]u8, 130 | struct {}, 131 | &r_str2, 132 | )); 133 | 134 | var r_ji = MakeEmoji2(); 135 | const res = try BlobStringParser.parse([2][4]u8, struct {}, &r_ji); 136 | try testing.expectEqualSlices(u8, "😈", &res[0]); 137 | try testing.expectEqualSlices(u8, "👿", &res[1]); 138 | } 139 | 140 | { 141 | const allocator = std.heap.page_allocator; 142 | { 143 | var r_str3 = MakeString(); 144 | const s = try BlobStringParser.parseAlloc( 145 | []u8, 146 | struct {}, 147 | allocator, 148 | &r_str3, 149 | ); 150 | defer allocator.free(s); 151 | try testing.expectEqualSlices(u8, s, "Hello World!"); 152 | } 153 | { 154 | var r_str4 = MakeString(); 155 | const s = try BlobStringParser.parseAlloc( 156 | [*c]u8, 157 | struct {}, 158 | allocator, 159 | &r_str4, 160 | ); 161 | defer allocator.free(s[0..12]); 162 | try testing.expectEqualSlices(u8, s[0..13], "Hello World!\x00"); 163 | } 164 | { 165 | var r_ji2 = MakeEmoji2(); 166 | const s = try BlobStringParser.parseAlloc( 167 | [][4]u8, 168 | struct {}, 169 | allocator, 170 | &r_ji2, 171 | ); 172 | defer allocator.free(s); 173 | try testing.expectEqualSlices(u8, "😈", &s[0]); 174 | try testing.expectEqualSlices(u8, "👿", &s[1]); 175 | } 176 | { 177 | var r_ji2 = MakeEmoji2(); 178 | const s = try BlobStringParser.parseAlloc( 179 | [*c][4]u8, 180 | struct {}, 181 | allocator, 182 | &r_ji2, 183 | ); 184 | defer allocator.free(s[0..3]); 185 | try testing.expectEqualSlices(u8, "😈", &s[0]); 186 | try testing.expectEqualSlices(u8, "👿", &s[1]); 187 | try testing.expectEqualSlices(u8, &[4]u8{ 0, 0, 0, 0 }, &s[3]); 188 | } 189 | { 190 | var r_str4 = MakeString(); 191 | try testing.expectError(error.LengthMismatch, BlobStringParser.parseAlloc( 192 | [][5]u8, 193 | struct {}, 194 | allocator, 195 | &r_str4, 196 | )); 197 | } 198 | } 199 | } 200 | 201 | // TODO: get rid of this 202 | fn MakeEmoji2() Reader { 203 | return std.Io.Reader.fixed("$8\r\n😈👿\r\n"[1..]); 204 | } 205 | fn MakeString() Reader { 206 | return std.Io.Reader.fixed("$12\r\nHello World!\r\n"[1..]); 207 | } 208 | fn MakeInt() Reader { 209 | return std.Io.Reader.fixed("$4\r\n1337\r\n"[1..]); 210 | } 211 | fn MakeFloat() Reader { 212 | return std.Io.Reader.fixed("$5\r\n12.34\r\n"[1..]); 213 | } 214 | -------------------------------------------------------------------------------- /COMMANDS.md: -------------------------------------------------------------------------------- 1 | # Sending commands 2 | 3 | ## Table of contents 4 | * [Base interface](#base-interface) 5 | * [Command builder interface](#command-builder-interface) 6 | * [Validating command syntax](#validating-command-syntax) 7 | * [Optimized command builders](#optimized-command-builders) 8 | * [Creating new command builders](#creating-new-command-builders) 9 | * [An afterword on command builders vs methods](#an-afterword-on-command-builders-vs-methods) 10 | 11 | ## Base interface 12 | The main way of sending commands using OkRedis is to just use an argument list 13 | or an array: 14 | 15 | ```zig 16 | // Using an argument list 17 | try client.send(void, .{ "SET", "key", 42 }); 18 | 19 | // Using an array 20 | const cmd = [_][]const u8{ "SET", "key", "42" }; 21 | try client.send(void, &cmd); 22 | 23 | // You can also nest one level of slices/arrays, 24 | // useful when some of the arguments are dynamic in number. 25 | const args = [_][]const u8{ "field1", "val1", "field2", "val2"}; 26 | try client.send(void, .{"HSET", "key", &args, "fixed-field", "fixed-val"}); 27 | ``` 28 | 29 | While simple and straightforward, this approach is prone to errors, as users 30 | might introduce typos or write a command that is syntactically wrong without any 31 | warning at comptime. 32 | 33 | Because of that, some other Redis clients consider such interface a fallback 34 | (or "escape hatch") that allows users to send commands that the library doesn't 35 | support, while the main usage looks like this: 36 | 37 | ```python 38 | # Python example 39 | client.xadd("key", "*", {"field1": "value1", "field2": 42}) 40 | client.set("fruit", "banana") 41 | ``` 42 | 43 | OkRedis doesn't provide any command-specific method and instead uses a different 44 | approach based on the idea of command builders. It might feel annoying at first 45 | to have to deal with a different way of doing things (and builders/factories are 46 | a huge turnoff -- believe me I get it), but I'll show in this document how this 47 | pattern brings enough advantages to the table to make the switch well worth. 48 | 49 | 50 | ## Command builder interface 51 | OkRedis includes command builders for all the basic Redis commands. 52 | All commands are grouped by the type of key they operate on (e.g., `strings`, 53 | `hashes`, `streams`), in the same way they are grouped on 54 | [https://redis.io/commands](https://redis.io/commands). 55 | 56 | Usage example: 57 | ```zig 58 | const cmds = okredis.commands; 59 | 60 | // SET key 42 NX 61 | try client.send(void, cmds.strings.SET.init("key", 42, .NoExpire, .IfNotExisting)); 62 | 63 | // GET key 64 | _ = try client.send(i64, cmds.strings.GET.init("key")); 65 | ``` 66 | 67 | For the full list of available command builders consult 68 | [the documentation](https://kristoff.it/zig-okredis/#root). 69 | 70 | ## Validating command syntax 71 | The `init` function of each type helps ensuring the command is properly formed, 72 | but some commands have constraints that can't be enforced via a function 73 | signature, or that are relatively expensive to check. 74 | For this reason all command builders have a `validate` method that can be used 75 | to apply syntax checks. 76 | 77 | **In other words, `init` doesn't guarantee correctness, and it's 78 | the user's responsibility to use `validate` when appropriate.** 79 | 80 | Usage example: 81 | 82 | ```zig 83 | // FV is a type that represents Field-Value pairs. 84 | const FV = okredis.types.FV; 85 | const fields = &[_]FV{ .{.field = "field1", .value = "value1"} }; 86 | 87 | 88 | // Case 1: well-formed command 89 | var readCmd1 = cmds.streams.XADD.init("stream-key", "*", .NoMaxLen, fields); 90 | try readCmd1.validate(); // Validation will succeed 91 | 92 | 93 | // Case 2: invalid ID 94 | var readCmd2 = cmds.streams.XADD.init("stream-key", "INVALID_ID", .NoMaxLen, fields); 95 | try readCmd2.validate(); // -> error.InvalidID 96 | 97 | ``` 98 | 99 | Validation of a command that doesn't depend on runtime values can be performed 100 | at comptime: 101 | 102 | ```zig 103 | comptime readCmd.validate() catch unreachable; 104 | ``` 105 | 106 | With the command builder interface it's easier to let the user choose whether 107 | to apply validation or not, and when (comptime vs runtime). Using a method-based 108 | interface we would lose many of those options. 109 | 110 | ## Optimized command builders 111 | Some command builders implement commands that deal with struct-shaped data. 112 | Two notable examples are `HSET` and `XADD`. 113 | In the previous example we saw how `commands.streams.XADD` takes a slice of `FV` 114 | pairs, but it would be convinient to be able to use a struct to convey the same 115 | request in a more precise (and optimized) way. 116 | 117 | To answer this need, some command builders offer a `forStruct` function that 118 | can be used to create a specialized version of the command builder: 119 | 120 | ```zig 121 | const Person = struct { 122 | name: []const u8, 123 | age: u64, 124 | }; 125 | 126 | // This creates a new type. 127 | const XADDPerson = cmds.streams.XADD.forStruct(Person); 128 | 129 | // This is an instance of a command. 130 | const xadd_loris = XADDPerson.init("people-stream", "*", .{ 131 | .name = "loris", 132 | .age = 29, 133 | }); 134 | 135 | try client.send(void, xadd_loris); 136 | ``` 137 | 138 | ## Creating new command builders 139 | Another advantage of command builders is the possibility of adding new commands 140 | to the ones that are included in OkRedis. 141 | While in some languages it's trivial to monkey patch new methods onto a 142 | pre-existing class, in others it's either not possible or the avaliable means 143 | have other types of issues and limitations (e.g., extension methods). 144 | 145 | **Creators of Redis modules might want to provide their users with client-side 146 | tooling for their module and this approach makes module commands feel as native 147 | as the built-in ones.** 148 | 149 | OkRedis uses two traits to delegate serialization to a struct that implements 150 | a command: `RedisCommand` and `RedisArguments`. 151 | 152 | For now I recommend reading the source code of existing commands to get an idea 153 | of how they work, possibly starting with simple commands (e.g., avoid staring 154 | with `SET` as the many options make it unexpectedly complex). 155 | 156 | 157 | ## An afterword on command builders vs methods 158 | You saw a couple of reasons why command builders are preferable over methods, 159 | especially in Zig where it's easy to execute isolated pieces of computation at 160 | comptime. Another place where this approach shines is with pipelining and 161 | transactions, where passing commands around as data makes it very easy to 162 | unsterstand what's happening. 163 | 164 | One last, and in some ways even more important, reason why I opted for command 165 | builders is that it's clear that these two things are conceptually the same: 166 | 167 | ```zig 168 | const cmd = SET.init("key", 1, .NoExpire, .NoConditions); 169 | const cmd = .{"SET", "key", 1}; 170 | ``` 171 | 172 | Regardless of which interface you chose to build your command with, at the end 173 | you always have to do the same thing: 174 | 175 | ```zig 176 | try client.send(void, cmd); 177 | 178 | try client.pipe([2]i64, .{ 179 | INCR.init("key"), 180 | .{ "INCR", "key" }, 181 | }); 182 | 183 | try client.trans(OrErr(void), .{ 184 | INCRBY.init("key", 10), 185 | .{ "INCRBY", "key", 10 }, 186 | }); 187 | ``` 188 | 189 | This might seem a small detail, but it really helps users build a mental model 190 | of the client that is simpler, but still equally useful. 191 | 192 | This choice also frees space in the `client` namespace to add methods that 193 | instead **do** imply different communication behavior, like `pipe` and `trans`. 194 | It's easy to miss the implications behind calling `client.xadd()` vs 195 | `client.subscribe()` in a method-based client. -------------------------------------------------------------------------------- /src/parser/t_list.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Reader = std.Io.Reader; 3 | const fmt = std.fmt; 4 | const builtin = @import("builtin"); 5 | 6 | /// Parses RedisList values. 7 | /// Uses RESP3Parser to delegate parsing of the list contents recursively. 8 | pub const ListParser = struct { 9 | // TODO: prevent users from unmarshaling structs out of strings 10 | pub fn isSupported(comptime T: type) bool { 11 | return switch (@typeInfo(T)) { 12 | .array => true, 13 | .@"struct" => |stc| { 14 | for (stc.fields) |f| 15 | if (f.type == *anyopaque) 16 | return false; 17 | return true; 18 | }, 19 | else => false, 20 | }; 21 | } 22 | 23 | pub fn isSupportedAlloc(comptime T: type) bool { 24 | return switch (@typeInfo(T)) { 25 | .pointer => true, 26 | else => isSupported(T), 27 | }; 28 | } 29 | 30 | pub fn parse(comptime T: type, comptime rootParser: type, r: *Reader) !T { 31 | return parseImpl(T, rootParser, .{}, r); 32 | } 33 | pub fn parseAlloc( 34 | comptime T: type, 35 | comptime rootParser: type, 36 | allocator: std.mem.Allocator, 37 | r: *Reader, 38 | ) !T { 39 | return parseImpl(T, rootParser, .{ .ptr = allocator }, r); 40 | } 41 | 42 | fn decodeArray( 43 | comptime T: type, 44 | result: []T, 45 | rootParser: anytype, 46 | allocator: anytype, 47 | r: *Reader, 48 | ) !void { 49 | var foundNil = false; 50 | var foundErr = false; 51 | for (result) |*elem| { 52 | if (foundNil or foundErr) { 53 | rootParser.parse(void, r) catch |err| switch (err) { 54 | error.GotErrorReply => { 55 | foundErr = true; 56 | }, 57 | else => return err, 58 | }; 59 | } else { 60 | elem.* = (if (@hasField(@TypeOf(allocator), "ptr")) 61 | rootParser.parseAlloc(T, allocator.ptr, r) 62 | else 63 | rootParser.parse(T, r)) catch |err| switch (err) { 64 | error.GotNilReply => { 65 | foundNil = true; 66 | continue; 67 | }, 68 | error.GotErrorReply => { 69 | foundErr = true; 70 | continue; 71 | }, 72 | else => return err, 73 | }; 74 | } 75 | } 76 | 77 | // Error takes precedence over Nil 78 | if (foundErr) return error.GotErrorReply; 79 | if (foundNil) return error.GotNilReply; 80 | return; 81 | } 82 | 83 | pub fn parseImpl(comptime T: type, comptime rootParser: type, allocator: anytype, r: *Reader) !T { 84 | // TODO: write real implementation 85 | var buf: [100]u8 = undefined; 86 | var end: usize = 0; 87 | for (&buf, 0..) |*elem, i| { 88 | const ch = try r.takeByte(); 89 | elem.* = ch; 90 | if (ch == '\r') { 91 | end = i; 92 | break; 93 | } 94 | } 95 | try r.discardAll(1); 96 | const size = try fmt.parseInt(usize, buf[0..end], 10); 97 | 98 | switch (@typeInfo(T)) { 99 | else => unreachable, 100 | .pointer => |ptr| { 101 | if (!@hasField(@TypeOf(allocator), "ptr")) { 102 | @compileError("To decode a slice you need to use sendAlloc / pipeAlloc / transAlloc!"); 103 | } 104 | 105 | const result = try allocator.ptr.alloc(ptr.child, size); 106 | errdefer allocator.ptr.free(result); 107 | try decodeArray(ptr.child, result, rootParser, allocator, r); 108 | return result; 109 | }, 110 | .array => |arr| { 111 | if (arr.len != size) { 112 | // The user requested an array but the list reply from Redis 113 | // contains a different amount of items. 114 | var foundErr = false; 115 | var i: usize = 0; 116 | while (i < size) : (i += 1) { 117 | // Discard all the items 118 | rootParser.parse(void, r) catch |err| switch (err) { 119 | error.GotErrorReply => { 120 | foundErr = true; 121 | }, 122 | else => return err, 123 | }; 124 | } 125 | 126 | // GotErrorReply takes precedence over LengthMismatch 127 | if (foundErr) return error.GotErrorReply; 128 | return error.LengthMismatch; 129 | } 130 | 131 | var result: T = undefined; 132 | try decodeArray(arr.child, result[0..], rootParser, allocator, r); 133 | return result; 134 | }, 135 | .@"struct" => |stc| { 136 | var foundNil = false; 137 | var foundErr = false; 138 | if (stc.fields.len != size) { 139 | // The user requested a struct but the list reply from Redis 140 | // contains a different amount of items. 141 | var i: usize = 0; 142 | while (i < size) : (i += 1) { 143 | // Discard all the items 144 | rootParser.parse(void, r) catch |err| switch (err) { 145 | error.GotErrorReply => { 146 | foundErr = true; 147 | }, 148 | else => return err, 149 | }; 150 | } 151 | 152 | // GotErrorReply takes precedence over LengthMismatch 153 | if (foundErr) return error.GotErrorReply; 154 | return error.LengthMismatch; 155 | } 156 | 157 | var result: T = undefined; 158 | inline for (stc.fields) |field| { 159 | if (foundNil or foundErr) { 160 | rootParser.parse(void, r) catch |err| switch (err) { 161 | error.GotErrorReply => { 162 | foundErr = true; 163 | }, 164 | else => return err, 165 | }; 166 | } else { 167 | @field(result, field.name) = (if (@hasField(@TypeOf(allocator), "ptr")) 168 | rootParser.parseAlloc(field.type, allocator.ptr, r) 169 | else 170 | rootParser.parse(field.type, r)) catch |err| switch (err) { 171 | else => return err, 172 | error.GotNilReply => blk: { 173 | foundNil = true; 174 | break :blk undefined; // I don't think I can continue here, given the inlining. 175 | }, 176 | error.GotErrorReply => blk: { 177 | foundErr = true; 178 | break :blk undefined; 179 | }, 180 | }; 181 | } 182 | } 183 | if (foundErr) return error.GotErrorReply; 184 | if (foundNil) return error.GotNilReply; 185 | return result; 186 | }, 187 | } 188 | } 189 | }; 190 | --------------------------------------------------------------------------------