Skip to content

Commit 45146d9

Browse files
Add plain enum support (#8)
* Add plain enum support - Add src/enum.zig with pack/unpack functions for all enum types - Support plain enums: enum { foo, bar } - Support explicit backing type enums: enum(u8) { foo = 1, bar = 2 } - Support mixed enums with auto and explicit values - Support optional enums with null handling - Update any.zig to handle .@"enum" type info - Add enum methods to Packer/Unpacker APIs - Add comprehensive tests for all enum variations Enums are serialized as their underlying integer values using @intFromEnum/@enumFromInt conversions. Backing type is automatically determined via @typeinfo(T).@"enum".tag_type. * Fix enum optional handling issues - Fix getEnumSize to properly handle optional enum types - Fix unpackEnum to support null handling for optional enums - Add comprehensive tests for optional enum functionality - Addresses CodeRabbit review feedback Co-authored-by: Lukáš Lalinský <lalinsky@users.noreply.github.com> * Refactor enum optional handling to use existing framework - Remove custom backup reader mechanism (44 lines removed) - Use maybeUnpackNull(header, T) pattern like int.zig - Simplify getEnumSize to handle optionals recursively - Follow established unpackInt dispatch pattern for header handling - Maintains full compatibility with existing tests Co-authored-by: Lukáš Lalinský <lalinsky@users.noreply.github.com> * Optimize enum unpacking to use unpackInt directly - Construct optional tag type (?tag_type vs tag_type) based on enum optionality - Delegate all msgpack parsing logic to existing unpackInt function - Simplify unpackEnum from 31 lines to 15 lines - Remove complex header parsing and format-specific handling - Clean up unused imports (hdrs, NonOptional, maybeUnpackNull) Co-authored-by: Lukáš Lalinský <lalinsky@users.noreply.github.com> --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Lukáš Lalinský <lalinsky@users.noreply.github.com>
1 parent 0234d5e commit 45146d9

3 files changed

Lines changed: 270 additions & 0 deletions

File tree

src/any.zig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ const unpackStruct = @import("struct.zig").unpackStruct;
3434
const packUnion = @import("union.zig").packUnion;
3535
const unpackUnion = @import("union.zig").unpackUnion;
3636

37+
const getEnumSize = @import("enum.zig").getEnumSize;
38+
const packEnum = @import("enum.zig").packEnum;
39+
const unpackEnum = @import("enum.zig").unpackEnum;
40+
3741
inline fn isString(comptime T: type) bool {
3842
switch (@typeInfo(T)) {
3943
.pointer => |ptr_info| {
@@ -56,6 +60,7 @@ pub fn sizeOfPackedAny(comptime T: type, value: T) usize {
5660
.bool => return getBoolSize(),
5761
.int => return getIntSize(T, value),
5862
.float => return getFloatSize(T, value),
63+
.@"enum" => return getEnumSize(T, value),
5964
.pointer => |ptr_info| {
6065
if (ptr_info.size == .Slice) {
6166
if (isString(T)) {
@@ -105,6 +110,7 @@ pub fn packAny(writer: anytype, value: anytype) !void {
105110
},
106111
.@"struct" => return packStruct(writer, T, value),
107112
.@"union" => return packUnion(writer, T, value),
113+
.@"enum" => return packEnum(writer, T, value),
108114
.optional => {
109115
if (value) |val| {
110116
return packAny(writer, val);
@@ -125,6 +131,7 @@ pub fn unpackAny(reader: anytype, allocator: std.mem.Allocator, comptime T: type
125131
.float => return unpackFloat(reader, T),
126132
.@"struct" => return unpackStruct(reader, allocator, T),
127133
.@"union" => return unpackUnion(reader, allocator, T),
134+
.@"enum" => return unpackEnum(reader, T),
128135
.pointer => |ptr_info| {
129136
if (ptr_info.size == .slice) {
130137
if (isString(T)) {

src/enum.zig

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
const std = @import("std");
2+
3+
const maybePackNull = @import("null.zig").maybePackNull;
4+
5+
const getIntSize = @import("int.zig").getIntSize;
6+
const packInt = @import("int.zig").packInt;
7+
const unpackInt = @import("int.zig").unpackInt;
8+
9+
inline fn assertEnumType(comptime T: type) type {
10+
switch (@typeInfo(T)) {
11+
.@"enum" => return T,
12+
.optional => |opt_info| {
13+
return assertEnumType(opt_info.child);
14+
},
15+
else => @compileError("Expected enum, got " ++ @typeName(T)),
16+
}
17+
}
18+
19+
pub fn getMaxEnumSize(comptime T: type) usize {
20+
const Type = assertEnumType(T);
21+
const tag_type = @typeInfo(Type).@"enum".tag_type;
22+
return 1 + @sizeOf(tag_type);
23+
}
24+
25+
pub fn getEnumSize(comptime T: type, value: T) usize {
26+
if (@typeInfo(T) == .optional) {
27+
if (value) |v| {
28+
return getEnumSize(@typeInfo(T).optional.child, v);
29+
} else {
30+
return 1; // size of null
31+
}
32+
}
33+
34+
const tag_type = @typeInfo(T).@"enum".tag_type;
35+
const int_value = @intFromEnum(value);
36+
return getIntSize(tag_type, int_value);
37+
}
38+
39+
pub fn packEnum(writer: anytype, comptime T: type, value_or_maybe_null: T) !void {
40+
const Type = assertEnumType(T);
41+
const value: Type = try maybePackNull(writer, T, value_or_maybe_null) orelse return;
42+
43+
const tag_type = @typeInfo(Type).@"enum".tag_type;
44+
const int_value = @intFromEnum(value);
45+
46+
try packInt(writer, tag_type, int_value);
47+
}
48+
49+
pub fn unpackEnum(reader: anytype, comptime T: type) !T {
50+
const Type = assertEnumType(T);
51+
const tag_type = @typeInfo(Type).@"enum".tag_type;
52+
53+
// Construct the optional tag type to match T's optionality
54+
const OptionalTagType = if (@typeInfo(T) == .optional) ?tag_type else tag_type;
55+
56+
// Use unpackInt directly with the constructed optional tag type
57+
const int_value = try unpackInt(reader, OptionalTagType);
58+
59+
// Handle the optional case
60+
if (@typeInfo(T) == .optional) {
61+
if (int_value) |value| {
62+
return @enumFromInt(value);
63+
} else {
64+
return null;
65+
}
66+
} else {
67+
return @enumFromInt(int_value);
68+
}
69+
}
70+
71+
test "getMaxEnumSize" {
72+
const PlainEnum = enum { foo, bar };
73+
const U8Enum = enum(u8) { foo = 1, bar = 2 };
74+
const U16Enum = enum(u16) { foo, bar };
75+
76+
try std.testing.expectEqual(2, getMaxEnumSize(PlainEnum)); // u1 + header
77+
try std.testing.expectEqual(2, getMaxEnumSize(U8Enum)); // u8 + header
78+
try std.testing.expectEqual(3, getMaxEnumSize(U16Enum)); // u16 + header
79+
}
80+
81+
test "getEnumSize" {
82+
const U8Enum = enum(u8) { foo = 0, bar = 150 };
83+
84+
try std.testing.expectEqual(1, getEnumSize(U8Enum, .foo)); // fits in positive fixint
85+
try std.testing.expectEqual(2, getEnumSize(U8Enum, .bar)); // requires u8 format
86+
}
87+
88+
test "pack/unpack enum" {
89+
const PlainEnum = enum { foo, bar };
90+
const U8Enum = enum(u8) { foo = 1, bar = 2 };
91+
const U16Enum = enum(u16) { alpha = 1000, beta = 2000 };
92+
93+
// Test plain enum
94+
{
95+
var buffer = std.ArrayList(u8).init(std.testing.allocator);
96+
defer buffer.deinit();
97+
98+
try packEnum(buffer.writer(), PlainEnum, .bar);
99+
100+
var stream = std.io.fixedBufferStream(buffer.items);
101+
const result = try unpackEnum(stream.reader(), PlainEnum);
102+
try std.testing.expectEqual(PlainEnum.bar, result);
103+
}
104+
105+
// Test enum(u8)
106+
{
107+
var buffer = std.ArrayList(u8).init(std.testing.allocator);
108+
defer buffer.deinit();
109+
110+
try packEnum(buffer.writer(), U8Enum, .bar);
111+
112+
var stream = std.io.fixedBufferStream(buffer.items);
113+
const result = try unpackEnum(stream.reader(), U8Enum);
114+
try std.testing.expectEqual(U8Enum.bar, result);
115+
}
116+
117+
// Test enum(u16)
118+
{
119+
var buffer = std.ArrayList(u8).init(std.testing.allocator);
120+
defer buffer.deinit();
121+
122+
try packEnum(buffer.writer(), U16Enum, .alpha);
123+
124+
var stream = std.io.fixedBufferStream(buffer.items);
125+
const result = try unpackEnum(stream.reader(), U16Enum);
126+
try std.testing.expectEqual(U16Enum.alpha, result);
127+
}
128+
}
129+
130+
131+
test "enum edge cases" {
132+
// Test enum with explicit and auto values
133+
const MixedEnum = enum(u8) {
134+
first = 10,
135+
second, // auto-assigned to 11
136+
third = 20,
137+
fourth, // auto-assigned to 21
138+
};
139+
140+
var buffer = std.ArrayList(u8).init(std.testing.allocator);
141+
defer buffer.deinit();
142+
143+
try packEnum(buffer.writer(), MixedEnum, .second);
144+
145+
var stream = std.io.fixedBufferStream(buffer.items);
146+
const result = try unpackEnum(stream.reader(), MixedEnum);
147+
try std.testing.expectEqual(MixedEnum.second, result);
148+
try std.testing.expectEqual(11, @intFromEnum(result));
149+
}
150+
151+
test "optional enum" {
152+
const TestEnum = enum(u8) { foo = 1, bar = 2 };
153+
const OptionalEnum = ?TestEnum;
154+
155+
// Test non-null optional enum
156+
{
157+
var buffer = std.ArrayList(u8).init(std.testing.allocator);
158+
defer buffer.deinit();
159+
160+
const value: OptionalEnum = .bar;
161+
try packEnum(buffer.writer(), OptionalEnum, value);
162+
163+
var stream = std.io.fixedBufferStream(buffer.items);
164+
const result = try unpackEnum(stream.reader(), OptionalEnum);
165+
try std.testing.expectEqual(@as(OptionalEnum, .bar), result);
166+
}
167+
168+
// Test null optional enum
169+
{
170+
var buffer = std.ArrayList(u8).init(std.testing.allocator);
171+
defer buffer.deinit();
172+
173+
const value: OptionalEnum = null;
174+
try packEnum(buffer.writer(), OptionalEnum, value);
175+
176+
var stream = std.io.fixedBufferStream(buffer.items);
177+
const result = try unpackEnum(stream.reader(), OptionalEnum);
178+
try std.testing.expectEqual(@as(OptionalEnum, null), result);
179+
}
180+
}
181+
182+
test "getEnumSize with optional" {
183+
const TestEnum = enum(u8) { foo = 0, bar = 150 };
184+
const OptionalEnum = ?TestEnum;
185+
186+
// Test non-null optional enum size
187+
const value: OptionalEnum = .bar;
188+
try std.testing.expectEqual(2, getEnumSize(OptionalEnum, value)); // requires u8 format
189+
190+
// Test null optional enum size
191+
const null_value: OptionalEnum = null;
192+
try std.testing.expectEqual(1, getEnumSize(OptionalEnum, null_value)); // size of null
193+
}

src/msgpack.zig

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ pub const UnionAsMapOptions = @import("union.zig").UnionAsMapOptions;
6565
pub const packUnion = @import("union.zig").packUnion;
6666
pub const unpackUnion = @import("union.zig").unpackUnion;
6767

68+
pub const getEnumSize = @import("enum.zig").getEnumSize;
69+
pub const getMaxEnumSize = @import("enum.zig").getMaxEnumSize;
70+
pub const packEnum = @import("enum.zig").packEnum;
71+
pub const unpackEnum = @import("enum.zig").unpackEnum;
72+
6873
pub const packAny = @import("any.zig").packAny;
6974
pub const unpackAny = @import("any.zig").unpackAny;
7075

@@ -144,6 +149,10 @@ pub fn Packer(comptime Writer: type) type {
144149
return packUnion(self.writer, @TypeOf(value), value);
145150
}
146151

152+
pub fn writeEnum(self: Self, value: anytype) !void {
153+
return packEnum(self.writer, @TypeOf(value), value);
154+
}
155+
147156
pub fn write(self: Self, value: anytype) !void {
148157
return packAny(self.writer, value);
149158
}
@@ -232,6 +241,10 @@ pub fn Unpacker(comptime Reader: type) type {
232241
return unpackUnion(self.reader, self.allocator, T);
233242
}
234243

244+
pub fn readEnum(self: Self, comptime T: type) !T {
245+
return unpackEnum(self.reader, T);
246+
}
247+
235248
pub fn read(self: Self, comptime T: type) !T {
236249
return unpackAny(self.reader, self.allocator, T);
237250
}
@@ -304,3 +317,60 @@ test "encode/decode" {
304317
try std.testing.expectEqualStrings("John", decoded.value.name);
305318
try std.testing.expectEqual(20, decoded.value.age);
306319
}
320+
321+
test "encode/decode enum" {
322+
const Status = enum(u8) { pending = 1, active = 2, inactive = 3 };
323+
const PlainEnum = enum { foo, bar, baz };
324+
325+
// Test enum(u8)
326+
{
327+
var buffer = std.ArrayList(u8).init(std.testing.allocator);
328+
defer buffer.deinit();
329+
330+
try encode(Status.active, buffer.writer());
331+
332+
const decoded = try decodeFromSlice(Status, std.testing.allocator, buffer.items);
333+
defer decoded.deinit();
334+
335+
try std.testing.expectEqual(Status.active, decoded.value);
336+
}
337+
338+
// Test plain enum
339+
{
340+
var buffer = std.ArrayList(u8).init(std.testing.allocator);
341+
defer buffer.deinit();
342+
343+
try encode(PlainEnum.bar, buffer.writer());
344+
345+
const decoded = try decodeFromSlice(PlainEnum, std.testing.allocator, buffer.items);
346+
defer decoded.deinit();
347+
348+
try std.testing.expectEqual(PlainEnum.bar, decoded.value);
349+
}
350+
351+
// Test optional enum with null
352+
{
353+
var buffer = std.ArrayList(u8).init(std.testing.allocator);
354+
defer buffer.deinit();
355+
356+
try encode(@as(?Status, null), buffer.writer());
357+
358+
const decoded = try decodeFromSlice(?Status, std.testing.allocator, buffer.items);
359+
defer decoded.deinit();
360+
361+
try std.testing.expectEqual(@as(?Status, null), decoded.value);
362+
}
363+
364+
// Test optional enum with value
365+
{
366+
var buffer = std.ArrayList(u8).init(std.testing.allocator);
367+
defer buffer.deinit();
368+
369+
try encode(@as(?Status, .pending), buffer.writer());
370+
371+
const decoded = try decodeFromSlice(?Status, std.testing.allocator, buffer.items);
372+
defer decoded.deinit();
373+
374+
try std.testing.expectEqual(@as(?Status, .pending), decoded.value);
375+
}
376+
}

0 commit comments

Comments
 (0)