Compare commits
No commits in common. "84b8be4b25049f0372f958679fdea7d384194026" and "569d7c877383c6d1235beb2ed882a9949cd03b69" have entirely different histories.
84b8be4b25
...
569d7c8773
1 changed files with 18 additions and 233 deletions
251
src/srf.zig
251
src/srf.zig
|
|
@ -345,145 +345,6 @@ pub const Record = struct {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn OwnedRecord(comptime T: type) type {
|
|
||||||
return struct {
|
|
||||||
fields_buf: [fields_len]Field,
|
|
||||||
fields_allocated: [fields_len]bool = .{false} ** fields_len,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
source_value: T,
|
|
||||||
cached_record: ?Record = null,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
const fields_len = std.meta.fields(T).len;
|
|
||||||
|
|
||||||
pub const SourceType = T;
|
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, source: T) Self {
|
|
||||||
return .{
|
|
||||||
// SAFETY: fields_buf is set by record() and is guarded by fields_set
|
|
||||||
.fields_buf = undefined,
|
|
||||||
.allocator = allocator,
|
|
||||||
.source_value = source,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormatResult = struct {
|
|
||||||
value: ?Value,
|
|
||||||
allocated: bool = false,
|
|
||||||
};
|
|
||||||
fn setField(
|
|
||||||
self: *Self,
|
|
||||||
inx: usize,
|
|
||||||
comptime field_name: []const u8,
|
|
||||||
comptime field_type: type,
|
|
||||||
comptime default_value_ptr: ?*const anyopaque,
|
|
||||||
) !usize {
|
|
||||||
const val = @field(self.source_value, field_name);
|
|
||||||
if (default_value_ptr) |d| {
|
|
||||||
const default_val: *const field_type = @ptrCast(@alignCast(d));
|
|
||||||
if (std.meta.eql(val, default_val.*)) return inx;
|
|
||||||
}
|
|
||||||
const value = try self.formatField(field_type, field_name, val);
|
|
||||||
self.fields_buf[inx] = .{
|
|
||||||
.key = field_name,
|
|
||||||
.value = value.value,
|
|
||||||
};
|
|
||||||
self.fields_allocated[inx] = value.allocated;
|
|
||||||
return inx + 1;
|
|
||||||
}
|
|
||||||
fn formatField(self: Self, comptime field_type: type, comptime field_name: []const u8, val: anytype) !FormatResult {
|
|
||||||
const ti = @typeInfo(field_type);
|
|
||||||
switch (ti) {
|
|
||||||
.optional => |o| {
|
|
||||||
if (val) |v|
|
|
||||||
return self.formatField(o.child, field_name, v);
|
|
||||||
return .{ .value = null };
|
|
||||||
},
|
|
||||||
.pointer => |p| {
|
|
||||||
// We don't have an allocator, so the only thing we can do
|
|
||||||
// here is manage []const u8 or []u8
|
|
||||||
if (p.size != .slice or p.child != u8)
|
|
||||||
return error.CoercionNotPossible;
|
|
||||||
return .{ .value = .{ .string = val } };
|
|
||||||
},
|
|
||||||
.type, .void, .noreturn => return error.CoercionNotPossible,
|
|
||||||
.comptime_float, .comptime_int, .undefined, .null, .error_union => return error.CoercionNotPossible,
|
|
||||||
.error_set, .@"fn", .@"opaque", .frame => return error.CoercionNotPossible,
|
|
||||||
.@"anyframe", .vector, .enum_literal => return error.CoercionNotPossible,
|
|
||||||
.int => return .{ .value = .{ .number = @floatFromInt(val) } },
|
|
||||||
.float => return .{ .value = .{ .number = @floatCast(val) } },
|
|
||||||
.bool => return .{ .value = .{ .boolean = val } },
|
|
||||||
.@"enum" => return .{ .value = .{ .string = @tagName(val) } },
|
|
||||||
.array => return error.NotImplemented,
|
|
||||||
.@"struct", .@"union" => {
|
|
||||||
if (std.meta.hasMethod(field_type, "srfFormat")) {
|
|
||||||
return .{
|
|
||||||
.value = field_type.srfFormat(self.allocator, field_name) catch |e| {
|
|
||||||
log.err(
|
|
||||||
"custom format of field {s} failed : {}",
|
|
||||||
.{ field_name, e },
|
|
||||||
);
|
|
||||||
return error.CustomFormatFailed;
|
|
||||||
},
|
|
||||||
.allocated = true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return error.CoercionNotPossible;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn record(self: *Self) !Record {
|
|
||||||
if (self.cached_record) |r| return r;
|
|
||||||
var inx: usize = 0;
|
|
||||||
const ti = @typeInfo(SourceType);
|
|
||||||
switch (ti) {
|
|
||||||
.@"struct" => |info| {
|
|
||||||
inline for (info.fields) |f|
|
|
||||||
inx = try self.setField(inx, f.name, f.type, f.default_value_ptr);
|
|
||||||
},
|
|
||||||
.@"union" => |info| {
|
|
||||||
inline for (info.fields) |f|
|
|
||||||
inx = try self.setField(inx, f.name, f.type, null);
|
|
||||||
},
|
|
||||||
.@"enum" => |info| {
|
|
||||||
inline for (info.fields) |f|
|
|
||||||
inx = try self.setField(inx, f.name, self.SourceType, null);
|
|
||||||
},
|
|
||||||
.error_set => return error.ErrorSetNotSupported,
|
|
||||||
else => @compileError("Expected struct, union, error set or enum type, found '" ++ @typeName(T) ++ "'"),
|
|
||||||
}
|
|
||||||
self.cached_record = .{ .fields = self.fields_buf[0..inx] };
|
|
||||||
return self.cached_record.?;
|
|
||||||
}
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
for (0..fields_len) |i| {
|
|
||||||
if (self.fields_allocated[i]) {
|
|
||||||
if (self.fields_buf[i].value) |v| switch (v) {
|
|
||||||
.string => |s| self.allocator.free(s),
|
|
||||||
.bytes => |b| self.allocator.free(b),
|
|
||||||
else => {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/// Create an OwnedRecord from a Zig struct value. Fields are mapped by name:
|
|
||||||
/// string/optional string fields become string Values, numeric fields become
|
|
||||||
/// number Values, bools become boolean Values, and enums are converted via
|
|
||||||
/// @tagName. Struct/union fields with a `srfFormat` method use that for
|
|
||||||
/// custom serialization (allocated via the provided allocator).
|
|
||||||
///
|
|
||||||
/// The returned OwnedRecord borrows string data from `val` for any
|
|
||||||
/// []const u8 fields. The caller must ensure `val` (and any data it
|
|
||||||
/// references) outlives the OwnedRecord.
|
|
||||||
///
|
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Coerce Record to a type. Does not handle fields with arrays
|
/// Coerce Record to a type. Does not handle fields with arrays
|
||||||
pub fn to(self: Record, comptime T: type) !T {
|
pub fn to(self: Record, comptime T: type) !T {
|
||||||
// SAFETY: all fields updated below or error is returned
|
// SAFETY: all fields updated below or error is returned
|
||||||
|
|
@ -498,7 +359,7 @@ pub const Record = struct {
|
||||||
if (type_field.default_value_ptr) |ptr| {
|
if (type_field.default_value_ptr) |ptr| {
|
||||||
@field(obj, type_field.name) = @as(*const type_field.type, @ptrCast(@alignCast(ptr))).*;
|
@field(obj, type_field.name) = @as(*const type_field.type, @ptrCast(@alignCast(ptr))).*;
|
||||||
} else {
|
} 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});
|
log.err("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 error.FieldNotFoundOnFieldWithoutDefaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -604,38 +465,7 @@ pub const FormatOptions = struct {
|
||||||
|
|
||||||
/// Returns a formatter that formats the given value
|
/// Returns a formatter that formats the given value
|
||||||
pub fn fmt(value: []const Record, options: FormatOptions) Formatter {
|
pub fn fmt(value: []const Record, options: FormatOptions) Formatter {
|
||||||
return .{ .value = value, .options = options };
|
return Formatter{ .value = value, .options = options };
|
||||||
}
|
|
||||||
/// Returns a formatter that formats the given value. This will take a concrete
|
|
||||||
/// type, convert it to the SRF record format automatically (using srfFormat if
|
|
||||||
/// found), and output to the writer. It is recommended to use a FixedBufferAllocator
|
|
||||||
/// for the allocator, which is only used for custom srfFormat functions (I think - what about enum tag names?)
|
|
||||||
pub fn fmtFrom(comptime T: type, allocator: std.mem.Allocator, value: []const T, options: FormatOptions) FromFormatter(T) {
|
|
||||||
return .{ .value = value, .options = options, .allocator = allocator };
|
|
||||||
}
|
|
||||||
pub fn FromFormatter(comptime T: type) type {
|
|
||||||
return struct {
|
|
||||||
value: []const T,
|
|
||||||
options: FormatOptions,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
pub fn format(self: Self, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
|
||||||
try frontMatter(writer, self.options);
|
|
||||||
var first = true;
|
|
||||||
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
|
|
||||||
return std.Io.Writer.Error.WriteFailed;
|
|
||||||
defer owned_record.deinit();
|
|
||||||
const record = owned_record.record() catch return std.Io.Writer.Error.WriteFailed;
|
|
||||||
try writer.print("{f}\n", .{Record.fmt(record, self.options)});
|
|
||||||
}
|
|
||||||
try epilogue(writer, self.options);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
test fmt {
|
test fmt {
|
||||||
const records: []const Record = &.{
|
const records: []const Record = &.{
|
||||||
|
|
@ -654,33 +484,26 @@ test fmt {
|
||||||
\\
|
\\
|
||||||
, formatted);
|
, formatted);
|
||||||
}
|
}
|
||||||
fn frontMatter(writer: *std.Io.Writer, options: FormatOptions) !void {
|
|
||||||
try writer.writeAll("#!srfv1\n");
|
|
||||||
if (options.long_format)
|
|
||||||
try writer.writeAll("#!long\n");
|
|
||||||
if (options.emit_eof)
|
|
||||||
try writer.writeAll("#!requireeof\n");
|
|
||||||
if (options.expires) |e|
|
|
||||||
try writer.print("#!expires={d}\n", .{e});
|
|
||||||
}
|
|
||||||
fn epilogue(writer: *std.Io.Writer, options: FormatOptions) !void {
|
|
||||||
if (options.emit_eof)
|
|
||||||
try writer.writeAll("#!eof\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const Formatter = struct {
|
pub const Formatter = struct {
|
||||||
value: []const Record,
|
value: []const Record,
|
||||||
options: FormatOptions,
|
options: FormatOptions,
|
||||||
|
|
||||||
pub fn format(self: Formatter, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
pub fn format(self: Formatter, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||||
try frontMatter(writer, self.options);
|
try writer.writeAll("#!srfv1\n");
|
||||||
|
if (self.options.long_format)
|
||||||
|
try writer.writeAll("#!long\n");
|
||||||
|
if (self.options.emit_eof)
|
||||||
|
try writer.writeAll("#!requireeof\n");
|
||||||
|
if (self.options.expires) |e|
|
||||||
|
try writer.print("#!expires={d}\n", .{e});
|
||||||
var first = true;
|
var first = true;
|
||||||
for (self.value) |record| {
|
for (self.value) |record| {
|
||||||
if (!first and self.options.long_format) try writer.writeByte('\n');
|
if (!first and self.options.long_format) try writer.writeByte('\n');
|
||||||
first = false;
|
first = false;
|
||||||
try writer.print("{f}\n", .{Record.fmt(record, self.options)});
|
try writer.print("{f}\n", .{Record.fmt(record, self.options)});
|
||||||
}
|
}
|
||||||
try epilogue(writer, self.options);
|
if (self.options.emit_eof)
|
||||||
|
try writer.writeAll("#!eof\n");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
pub const RecordFormatter = struct {
|
pub const RecordFormatter = struct {
|
||||||
|
|
@ -703,7 +526,7 @@ pub const RecordFormatter = struct {
|
||||||
try writer.writeByte(':');
|
try writer.writeByte(':');
|
||||||
try writer.writeAll(s);
|
try writer.writeAll(s);
|
||||||
},
|
},
|
||||||
.number => |n| try writer.print("num:{d}", .{@as(f64, @floatCast(n))}),
|
.number => |n| try writer.print("num:{d}", .{n}),
|
||||||
.boolean => |b| try writer.print("bool:{}", .{b}),
|
.boolean => |b| try writer.print("bool:{}", .{b}),
|
||||||
.bytes => |b| try writer.print("binary:{b64}", .{b}),
|
.bytes => |b| try writer.print("binary:{b64}", .{b}),
|
||||||
}
|
}
|
||||||
|
|
@ -1166,12 +989,6 @@ test "serialize/deserialize" {
|
||||||
if (std.mem.eql(u8, "hi", val)) return .{};
|
if (std.mem.eql(u8, "hi", val)) return .{};
|
||||||
return error.ValueNotEqualHi;
|
return error.ValueNotEqualHi;
|
||||||
}
|
}
|
||||||
pub fn srfFormat(allocator: std.mem.Allocator, comptime field_name: []const u8) !Value {
|
|
||||||
_ = field_name;
|
|
||||||
return .{
|
|
||||||
.string = try allocator.dupe(u8, "hi"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Data = struct {
|
const Data = struct {
|
||||||
|
|
@ -1183,6 +1000,12 @@ test "serialize/deserialize" {
|
||||||
custom: ?Custom = null,
|
custom: ?Custom = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// var buf: [4096]u8 = undefined;
|
||||||
|
// const compact = try std.fmt.bufPrint(
|
||||||
|
// &buf,
|
||||||
|
// "{f}",
|
||||||
|
// .{fmt(records, .{})},
|
||||||
|
// );
|
||||||
const compact =
|
const compact =
|
||||||
\\#!srfv1
|
\\#!srfv1
|
||||||
\\foo::bar,foo:null:,foo:binary:YmFy,foo:num:42,bar:num:42
|
\\foo::bar,foo:null:,foo:binary:YmFy,foo:num:42,bar:num:42
|
||||||
|
|
@ -1206,44 +1029,6 @@ test "serialize/deserialize" {
|
||||||
try std.testing.expectEqual(@as(RecType, .bar), rec4.qux.?);
|
try std.testing.expectEqual(@as(RecType, .bar), rec4.qux.?);
|
||||||
try std.testing.expectEqual(true, rec4.b);
|
try std.testing.expectEqual(true, rec4.b);
|
||||||
try std.testing.expectEqual(@as(f32, 6.9), rec4.f);
|
try std.testing.expectEqual(@as(f32, 6.9), rec4.f);
|
||||||
|
|
||||||
const alloc = std.testing.allocator;
|
|
||||||
var owned_record_1 = try Record.from(Data, alloc, rec1);
|
|
||||||
defer owned_record_1.deinit();
|
|
||||||
const record_1 = try owned_record_1.record();
|
|
||||||
try std.testing.expectEqual(@as(usize, 2), record_1.fields.len);
|
|
||||||
var owned_record_4 = try Record.from(Data, alloc, rec4);
|
|
||||||
defer owned_record_4.deinit();
|
|
||||||
try std.testing.expectEqual(std.meta.fields(Data).len, owned_record_4.fields_buf.len);
|
|
||||||
const record_4 = try owned_record_4.record();
|
|
||||||
// const Data = struct {
|
|
||||||
// foo: []const u8,
|
|
||||||
// bar: u8,
|
|
||||||
// qux: ?RecType = .foo,
|
|
||||||
// b: bool = false,
|
|
||||||
// f: f32 = 4.2,
|
|
||||||
// custom: ?Custom = null,
|
|
||||||
// };
|
|
||||||
try std.testing.expectEqual(@as(usize, 6), record_4.fields.len);
|
|
||||||
|
|
||||||
const all_data: []const Data = &.{
|
|
||||||
.{ .foo = "hi", .bar = 42, .qux = .bar, .b = true, .f = 6.0, .custom = .{} },
|
|
||||||
.{ .foo = "bar", .bar = 69 },
|
|
||||||
};
|
|
||||||
var buf: [4096]u8 = undefined;
|
|
||||||
const compact_from = try std.fmt.bufPrint(
|
|
||||||
&buf,
|
|
||||||
"{f}",
|
|
||||||
.{fmtFrom(Data, alloc, all_data, .{})},
|
|
||||||
);
|
|
||||||
|
|
||||||
const expect =
|
|
||||||
\\#!srfv1
|
|
||||||
\\foo::hi,bar:num:42,qux::bar,b:bool:true,f:num:6,custom::hi
|
|
||||||
\\foo::bar,bar:num:69
|
|
||||||
\\
|
|
||||||
;
|
|
||||||
try std.testing.expectEqualStrings(expect, compact_from);
|
|
||||||
}
|
}
|
||||||
test "compact format length-prefixed string as last field" {
|
test "compact format length-prefixed string as last field" {
|
||||||
// When a length-prefixed value is the last field on the line,
|
// When a length-prefixed value is the last field on the line,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue