introduce iterator version of to()
All checks were successful
Generic zig build / build (push) Successful in 34s

This commit is contained in:
Emil Lerch 2026-03-09 09:45:21 -07:00
parent bb4a5e0bf3
commit 8e12b7396a
Signed by: lobo
GPG key ID: A7B62D657EF764F8

View file

@ -250,29 +250,6 @@ pub const Field = struct {
value: ?Value,
};
// A record has a list of fields, with no assumptions regarding duplication,
// etc. This is for parsing speed, but also for more flexibility in terms of
// use cases. One can make a defacto array out of this structure by having
// something like:
//
// arr:string:foo
// arr:string:bar
//
// and when you coerce to zig struct have an array .arr that gets populated
// with strings "foo" and "bar".
pub const Record = struct {
fields: []const Field,
pub fn fmt(value: Record, options: FormatOptions) RecordFormatter {
return .{ .value = value, .options = options };
}
pub fn firstFieldByName(self: Record, field_name: []const u8) ?Field {
for (self.fields) |f|
if (std.mem.eql(u8, f.key, field_name)) return f;
return null;
}
fn coerce(name: []const u8, comptime T: type, val: ?Value) !T {
// Here's the deduplicated set of field types that coerce needs to handle:
// Direct from SRF values:
@ -330,6 +307,29 @@ pub const Record = struct {
return null;
}
// A record has a list of fields, with no assumptions regarding duplication,
// etc. This is for parsing speed, but also for more flexibility in terms of
// use cases. One can make a defacto array out of this structure by having
// something like:
//
// arr:string:foo
// arr:string:bar
//
// and when you coerce to zig struct have an array .arr that gets populated
// with strings "foo" and "bar".
pub const Record = struct {
fields: []const Field,
pub fn fmt(value: Record, options: FormatOptions) RecordFormatter {
return .{ .value = value, .options = options };
}
pub fn firstFieldByName(self: Record, field_name: []const u8) ?Field {
for (self.fields) |f|
if (std.mem.eql(u8, f.key, field_name)) return f;
return null;
}
fn maxFields(comptime T: type) usize {
const ti = @typeInfo(T);
if (ti != .@"union") return std.meta.fields(T).len;
@ -766,8 +766,84 @@ pub const RecordIterator = struct {
}
return field;
}
};
/// Coerce Record to a type. Does not handle fields with arrays
pub fn to(self: FieldIterator, comptime T: type) !T {
const ti = @typeInfo(T);
switch (ti) {
.@"struct" => {
// What is this magic? The FieldEnum creates a type (an enum)
// where each enum member has the name of a field in the struct
//
// So... struct { a: u8, b: u8 } will yield enum { a, b }
const FieldEnum = std.meta.FieldEnum(T);
// Then...EnumFieldStruct will create a struct from this, where
// each enum value becomes a field. We will specify the field
// type, and the default value. Combining these two calls gets
// us a struct with all the same field names, but we get a chance
// to make all the fields boolean, so we can use it to track
// which fields have been set
var found: std.enums.EnumFieldStruct(FieldEnum, bool, false) = .{};
// SAFETY: all fields updated below or error is returned
var obj: T = undefined;
while (try self.next()) |f| {
inline for (std.meta.fields(T)) |type_field| {
// To replicate the behavior of the record version of to,
// we need to only take the first version of the field,
// so if it's specified twice in the data, we will ignore
// all but the first instance
if (std.mem.eql(u8, f.key, type_field.name) and
!@field(found, type_field.name))
{
@field(obj, type_field.name) =
try coerce(type_field.name, type_field.type, f.value);
// Now account for this in our magic found struct...
@field(found, type_field.name) = true;
}
}
}
// Fill in the defaults for remaining fields. Throw if anything
// is missing both value (from above) and default (from here)
inline for (std.meta.fields(T)) |type_field| {
if (!@field(found, type_field.name)) {
// We did not find this field above...revert to default value
if (type_field.default_value_ptr) |ptr| {
@field(obj, type_field.name) = @as(*const type_field.type, @ptrCast(@alignCast(ptr))).*;
} else {
log.debug("Record could not be coerced. Field {s} not found on srf data, and no default value exists on the type", .{type_field.name});
return error.FieldNotFoundOnFieldWithoutDefaultValue;
}
}
}
return obj;
},
.@"union" => {
const active_tag_name = if (@hasDecl(T, "srf_tag_field"))
T.srf_tag_field
else
"active_tag";
const first_try = try self.next();
if (first_try == null) return error.ActiveTagFieldNotFound;
const f = first_try.?;
if (!std.mem.eql(u8, f.key, active_tag_name))
return error.ActiveTagNotFirstField; // required here, but not on the Record version of to
if (f.value == null or f.value.? != .string)
return error.ActiveTagValueMustBeAString;
const active_tag = f.value.?.string;
inline for (std.meta.fields(T)) |field_type| {
if (std.mem.eql(u8, active_tag, field_type.name)) {
return @unionInit(T, field_type.name, try self.to(field_type.type));
}
}
return error.ActiveTagDoesNotExist;
},
else => @compileError("Deserialization not supported on " ++ @tagName(ti) ++ " types"),
}
return error.CoercionNotPossible;
}
};
pub fn deinit(self: RecordIterator) void {
const child_allocator = self.arena.child_allocator;
self.arena.deinit();
@ -1351,6 +1427,23 @@ test "serialize/deserialize" {
try std.testing.expectEqual(true, rec4.b);
try std.testing.expectEqual(@as(f32, 6.9), rec4.f);
// Now we'll do it with the iterator version
var it_reader = std.Io.Reader.fixed(compact);
const ri = try iterator(&it_reader, std.testing.allocator, .{});
defer ri.deinit();
const rec1_it = try (try ri.next()).?.to(Data);
try std.testing.expectEqualStrings("bar", rec1_it.foo);
try std.testing.expectEqual(@as(u8, 42), rec1_it.bar);
try std.testing.expectEqual(@as(RecType, .foo), rec1_it.qux);
_ = try ri.next();
_ = try ri.next();
const rec4_it = try (try ri.next()).?.to(Data);
try std.testing.expectEqualStrings("bar", rec4_it.foo);
try std.testing.expectEqual(@as(u8, 42), rec4_it.bar);
try std.testing.expectEqual(@as(RecType, .bar), rec4_it.qux.?);
try std.testing.expectEqual(true, rec4_it.b);
try std.testing.expectEqual(@as(f32, 6.9), rec4_it.f);
const alloc = std.testing.allocator;
var owned_record_1 = try Record.from(Data, alloc, rec1);
defer owned_record_1.deinit();