const std = @import("std"); const debug = std.debug; const ArenaAllocator = std.heap.ArenaAllocator; const ArrayList = std.ArrayList; const StringArrayHashMap = std.StringArrayHashMap; const Allocator = std.mem.Allocator; const StringifyOptions = @import("./stringify.zig").StringifyOptions; const stringify = @import("./stringify.zig").stringify; const ParseOptions = @import("./static.zig").ParseOptions; const ParseError = @import("./static.zig").ParseError; const JsonScanner = @import("./scanner.zig").Scanner; const AllocWhen = @import("./scanner.zig").AllocWhen; const Token = @import("./scanner.zig").Token; const isNumberFormattedLikeAnInteger = @import("./scanner.zig").isNumberFormattedLikeAnInteger; pub const ObjectMap = StringArrayHashMap(Value); pub const Array = ArrayList(Value); /// Represents any JSON value, potentially containing other JSON values. /// A .float value may be an approximation of the original value. /// Arbitrary precision numbers can be represented by .number_string values. pub const Value = union(enum) { null, bool: bool, integer: i64, float: f64, number_string: []const u8, string: []const u8, array: Array, object: ObjectMap, pub fn parseFromNumberSlice(s: []const u8) Value { if (!isNumberFormattedLikeAnInteger(s)) { const f = std.fmt.parseFloat(f64, s) catch unreachable; if (std.math.isFinite(f)) { return Value{ .float = f }; } else { return Value{ .number_string = s }; } } if (std.fmt.parseInt(i64, s, 10)) |i| { return Value{ .integer = i }; } else |e| { switch (e) { error.Overflow => return Value{ .number_string = s }, error.InvalidCharacter => unreachable, } } } pub fn dump(self: Value) void { std.debug.getStderrMutex().lock(); defer std.debug.getStderrMutex().unlock(); const stderr = std.io.getStdErr().writer(); stringify(self, .{}, stderr) catch return; } pub fn jsonStringify( value: @This(), options: StringifyOptions, out_stream: anytype, ) @TypeOf(out_stream).Error!void { switch (value) { .null => try stringify(null, options, out_stream), .bool => |inner| try stringify(inner, options, out_stream), .integer => |inner| try stringify(inner, options, out_stream), .float => |inner| try stringify(inner, options, out_stream), .number_string => |inner| try out_stream.writeAll(inner), .string => |inner| try stringify(inner, options, out_stream), .array => |inner| try stringify(inner.items, options, out_stream), .object => |inner| { try out_stream.writeByte('{'); var field_output = false; var child_options = options; child_options.whitespace.indent_level += 1; var it = inner.iterator(); while (it.next()) |entry| { if (!field_output) { field_output = true; } else { try out_stream.writeByte(','); } try child_options.whitespace.outputIndent(out_stream); try stringify(entry.key_ptr.*, options, out_stream); try out_stream.writeByte(':'); if (child_options.whitespace.separator) { try out_stream.writeByte(' '); } try stringify(entry.value_ptr.*, child_options, out_stream); } if (field_output) { try options.whitespace.outputIndent(out_stream); } try out_stream.writeByte('}'); }, } } pub fn jsonParse(allocator: Allocator, source: anytype, options: ParseOptions) ParseError(@TypeOf(source.*))!@This() { _ = options; // The grammar of the stack is: // (.array | .object .string)* var stack = Array.init(allocator); defer stack.deinit(); while (true) { // Assert the stack grammar at the top of the stack. debug.assert(stack.items.len == 0 or stack.items[stack.items.len - 1] == .array or (stack.items[stack.items.len - 2] == .object and stack.items[stack.items.len - 1] == .string)); switch (try source.nextAlloc(allocator, .alloc_if_needed)) { inline .string, .allocated_string => |s| { return try handleCompleteValue(&stack, allocator, source, Value{ .string = s }) orelse continue; }, inline .number, .allocated_number => |slice| { return try handleCompleteValue(&stack, allocator, source, Value.parseFromNumberSlice(slice)) orelse continue; }, .null => return try handleCompleteValue(&stack, allocator, source, .null) orelse continue, .true => return try handleCompleteValue(&stack, allocator, source, Value{ .bool = true }) orelse continue, .false => return try handleCompleteValue(&stack, allocator, source, Value{ .bool = false }) orelse continue, .object_begin => { switch (try source.nextAlloc(allocator, .alloc_if_needed)) { .object_end => return try handleCompleteValue(&stack, allocator, source, Value{ .object = ObjectMap.init(allocator) }) orelse continue, inline .string, .allocated_string => |key| { try stack.appendSlice(&[_]Value{ Value{ .object = ObjectMap.init(allocator) }, Value{ .string = key }, }); }, else => unreachable, } }, .array_begin => { try stack.append(Value{ .array = Array.init(allocator) }); }, .array_end => return try handleCompleteValue(&stack, allocator, source, stack.pop()) orelse continue, else => unreachable, } } } pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { _ = allocator; _ = options; return source; } }; fn handleCompleteValue(stack: *Array, allocator: Allocator, source: anytype, value_: Value) !?Value { if (stack.items.len == 0) return value_; var value = value_; while (true) { // Assert the stack grammar at the top of the stack. debug.assert(stack.items[stack.items.len - 1] == .array or (stack.items[stack.items.len - 2] == .object and stack.items[stack.items.len - 1] == .string)); switch (stack.items[stack.items.len - 1]) { .string => |key| { // stack: [..., .object, .string] _ = stack.pop(); // stack: [..., .object] var object = &stack.items[stack.items.len - 1].object; try object.put(key, value); // This is an invalid state to leave the stack in, // so we have to process the next token before we return. switch (try source.nextAlloc(allocator, .alloc_if_needed)) { .object_end => { // This object is complete. value = stack.pop(); // Effectively recurse now that we have a complete value. if (stack.items.len == 0) return value; continue; }, inline .string, .allocated_string => |next_key| { // We've got another key. try stack.append(Value{ .string = next_key }); // stack: [..., .object, .string] return null; }, else => unreachable, } }, .array => |*array| { // stack: [..., .array] try array.append(value); return null; }, else => unreachable, } } } test { _ = @import("dynamic_test.zig"); }