feat: add fromByte and kind to protocol

This commit is contained in:
dualfroz committed 2026-09-07 19:08:01 +02:00
1 parent fc90b6c8a3
commit eb441341cb
1 file changed
+160
+160
View File
@@ -489,3 +489,163 @@ pub const CollAction = enum(u8) {
zset_rangebyscore = 28,
zset_rank = 29,
pub fn fromByte(b: u8) !CollAction {
return std.meta.intToEnum(CollAction, b) catch error.Malformed;
}
/// Which `collections.Kind` namespace this action's lazy TTL check
/// (`collttl.expireIfDue`, called on `name` before every dispatch in
/// `handleCollOp`) and `requiredRole` gating both need - grouped by the
/// action's numeric range rather than a second explicit table, since
/// the enum above is itself laid out kind-by-kind.
pub fn kind(self: CollAction) collmod.Kind {
return switch (self) {
.counter_get, .counter_set, .counter_incrby => .counter,
.hash_set, .hash_get, .hash_del, .hash_len, .hash_getall => .hash,
.set_add, .set_rem, .set_ismember, .set_card, .set_members, .set_inter, .set_union, .set_diff => .set,
.list_lpush, .list_rpush, .list_lpop, .list_rpop, .list_len, .list_range => .list,
.zset_add, .zset_score, .zset_incrby, .zset_rem, .zset_card, .zset_rangebyrank, .zset_rangebyscore, .zset_rank => .zset,
};
}
/// True for actions that only read - see `server.zig`'s `requiredRole`,
/// which needs this to decide `readonly` vs `readwrite` from the action
/// byte alone, the same action-byte peek `ttl_op`/`coll_ttl_op` already
/// use for their own bundled actions.
pub fn isReadOnly(self: CollAction) bool {
return switch (self) {
.counter_get, .hash_get, .hash_len, .hash_getall, .set_ismember, .set_card, .set_members, .set_inter, .set_union, .set_diff, .list_len, .list_range, .zset_score, .zset_card, .zset_rangebyrank, .zset_rangebyscore, .zset_rank => true,
else => false,
};
}
};
pub fn encodeCollOp(allocator: Allocator, action: CollAction, name: []const u8, args: []const []const u8) ![]u8 {
var buf = std.ArrayList(u8).init(allocator);
errdefer buf.deinit();
const w = encode.ByteWriter.init(&buf);
try w.byte(@intFromEnum(action));
try putLenPrefixed(w, name);
try w.putU32(@intCast(args.len));
for (args) |a| try putLenPrefixed(w, a);
return buf.toOwnedSlice();
}
pub const CollOpArgs = struct {
action: CollAction,
name: []const u8,
args: [][]const u8,
};
/// `name` and every entry of `args` are borrowed slices into `payload`,
/// valid only as long as the frame that carried them is alive - the same
/// convention `decodeBatch`/`decodeTableSelect` use. Only the `args`
/// pointer array itself is owned by the caller (free with
/// `allocator.free`); never its contents.
pub fn decodeCollOp(allocator: Allocator, payload: []const u8) !CollOpArgs {
var c = Cursor{ .data = payload };
const action = try CollAction.fromByte(try c.takeByte());
const name = try c.takeLenPrefixed();
const count = try c.takeU32();
var list = try std.ArrayList([]const u8).initCapacity(allocator, count);
errdefer list.deinit();
var i: u32 = 0;
while (i < count) : (i += 1) {
list.appendAssumeCapacity(try c.takeLenPrefixed());
}
return .{ .action = action, .name = name, .args = try list.toOwnedSlice() };
}
/// One reply shape covers every `coll_op` action, tagged by the first byte -
/// the response-side counterpart of the request-side `CollAction`. `.nil`
/// is the wire form of the CLI's `(nil)` (a hash field/list pop/zset score
/// that does not exist); `.ok` is a bare acknowledgement (counter set,
/// hash set/del as a flag would double up with `.int`, so those still use
/// `.int` with 0/1 - see `encodeCollReply`).
pub const CollReplyTag = enum(u8) {
nil = 0,
ok = 1,
int = 2,
real = 3,
bytes = 4,
stringList = 5,
pairList = 6,
scorePairList = 7,
};
pub const CollPair = struct { field: []const u8, value: []const u8 };
pub const CollScorePair = struct { member: []const u8, score: f64 };
pub const CollReply = union(enum) {
nil,
ok,
int: i64,
real: f64,
bytes: []const u8,
stringList: []const []const u8,
pairList: []const CollPair,
scorePairList: []const CollScorePair,
};
pub fn encodeCollReply(allocator: Allocator, reply: CollReply) ![]u8 {
var buf = std.ArrayList(u8).init(allocator);
errdefer buf.deinit();
const w = encode.ByteWriter.init(&buf);
switch (reply) {
.nil => try w.byte(@intFromEnum(CollReplyTag.nil)),
.ok => try w.byte(@intFromEnum(CollReplyTag.ok)),
.int => |v| {
try w.byte(@intFromEnum(CollReplyTag.int));
try w.putU64(@bitCast(v));
},
.real => |v| {
try w.byte(@intFromEnum(CollReplyTag.real));
try w.putU64(@bitCast(v));
},
.bytes => |v| {
try w.byte(@intFromEnum(CollReplyTag.bytes));
try w.slice(v);
},
.stringList => |items| {
try w.byte(@intFromEnum(CollReplyTag.stringList));
try w.putU32(@intCast(items.len));
for (items) |it| try putLenPrefixed(w, it);
},
.pairList => |items| {
try w.byte(@intFromEnum(CollReplyTag.pairList));
try w.putU32(@intCast(items.len));
for (items) |p| {
try putLenPrefixed(w, p.field);
try putLenPrefixed(w, p.value);
}
},
.scorePairList => |items| {
try w.byte(@intFromEnum(CollReplyTag.scorePairList));
try w.putU32(@intCast(items.len));
for (items) |p| {
try putLenPrefixed(w, p.member);
try w.putU64(@bitCast(p.score));
}
},
}
return buf.toOwnedSlice();
}
pub const OwnedCollPair = struct { field: []u8, value: []u8 };
pub const OwnedCollScorePair = struct { member: []u8, score: f64 };
/// Decoded, independently owned counterpart of `CollReply` - every byte
/// slice is a fresh allocation the caller must free via `deinit`, unlike
/// `CollOpArgs` above which stays borrowed (that side is consumed
/// immediately inside `dispatch` before the frame is freed; this side
/// crosses back out to a `Client` caller that keeps it around).
pub const OwnedCollReply = union(enum) {
nil,
ok,
int: i64,
real: f64,
bytes: []u8,
stringList: [][]u8,
pairList: []OwnedCollPair,
scorePairList: []OwnedCollScorePair,