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 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 ValueTree = struct { arena: *ArenaAllocator, root: Value, pub fn deinit(self: *ValueTree) void { self.arena.deinit(); self.arena.child_allocator.destroy(self.arena); } }; pub const ObjectMap = StringArrayHashMap(Value); pub const Array = ArrayList(Value); /// Represents a JSON value /// Currently only supports numbers that fit into i64 or f64. 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 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 dump(self: Value) void { std.debug.getStderrMutex().lock(); defer std.debug.getStderrMutex().unlock(); const stderr = std.io.getStdErr().writer(); stringify(self, .{}, stderr) catch return; } }; /// A non-stream JSON parser which constructs a tree of Value's. pub const Parser = struct { allocator: Allocator, state: State, alloc_when: AllocWhen, // Stores parent nodes and un-combined Values. stack: Array, const State = enum { object_key, object_value, array_value, simple, }; pub fn init(allocator: Allocator, alloc_when: AllocWhen) Parser { return Parser{ .allocator = allocator, .state = .simple, .alloc_when = alloc_when, .stack = Array.init(allocator), }; } pub fn deinit(p: *Parser) void { p.stack.deinit(); } pub fn reset(p: *Parser) void { p.state = .simple; p.stack.shrinkRetainingCapacity(0); } pub fn parse(p: *Parser, input: []const u8) !ValueTree { var scanner = JsonScanner.initCompleteInput(p.allocator, input); defer scanner.deinit(); var arena = try p.allocator.create(ArenaAllocator); errdefer p.allocator.destroy(arena); arena.* = ArenaAllocator.init(p.allocator); errdefer arena.deinit(); const allocator = arena.allocator(); while (true) { const token = try scanner.nextAlloc(allocator, p.alloc_when); if (token == .end_of_document) break; try p.transition(allocator, token); } debug.assert(p.stack.items.len == 1); return ValueTree{ .arena = arena, .root = p.stack.items[0], }; } // Even though p.allocator exists, we take an explicit allocator so that allocation state // can be cleaned up on error correctly during a `parse` on call. fn transition(p: *Parser, allocator: Allocator, token: Token) !void { switch (p.state) { .object_key => switch (token) { .object_end => { if (p.stack.items.len == 1) { return; } var value = p.stack.pop(); try p.pushToParent(&value); }, .string => |s| { try p.stack.append(Value{ .string = s }); p.state = .object_value; }, .allocated_string => |s| { try p.stack.append(Value{ .string = s }); p.state = .object_value; }, else => unreachable, }, .object_value => { var object = &p.stack.items[p.stack.items.len - 2].object; var key = p.stack.items[p.stack.items.len - 1].string; switch (token) { .object_begin => { try p.stack.append(Value{ .object = ObjectMap.init(allocator) }); p.state = .object_key; }, .array_begin => { try p.stack.append(Value{ .array = Array.init(allocator) }); p.state = .array_value; }, .string => |s| { try object.put(key, Value{ .string = s }); _ = p.stack.pop(); p.state = .object_key; }, .allocated_string => |s| { try object.put(key, Value{ .string = s }); _ = p.stack.pop(); p.state = .object_key; }, .number => |slice| { try object.put(key, try p.parseNumber(slice)); _ = p.stack.pop(); p.state = .object_key; }, .allocated_number => |slice| { try object.put(key, try p.parseNumber(slice)); _ = p.stack.pop(); p.state = .object_key; }, .true => { try object.put(key, Value{ .bool = true }); _ = p.stack.pop(); p.state = .object_key; }, .false => { try object.put(key, Value{ .bool = false }); _ = p.stack.pop(); p.state = .object_key; }, .null => { try object.put(key, .null); _ = p.stack.pop(); p.state = .object_key; }, .object_end, .array_end, .end_of_document => unreachable, .partial_number, .partial_string, .partial_string_escaped_1, .partial_string_escaped_2, .partial_string_escaped_3, .partial_string_escaped_4 => unreachable, } }, .array_value => { var array = &p.stack.items[p.stack.items.len - 1].array; switch (token) { .array_end => { if (p.stack.items.len == 1) { return; } var value = p.stack.pop(); try p.pushToParent(&value); }, .object_begin => { try p.stack.append(Value{ .object = ObjectMap.init(allocator) }); p.state = .object_key; }, .array_begin => { try p.stack.append(Value{ .array = Array.init(allocator) }); p.state = .array_value; }, .string => |s| { try array.append(Value{ .string = s }); }, .allocated_string => |s| { try array.append(Value{ .string = s }); }, .number => |slice| { try array.append(try p.parseNumber(slice)); }, .allocated_number => |slice| { try array.append(try p.parseNumber(slice)); }, .true => { try array.append(Value{ .bool = true }); }, .false => { try array.append(Value{ .bool = false }); }, .null => { try array.append(.null); }, .object_end, .end_of_document => unreachable, .partial_number, .partial_string, .partial_string_escaped_1, .partial_string_escaped_2, .partial_string_escaped_3, .partial_string_escaped_4 => unreachable, } }, .simple => switch (token) { .object_begin => { try p.stack.append(Value{ .object = ObjectMap.init(allocator) }); p.state = .object_key; }, .array_begin => { try p.stack.append(Value{ .array = Array.init(allocator) }); p.state = .array_value; }, .string => |s| { try p.stack.append(Value{ .string = s }); }, .allocated_string => |s| { try p.stack.append(Value{ .string = s }); }, .number => |slice| { try p.stack.append(try p.parseNumber(slice)); }, .allocated_number => |slice| { try p.stack.append(try p.parseNumber(slice)); }, .true => { try p.stack.append(Value{ .bool = true }); }, .false => { try p.stack.append(Value{ .bool = false }); }, .null => { try p.stack.append(.null); }, .object_end, .array_end, .end_of_document => unreachable, .partial_number, .partial_string, .partial_string_escaped_1, .partial_string_escaped_2, .partial_string_escaped_3, .partial_string_escaped_4 => unreachable, }, } } fn pushToParent(p: *Parser, value: *const Value) !void { switch (p.stack.items[p.stack.items.len - 1]) { // Object Parent -> [ ..., object, , value ] .string => |key| { _ = p.stack.pop(); var object = &p.stack.items[p.stack.items.len - 1].object; try object.put(key, value.*); p.state = .object_key; }, // Array Parent -> [ ..., , value ] .array => |*array| { try array.append(value.*); p.state = .array_value; }, else => { unreachable; }, } } fn parseNumber(p: *Parser, slice: []const u8) !Value { _ = p; return if (isNumberFormattedLikeAnInteger(slice)) Value{ .integer = std.fmt.parseInt(i64, slice, 10) catch |e| switch (e) { error.Overflow => return Value{ .number_string = slice }, error.InvalidCharacter => |err| return err, }, } else Value{ .float = try std.fmt.parseFloat(f64, slice) }; } }; test { _ = @import("dynamic_test.zig"); }