Files
zig/lib/std/json/dynamic.zig
T
Andrew Kelley 1925e0319f update lockStderrWriter sites
use the application's Io implementation where possible. This correctly
makes writing to stderr cancelable, fallible, and participate in the
application's event loop. It also removes one more hard-coded
dependency on a secondary Io implementation.
2025-12-23 22:15:09 -08:00

193 lines
7.8 KiB
Zig

const std = @import("std");
const debug = std.debug;
const ArenaAllocator = std.heap.ArenaAllocator;
const StringArrayHashMap = std.StringArrayHashMap;
const Allocator = std.mem.Allocator;
const json = std.json;
const ParseOptions = @import("./static.zig").ParseOptions;
const ParseError = @import("./static.zig").ParseError;
const isNumberFormattedLikeAnInteger = @import("Scanner.zig").isNumberFormattedLikeAnInteger;
pub const ObjectMap = StringArrayHashMap(Value);
pub const Array = std.array_list.Managed(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.
/// See also `std.json.ParseOptions.parse_numbers`.
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(v: Value) void {
const stderr = std.debug.lockStderrWriter(&.{});
defer std.debug.unlockStderrWriter();
json.Stringify.value(v, .{}, &stderr.interface) catch return;
}
pub fn jsonStringify(value: @This(), jws: anytype) !void {
switch (value) {
.null => try jws.write(null),
.bool => |inner| try jws.write(inner),
.integer => |inner| try jws.write(inner),
.float => |inner| try jws.write(inner),
.number_string => |inner| try jws.print("{s}", .{inner}),
.string => |inner| try jws.write(inner),
.array => |inner| try jws.write(inner.items),
.object => |inner| {
try jws.beginObject();
var it = inner.iterator();
while (it.next()) |entry| {
try jws.objectField(entry.key_ptr.*);
try jws.write(entry.value_ptr.*);
}
try jws.endObject();
},
}
}
pub fn jsonParse(allocator: Allocator, source: anytype, options: ParseOptions) ParseError(@TypeOf(source.*))!@This() {
// 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.nextAllocMax(allocator, .alloc_always, options.max_value_len.?)) {
.allocated_string => |s| {
return try handleCompleteValue(&stack, allocator, source, Value{ .string = s }, options) orelse continue;
},
.allocated_number => |slice| {
if (options.parse_numbers) {
return try handleCompleteValue(&stack, allocator, source, Value.parseFromNumberSlice(slice), options) orelse continue;
} else {
return try handleCompleteValue(&stack, allocator, source, Value{ .number_string = slice }, options) orelse continue;
}
},
.null => return try handleCompleteValue(&stack, allocator, source, .null, options) orelse continue,
.true => return try handleCompleteValue(&stack, allocator, source, Value{ .bool = true }, options) orelse continue,
.false => return try handleCompleteValue(&stack, allocator, source, Value{ .bool = false }, options) orelse continue,
.object_begin => {
switch (try source.nextAllocMax(allocator, .alloc_always, options.max_value_len.?)) {
.object_end => return try handleCompleteValue(&stack, allocator, source, Value{ .object = ObjectMap.init(allocator) }, options) orelse continue,
.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().?, options) 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, options: ParseOptions) !?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;
const gop = try object.getOrPut(key);
if (gop.found_existing) {
switch (options.duplicate_field_behavior) {
.use_first => {},
.@"error" => return error.DuplicateField,
.use_last => {
gop.value_ptr.* = value;
},
}
} else {
gop.value_ptr.* = 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.nextAllocMax(allocator, .alloc_always, options.max_value_len.?)) {
.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;
},
.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");
}