mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 13:54:21 +00:00
344 lines
12 KiB
Zig
344 lines
12 KiB
Zig
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, <key>, 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 -> [ ..., <array>, 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");
|
|
}
|