Compare commits

...

3 commits

Author SHA1 Message Date
12b755660e
allow outputting default values
All checks were successful
Generic zig build / build (push) Successful in 30s
2026-05-25 13:13:36 -07:00
a99adb0b29
handle commas in compact form 2026-05-25 13:03:59 -07:00
19246e5f83
switch active_tag default to type, which makes more sense from user perspective 2026-05-25 13:02:41 -07:00

View file

@ -412,6 +412,7 @@ pub const Record = struct {
fields_allocated: [fields_len]bool = .{false} ** fields_len,
allocator: std.mem.Allocator,
source_value: T,
format_options: FormatOptions,
cached_record: ?Record = null,
const Self = @This();
@ -419,12 +420,13 @@ pub const Record = struct {
pub const SourceType = T;
pub fn init(allocator: std.mem.Allocator, source: T) Self {
pub fn init(allocator: std.mem.Allocator, source: T, options: FormatOptions) Self {
return .{
// SAFETY: fields_buf is set by record() and is guarded by fields_set
.fields_buf = undefined,
.allocator = allocator,
.source_value = source,
.format_options = options,
};
}
@ -442,7 +444,7 @@ pub const Record = struct {
) !usize {
if (default_value_ptr) |d| {
const default_val: *const field_type = @ptrCast(@alignCast(d));
if (std.meta.eql(val, default_val.*)) return inx;
if (!self.format_options.emit_default_values and std.meta.eql(val, default_val.*)) return inx;
}
const value = try self.formatField(field_type, field_name, val);
self.fields_buf[inx] = .{
@ -512,7 +514,7 @@ pub const Record = struct {
const key = if (@hasDecl(U, "srf_tag_field"))
U.srf_tag_field
else
"active_tag";
"type";
self.fields_buf[inx] = .{
.key = key,
.value = .{ .string = active_tag_name },
@ -562,7 +564,13 @@ pub const Record = struct {
///
/// Call `deinit()` to free any allocations made for custom-formatted fields.
pub fn from(comptime T: type, allocator: std.mem.Allocator, val: T) !OwnedRecord(T) {
return OwnedRecord(T).init(allocator, val);
return OwnedRecord(T).init(allocator, val, .{});
}
/// Internal function to allow an OwnedRecord to see format options necessary
/// to emit default values
fn fromWithOptions(comptime T: type, allocator: std.mem.Allocator, val: T, options: FormatOptions) !OwnedRecord(T) {
return OwnedRecord(T).init(allocator, val, options);
}
/// Coerce a `Record` to a Zig struct or tagged union. For each field in `T`,
@ -573,7 +581,7 @@ pub const Record = struct {
/// first value silently ignored.
///
/// For tagged unions, the active variant is determined by a field named
/// `"active_tag"` (or the value of `T.srf_tag_field` if declared). The
/// `"type"` (or the value of `T.srf_tag_field` if declared). The
/// remaining fields are coerced into the payload struct of that variant.
///
/// For streaming data without collecting fields first, prefer
@ -607,7 +615,7 @@ pub const Record = struct {
const active_tag_name = if (@hasDecl(T, "srf_tag_field"))
T.srf_tag_field
else
"active_tag";
"type";
if (self.firstFieldByName(active_tag_name)) |srf_field| {
if (srf_field.value == null or srf_field.value.? != .string)
return error.ActiveTagValueMustBeAString;
@ -900,7 +908,7 @@ pub const RecordIterator = struct {
///
/// For tagged unions, the active tag field must appear first in the
/// stream (unlike `Record.to` which can do random access). The tag
/// field name defaults to `"active_tag"` or `T.srf_tag_field` if
/// field name defaults to `"type"` or `T.srf_tag_field` if
/// declared.
pub fn to(self: FieldIterator, comptime T: type) !T {
const ti = @typeInfo(T);
@ -957,7 +965,7 @@ pub const RecordIterator = struct {
const active_tag_name = if (@hasDecl(T, "srf_tag_field"))
T.srf_tag_field
else
"active_tag";
"type";
const first_try = try self.next();
if (first_try == null) return error.ActiveTagFieldNotFound;
const f = first_try.?;
@ -1118,6 +1126,9 @@ pub const FormatOptions = struct {
/// and just format the record. This is useful for appending to an existing
/// srf file rather than overwriting all the data
emit_directives: bool = true,
/// When set to true, this will output all values, even if they are the default values
emit_default_values: bool = false,
};
/// Returns a `Formatter` for writing pre-built `Record` values to a writer.
@ -1150,7 +1161,12 @@ pub fn FromFormatter(comptime T: type) type {
for (self.value) |item| {
if (!first and self.options.long_format) try writer.writeByte('\n');
first = false;
var owned_record = Record.from(T, self.allocator, item) catch
var owned_record = Record.fromWithOptions(
T,
self.allocator,
item,
self.options,
) catch
return std.Io.Writer.Error.WriteFailed;
defer owned_record.deinit();
const record = owned_record.record() catch return std.Io.Writer.Error.WriteFailed;
@ -1226,8 +1242,9 @@ pub const RecordFormatter = struct {
switch (f.value.?) {
.string => |s| {
const newlines = std.mem.containsAtLeastScalar(u8, s, 1, '\n');
const commas = !self.options.long_format and std.mem.containsAtLeastScalar(u8, s, 1, ',');
// Output the count if newlines exist
const count = if (newlines) s.len else null;
const count = if (newlines or commas) s.len else null;
if (count) |c| try writer.print("{d}", .{c});
try writer.writeByte(':');
try writer.writeAll(s);
@ -1842,8 +1859,8 @@ test "unions" {
);
const expect =
\\#!srfv1
\\active_tag::foo,number:num:42,true_or_false:bool:true
\\active_tag::bar,sentence::foobar,decimal:num:6.9
\\type::foo,number:num:42,true_or_false:bool:true
\\type::bar,sentence::foobar,decimal:num:6.9
\\
;
try std.testing.expectEqualStrings(expect, compact_from);
@ -2016,6 +2033,48 @@ test fmtFrom {
\\
, result);
}
test "fmtFrom commas" {
// Example: serialize typed Zig values directly to SRF format.
const Data = struct {
name: []const u8 = "bob",
age: u8,
};
const values: []const Data = &.{
.{ .name = "alice, yo", .age = 30 },
};
var buf: [4096]u8 = undefined;
const result = try std.fmt.bufPrint(
&buf,
"{f}",
.{fmtFrom(Data, std.testing.allocator, values, .{})},
);
try std.testing.expectEqualStrings(
\\#!srfv1
\\name:9:alice, yo,age:num:30
\\
, result);
}
test "fmtFrom outputs defaults with option" {
// Example: serialize typed Zig values directly to SRF format.
const Data = struct {
name: []const u8 = "bob",
age: u8,
};
const values: []const Data = &.{
.{ .age = 30 },
};
var buf: [4096]u8 = undefined;
const result = try std.fmt.bufPrint(
&buf,
"{f}",
.{fmtFrom(Data, std.testing.allocator, values, .{ .emit_default_values = true })},
);
try std.testing.expectEqualStrings(
\\#!srfv1
\\name::bob,age:num:30
\\
, result);
}
test "parse with diagnostics" {
// Example: batch parsing collects all records and fields into slices.
// Prefer `iterator` for streaming; use `parse` when random access to