mirror of
https://github.com/ziglang/zig.git
synced 2026-01-03 03:53:20 +00:00
std: json.parseFromValue() (#15981)
This commit is contained in:
parent
d2b2567119
commit
0f2339f55b
@ -1,6 +1,8 @@
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const testing = std.testing;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const ObjectMap = @import("dynamic.zig").ObjectMap;
|
||||
const Array = @import("dynamic.zig").Array;
|
||||
@ -9,6 +11,8 @@ const Value = @import("dynamic.zig").Value;
|
||||
const parseFromSlice = @import("static.zig").parseFromSlice;
|
||||
const parseFromSliceLeaky = @import("static.zig").parseFromSliceLeaky;
|
||||
const parseFromTokenSource = @import("static.zig").parseFromTokenSource;
|
||||
const parseFromValueLeaky = @import("static.zig").parseFromValueLeaky;
|
||||
const ParseOptions = @import("static.zig").ParseOptions;
|
||||
|
||||
const jsonReader = @import("scanner.zig").reader;
|
||||
|
||||
@ -240,3 +244,48 @@ test "Value.jsonStringify" {
|
||||
try testing.expectEqualSlices(u8, fbs.getWritten(), "{\"a\":\"b\"}");
|
||||
}
|
||||
}
|
||||
|
||||
test "polymorphic parsing" {
|
||||
if (true) return error.SkipZigTest; // See https://github.com/ziglang/zig/issues/16108
|
||||
const doc =
|
||||
\\{ "type": "div",
|
||||
\\ "color": "blue",
|
||||
\\ "children": [
|
||||
\\ { "type": "button",
|
||||
\\ "caption": "OK" },
|
||||
\\ { "type": "button",
|
||||
\\ "caption": "Cancel" } ] }
|
||||
;
|
||||
const Node = union(enum) {
|
||||
div: Div,
|
||||
button: Button,
|
||||
const Self = @This();
|
||||
const Div = struct {
|
||||
color: enum { red, blue },
|
||||
children: []Self,
|
||||
};
|
||||
const Button = struct {
|
||||
caption: []const u8,
|
||||
};
|
||||
|
||||
pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() {
|
||||
if (source != .object) return error.UnexpectedToken;
|
||||
const type_value = source.object.get("type") orelse return error.UnexpectedToken; // Missing "type" field.
|
||||
if (type_value != .string) return error.UnexpectedToken; // "type" expected to be string.
|
||||
const type_str = type_value.string;
|
||||
var child_options = options;
|
||||
child_options.ignore_unknown_fields = true;
|
||||
if (std.mem.eql(u8, type_str, "div")) return .{ .div = try parseFromValueLeaky(Div, allocator, source, child_options) };
|
||||
if (std.mem.eql(u8, type_str, "button")) return .{ .button = try parseFromValueLeaky(Button, allocator, source, child_options) };
|
||||
return error.UnexpectedToken; // unknown type.
|
||||
}
|
||||
};
|
||||
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const dynamic_tree = try parseFromSliceLeaky(Value, arena.allocator(), doc, .{});
|
||||
const tree = try parseFromValueLeaky(Node, arena.allocator(), dynamic_tree, .{});
|
||||
|
||||
try testing.expect(tree.div.color == .blue);
|
||||
try testing.expectEqualStrings("Cancel", tree.div.children[1].button.caption);
|
||||
}
|
||||
|
||||
@ -10,21 +10,29 @@ const AllocWhen = @import("./scanner.zig").AllocWhen;
|
||||
const default_max_value_len = @import("./scanner.zig").default_max_value_len;
|
||||
const isNumberFormattedLikeAnInteger = @import("./scanner.zig").isNumberFormattedLikeAnInteger;
|
||||
|
||||
const Value = @import("./dynamic.zig").Value;
|
||||
const Array = @import("./dynamic.zig").Array;
|
||||
|
||||
/// Controls how to deal with various inconsistencies between the JSON document and the Zig struct type passed in.
|
||||
/// For duplicate fields or unknown fields, set options in this struct.
|
||||
/// For missing fields, give the Zig struct fields default values.
|
||||
pub const ParseOptions = struct {
|
||||
/// Behaviour when a duplicate field is encountered.
|
||||
/// The default is to return `error.DuplicateField`.
|
||||
duplicate_field_behavior: enum {
|
||||
use_first,
|
||||
@"error",
|
||||
use_last,
|
||||
} = .@"error",
|
||||
|
||||
/// If false, finding an unknown field returns an error.
|
||||
/// If false, finding an unknown field returns `error.UnknownField`.
|
||||
ignore_unknown_fields: bool = false,
|
||||
|
||||
/// Passed to json.Scanner.nextAllocMax() or json.Reader.nextAllocMax().
|
||||
/// The default for parseFromSlice() or parseFromTokenSource() with a *json.Scanner input
|
||||
/// is the length of the input slice, which means error.ValueTooLong will never be returned.
|
||||
/// The default for parseFromTokenSource() with a *json.Reader is default_max_value_len.
|
||||
/// Passed to `std.json.Scanner.nextAllocMax` or `std.json.Reader.nextAllocMax`.
|
||||
/// The default for `parseFromSlice` or `parseFromTokenSource` with a `*std.json.Scanner` input
|
||||
/// is the length of the input slice, which means `error.ValueTooLong` will never be returned.
|
||||
/// The default for `parseFromTokenSource` with a `*std.json.Reader` is `std.json.default_max_value_len`.
|
||||
/// Ignored for `parseFromValue` and `parseFromValueLeaky`.
|
||||
max_value_len: ?usize = null,
|
||||
};
|
||||
|
||||
@ -43,6 +51,7 @@ pub fn Parsed(comptime T: type) type {
|
||||
|
||||
/// Parses the json document from `s` and returns the result packaged in a `std.json.Parsed`.
|
||||
/// You must call `deinit()` of the returned object to clean up allocated resources.
|
||||
/// If you are using a `std.heap.ArenaAllocator` or similar, consider calling `parseFromSliceLeaky` instead.
|
||||
/// Note that `error.BufferUnderrun` is not actually possible to return from this function.
|
||||
pub fn parseFromSlice(
|
||||
comptime T: type,
|
||||
@ -114,33 +123,65 @@ pub fn parseFromTokenSourceLeaky(
|
||||
}
|
||||
}
|
||||
|
||||
const value = try parseInternal(T, allocator, scanner_or_reader, resolved_options);
|
||||
const value = try internalParse(T, allocator, scanner_or_reader, resolved_options);
|
||||
|
||||
assert(.end_of_document == try scanner_or_reader.next());
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Like `parseFromSlice`, but the input is an already-parsed `std.json.Value` object.
|
||||
pub fn parseFromValue(
|
||||
comptime T: type,
|
||||
allocator: Allocator,
|
||||
source: Value,
|
||||
options: ParseOptions,
|
||||
) ParseFromValueError!Parsed(T) {
|
||||
var parsed = Parsed(T){
|
||||
.arena = try allocator.create(ArenaAllocator),
|
||||
.value = undefined,
|
||||
};
|
||||
errdefer allocator.destroy(parsed.arena);
|
||||
parsed.arena.* = ArenaAllocator.init(allocator);
|
||||
errdefer parsed.arena.deinit();
|
||||
|
||||
parsed.value = try parseFromValueLeaky(T, parsed.arena.allocator(), source, options);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
pub fn parseFromValueLeaky(
|
||||
comptime T: type,
|
||||
allocator: Allocator,
|
||||
source: Value,
|
||||
options: ParseOptions,
|
||||
) ParseFromValueError!T {
|
||||
// I guess this function doesn't need to exist,
|
||||
// but the flow of the sourcecode is easy to follow and grouped nicely with
|
||||
// this pub redirect function near the top and the implementation near the bottom.
|
||||
return internalParseFromValue(T, allocator, source, options);
|
||||
}
|
||||
|
||||
/// The error set that will be returned when parsing from `*Source`.
|
||||
/// Note that this may contain `error.BufferUnderrun`, but that error will never actually be returned.
|
||||
pub fn ParseError(comptime Source: type) type {
|
||||
// A few of these will either always be present or present enough of the time that
|
||||
// omitting them is more confusing than always including them.
|
||||
return error{
|
||||
UnexpectedToken,
|
||||
InvalidNumber,
|
||||
Overflow,
|
||||
InvalidEnumTag,
|
||||
DuplicateField,
|
||||
UnknownField,
|
||||
MissingField,
|
||||
LengthMismatch,
|
||||
} ||
|
||||
std.fmt.ParseIntError || std.fmt.ParseFloatError ||
|
||||
Source.NextError || Source.PeekError || Source.AllocError;
|
||||
return ParseFromValueError || Source.NextError || Source.PeekError || Source.AllocError;
|
||||
}
|
||||
|
||||
fn parseInternal(
|
||||
pub const ParseFromValueError = std.fmt.ParseIntError || std.fmt.ParseFloatError || Allocator.Error || error{
|
||||
UnexpectedToken,
|
||||
InvalidNumber,
|
||||
Overflow,
|
||||
InvalidEnumTag,
|
||||
DuplicateField,
|
||||
UnknownField,
|
||||
MissingField,
|
||||
LengthMismatch,
|
||||
};
|
||||
|
||||
fn internalParse(
|
||||
comptime T: type,
|
||||
allocator: Allocator,
|
||||
source: anytype,
|
||||
@ -170,13 +211,7 @@ fn parseInternal(
|
||||
inline .number, .allocated_number, .string, .allocated_string => |slice| slice,
|
||||
else => return error.UnexpectedToken,
|
||||
};
|
||||
if (isNumberFormattedLikeAnInteger(slice))
|
||||
return std.fmt.parseInt(T, slice, 10);
|
||||
// Try to coerce a float to an integer.
|
||||
const float = try std.fmt.parseFloat(f128, slice);
|
||||
if (@round(float) != float) return error.InvalidNumber;
|
||||
if (float > std.math.maxInt(T) or float < std.math.minInt(T)) return error.Overflow;
|
||||
return @intFromFloat(T, float);
|
||||
return sliceToInt(T, slice);
|
||||
},
|
||||
.Optional => |optionalInfo| {
|
||||
switch (try source.peekNextTokenType()) {
|
||||
@ -185,11 +220,11 @@ fn parseInternal(
|
||||
return null;
|
||||
},
|
||||
else => {
|
||||
return try parseInternal(optionalInfo.child, allocator, source, options);
|
||||
return try internalParse(optionalInfo.child, allocator, source, options);
|
||||
},
|
||||
}
|
||||
},
|
||||
.Enum => |enumInfo| {
|
||||
.Enum => {
|
||||
if (comptime std.meta.trait.hasFn("jsonParse")(T)) {
|
||||
return T.jsonParse(allocator, source, options);
|
||||
}
|
||||
@ -200,12 +235,7 @@ fn parseInternal(
|
||||
inline .number, .allocated_number, .string, .allocated_string => |slice| slice,
|
||||
else => return error.UnexpectedToken,
|
||||
};
|
||||
// Check for a named value.
|
||||
if (std.meta.stringToEnum(T, slice)) |value| return value;
|
||||
// Check for a numeric value.
|
||||
if (!isNumberFormattedLikeAnInteger(slice)) return error.InvalidEnumTag;
|
||||
const n = std.fmt.parseInt(enumInfo.tag_type, slice, 10) catch return error.InvalidEnumTag;
|
||||
return try std.meta.intToEnum(T, n);
|
||||
return sliceToEnum(T, slice);
|
||||
},
|
||||
.Union => |unionInfo| {
|
||||
if (comptime std.meta.trait.hasFn("jsonParse")(T)) {
|
||||
@ -226,7 +256,7 @@ fn parseInternal(
|
||||
inline for (unionInfo.fields) |u_field| {
|
||||
if (std.mem.eql(u8, u_field.name, field_name)) {
|
||||
// Free the name token now in case we're using an allocator that optimizes freeing the last allocated object.
|
||||
// (Recursing into parseInternal() might trigger more allocations.)
|
||||
// (Recursing into internalParse() might trigger more allocations.)
|
||||
freeAllocated(allocator, name_token.?);
|
||||
name_token = null;
|
||||
|
||||
@ -237,7 +267,7 @@ fn parseInternal(
|
||||
result = @unionInit(T, u_field.name, {});
|
||||
} else {
|
||||
// Recurse.
|
||||
result = @unionInit(T, u_field.name, try parseInternal(u_field.type, allocator, source, options));
|
||||
result = @unionInit(T, u_field.name, try internalParse(u_field.type, allocator, source, options));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -256,10 +286,8 @@ fn parseInternal(
|
||||
if (.array_begin != try source.next()) return error.UnexpectedToken;
|
||||
|
||||
var r: T = undefined;
|
||||
var fields_seen: usize = 0;
|
||||
inline for (0..structInfo.fields.len) |i| {
|
||||
r[i] = try parseInternal(structInfo.fields[i].type, allocator, source, options);
|
||||
fields_seen = i + 1;
|
||||
r[i] = try internalParse(structInfo.fields[i].type, allocator, source, options);
|
||||
}
|
||||
|
||||
if (.array_end != try source.next()) return error.UnexpectedToken;
|
||||
@ -288,7 +316,7 @@ fn parseInternal(
|
||||
if (field.is_comptime) @compileError("comptime fields are not supported: " ++ @typeName(T) ++ "." ++ field.name);
|
||||
if (std.mem.eql(u8, field.name, field_name)) {
|
||||
// Free the name token now in case we're using an allocator that optimizes freeing the last allocated object.
|
||||
// (Recursing into parseInternal() might trigger more allocations.)
|
||||
// (Recursing into internalParse() might trigger more allocations.)
|
||||
freeAllocated(allocator, name_token.?);
|
||||
name_token = null;
|
||||
|
||||
@ -297,14 +325,14 @@ fn parseInternal(
|
||||
.use_first => {
|
||||
// Parse and ignore the redundant value.
|
||||
// We don't want to skip the value, because we want type checking.
|
||||
_ = try parseInternal(field.type, allocator, source, options);
|
||||
_ = try internalParse(field.type, allocator, source, options);
|
||||
break;
|
||||
},
|
||||
.@"error" => return error.DuplicateField,
|
||||
.use_last => {},
|
||||
}
|
||||
}
|
||||
@field(r, field.name) = try parseInternal(field.type, allocator, source, options);
|
||||
@field(r, field.name) = try internalParse(field.type, allocator, source, options);
|
||||
fields_seen[i] = true;
|
||||
break;
|
||||
}
|
||||
@ -318,16 +346,7 @@ fn parseInternal(
|
||||
}
|
||||
}
|
||||
}
|
||||
inline for (structInfo.fields, 0..) |field, i| {
|
||||
if (!fields_seen[i]) {
|
||||
if (field.default_value) |default_ptr| {
|
||||
const default = @ptrCast(*align(1) const field.type, default_ptr).*;
|
||||
@field(r, field.name) = default;
|
||||
} else {
|
||||
return error.MissingField;
|
||||
}
|
||||
}
|
||||
}
|
||||
try fillDefaultStructValues(T, &r, &fields_seen);
|
||||
return r;
|
||||
},
|
||||
|
||||
@ -335,7 +354,7 @@ fn parseInternal(
|
||||
switch (try source.peekNextTokenType()) {
|
||||
.array_begin => {
|
||||
// Typical array.
|
||||
return parseInternalArray(T, arrayInfo.child, arrayInfo.len, allocator, source, options);
|
||||
return internalParseArray(T, arrayInfo.child, arrayInfo.len, allocator, source, options);
|
||||
},
|
||||
.string => {
|
||||
if (arrayInfo.child != u8) return error.UnexpectedToken;
|
||||
@ -389,7 +408,7 @@ fn parseInternal(
|
||||
.Vector => |vecInfo| {
|
||||
switch (try source.peekNextTokenType()) {
|
||||
.array_begin => {
|
||||
return parseInternalArray(T, vecInfo.child, vecInfo.len, allocator, source, options);
|
||||
return internalParseArray(T, vecInfo.child, vecInfo.len, allocator, source, options);
|
||||
},
|
||||
else => return error.UnexpectedToken,
|
||||
}
|
||||
@ -399,7 +418,7 @@ fn parseInternal(
|
||||
switch (ptrInfo.size) {
|
||||
.One => {
|
||||
const r: *ptrInfo.child = try allocator.create(ptrInfo.child);
|
||||
r.* = try parseInternal(ptrInfo.child, allocator, source, options);
|
||||
r.* = try internalParse(ptrInfo.child, allocator, source, options);
|
||||
return r;
|
||||
},
|
||||
.Slice => {
|
||||
@ -419,7 +438,7 @@ fn parseInternal(
|
||||
}
|
||||
|
||||
try arraylist.ensureUnusedCapacity(1);
|
||||
arraylist.appendAssumeCapacity(try parseInternal(ptrInfo.child, allocator, source, options));
|
||||
arraylist.appendAssumeCapacity(try internalParse(ptrInfo.child, allocator, source, options));
|
||||
}
|
||||
|
||||
if (ptrInfo.sentinel) |some| {
|
||||
@ -463,7 +482,7 @@ fn parseInternal(
|
||||
unreachable;
|
||||
}
|
||||
|
||||
fn parseInternalArray(
|
||||
fn internalParseArray(
|
||||
comptime T: type,
|
||||
comptime Child: type,
|
||||
comptime len: comptime_int,
|
||||
@ -476,7 +495,7 @@ fn parseInternalArray(
|
||||
var r: T = undefined;
|
||||
var i: usize = 0;
|
||||
while (i < len) : (i += 1) {
|
||||
r[i] = try parseInternal(Child, allocator, source, options);
|
||||
r[i] = try internalParse(Child, allocator, source, options);
|
||||
}
|
||||
|
||||
if (.array_end != try source.next()) return error.UnexpectedToken;
|
||||
@ -484,6 +503,271 @@ fn parseInternalArray(
|
||||
return r;
|
||||
}
|
||||
|
||||
fn internalParseFromValue(
|
||||
comptime T: type,
|
||||
allocator: Allocator,
|
||||
source: Value,
|
||||
options: ParseOptions,
|
||||
) ParseFromValueError!T {
|
||||
switch (@typeInfo(T)) {
|
||||
.Bool => {
|
||||
switch (source) {
|
||||
.bool => |b| return b,
|
||||
else => return error.UnexpectedToken,
|
||||
}
|
||||
},
|
||||
.Float, .ComptimeFloat => {
|
||||
switch (source) {
|
||||
.float => |f| return @floatCast(T, f),
|
||||
.integer => |i| return @floatFromInt(T, i),
|
||||
.number_string, .string => |s| return std.fmt.parseFloat(T, s),
|
||||
else => return error.UnexpectedToken,
|
||||
}
|
||||
},
|
||||
.Int, .ComptimeInt => {
|
||||
switch (source) {
|
||||
.float => |f| {
|
||||
if (@round(f) != f) return error.InvalidNumber;
|
||||
if (f > std.math.maxInt(T)) return error.Overflow;
|
||||
if (f < std.math.minInt(T)) return error.Overflow;
|
||||
return @intFromFloat(T, f);
|
||||
},
|
||||
.integer => |i| {
|
||||
if (i > std.math.maxInt(T)) return error.Overflow;
|
||||
if (i < std.math.minInt(T)) return error.Overflow;
|
||||
return @intCast(T, i);
|
||||
},
|
||||
.number_string, .string => |s| {
|
||||
return sliceToInt(T, s);
|
||||
},
|
||||
else => return error.UnexpectedToken,
|
||||
}
|
||||
},
|
||||
.Optional => |optionalInfo| {
|
||||
switch (source) {
|
||||
.null => return null,
|
||||
else => return try internalParseFromValue(optionalInfo.child, allocator, source, options),
|
||||
}
|
||||
},
|
||||
.Enum => {
|
||||
if (comptime std.meta.trait.hasFn("jsonParseFromValue")(T)) {
|
||||
return T.jsonParseFromValue(allocator, source, options);
|
||||
}
|
||||
|
||||
switch (source) {
|
||||
.float => return error.InvalidEnumTag,
|
||||
.integer => |i| return std.meta.intToEnum(T, i),
|
||||
.number_string, .string => |s| return sliceToEnum(T, s),
|
||||
else => return error.UnexpectedToken,
|
||||
}
|
||||
},
|
||||
.Union => |unionInfo| {
|
||||
if (comptime std.meta.trait.hasFn("jsonParseFromValue")(T)) {
|
||||
return T.jsonParseFromValue(allocator, source, options);
|
||||
}
|
||||
|
||||
if (unionInfo.tag_type == null) @compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'");
|
||||
|
||||
if (source != .object) return error.UnexpectedToken;
|
||||
if (source.object.count() != 1) return error.UnexpectedToken;
|
||||
|
||||
var it = source.object.iterator();
|
||||
const kv = it.next().?;
|
||||
const field_name = kv.key_ptr.*;
|
||||
|
||||
inline for (unionInfo.fields) |u_field| {
|
||||
if (std.mem.eql(u8, u_field.name, field_name)) {
|
||||
if (u_field.type == void) {
|
||||
// void isn't really a json type, but we can support void payload union tags with {} as a value.
|
||||
if (kv.value_ptr.* != .object) return error.UnexpectedToken;
|
||||
if (kv.value_ptr.*.object.count() != 0) return error.UnexpectedToken;
|
||||
return @unionInit(T, u_field.name, {});
|
||||
}
|
||||
// Recurse.
|
||||
return @unionInit(T, u_field.name, try internalParseFromValue(u_field.type, allocator, kv.value_ptr.*, options));
|
||||
}
|
||||
}
|
||||
// Didn't match anything.
|
||||
return error.UnknownField;
|
||||
},
|
||||
|
||||
.Struct => |structInfo| {
|
||||
if (structInfo.is_tuple) {
|
||||
if (source != .array) return error.UnexpectedToken;
|
||||
if (source.array.items.len != structInfo.fields.len) return error.UnexpectedToken;
|
||||
|
||||
var r: T = undefined;
|
||||
inline for (0..structInfo.fields.len, source.array.items) |i, item| {
|
||||
r[i] = try internalParseFromValue(structInfo.fields[i].type, allocator, item, options);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
if (comptime std.meta.trait.hasFn("jsonParseFromValue")(T)) {
|
||||
return T.jsonParseFromValue(allocator, source, options);
|
||||
}
|
||||
|
||||
if (source != .object) return error.UnexpectedToken;
|
||||
|
||||
var r: T = undefined;
|
||||
var fields_seen = [_]bool{false} ** structInfo.fields.len;
|
||||
|
||||
var it = source.object.iterator();
|
||||
while (it.next()) |kv| {
|
||||
const field_name = kv.key_ptr.*;
|
||||
|
||||
inline for (structInfo.fields, 0..) |field, i| {
|
||||
if (field.is_comptime) @compileError("comptime fields are not supported: " ++ @typeName(T) ++ "." ++ field.name);
|
||||
if (std.mem.eql(u8, field.name, field_name)) {
|
||||
if (fields_seen[i]) {
|
||||
switch (options.duplicate_field_behavior) {
|
||||
.use_first => {
|
||||
// Parse and ignore the redundant value.
|
||||
// We don't want to skip the value, because we want type checking.
|
||||
_ = try internalParseFromValue(field.type, allocator, kv.value_ptr.*, options);
|
||||
break;
|
||||
},
|
||||
.@"error" => return error.DuplicateField,
|
||||
.use_last => {},
|
||||
}
|
||||
}
|
||||
@field(r, field.name) = try internalParseFromValue(field.type, allocator, kv.value_ptr.*, options);
|
||||
fields_seen[i] = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Didn't match anything.
|
||||
if (!options.ignore_unknown_fields) return error.UnknownField;
|
||||
}
|
||||
}
|
||||
try fillDefaultStructValues(T, &r, &fields_seen);
|
||||
return r;
|
||||
},
|
||||
|
||||
.Array => |arrayInfo| {
|
||||
switch (source) {
|
||||
.array => |array| {
|
||||
// Typical array.
|
||||
return internalParseArrayFromArrayValue(T, arrayInfo.child, arrayInfo.len, allocator, array, options);
|
||||
},
|
||||
.string => |s| {
|
||||
if (arrayInfo.child != u8) return error.UnexpectedToken;
|
||||
// Fixed-length string.
|
||||
|
||||
if (s.len != arrayInfo.len) return error.LengthMismatch;
|
||||
|
||||
var r: T = undefined;
|
||||
@memcpy(r[0..], s);
|
||||
return r;
|
||||
},
|
||||
|
||||
else => return error.UnexpectedToken,
|
||||
}
|
||||
},
|
||||
|
||||
.Vector => |vecInfo| {
|
||||
switch (source) {
|
||||
.array => |array| {
|
||||
return internalParseArrayFromArrayValue(T, vecInfo.child, vecInfo.len, allocator, array, options);
|
||||
},
|
||||
else => return error.UnexpectedToken,
|
||||
}
|
||||
},
|
||||
|
||||
.Pointer => |ptrInfo| {
|
||||
switch (ptrInfo.size) {
|
||||
.One => {
|
||||
const r: *ptrInfo.child = try allocator.create(ptrInfo.child);
|
||||
r.* = try internalParseFromValue(ptrInfo.child, allocator, source, options);
|
||||
return r;
|
||||
},
|
||||
.Slice => {
|
||||
switch (source) {
|
||||
.array => |array| {
|
||||
const r = if (ptrInfo.sentinel) |sentinel_ptr|
|
||||
try allocator.allocSentinel(ptrInfo.child, array.items.len, @ptrCast(*align(1) const ptrInfo.child, sentinel_ptr).*)
|
||||
else
|
||||
try allocator.alloc(ptrInfo.child, array.items.len);
|
||||
|
||||
for (array.items, r) |item, *dest| {
|
||||
dest.* = try internalParseFromValue(ptrInfo.child, allocator, item, options);
|
||||
}
|
||||
|
||||
return r;
|
||||
},
|
||||
.string => |s| {
|
||||
if (ptrInfo.child != u8) return error.UnexpectedToken;
|
||||
// Dynamic length string.
|
||||
|
||||
const r = if (ptrInfo.sentinel) |sentinel_ptr|
|
||||
try allocator.allocSentinel(ptrInfo.child, s.len, @ptrCast(*align(1) const ptrInfo.child, sentinel_ptr).*)
|
||||
else
|
||||
try allocator.alloc(ptrInfo.child, s.len);
|
||||
@memcpy(r[0..], s);
|
||||
|
||||
return r;
|
||||
},
|
||||
else => return error.UnexpectedToken,
|
||||
}
|
||||
},
|
||||
else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"),
|
||||
}
|
||||
},
|
||||
else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"),
|
||||
}
|
||||
}
|
||||
|
||||
fn internalParseArrayFromArrayValue(
|
||||
comptime T: type,
|
||||
comptime Child: type,
|
||||
comptime len: comptime_int,
|
||||
allocator: Allocator,
|
||||
array: Array,
|
||||
options: ParseOptions,
|
||||
) !T {
|
||||
if (array.items.len != len) return error.LengthMismatch;
|
||||
|
||||
var r: T = undefined;
|
||||
for (array.items, 0..) |item, i| {
|
||||
r[i] = try internalParseFromValue(Child, allocator, item, options);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
fn sliceToInt(comptime T: type, slice: []const u8) !T {
|
||||
if (isNumberFormattedLikeAnInteger(slice))
|
||||
return std.fmt.parseInt(T, slice, 10);
|
||||
// Try to coerce a float to an integer.
|
||||
const float = try std.fmt.parseFloat(f128, slice);
|
||||
if (@round(float) != float) return error.InvalidNumber;
|
||||
if (float > std.math.maxInt(T) or float < std.math.minInt(T)) return error.Overflow;
|
||||
return @intCast(T, @intFromFloat(i128, float));
|
||||
}
|
||||
|
||||
fn sliceToEnum(comptime T: type, slice: []const u8) !T {
|
||||
// Check for a named value.
|
||||
if (std.meta.stringToEnum(T, slice)) |value| return value;
|
||||
// Check for a numeric value.
|
||||
if (!isNumberFormattedLikeAnInteger(slice)) return error.InvalidEnumTag;
|
||||
const n = std.fmt.parseInt(@typeInfo(T).Enum.tag_type, slice, 10) catch return error.InvalidEnumTag;
|
||||
return std.meta.intToEnum(T, n);
|
||||
}
|
||||
|
||||
fn fillDefaultStructValues(comptime T: type, r: *T, fields_seen: *[@typeInfo(T).Struct.fields.len]bool) !void {
|
||||
inline for (@typeInfo(T).Struct.fields, 0..) |field, i| {
|
||||
if (!fields_seen[i]) {
|
||||
if (field.default_value) |default_ptr| {
|
||||
const default = @ptrCast(*align(1) const field.type, default_ptr).*;
|
||||
@field(r, field.name) = default;
|
||||
} else {
|
||||
return error.MissingField;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn freeAllocated(allocator: Allocator, token: Token) void {
|
||||
switch (token) {
|
||||
.allocated_number, .allocated_string => |slice| {
|
||||
|
||||
@ -1,14 +1,373 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const parseFromSlice = @import("./static.zig").parseFromSlice;
|
||||
const parseFromSliceLeaky = @import("./static.zig").parseFromSliceLeaky;
|
||||
const parseFromTokenSource = @import("./static.zig").parseFromTokenSource;
|
||||
const parseFromTokenSourceLeaky = @import("./static.zig").parseFromTokenSourceLeaky;
|
||||
const parseFromValue = @import("./static.zig").parseFromValue;
|
||||
const parseFromValueLeaky = @import("./static.zig").parseFromValueLeaky;
|
||||
const ParseOptions = @import("./static.zig").ParseOptions;
|
||||
|
||||
const JsonScanner = @import("./scanner.zig").Scanner;
|
||||
const jsonReader = @import("./scanner.zig").reader;
|
||||
const Diagnostics = @import("./scanner.zig").Diagnostics;
|
||||
|
||||
const Value = @import("./dynamic.zig").Value;
|
||||
|
||||
const Primitives = struct {
|
||||
bool: bool,
|
||||
// f16, f80, f128: don't work in std.fmt.parseFloat(T).
|
||||
f32: f32,
|
||||
f64: f64,
|
||||
u0: u0,
|
||||
i0: i0,
|
||||
u1: u1,
|
||||
i1: i1,
|
||||
u8: u8,
|
||||
i8: i8,
|
||||
i130: i130,
|
||||
};
|
||||
|
||||
const primitives_0 = Primitives{
|
||||
.bool = false,
|
||||
.f32 = 0,
|
||||
.f64 = 0,
|
||||
.u0 = 0,
|
||||
.i0 = 0,
|
||||
.u1 = 0,
|
||||
.i1 = 0,
|
||||
.u8 = 0,
|
||||
.i8 = 0,
|
||||
.i130 = 0,
|
||||
};
|
||||
const primitives_0_doc_0 =
|
||||
\\{
|
||||
\\ "bool": false,
|
||||
\\ "f32": 0,
|
||||
\\ "f64": 0,
|
||||
\\ "u0": 0,
|
||||
\\ "i0": 0,
|
||||
\\ "u1": 0,
|
||||
\\ "i1": 0,
|
||||
\\ "u8": 0,
|
||||
\\ "i8": 0,
|
||||
\\ "i130": 0
|
||||
\\}
|
||||
;
|
||||
const primitives_0_doc_1 = // looks like a float.
|
||||
\\{
|
||||
\\ "bool": false,
|
||||
\\ "f32": 0.0,
|
||||
\\ "f64": 0.0,
|
||||
\\ "u0": 0.0,
|
||||
\\ "i0": 0.0,
|
||||
\\ "u1": 0.0,
|
||||
\\ "i1": 0.0,
|
||||
\\ "u8": 0.0,
|
||||
\\ "i8": 0.0,
|
||||
\\ "i130": 0.0
|
||||
\\}
|
||||
;
|
||||
|
||||
const primitives_1 = Primitives{
|
||||
.bool = true,
|
||||
.f32 = 1073741824,
|
||||
.f64 = 1152921504606846976,
|
||||
.u0 = 0,
|
||||
.i0 = 0,
|
||||
.u1 = 1,
|
||||
.i1 = -1,
|
||||
.u8 = 255,
|
||||
.i8 = -128,
|
||||
.i130 = -680564733841876926926749214863536422911,
|
||||
};
|
||||
const primitives_1_doc_0 =
|
||||
\\{
|
||||
\\ "bool": true,
|
||||
\\ "f32": 1073741824,
|
||||
\\ "f64": 1152921504606846976,
|
||||
\\ "u0": 0,
|
||||
\\ "i0": 0,
|
||||
\\ "u1": 1,
|
||||
\\ "i1": -1,
|
||||
\\ "u8": 255,
|
||||
\\ "i8": -128,
|
||||
\\ "i130": -680564733841876926926749214863536422911
|
||||
\\}
|
||||
;
|
||||
const primitives_1_doc_1 = // float rounding.
|
||||
\\{
|
||||
\\ "bool": true,
|
||||
\\ "f32": 1073741825,
|
||||
\\ "f64": 1152921504606846977,
|
||||
\\ "u0": 0,
|
||||
\\ "i0": 0,
|
||||
\\ "u1": 1,
|
||||
\\ "i1": -1,
|
||||
\\ "u8": 255,
|
||||
\\ "i8": -128,
|
||||
\\ "i130": -680564733841876926926749214863536422911
|
||||
\\}
|
||||
;
|
||||
|
||||
const Aggregates = struct {
|
||||
optional: ?i32,
|
||||
array: [4]i32,
|
||||
vector: @Vector(4, i32),
|
||||
pointer: *i32,
|
||||
pointer_const: *const i32,
|
||||
slice: []i32,
|
||||
slice_const: []const i32,
|
||||
slice_sentinel: [:0]i32,
|
||||
slice_sentinel_const: [:0]const i32,
|
||||
};
|
||||
|
||||
var zero: i32 = 0;
|
||||
const zero_const: i32 = 0;
|
||||
var array_of_zeros: [4:0]i32 = [_:0]i32{ 0, 0, 0, 0 };
|
||||
var one: i32 = 1;
|
||||
const one_const: i32 = 1;
|
||||
var array_countdown: [4:0]i32 = [_:0]i32{ 4, 3, 2, 1 };
|
||||
|
||||
const aggregates_0 = Aggregates{
|
||||
.optional = null,
|
||||
.array = [4]i32{ 0, 0, 0, 0 },
|
||||
.vector = @Vector(4, i32){ 0, 0, 0, 0 },
|
||||
.pointer = &zero,
|
||||
.pointer_const = &zero_const,
|
||||
.slice = array_of_zeros[0..0],
|
||||
.slice_const = &[_]i32{},
|
||||
.slice_sentinel = array_of_zeros[0..0 :0],
|
||||
.slice_sentinel_const = &[_:0]i32{},
|
||||
};
|
||||
const aggregates_0_doc =
|
||||
\\{
|
||||
\\ "optional": null,
|
||||
\\ "array": [0, 0, 0, 0],
|
||||
\\ "vector": [0, 0, 0, 0],
|
||||
\\ "pointer": 0,
|
||||
\\ "pointer_const": 0,
|
||||
\\ "slice": [],
|
||||
\\ "slice_const": [],
|
||||
\\ "slice_sentinel": [],
|
||||
\\ "slice_sentinel_const": []
|
||||
\\}
|
||||
;
|
||||
|
||||
const aggregates_1 = Aggregates{
|
||||
.optional = 1,
|
||||
.array = [4]i32{ 1, 2, 3, 4 },
|
||||
.vector = @Vector(4, i32){ 1, 2, 3, 4 },
|
||||
.pointer = &one,
|
||||
.pointer_const = &one_const,
|
||||
.slice = array_countdown[0..],
|
||||
.slice_const = array_countdown[0..],
|
||||
.slice_sentinel = array_countdown[0.. :0],
|
||||
.slice_sentinel_const = array_countdown[0.. :0],
|
||||
};
|
||||
const aggregates_1_doc =
|
||||
\\{
|
||||
\\ "optional": 1,
|
||||
\\ "array": [1, 2, 3, 4],
|
||||
\\ "vector": [1, 2, 3, 4],
|
||||
\\ "pointer": 1,
|
||||
\\ "pointer_const": 1,
|
||||
\\ "slice": [4, 3, 2, 1],
|
||||
\\ "slice_const": [4, 3, 2, 1],
|
||||
\\ "slice_sentinel": [4, 3, 2, 1],
|
||||
\\ "slice_sentinel_const": [4, 3, 2, 1]
|
||||
\\}
|
||||
;
|
||||
|
||||
const Strings = struct {
|
||||
slice_u8: []u8,
|
||||
slice_const_u8: []const u8,
|
||||
array_u8: [4]u8,
|
||||
slice_sentinel_u8: [:0]u8,
|
||||
slice_const_sentinel_u8: [:0]const u8,
|
||||
array_sentinel_u8: [4:0]u8,
|
||||
};
|
||||
|
||||
var abcd = [4:0]u8{ 'a', 'b', 'c', 'd' };
|
||||
const strings_0 = Strings{
|
||||
.slice_u8 = abcd[0..],
|
||||
.slice_const_u8 = "abcd",
|
||||
.array_u8 = [4]u8{ 'a', 'b', 'c', 'd' },
|
||||
.slice_sentinel_u8 = abcd[0..],
|
||||
.slice_const_sentinel_u8 = "abcd",
|
||||
.array_sentinel_u8 = [4:0]u8{ 'a', 'b', 'c', 'd' },
|
||||
};
|
||||
const strings_0_doc_0 =
|
||||
\\{
|
||||
\\ "slice_u8": "abcd",
|
||||
\\ "slice_const_u8": "abcd",
|
||||
\\ "array_u8": "abcd",
|
||||
\\ "slice_sentinel_u8": "abcd",
|
||||
\\ "slice_const_sentinel_u8": "abcd",
|
||||
\\ "array_sentinel_u8": "abcd"
|
||||
\\}
|
||||
;
|
||||
const strings_0_doc_1 =
|
||||
\\{
|
||||
\\ "slice_u8": [97, 98, 99, 100],
|
||||
\\ "slice_const_u8": [97, 98, 99, 100],
|
||||
\\ "array_u8": [97, 98, 99, 100],
|
||||
\\ "slice_sentinel_u8": [97, 98, 99, 100],
|
||||
\\ "slice_const_sentinel_u8": [97, 98, 99, 100],
|
||||
\\ "array_sentinel_u8": [97, 98, 99, 100]
|
||||
\\}
|
||||
;
|
||||
|
||||
const Subnamespaces = struct {
|
||||
packed_struct: packed struct { a: u32, b: u32 },
|
||||
union_enum: union(enum) { i: i32, s: []const u8, v },
|
||||
inferred_enum: enum { a, b },
|
||||
explicit_enum: enum(u8) { a = 0, b = 1 },
|
||||
|
||||
custom_struct: struct {
|
||||
pub fn jsonParse(allocator: Allocator, source: anytype, options: ParseOptions) !@This() {
|
||||
_ = allocator;
|
||||
_ = options;
|
||||
try source.skipValue();
|
||||
return @This(){};
|
||||
}
|
||||
pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() {
|
||||
_ = allocator;
|
||||
_ = source;
|
||||
_ = options;
|
||||
return @This(){};
|
||||
}
|
||||
},
|
||||
custom_union: union(enum) {
|
||||
i: i32,
|
||||
s: []const u8,
|
||||
pub fn jsonParse(allocator: Allocator, source: anytype, options: ParseOptions) !@This() {
|
||||
_ = allocator;
|
||||
_ = options;
|
||||
try source.skipValue();
|
||||
return @This(){ .i = 0 };
|
||||
}
|
||||
pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() {
|
||||
_ = allocator;
|
||||
_ = source;
|
||||
_ = options;
|
||||
return @This(){ .i = 0 };
|
||||
}
|
||||
},
|
||||
custom_enum: enum {
|
||||
a,
|
||||
b,
|
||||
pub fn jsonParse(allocator: Allocator, source: anytype, options: ParseOptions) !@This() {
|
||||
_ = allocator;
|
||||
_ = options;
|
||||
try source.skipValue();
|
||||
return .a;
|
||||
}
|
||||
pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() {
|
||||
_ = allocator;
|
||||
_ = source;
|
||||
_ = options;
|
||||
return .a;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const subnamespaces_0 = Subnamespaces{
|
||||
.packed_struct = .{ .a = 0, .b = 0 },
|
||||
.union_enum = .{ .i = 0 },
|
||||
.inferred_enum = .a,
|
||||
.explicit_enum = .a,
|
||||
.custom_struct = .{},
|
||||
.custom_union = .{ .i = 0 },
|
||||
.custom_enum = .a,
|
||||
};
|
||||
const subnamespaces_0_doc =
|
||||
\\{
|
||||
\\ "packed_struct": {"a": 0, "b": 0},
|
||||
\\ "union_enum": {"i": 0},
|
||||
\\ "inferred_enum": "a",
|
||||
\\ "explicit_enum": "a",
|
||||
\\ "custom_struct": null,
|
||||
\\ "custom_union": null,
|
||||
\\ "custom_enum": null
|
||||
\\}
|
||||
;
|
||||
|
||||
fn testAllParseFunctions(comptime T: type, expected: T, doc: []const u8) !void {
|
||||
// First do the one with the debug info in case we get a SyntaxError or something.
|
||||
{
|
||||
var scanner = JsonScanner.initCompleteInput(testing.allocator, doc);
|
||||
defer scanner.deinit();
|
||||
var diagnostics = Diagnostics{};
|
||||
scanner.enableDiagnostics(&diagnostics);
|
||||
var parsed = parseFromTokenSource(T, testing.allocator, &scanner, .{}) catch |e| {
|
||||
std.debug.print("at line,col: {}:{}\n", .{ diagnostics.getLine(), diagnostics.getColumn() });
|
||||
return e;
|
||||
};
|
||||
defer parsed.deinit();
|
||||
try testing.expectEqualDeep(expected, parsed.value);
|
||||
}
|
||||
{
|
||||
const parsed = try parseFromSlice(T, testing.allocator, doc, .{});
|
||||
defer parsed.deinit();
|
||||
try testing.expectEqualDeep(expected, parsed.value);
|
||||
}
|
||||
{
|
||||
var stream = std.io.fixedBufferStream(doc);
|
||||
var json_reader = jsonReader(std.testing.allocator, stream.reader());
|
||||
defer json_reader.deinit();
|
||||
var parsed = try parseFromTokenSource(T, testing.allocator, &json_reader, .{});
|
||||
defer parsed.deinit();
|
||||
try testing.expectEqualDeep(expected, parsed.value);
|
||||
}
|
||||
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
{
|
||||
try testing.expectEqualDeep(expected, try parseFromSliceLeaky(T, arena.allocator(), doc, .{}));
|
||||
}
|
||||
{
|
||||
var scanner = JsonScanner.initCompleteInput(testing.allocator, doc);
|
||||
defer scanner.deinit();
|
||||
try testing.expectEqualDeep(expected, try parseFromTokenSourceLeaky(T, arena.allocator(), &scanner, .{}));
|
||||
}
|
||||
{
|
||||
var stream = std.io.fixedBufferStream(doc);
|
||||
var json_reader = jsonReader(std.testing.allocator, stream.reader());
|
||||
defer json_reader.deinit();
|
||||
try testing.expectEqualDeep(expected, try parseFromTokenSourceLeaky(T, arena.allocator(), &json_reader, .{}));
|
||||
}
|
||||
|
||||
const parsed_dynamic = try parseFromSlice(Value, testing.allocator, doc, .{});
|
||||
defer parsed_dynamic.deinit();
|
||||
{
|
||||
const parsed = try parseFromValue(T, testing.allocator, parsed_dynamic.value, .{});
|
||||
defer parsed.deinit();
|
||||
try testing.expectEqualDeep(expected, parsed.value);
|
||||
}
|
||||
{
|
||||
try testing.expectEqualDeep(expected, try parseFromValueLeaky(T, arena.allocator(), parsed_dynamic.value, .{}));
|
||||
}
|
||||
}
|
||||
|
||||
test "test all types" {
|
||||
if (true) return error.SkipZigTest; // See https://github.com/ziglang/zig/issues/16108
|
||||
try testAllParseFunctions(Primitives, primitives_0, primitives_0_doc_0);
|
||||
try testAllParseFunctions(Primitives, primitives_0, primitives_0_doc_1);
|
||||
try testAllParseFunctions(Primitives, primitives_1, primitives_1_doc_0);
|
||||
try testAllParseFunctions(Primitives, primitives_1, primitives_1_doc_1);
|
||||
|
||||
try testAllParseFunctions(Aggregates, aggregates_0, aggregates_0_doc);
|
||||
try testAllParseFunctions(Aggregates, aggregates_1, aggregates_1_doc);
|
||||
|
||||
try testAllParseFunctions(Strings, strings_0, strings_0_doc_0);
|
||||
try testAllParseFunctions(Strings, strings_0, strings_0_doc_1);
|
||||
|
||||
try testAllParseFunctions(Subnamespaces, subnamespaces_0, subnamespaces_0_doc);
|
||||
}
|
||||
|
||||
test "parse" {
|
||||
try testing.expectEqual(false, try parseFromSliceLeaky(bool, testing.allocator, "false", .{}));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user