zig/lib/std/json/dynamic.zig
Matthew Lugg 74931fe25c
std.debug.lockStderrWriter: also return ttyconf
`std.Io.tty.Config.detect` may be an expensive check (e.g. involving
syscalls), and doing it every time we need to print isn't really
necessary; under normal usage, we can compute the value once and cache
it for the whole program's execution. Since anyone outputting to stderr
may reasonably want this information (in fact they are very likely to),
it makes sense to cache it and return it from `lockStderrWriter`. Call
sites who do not need it will experience no significant overhead, and
can just ignore the TTY config with a `const w, _` destructure.
2025-10-30 09:31:28 +00: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 w, _ = std.debug.lockStderrWriter(&.{});
defer std.debug.unlockStderrWriter();
json.Stringify.value(v, .{}, w) 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");
}