From 0f2339f55b2fa45a8af94c26a7ceb8377e3acfef Mon Sep 17 00:00:00 2001 From: Josh Wolfe Date: Tue, 20 Jun 2023 19:01:34 -0400 Subject: [PATCH] std: json.parseFromValue() (#15981) --- lib/std/json/dynamic_test.zig | 49 +++++ lib/std/json/static.zig | 400 +++++++++++++++++++++++++++++----- lib/std/json/static_test.zig | 359 ++++++++++++++++++++++++++++++ 3 files changed, 750 insertions(+), 58 deletions(-) diff --git a/lib/std/json/dynamic_test.zig b/lib/std/json/dynamic_test.zig index d764e387af..ae4ccef6d3 100644 --- a/lib/std/json/dynamic_test.zig +++ b/lib/std/json/dynamic_test.zig @@ -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); +} diff --git a/lib/std/json/static.zig b/lib/std/json/static.zig index 3a3af8f8d9..fd3d12d73a 100644 --- a/lib/std/json/static.zig +++ b/lib/std/json/static.zig @@ -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| { diff --git a/lib/std/json/static_test.zig b/lib/std/json/static_test.zig index c4e2ab8d2d..63617850d3 100644 --- a/lib/std/json/static_test.zig +++ b/lib/std/json/static_test.zig @@ -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", .{}));