std: json.parseFromValue() (#15981)

This commit is contained in:
Josh Wolfe 2023-06-20 19:01:34 -04:00 committed by GitHub
parent d2b2567119
commit 0f2339f55b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 750 additions and 58 deletions

View File

@ -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);
}

View File

@ -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| {

View File

@ -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", .{}));