Compare commits

...

8 Commits

Author SHA1 Message Date
0f4a9fcaa7
misc: commit things at random 2023-11-23 18:38:03 -08:00
bd079b42d9
compile with zig master
I was actually anticipating a bit more stdlib breakage than this, so I
ended up just shimming it. Well, it works and also still works with
0.11.0, which is cool.
2023-11-23 18:37:19 -08:00
bd0d74ee6a
examples.reify: add default value field 2023-11-23 17:56:27 -08:00
2208079355
parser.Options: embellish expect_enum_dot description
This affects tagged union parsing, and that should be mentioned here.
So now it is.
2023-11-23 17:55:47 -08:00
98eac68929
value: simplify list conversion code
There was really no reason to use ArrayLists here when the list length
is known ahead of time. This slightly shortens the code and should be
slightly more memory/stack efficient.
2023-11-23 17:54:14 -08:00
39619e7d6b
value: fix use of parseFloat 2023-11-23 17:52:38 -08:00
33ab092a06
value: store strings/scalars as null-terminated
Since these were already always copied from the source data, this was a
very easy change to implement. This makes our output schema string
detection a bit stricter, and saves performing a copy in the case that
the output string needs to be 0 terminated.

Unfortunately, we can't skip copies in the general slice case since
each child element needs to get converted to the appropriate type.
2023-11-23 17:52:38 -08:00
21a9753d46
parser: change omitted value behavior to work with all default values
Special casing optional values was a little odd before. Now, the user
can supply a default value for any field that may be omitted from the
serialized data. This behaves the same way as the stdlib JSON parser
as well.
2023-11-23 17:47:21 -08:00
5 changed files with 69 additions and 47 deletions

View File

@ -2,4 +2,11 @@
.name = "nice-data",
.version = "0.1.0-pre",
.dependencies = .{},
.paths = .{
"src",
"build.zig",
"build.zig.zon",
"license",
"readme.md",
},
}

View File

@ -21,6 +21,7 @@ const Example = struct {
again: ?bool,
array: [5]i16,
nested: [3]struct { index: usize, title: []const u8 },
default: u64 = 0xDEADCAFE,
};
const source =
@ -98,5 +99,6 @@ pub fn main() !void {
std.debug.print(" {{ index: {d}, title: {s} }}\n", .{ item.index, item.title });
}
std.debug.print(" ]\n", .{});
std.debug.print(" default: 0x{X}\n", .{loaded.value.default});
std.debug.print("}}\n", .{});
}

View File

