Compare commits
3 Commits
0028092a4e
...
1683197bc0
Author | SHA1 | Date | |
---|---|---|---|
1683197bc0 | |||
c5e8921eb2 | |||
34ec58e0d2 |
@ -3,7 +3,8 @@ const std = @import("std");
|
|||||||
const buffers = @import("./linebuffer.zig");
|
const buffers = @import("./linebuffer.zig");
|
||||||
const tokenizer = @import("./tokenizer.zig");
|
const tokenizer = @import("./tokenizer.zig");
|
||||||
const State = @import("./parser/state.zig").State;
|
const State = @import("./parser/state.zig").State;
|
||||||
pub const Document = @import("./parser/state.zig").Document;
|
pub const Document = @import("./parser/value.zig").Document;
|
||||||
|
pub const Parsed = @import("./parser/value.zig").Parsed;
|
||||||
pub const Value = @import("./parser/value.zig").Value;
|
pub const Value = @import("./parser/value.zig").Value;
|
||||||
|
|
||||||
pub const Diagnostics = struct {
|
pub const Diagnostics = struct {
|
||||||
@ -40,9 +41,62 @@ pub const Options = struct {
|
|||||||
// resulting document root object. The default behavior is to emit an error if the
|
// resulting document root object. The default behavior is to emit an error if the
|
||||||
// document is empty.
|
// document is empty.
|
||||||
default_object: enum { string, list, map, fail } = .fail,
|
default_object: enum { string, list, map, fail } = .fail,
|
||||||
|
|
||||||
|
// Only used by the parseTo family of functions.
|
||||||
|
// If false, and a mapping contains additional keys that do not map to the fields of
|
||||||
|
// the corresponding object, an error will be raised. By default, additional keys
|
||||||
|
// will be skipped and no error will be raised. Note that tagged unions must be
|
||||||
|
// represented by a map with a single key, and having more than one key will always
|
||||||
|
// be an error, even if this option is set to true.
|
||||||
|
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,
|
||||||
|
|
||||||
|
// Only used by the parseTo family of functions.
|
||||||
|
// If true, strings may be coerced into other scalar types, like booleans or
|
||||||
|
// numbers. By default, only document scalar fields will attempt to coerce to
|
||||||
|
// non-string values.
|
||||||
|
coerce_strings: bool = false,
|
||||||
|
|
||||||
|
// Only used by the parseTo family of functions.
|
||||||
|
// Two lists of strings. Truthy strings will be parsed to boolean true. Falsy
|
||||||
|
// strings will be parsed to boolean false. All other strings will raise an
|
||||||
|
// error.
|
||||||
|
boolean_strings: struct { truthy: []const []const u8, falsy: []const []const u8 } = .{
|
||||||
|
.truthy = &.{ "true", "True", "yes", "on" },
|
||||||
|
.falsy = &.{ "false", "False", "no", "off" },
|
||||||
|
},
|
||||||
|
|
||||||
|
null_strings: []const []const u8 = &.{ "null", "nil", "None" },
|
||||||
|
|
||||||
|
// Only used by the parseTo family of functions.
|
||||||
|
// If true, document scalars that appear to be numbers will attempt to convert into
|
||||||
|
// enum values as an integer. By default, all enums in the document must be
|
||||||
|
// specified by name, not by numeric value. Note that conversion by name will always
|
||||||
|
// be tried first, even if this option is enabled, so if you're stupid enough to do:
|
||||||
|
//
|
||||||
|
// const Horrible = enum {
|
||||||
|
// @"1" = 0,
|
||||||
|
// @"0" = 1,
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// then you deserve what you get. And what you'll get is confusing results.
|
||||||
|
// Also note that this option does not apply to tagged unions, despite those being
|
||||||
|
// backed by possibly ordered enums.
|
||||||
|
allow_numeric_enums: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn parseBuffer(allocator: std.mem.Allocator, buffer: []const u8, diagnostics: *Diagnostics, options: Options) !Document {
|
pub fn parseBuffer(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
buffer: []const u8,
|
||||||
|
diagnostics: *Diagnostics,
|
||||||
|
options: Options,
|
||||||
|
) !Document {
|
||||||
var state = State.init(allocator, diagnostics);
|
var state = State.init(allocator, diagnostics);
|
||||||
defer state.deinit();
|
defer state.deinit();
|
||||||
errdefer state.document.deinit();
|
errdefer state.document.deinit();
|
||||||
@ -59,6 +113,17 @@ pub fn parseBuffer(allocator: std.mem.Allocator, buffer: []const u8, diagnostics
|
|||||||
return try state.finish(options);
|
return try state.finish(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parseBufferTo(
|
||||||
|
comptime T: type,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
buffer: []const u8,
|
||||||
|
diagnostics: *Diagnostics,
|
||||||
|
options: Options,
|
||||||
|
) !Parsed(T) {
|
||||||
|
var doc = try parseBuffer(allocator, buffer, diagnostics, options);
|
||||||
|
return try doc.convertTo(T, options);
|
||||||
|
}
|
||||||
|
|
||||||
pub const StreamParser = struct {
|
pub const StreamParser = struct {
|
||||||
linetok: tokenizer.LineTokenizer(buffers.ValidatingLineBuffer),
|
linetok: tokenizer.LineTokenizer(buffers.ValidatingLineBuffer),
|
||||||
parse_state: State,
|
parse_state: State,
|
||||||
|
@ -5,28 +5,9 @@ const Error = @import("../parser.zig").Error;
|
|||||||
const DuplicateKeyBehavior = @import("../parser.zig").DuplicateKeyBehavior;
|
const DuplicateKeyBehavior = @import("../parser.zig").DuplicateKeyBehavior;
|
||||||
const Options = @import("../parser.zig").Options;
|
const Options = @import("../parser.zig").Options;
|
||||||
const Diagnostics = @import("../parser.zig").Diagnostics;
|
const Diagnostics = @import("../parser.zig").Diagnostics;
|
||||||
|
const Document = @import("./value.zig").Document;
|
||||||
const Value = @import("./value.zig").Value;
|
const Value = @import("./value.zig").Value;
|
||||||
|
|
||||||
pub const Document = struct {
|
|
||||||
arena: std.heap.ArenaAllocator,
|
|
||||||
root: Value,
|
|
||||||
|
|
||||||
pub fn init(alloc: std.mem.Allocator) Document {
|
|
||||||
return .{
|
|
||||||
.arena = std.heap.ArenaAllocator.init(alloc),
|
|
||||||
.root = undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn printDebug(self: Document) void {
|
|
||||||
return self.root.printDebug();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: Document) void {
|
|
||||||
self.arena.deinit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const FlowParseState = enum {
|
const FlowParseState = enum {
|
||||||
want_list_item,
|
want_list_item,
|
||||||
consuming_list_item,
|
consuming_list_item,
|
||||||
@ -61,7 +42,7 @@ pub const State = struct {
|
|||||||
self.value_stack.deinit();
|
self.value_stack.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finish(state: *State, options: Options) Error!Document {
|
pub fn finish(state: *State, options: Options) !Document {
|
||||||
const arena_alloc = state.document.arena.allocator();
|
const arena_alloc = state.document.arena.allocator();
|
||||||
|
|
||||||
switch (state.mode) {
|
switch (state.mode) {
|
||||||
@ -95,7 +76,7 @@ pub const State = struct {
|
|||||||
return state.document;
|
return state.document;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parseLine(state: *State, line: tokenizer.Line, dkb: DuplicateKeyBehavior) Error!void {
|
pub fn parseLine(state: *State, line: tokenizer.Line, dkb: DuplicateKeyBehavior) !void {
|
||||||
if (line.contents == .comment) return;
|
if (line.contents == .comment) return;
|
||||||
|
|
||||||
// this gives us a second loop when the stack tip changes (i.e. during dedent or
|
// this gives us a second loop when the stack tip changes (i.e. during dedent or
|
||||||
@ -444,7 +425,7 @@ pub const State = struct {
|
|||||||
contents: []const u8,
|
contents: []const u8,
|
||||||
root_type: Value.TagType,
|
root_type: Value.TagType,
|
||||||
dkb: DuplicateKeyBehavior,
|
dkb: DuplicateKeyBehavior,
|
||||||
) Error!Value {
|
) !Value {
|
||||||
const arena_alloc = state.document.arena.allocator();
|
const arena_alloc = state.document.arena.allocator();
|
||||||
|
|
||||||
var root: Value = switch (root_type) {
|
var root: Value = switch (root_type) {
|
||||||
@ -462,7 +443,7 @@ pub const State = struct {
|
|||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
|
|
||||||
// used to distinguish betwen [] and [ ], and it also tracks
|
// used to distinguish between [] and [ ], and it also tracks
|
||||||
// a continuous value between different states
|
// a continuous value between different states
|
||||||
var item_start: usize = 0;
|
var item_start: usize = 0;
|
||||||
var dangling_key: ?[]const u8 = null;
|
var dangling_key: ?[]const u8 = null;
|
||||||
@ -474,6 +455,7 @@ pub const State = struct {
|
|||||||
' ', '\t' => continue :charloop,
|
' ', '\t' => continue :charloop,
|
||||||
',' => {
|
',' => {
|
||||||
// empty value
|
// empty value
|
||||||
|
// don't check for whitespace here: [ , ] is okay, as is [ , , ]
|
||||||
const tip = try state.getStackTip();
|
const tip = try state.getStackTip();
|
||||||
try tip.flow_list.append(Value.newScalar(arena_alloc));
|
try tip.flow_list.append(Value.newScalar(arena_alloc));
|
||||||
item_start = idx + 1;
|
item_start = idx + 1;
|
||||||
@ -518,9 +500,18 @@ pub const State = struct {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
.consuming_list_item => switch (char) {
|
.consuming_list_item => switch (char) {
|
||||||
|
// consider: detecting trailing whitespace. "[ 1 ]" should
|
||||||
|
// produce "1" and not "1 " as it currently does, which breaks
|
||||||
|
// the principle of least astonishment. design: no trailing
|
||||||
|
// whitespace before "," and only a single space is allowed before "]"
|
||||||
',' => {
|
',' => {
|
||||||
const tip = try state.getStackTip();
|
if (contents[idx - 1] == ' ' or contents[idx - 1] == '\t') {
|
||||||
|
state.diagnostics.length = 1;
|
||||||
|
state.diagnostics.message = "the flow list contains whitespace before ,";
|
||||||
|
return error.TrailingWhitespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tip = try state.getStackTip();
|
||||||
try tip.flow_list.append(
|
try tip.flow_list.append(
|
||||||
try Value.fromScalar(arena_alloc, contents[item_start..idx]),
|
try Value.fromScalar(arena_alloc, contents[item_start..idx]),
|
||||||
);
|
);
|
||||||
@ -529,13 +520,23 @@ pub const State = struct {
|
|||||||
pstate = .want_list_item;
|
pstate = .want_list_item;
|
||||||
},
|
},
|
||||||
']' => {
|
']' => {
|
||||||
|
var end = idx;
|
||||||
|
if (contents[idx - 1] == ' ' or contents[idx - 1] == '\t') {
|
||||||
|
if (idx > 1 and (contents[idx - 2] == ' ' or contents[idx - 2] == '\t')) {
|
||||||
|
state.diagnostics.length = 1;
|
||||||
|
state.diagnostics.message = "the flow list contains extra whitespace before ]";
|
||||||
|
return error.TrailingWhitespace;
|
||||||
|
}
|
||||||
|
end = idx - 1;
|
||||||
|
}
|
||||||
|
|
||||||
const finished = state.value_stack.getLastOrNull() orelse {
|
const finished = state.value_stack.getLastOrNull() orelse {
|
||||||
state.diagnostics.length = 1;
|
state.diagnostics.length = 1;
|
||||||
state.diagnostics.message = "the flow list was closed too many times";
|
state.diagnostics.message = "the flow list was closed too many times";
|
||||||
return error.BadState;
|
return error.BadState;
|
||||||
};
|
};
|
||||||
try finished.flow_list.append(
|
try finished.flow_list.append(
|
||||||
try Value.fromScalar(arena_alloc, contents[item_start..idx]),
|
try Value.fromScalar(arena_alloc, contents[item_start..end]),
|
||||||
);
|
);
|
||||||
pstate = try state.popFlowStack();
|
pstate = try state.popFlowStack();
|
||||||
},
|
},
|
||||||
@ -577,6 +578,11 @@ pub const State = struct {
|
|||||||
},
|
},
|
||||||
.consuming_map_key => switch (char) {
|
.consuming_map_key => switch (char) {
|
||||||
':' => {
|
':' => {
|
||||||
|
if (contents[idx - 1] == ' ' or contents[idx - 1] == '\t') {
|
||||||
|
state.diagnostics.length = 1;
|
||||||
|
state.diagnostics.message = "the flow map contains whitespace before :";
|
||||||
|
return error.TrailingWhitespace;
|
||||||
|
}
|
||||||
dangling_key = try arena_alloc.dupe(u8, contents[item_start..idx]);
|
dangling_key = try arena_alloc.dupe(u8, contents[item_start..idx]);
|
||||||
pstate = .want_map_value;
|
pstate = .want_map_value;
|
||||||
},
|
},
|
||||||
@ -644,7 +650,12 @@ pub const State = struct {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
.consuming_map_value => switch (char) {
|
.consuming_map_value => switch (char) {
|
||||||
',', '}' => |term| {
|
',' => {
|
||||||
|
if (contents[idx - 1] == ' ' or contents[idx - 1] == '\t') {
|
||||||
|
state.diagnostics.length = 1;
|
||||||
|
state.diagnostics.message = "the flow map contains whitespace before ,";
|
||||||
|
return error.TrailingWhitespace;
|
||||||
|
}
|
||||||
const tip = try state.getStackTip();
|
const tip = try state.getStackTip();
|
||||||
try state.putMap(
|
try state.putMap(
|
||||||
&tip.flow_map,
|
&tip.flow_map,
|
||||||
@ -654,7 +665,27 @@ pub const State = struct {
|
|||||||
);
|
);
|
||||||
dangling_key = null;
|
dangling_key = null;
|
||||||
pstate = .want_map_key;
|
pstate = .want_map_key;
|
||||||
if (term == '}') pstate = try state.popFlowStack();
|
},
|
||||||
|
'}' => {
|
||||||
|
var end = idx;
|
||||||
|
if (contents[idx - 1] == ' ' or contents[idx - 1] == '\t') {
|
||||||
|
if (idx > 1 and (contents[idx - 2] == ' ' or contents[idx - 2] == '\t')) {
|
||||||
|
state.diagnostics.length = 1;
|
||||||
|
state.diagnostics.message = "the flow map contains extra whitespace before }";
|
||||||
|
return error.TrailingWhitespace;
|
||||||
|
}
|
||||||
|
end = idx - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tip = try state.getStackTip();
|
||||||
|
try state.putMap(
|
||||||
|
&tip.flow_map,
|
||||||
|
dangling_key.?,
|
||||||
|
try Value.fromScalar(arena_alloc, contents[item_start..end]),
|
||||||
|
dkb,
|
||||||
|
);
|
||||||
|
dangling_key = null;
|
||||||
|
pstate = try state.popFlowStack();
|
||||||
},
|
},
|
||||||
else => continue :charloop,
|
else => continue :charloop,
|
||||||
},
|
},
|
||||||
@ -687,7 +718,7 @@ pub const State = struct {
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn getStackTip(state: State) Error!*Value {
|
inline fn getStackTip(state: State) !*Value {
|
||||||
if (state.value_stack.items.len == 0) return {
|
if (state.value_stack.items.len == 0) return {
|
||||||
state.diagnostics.length = 1;
|
state.diagnostics.length = 1;
|
||||||
state.diagnostics.message = "this document contains an unexpected bottom of the stack";
|
state.diagnostics.message = "this document contains an unexpected bottom of the stack";
|
||||||
@ -696,7 +727,7 @@ pub const State = struct {
|
|||||||
return state.value_stack.items[state.value_stack.items.len - 1];
|
return state.value_stack.items[state.value_stack.items.len - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn popFlowStack(state: *State) Error!FlowParseState {
|
inline fn popFlowStack(state: *State) !FlowParseState {
|
||||||
if (state.value_stack.popOrNull() == null) {
|
if (state.value_stack.popOrNull() == null) {
|
||||||
state.diagnostics.length = 1;
|
state.diagnostics.length = 1;
|
||||||
state.diagnostics.message = "this document contains an unexpected bottom of the stack";
|
state.diagnostics.message = "this document contains an unexpected bottom of the stack";
|
||||||
@ -711,16 +742,16 @@ pub const State = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn appendListGetValue(list: *Value.List, value: Value) Error!*Value {
|
inline fn appendListGetValue(list: *Value.List, value: Value) !*Value {
|
||||||
try list.append(value);
|
try list.append(value);
|
||||||
return &list.items[list.items.len - 1];
|
return &list.items[list.items.len - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn putMap(state: *State, map: *Value.Map, key: []const u8, value: Value, dkb: DuplicateKeyBehavior) Error!void {
|
inline fn putMap(state: *State, map: *Value.Map, key: []const u8, value: Value, dkb: DuplicateKeyBehavior) !void {
|
||||||
_ = try state.putMapGetValue(map, key, value, dkb);
|
_ = try state.putMapGetValue(map, key, value, dkb);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fn putMapGetValue(state: *State, map: *Value.Map, key: []const u8, value: Value, dkb: DuplicateKeyBehavior) Error!*Value {
|
inline fn putMapGetValue(state: *State, map: *Value.Map, key: []const u8, value: Value, dkb: DuplicateKeyBehavior) !*Value {
|
||||||
const gop = try map.getOrPut(key);
|
const gop = try map.getOrPut(key);
|
||||||
|
|
||||||
if (gop.found_existing)
|
if (gop.found_existing)
|
||||||
|
@ -1,5 +1,45 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Options = @import("../parser.zig").Options;
|
||||||
|
|
||||||
|
pub const Document = struct {
|
||||||
|
arena: std.heap.ArenaAllocator,
|
||||||
|
root: Value,
|
||||||
|
|
||||||
|
pub fn init(alloc: std.mem.Allocator) Document {
|
||||||
|
return .{
|
||||||
|
.arena = std.heap.ArenaAllocator.init(alloc),
|
||||||
|
.root = undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn convertTo(self: *Document, comptime T: type, options: Options) !Parsed(T) {
|
||||||
|
return .{
|
||||||
|
.value = try self.root.convertTo(T, self.arena.allocator(), options),
|
||||||
|
.arena = self.arena,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn printDebug(self: Document) void {
|
||||||
|
return self.root.printDebug();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Document) void {
|
||||||
|
self.arena.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn Parsed(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
value: T,
|
||||||
|
arena: std.heap.ArenaAllocator,
|
||||||
|
|
||||||
|
pub fn deinit(self: @This()) void {
|
||||||
|
self.arena.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub const Value = union(enum) {
|
pub const Value = union(enum) {
|
||||||
pub const String = std.ArrayList(u8);
|
pub const String = std.ArrayList(u8);
|
||||||
pub const Map = std.StringArrayHashMap(Value);
|
pub const Map = std.StringArrayHashMap(Value);
|
||||||
@ -13,6 +53,219 @@ pub const Value = union(enum) {
|
|||||||
map: Map,
|
map: Map,
|
||||||
flow_map: Map,
|
flow_map: Map,
|
||||||
|
|
||||||
|
pub fn convertTo(self: Value, comptime T: type, allocator: std.mem.Allocator, options: Options) !T {
|
||||||
|
switch (@typeInfo(T)) {
|
||||||
|
.Void => {
|
||||||
|
switch (self) {
|
||||||
|
.scalar => |str| return if (str.items.len == 0) void{} else error.BadValue,
|
||||||
|
.string => |str| return if (options.coerce_strings and str.items.len == 0) void{} else error.BadValue,
|
||||||
|
else => return error.BadValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Bool => {
|
||||||
|
switch (self) {
|
||||||
|
inline .scalar, .string => |str, tag| {
|
||||||
|
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
||||||
|
for (options.boolean_strings.truthy) |check|
|
||||||
|
if (std.mem.eql(u8, str.items, check)) return true;
|
||||||
|
for (options.boolean_strings.falsy) |check|
|
||||||
|
if (std.mem.eql(u8, str.items, check)) return false;
|
||||||
|
|
||||||
|
return error.BadValue;
|
||||||
|
},
|
||||||
|
else => return error.BadValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Int, .ComptimeInt => {
|
||||||
|
switch (self) {
|
||||||
|
inline .scalar, .string => |str, tag| {
|
||||||
|
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
||||||
|
std.debug.print("'{s}'\n", .{str.items});
|
||||||
|
return try std.fmt.parseInt(T, str.items, 0);
|
||||||
|
},
|
||||||
|
else => return error.BadValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Float, .ComptimeFloat => {
|
||||||
|
switch (self) {
|
||||||
|
inline .scalar, .string => |str, tag| {
|
||||||
|
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
||||||
|
return try std.fmt.parseFloat(T, str.items, 0);
|
||||||
|
},
|
||||||
|
else => return error.BadValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Pointer => |ptr| switch (ptr.size) {
|
||||||
|
.Slice => {
|
||||||
|
// TODO: There is ambiguity here because a document expecting a list
|
||||||
|
// of u8 could parse a string instead. Introduce a special
|
||||||
|
// type to use for this? the problem is that it becomes
|
||||||
|
// invasive into downstream code. Ultimately this should
|
||||||
|
// probably be solved in the zig stdlib or similar.
|
||||||
|
// TODO: This also doesn't handle sentinels properly.
|
||||||
|
switch (self) {
|
||||||
|
.scalar, .string => |str| return if (ptr.child == u8) str.items else error.BadValue,
|
||||||
|
.list, .flow_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));
|
||||||
|
}
|
||||||
|
return result.toOwnedSlice();
|
||||||
|
},
|
||||||
|
else => return error.BadValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.One => {
|
||||||
|
const result = try allocator.create(ptr.child);
|
||||||
|
errdefer allocator.destroy(result);
|
||||||
|
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.
|
||||||
|
},
|
||||||
|
.Array => |arr| {
|
||||||
|
// TODO: There is ambiguity here because a document expecting a list
|
||||||
|
// of u8 could parse a string instead. Introduce a special
|
||||||
|
// type to use for this? the problem is that it becomes
|
||||||
|
// invasive into downstream code. Ultimately this should
|
||||||
|
// probably be solved in the zig stdlib or similar.
|
||||||
|
// TODO: This also doesn't handle sentinels properly.
|
||||||
|
switch (self) {
|
||||||
|
.scalar, .string => |str| {
|
||||||
|
if (arr.child == u8 and str.items.len == arr.len) {
|
||||||
|
var result: T = undefined;
|
||||||
|
@memcpy(&result, str.items);
|
||||||
|
return result;
|
||||||
|
} else return error.BadValue;
|
||||||
|
},
|
||||||
|
.list, .flow_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
|
||||||
|
var result: T = undefined;
|
||||||
|
@memcpy(&result, storage.items);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
else => return error.BadValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Struct => |stt| {
|
||||||
|
if (comptime std.meta.trait.hasFn("deserializeNice")(T))
|
||||||
|
return T.deserializeNice(self, allocator, options);
|
||||||
|
|
||||||
|
if (stt.is_tuple) {
|
||||||
|
switch (self) {
|
||||||
|
.list, .flow_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);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
else => return error.BadValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (self) {
|
||||||
|
.map, .flow_map => |map| {
|
||||||
|
var result: T = undefined;
|
||||||
|
|
||||||
|
if (options.ignore_extra_fields) {
|
||||||
|
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 {
|
||||||
|
std.debug.print("{s}\n", .{field.name});
|
||||||
|
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.
|
||||||
|
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 return error.BadValue;
|
||||||
|
}
|
||||||
|
// there were extra fields in the data
|
||||||
|
if (clone.count() > 0) return error.BadValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
else => return error.BadValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Enum => {
|
||||||
|
if (comptime std.meta.trait.hasFn("deserializeNice")(T))
|
||||||
|
return T.deserializeNice(self, allocator, options);
|
||||||
|
|
||||||
|
switch (self) {
|
||||||
|
inline .scalar, .string => |str, tag| {
|
||||||
|
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
||||||
|
if (std.meta.stringToEnum(T, str.items)) |value| return value;
|
||||||
|
if (options.allow_numeric_enums) {
|
||||||
|
const parsed = std.fmt.parseInt(@typeInfo(T).Enum.tag_type, str.items, 10) catch
|
||||||
|
return error.BadValue;
|
||||||
|
return std.meta.intToEnum(T, parsed) catch error.BadValue;
|
||||||
|
}
|
||||||
|
return error.BadValue;
|
||||||
|
},
|
||||||
|
else => return error.BadValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Union => |unn| {
|
||||||
|
if (comptime std.meta.trait.hasFn("deserializeNice")(T))
|
||||||
|
return T.deserializeNice(self, allocator, options);
|
||||||
|
|
||||||
|
if (unn.tag_type == null) @compileError("Cannot deserialize into untagged union " ++ @typeName(T));
|
||||||
|
|
||||||
|
switch (self) {
|
||||||
|
.map, .flow_map => |map| {
|
||||||
|
// a union may not ever be deserialized from a map with more than one value
|
||||||
|
if (map.count() != 1) return error.BadValue;
|
||||||
|
const key = map.keys()[0];
|
||||||
|
inline for (unn.fields) |field| {
|
||||||
|
if (std.mem.eql(u8, key, field.name))
|
||||||
|
return @unionInit(T, field.name, try map.get(key).?.convertTo(field.type, allocator, options));
|
||||||
|
}
|
||||||
|
return error.BadValue;
|
||||||
|
},
|
||||||
|
// TODO: if the field is a 0 width type like void, we could parse it
|
||||||
|
// directly from a scalar/string value (i.e. a name with no
|
||||||
|
// corresponding value)
|
||||||
|
else => return error.BadValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Optional => |opt| {
|
||||||
|
switch (self) {
|
||||||
|
inline .scalar, .string => |str, tag| {
|
||||||
|
if (tag == .string and !options.coerce_strings) return error.BadValue;
|
||||||
|
for (options.null_strings) |check|
|
||||||
|
if (std.mem.eql(u8, str.items, check)) return null;
|
||||||
|
|
||||||
|
return try self.convertTo(opt.child, allocator, options);
|
||||||
|
},
|
||||||
|
else => return error.BadValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => @compileError("Cannot deserialize into unsupported type " ++ @typeName(T)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub inline fn fromScalar(alloc: std.mem.Allocator, input: []const u8) !Value {
|
pub inline fn fromScalar(alloc: std.mem.Allocator, input: []const u8) !Value {
|
||||||
return try _fromScalarOrString(alloc, .scalar, input);
|
return try _fromScalarOrString(alloc, .scalar, input);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user