@ -61,11 +61,11 @@ pub const Options = struct {
ignore_extra_fields: bool = true,
// Only used by the parseTo family of functions.
// If true, if a struct field is an optional type and the corresponding mapping key
// does not exist, the object field will be set to `null`. By default, if the
// parsed document is missing a mapping key for a given field, an error will be
// raised instead.
treat_omitted_as_null: bool = false,
// If true, if a struct field has a default value associated with it and the
// corresponding mapping key does not exist, the object field will be set to the
// default value. By default, this behavior is enabled, allowing succinct
// representation of objects that have default fields.
allow_omitting_default_values: bool = true,
// Only used by the parseTo family of functions.
// If true, strings may be coerced into other scalar types, like booleans or
@ -99,7 +99,9 @@ pub const Options = struct {
// look like source code enum literals. Any enum value missing the leading `.` will
// result in a conversion error. If set to false, no preprocessing will be done
// and enum values will be converted from the literal scalar/string. These two styles
// cannot be mixed in a single document.
// cannot be mixed in a single document. Note that this setting also affects how
// tagged unions are parsed (specifically, the union's field name must also have the
// leading `.` if this option is enabled.)
expect_enum_dot: bool = true,
// Only used by the parseTo family of functions.

View File

@ -70,7 +70,7 @@ pub const State = struct {
},
.value => switch (state.value_stack.getLast().*) {
// we have an in-progress string, finish it.
.string => |*string| string.* = try state.string_builder.toOwnedSlice(arena_alloc),
.string => |*string| string.* = try state.string_builder.toOwnedSliceSentinel(arena_alloc, 0),
// if we have a dangling -, attach an empty scalar to it
.list => |*list| if (state.expect_shift == .indent) try list.append(Value.emptyScalar()),
// if we have a dangling "key:", attach an empty scalar to it
@ -185,7 +185,7 @@ pub const State = struct {
if (firstpass and line.shift == .dedent) {
// copy the string into the document proper
string.* = try state.string_builder.toOwnedSlice(arena_alloc);
string.* = try state.string_builder.toOwnedSliceSentinel(arena_alloc, 0);
var dedent_depth = line.shift.dedent;
while (dedent_depth > 0) : (dedent_depth -= 1)
@ -199,9 +199,9 @@ pub const State = struct {
.in_line => |in_line| switch (in_line) {
.empty => unreachable,
inline .line_string, .space_string, .concat_string => |str, tag| {
if (tag == .line_string)
if (comptime tag == .line_string)
try state.string_builder.append(arena_alloc, '\n');
if (tag == .space_string)
if (comptime tag == .space_string)
try state.string_builder.append(arena_alloc, ' ');
try state.string_builder.appendSlice(arena_alloc, str);
},

View File

@ -9,6 +9,11 @@
// CONDITIONS OF ANY KIND, either express or implied.
const std = @import("std");
const hasFn = if (@hasDecl(std.meta, "trait")) struct {
fn hasFn(comptime T: type, comptime name: []const u8) bool {
return std.meta.trait.hasFn(name)(T);
}
}.hasFn else std.meta.hasFn;
const Options = @import("../parser.zig").Options;
@ -51,7 +56,7 @@ pub fn Parsed(comptime T: type) type {
}
pub const Value = union(enum) {
pub const String = []const u8;
pub const String = [:0]const u8;
pub const Map = std.StringArrayHashMap(Value);
pub const List = std.ArrayList(Value);
pub const TagType = @typeInfo(Value).Union.tag_type.?;
@ -106,7 +111,7 @@ pub const Value = union(enum) {
switch (self) {
inline .scalar, .string => |str, tag| {
if (tag == .string and !options.coerce_strings) return error.BadValue;
return try std.fmt.parseFloat(T, str, 0);
return try std.fmt.parseFloat(T, str);
},
else => return error.BadValue,
}
@ -120,27 +125,29 @@ pub const Value = union(enum) {
// probably be solved in the zig stdlib or similar.
switch (self) {
.scalar, .string => |str| {
if (ptr.child == u8) {
if (ptr.sentinel) |sent| {
var copy = try allocator.allocSentinel(u8, str.len, @as(*const u8, @ptrCast(sent)).*);
@memcpy(copy, str);
return copy;
}
if (comptime ptr.child == u8) {
if (comptime ptr.sentinel) |sentinel|
if (comptime @as(*align(1) const ptr.child, @ptrCast(sentinel)).* != 0)
return error.BadValue;
return str;
} else {
return error.BadValue;
}
},
.list, .inline_list => |lst| {
var result = try std.ArrayList(ptr.child).initCapacity(allocator, lst.items.len);
errdefer result.deinit();
for (lst.items) |item| {
result.appendAssumeCapacity(try item.convertTo(ptr.child, allocator, options));
const result = try allocator.alloc(ptr.child, lst.items.len + @intFromBool(ptr.sentinel != null));
for (result[0..lst.items.len], lst.items) |*res, item| {
res.* = try item.convertTo(ptr.child, allocator, options);
}
if (ptr.sentinel) |sent| {
return try result.toOwnedSliceSentinel(@as(*align(1) const ptr.child, @ptrCast(sent)).*);
if (comptime ptr.sentinel) |sentinel| {
const sval = @as(*align(1) const ptr.child, @ptrCast(sentinel)).*;
result[lst.items.len] = sval;
return result[0..lst.items.len :sval];
} else {
return try result.toOwnedSlice();
return result;
}
},
else => return error.BadValue,
@ -152,7 +159,7 @@ pub const Value = union(enum) {
result.* = try self.convertTo(ptr.child, allocator, options);
return result;
},
else => @compileError("Cannot deserialize into many-pointer or c-pointer " ++ @typeName(T)), // do not support many or C item pointers.
else => @compileError("Cannot deserialize into many-pointer or c-pointer " ++ @typeName(T)),
},
.Array => |arr| {
// TODO: There is ambiguity here because a document expecting a list
@ -169,21 +176,19 @@ pub const Value = union(enum) {
} else return error.BadValue;
},
.list, .inline_list => |lst| {
var storage = try std.ArrayList(arr.child).initCapacity(allocator, arr.len);
defer storage.deinit();
for (lst.items) |item| {
storage.appendAssumeCapacity(try item.convertTo(arr.child, allocator, options));
}
// this may result in a big stack allocation, which is not ideal
if (lst.items.len != arr.len) return error.BadValue;
var result: T = undefined;
@memcpy(&result, storage.items);
for (&result, lst.items) |*res, item| {
res.* = try item.convertTo(arr.child, allocator, options);
}
return result;
},
else => return error.BadValue,
}
},
.Struct => |stt| {
if (comptime std.meta.trait.hasFn("deserializeNice")(T))
if (comptime hasFn(T, "deserializeNice"))
return T.deserializeNice(self, allocator, options);
if (stt.is_tuple) {
@ -191,8 +196,8 @@ pub const Value = union(enum) {
.list, .inline_list => |list| {
if (list.items.len != stt.fields.len) return error.BadValue;
var result: T = undefined;
inline for (stt.fields, 0..) |field, idx| {
result[idx] = try list.items[idx].convertTo(field.type, allocator, options);
inline for (stt.fields, &result, list.items) |field, *res, item| {
res.* = try item.convertTo(field.type, allocator, options);
}
return result;
},
@ -208,24 +213,30 @@ pub const Value = union(enum) {
inline for (stt.fields) |field| {
if (map.get(field.name)) |value| {
@field(result, field.name) = try value.convertTo(field.type, allocator, options);
} else if (options.treat_omitted_as_null and @typeInfo(field.type) == .Optional) {
@field(result, field.name) = null;
} else if (options.allow_omitting_default_values) {
if (comptime field.default_value) |def|
@field(result, field.name) = @as(*align(1) const field.type, @ptrCast(def)).*
else
return error.BadValue;
} else {
return error.BadValue;
}
}
} else {
// we could iterate over each map key and do an exhaustive
// comparison with each struct field name. This would save
// memory and it would probably be a fair amount faster for
// small structs.
// TODO: consider not cloning the map here. This would
// result in the requirement that the raw value object
// not be used after it has been converted to a type,
// based on the parse options.
var clone = try map.clone();
defer clone.deinit();
inline for (stt.fields) |field| {
if (clone.fetchSwapRemove(field.name)) |kv| {
@field(result, field.name) = try kv.value.convertTo(field.type, allocator, options);
} else if (options.treat_omitted_as_null and @typeInfo(field.type) == .Optional) {
@field(result, field.name) = null;
} else if (options.allow_omitting_default_values) {
if (comptime field.default_value) |def|
@field(result, field.name) = @as(*align(1) const field.type, @ptrCast(def)).*
else
return error.BadValue;
} else return error.BadValue;
}
// there were extra fields in the data
@ -238,7 +249,7 @@ pub const Value = union(enum) {
}
},
.Enum => {
if (comptime std.meta.trait.hasFn("deserializeNice")(T))
if (comptime hasFn(T, "deserializeNice"))
return T.deserializeNice(self, allocator, options);
switch (self) {
@ -263,7 +274,7 @@ pub const Value = union(enum) {
}
},
.Union => |unn| {
if (comptime std.meta.trait.hasFn("deserializeNice")(T))
if (comptime hasFn(T, "deserializeNice"))
return T.deserializeNice(self, allocator, options);
if (unn.tag_type == null) @compileError("Cannot deserialize into untagged union " ++ @typeName(T));
@ -342,7 +353,7 @@ pub const Value = union(enum) {
}
inline fn _fromScalarOrString(alloc: std.mem.Allocator, comptime classification: TagType, input: []const u8) !Value {
return @unionInit(Value, @tagName(classification), try alloc.dupe(u8, input));
return @unionInit(Value, @tagName(classification), try alloc.dupeZ(u8, input));
}
pub inline fn emptyScalar() Value {