diff --git a/CMakeLists.txt b/CMakeLists.txt index b2ebf32bb07e..7cd315377f73 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -205,6 +205,7 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/lib/std/atomic/queue.zig" "${CMAKE_SOURCE_DIR}/lib/std/atomic/stack.zig" "${CMAKE_SOURCE_DIR}/lib/std/base64.zig" + "${CMAKE_SOURCE_DIR}/lib/std/BitStack.zig" "${CMAKE_SOURCE_DIR}/lib/std/buf_map.zig" "${CMAKE_SOURCE_DIR}/lib/std/Build.zig" "${CMAKE_SOURCE_DIR}/lib/std/Build/Cache.zig" @@ -260,7 +261,7 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/lib/std/io/seekable_stream.zig" "${CMAKE_SOURCE_DIR}/lib/std/io/writer.zig" "${CMAKE_SOURCE_DIR}/lib/std/json.zig" - "${CMAKE_SOURCE_DIR}/lib/std/json/write_stream.zig" + "${CMAKE_SOURCE_DIR}/lib/std/json/stringify.zig" "${CMAKE_SOURCE_DIR}/lib/std/leb128.zig" "${CMAKE_SOURCE_DIR}/lib/std/linked_list.zig" "${CMAKE_SOURCE_DIR}/lib/std/log.zig" diff --git a/lib/std/BitStack.zig b/lib/std/BitStack.zig new file mode 100644 index 000000000000..592b541d49bb --- /dev/null +++ b/lib/std/BitStack.zig @@ -0,0 +1,86 @@ +//! Effectively a stack of u1 values implemented using ArrayList(u8). + +const BitStack = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; + +bytes: std.ArrayList(u8), +bit_len: usize = 0, + +pub fn init(allocator: Allocator) @This() { + return .{ + .bytes = std.ArrayList(u8).init(allocator), + }; +} + +pub fn deinit(self: *@This()) void { + self.bytes.deinit(); + self.* = undefined; +} + +pub fn ensureTotalCapacity(self: *@This(), bit_capcity: usize) Allocator.Error!void { + const byte_capacity = (bit_capcity + 7) >> 3; + try self.bytes.ensureTotalCapacity(byte_capacity); +} + +pub fn push(self: *@This(), b: u1) Allocator.Error!void { + const byte_index = self.bit_len >> 3; + if (self.bytes.items.len <= byte_index) { + try self.bytes.append(0); + } + + pushWithStateAssumeCapacity(self.bytes.items, &self.bit_len, b); +} + +pub fn peek(self: *const @This()) u1 { + return peekWithState(self.bytes.items, self.bit_len); +} + +pub fn pop(self: *@This()) u1 { + return popWithState(self.bytes.items, &self.bit_len); +} + +/// Standalone function for working with a fixed-size buffer. +pub fn pushWithStateAssumeCapacity(buf: []u8, bit_len: *usize, b: u1) void { + const byte_index = bit_len.* >> 3; + const bit_index = @as(u3, @intCast(bit_len.* & 7)); + + buf[byte_index] &= ~(@as(u8, 1) << bit_index); + buf[byte_index] |= @as(u8, b) << bit_index; + + bit_len.* += 1; +} + +/// Standalone function for working with a fixed-size buffer. +pub fn peekWithState(buf: []const u8, bit_len: usize) u1 { + const byte_index = (bit_len - 1) >> 3; + const bit_index = @as(u3, @intCast((bit_len - 1) & 7)); + return @as(u1, @intCast((buf[byte_index] >> bit_index) & 1)); +} + +/// Standalone function for working with a fixed-size buffer. +pub fn popWithState(buf: []const u8, bit_len: *usize) u1 { + const b = peekWithState(buf, bit_len.*); + bit_len.* -= 1; + return b; +} + +const testing = std.testing; +test BitStack { + var stack = BitStack.init(testing.allocator); + defer stack.deinit(); + + try stack.push(1); + try stack.push(0); + try stack.push(0); + try stack.push(1); + + try testing.expectEqual(@as(u1, 1), stack.peek()); + try testing.expectEqual(@as(u1, 1), stack.pop()); + try testing.expectEqual(@as(u1, 0), stack.peek()); + try testing.expectEqual(@as(u1, 0), stack.pop()); + try testing.expectEqual(@as(u1, 0), stack.pop()); + try testing.expectEqual(@as(u1, 1), stack.pop()); +} diff --git a/lib/std/json.zig b/lib/std/json.zig index f8480d42070f..29f46c263ad6 100644 --- a/lib/std/json.zig +++ b/lib/std/json.zig @@ -43,14 +43,15 @@ test Value { test writeStream { var out = ArrayList(u8).init(testing.allocator); defer out.deinit(); - var write_stream = writeStream(out.writer(), 99); + var write_stream = writeStream(out.writer(), .{ .whitespace = .indent_2 }); + defer write_stream.deinit(); try write_stream.beginObject(); try write_stream.objectField("foo"); - try write_stream.emitNumber(123); + try write_stream.write(123); try write_stream.endObject(); const expected = \\{ - \\ "foo": 123 + \\ "foo": 123 \\} ; try testing.expectEqualSlices(u8, expected, out.items); @@ -98,13 +99,16 @@ pub const ParseError = @import("json/static.zig").ParseError; pub const ParseFromValueError = @import("json/static.zig").ParseFromValueError; pub const StringifyOptions = @import("json/stringify.zig").StringifyOptions; -pub const encodeJsonString = @import("json/stringify.zig").encodeJsonString; -pub const encodeJsonStringChars = @import("json/stringify.zig").encodeJsonStringChars; pub const stringify = @import("json/stringify.zig").stringify; +pub const stringifyMaxDepth = @import("json/stringify.zig").stringifyMaxDepth; +pub const stringifyArbitraryDepth = @import("json/stringify.zig").stringifyArbitraryDepth; pub const stringifyAlloc = @import("json/stringify.zig").stringifyAlloc; - -pub const WriteStream = @import("json/write_stream.zig").WriteStream; -pub const writeStream = @import("json/write_stream.zig").writeStream; +pub const writeStream = @import("json/stringify.zig").writeStream; +pub const writeStreamMaxDepth = @import("json/stringify.zig").writeStreamMaxDepth; +pub const writeStreamArbitraryDepth = @import("json/stringify.zig").writeStreamArbitraryDepth; +pub const WriteStream = @import("json/stringify.zig").WriteStream; +pub const encodeJsonString = @import("json/stringify.zig").encodeJsonString; +pub const encodeJsonStringChars = @import("json/stringify.zig").encodeJsonStringChars; // Deprecations pub const parse = @compileError("Deprecated; use parseFromSlice() or parseFromTokenSource() instead."); @@ -117,9 +121,8 @@ pub const TokenStream = @compileError("Deprecated; use json.Scanner or json.Read test { _ = @import("json/test.zig"); _ = @import("json/scanner.zig"); - _ = @import("json/write_stream.zig"); _ = @import("json/dynamic.zig"); - _ = @import("json/hashmap_test.zig"); + _ = @import("json/hashmap.zig"); _ = @import("json/static.zig"); _ = @import("json/stringify.zig"); _ = @import("json/JSONTestSuite_test.zig"); diff --git a/lib/std/json/dynamic.zig b/lib/std/json/dynamic.zig index aea3f1ac8290..18f7e385bfe7 100644 --- a/lib/std/json/dynamic.zig +++ b/lib/std/json/dynamic.zig @@ -59,44 +59,23 @@ pub const Value = union(enum) { stringify(self, .{}, stderr) catch return; } - pub fn jsonStringify( - value: @This(), - options: StringifyOptions, - out_stream: anytype, - ) @TypeOf(out_stream).Error!void { + pub fn jsonStringify(value: @This(), jws: anytype) !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), + .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.writePreformatted(inner), + .string => |inner| try jws.write(inner), + .array => |inner| try jws.write(inner.items), .object => |inner| { - try out_stream.writeByte('{'); - var field_output = false; - var child_options = options; - child_options.whitespace.indent_level += 1; + try jws.beginObject(); 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 jws.objectField(entry.key_ptr.*); + try jws.write(entry.value_ptr.*); } - try out_stream.writeByte('}'); + try jws.endObject(); }, } } diff --git a/lib/std/json/dynamic_test.zig b/lib/std/json/dynamic_test.zig index 98a9d41e85b6..220c94edcf86 100644 --- a/lib/std/json/dynamic_test.zig +++ b/lib/std/json/dynamic_test.zig @@ -69,38 +69,34 @@ test "json.parser.dynamic" { try testing.expect(mem.eql(u8, large_int.number_string, "18446744073709551615")); } -const writeStream = @import("./write_stream.zig").writeStream; +const writeStream = @import("./stringify.zig").writeStream; test "write json then parse it" { var out_buffer: [1000]u8 = undefined; var fixed_buffer_stream = std.io.fixedBufferStream(&out_buffer); const out_stream = fixed_buffer_stream.writer(); - var jw = writeStream(out_stream, 4); + var jw = writeStream(out_stream, .{}); + defer jw.deinit(); try jw.beginObject(); try jw.objectField("f"); - try jw.emitBool(false); + try jw.write(false); try jw.objectField("t"); - try jw.emitBool(true); + try jw.write(true); try jw.objectField("int"); - try jw.emitNumber(1234); + try jw.write(1234); try jw.objectField("array"); try jw.beginArray(); - - try jw.arrayElem(); - try jw.emitNull(); - - try jw.arrayElem(); - try jw.emitNumber(12.34); - + try jw.write(null); + try jw.write(12.34); try jw.endArray(); try jw.objectField("str"); - try jw.emitString("hello"); + try jw.write("hello"); try jw.endObject(); @@ -185,64 +181,50 @@ test "escaped characters" { } test "Value.jsonStringify" { - { - var buffer: [10]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buffer); - try @as(Value, .null).jsonStringify(.{}, fbs.writer()); - try testing.expectEqualSlices(u8, fbs.getWritten(), "null"); - } - { - var buffer: [10]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buffer); - try (Value{ .bool = true }).jsonStringify(.{}, fbs.writer()); - try testing.expectEqualSlices(u8, fbs.getWritten(), "true"); - } - { - var buffer: [10]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buffer); - try (Value{ .integer = 42 }).jsonStringify(.{}, fbs.writer()); - try testing.expectEqualSlices(u8, fbs.getWritten(), "42"); - } - { - var buffer: [10]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buffer); - try (Value{ .number_string = "43" }).jsonStringify(.{}, fbs.writer()); - try testing.expectEqualSlices(u8, fbs.getWritten(), "43"); - } - { - var buffer: [10]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buffer); - try (Value{ .float = 42 }).jsonStringify(.{}, fbs.writer()); - try testing.expectEqualSlices(u8, fbs.getWritten(), "4.2e+01"); - } - { - var buffer: [10]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buffer); - try (Value{ .string = "weeee" }).jsonStringify(.{}, fbs.writer()); - try testing.expectEqualSlices(u8, fbs.getWritten(), "\"weeee\""); - } - { - var buffer: [10]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buffer); - var vals = [_]Value{ - .{ .integer = 1 }, - .{ .integer = 2 }, - .{ .number_string = "3" }, - }; - try (Value{ - .array = Array.fromOwnedSlice(undefined, &vals), - }).jsonStringify(.{}, fbs.writer()); - try testing.expectEqualSlices(u8, fbs.getWritten(), "[1,2,3]"); - } - { - var buffer: [10]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buffer); - var obj = ObjectMap.init(testing.allocator); - defer obj.deinit(); - try obj.putNoClobber("a", .{ .string = "b" }); - try (Value{ .object = obj }).jsonStringify(.{}, fbs.writer()); - try testing.expectEqualSlices(u8, fbs.getWritten(), "{\"a\":\"b\"}"); - } + var vals = [_]Value{ + .{ .integer = 1 }, + .{ .integer = 2 }, + .{ .number_string = "3" }, + }; + var obj = ObjectMap.init(testing.allocator); + defer obj.deinit(); + try obj.putNoClobber("a", .{ .string = "b" }); + var array = [_]Value{ + Value.null, + Value{ .bool = true }, + Value{ .integer = 42 }, + Value{ .number_string = "43" }, + Value{ .float = 42 }, + Value{ .string = "weeee" }, + Value{ .array = Array.fromOwnedSlice(undefined, &vals) }, + Value{ .object = obj }, + }; + var buffer: [0x1000]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buffer); + + var jw = writeStream(fbs.writer(), .{ .whitespace = .indent_1 }); + defer jw.deinit(); + try jw.write(array); + + const expected = + \\[ + \\ null, + \\ true, + \\ 42, + \\ 43, + \\ 4.2e+01, + \\ "weeee", + \\ [ + \\ 1, + \\ 2, + \\ 3 + \\ ], + \\ { + \\ "a": "b" + \\ } + \\] + ; + try testing.expectEqualSlices(u8, expected, fbs.getWritten()); } test "parseFromValue(std.json.Value,...)" { diff --git a/lib/std/json/hashmap.zig b/lib/std/json/hashmap.zig index 320592589adb..7b66a95c87f9 100644 --- a/lib/std/json/hashmap.zig +++ b/lib/std/json/hashmap.zig @@ -5,9 +5,6 @@ const ParseOptions = @import("static.zig").ParseOptions; const innerParse = @import("static.zig").innerParse; const innerParseFromValue = @import("static.zig").innerParseFromValue; const Value = @import("dynamic.zig").Value; -const StringifyOptions = @import("stringify.zig").StringifyOptions; -const stringify = @import("stringify.zig").stringify; -const encodeJsonString = @import("stringify.zig").encodeJsonString; /// A thin wrapper around `std.StringArrayHashMapUnmanaged` that implements /// `jsonParse`, `jsonParseFromValue`, and `jsonStringify`. @@ -70,30 +67,14 @@ pub fn ArrayHashMap(comptime T: type) type { return .{ .map = map }; } - pub fn jsonStringify(self: @This(), options: StringifyOptions, out_stream: anytype) !void { - try out_stream.writeByte('{'); - var field_output = false; - var child_options = options; - child_options.whitespace.indent_level += 1; + pub fn jsonStringify(self: @This(), jws: anytype) !void { + try jws.beginObject(); var it = self.map.iterator(); while (it.next()) |kv| { - if (!field_output) { - field_output = true; - } else { - try out_stream.writeByte(','); - } - try child_options.whitespace.outputIndent(out_stream); - try encodeJsonString(kv.key_ptr.*, options, out_stream); - try out_stream.writeByte(':'); - if (child_options.whitespace.separator) { - try out_stream.writeByte(' '); - } - try stringify(kv.value_ptr.*, child_options, out_stream); - } - if (field_output) { - try options.whitespace.outputIndent(out_stream); + try jws.objectField(kv.key_ptr.*); + try jws.write(kv.value_ptr.*); } - try out_stream.writeByte('}'); + try jws.endObject(); } }; } diff --git a/lib/std/json/hashmap_test.zig b/lib/std/json/hashmap_test.zig index 3baead972a4c..674079025c42 100644 --- a/lib/std/json/hashmap_test.zig +++ b/lib/std/json/hashmap_test.zig @@ -101,11 +101,7 @@ test "stringify json hashmap whitespace" { try value.map.put(testing.allocator, "xyz", .{ .i = 1, .s = "w" }); { - const doc = try stringifyAlloc(testing.allocator, value, .{ - .whitespace = .{ - .indent = .{ .space = 2 }, - }, - }); + const doc = try stringifyAlloc(testing.allocator, value, .{ .whitespace = .indent_2 }); defer testing.allocator.free(doc); try testing.expectEqualStrings( \\{ diff --git a/lib/std/json/scanner.zig b/lib/std/json/scanner.zig index 274faba2ff60..6fc374dda4b2 100644 --- a/lib/std/json/scanner.zig +++ b/lib/std/json/scanner.zig @@ -33,6 +33,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const assert = std.debug.assert; +const BitStack = std.BitStack; /// Scan the input and check for malformed JSON. /// On `SyntaxError` or `UnexpectedEndOfInput`, returns `false`. @@ -337,7 +338,7 @@ pub fn Reader(comptime buffer_size: usize, comptime ReaderType: type) type { } } /// Like `std.json.Scanner.skipUntilStackHeight()` but handles `error.BufferUnderrun`. - pub fn skipUntilStackHeight(self: *@This(), terminal_stack_height: u32) NextError!void { + pub fn skipUntilStackHeight(self: *@This(), terminal_stack_height: usize) NextError!void { while (true) { return self.scanner.skipUntilStackHeight(terminal_stack_height) catch |err| switch (err) { error.BufferUnderrun => { @@ -350,11 +351,11 @@ pub fn Reader(comptime buffer_size: usize, comptime ReaderType: type) type { } /// Calls `std.json.Scanner.stackHeight`. - pub fn stackHeight(self: *const @This()) u32 { + pub fn stackHeight(self: *const @This()) usize { return self.scanner.stackHeight(); } /// Calls `std.json.Scanner.ensureTotalStackCapacity`. - pub fn ensureTotalStackCapacity(self: *@This(), height: u32) Allocator.Error!void { + pub fn ensureTotalStackCapacity(self: *@This(), height: usize) Allocator.Error!void { try self.scanner.ensureTotalStackCapacity(height); } @@ -654,7 +655,7 @@ pub const Scanner = struct { /// Skip tokens until an `.object_end` or `.array_end` token results in a `stackHeight()` equal the given stack height. /// Unlike `skipValue()`, this function is available in streaming mode. - pub fn skipUntilStackHeight(self: *@This(), terminal_stack_height: u32) NextError!void { + pub fn skipUntilStackHeight(self: *@This(), terminal_stack_height: usize) NextError!void { while (true) { switch (try self.next()) { .object_end, .array_end => { @@ -667,13 +668,13 @@ pub const Scanner = struct { } /// The depth of `{}` or `[]` nesting levels at the current position. - pub fn stackHeight(self: *const @This()) u32 { + pub fn stackHeight(self: *const @This()) usize { return self.stack.bit_len; } /// Pre allocate memory to hold the given number of nesting levels. /// `stackHeight()` up to the given number will not cause allocations. - pub fn ensureTotalStackCapacity(self: *@This(), height: u32) Allocator.Error!void { + pub fn ensureTotalStackCapacity(self: *@This(), height: usize) Allocator.Error!void { try self.stack.ensureTotalCapacity(height); } @@ -1697,53 +1698,6 @@ pub const Scanner = struct { const OBJECT_MODE = 0; const ARRAY_MODE = 1; -const BitStack = struct { - bytes: std.ArrayList(u8), - bit_len: u32 = 0, - - pub fn init(allocator: Allocator) @This() { - return .{ - .bytes = std.ArrayList(u8).init(allocator), - }; - } - - pub fn deinit(self: *@This()) void { - self.bytes.deinit(); - self.* = undefined; - } - - pub fn ensureTotalCapacity(self: *@This(), bit_capcity: u32) Allocator.Error!void { - const byte_capacity = (bit_capcity + 7) >> 3; - try self.bytes.ensureTotalCapacity(byte_capacity); - } - - pub fn push(self: *@This(), b: u1) Allocator.Error!void { - const byte_index = self.bit_len >> 3; - const bit_index = @as(u3, @intCast(self.bit_len & 7)); - - if (self.bytes.items.len <= byte_index) { - try self.bytes.append(0); - } - - self.bytes.items[byte_index] &= ~(@as(u8, 1) << bit_index); - self.bytes.items[byte_index] |= @as(u8, b) << bit_index; - - self.bit_len += 1; - } - - pub fn peek(self: *const @This()) u1 { - const byte_index = (self.bit_len - 1) >> 3; - const bit_index = @as(u3, @intCast((self.bit_len - 1) & 7)); - return @as(u1, @intCast((self.bytes.items[byte_index] >> bit_index) & 1)); - } - - pub fn pop(self: *@This()) u1 { - const b = self.peek(); - self.bit_len -= 1; - return b; - } -}; - fn appendSlice(list: *std.ArrayList(u8), buf: []const u8, max_value_len: usize) !void { const new_len = std.math.add(usize, list.items.len, buf.len) catch return error.ValueTooLong; if (new_len > max_value_len) return error.ValueTooLong; diff --git a/lib/std/json/stringify.zig b/lib/std/json/stringify.zig index 17ab3a1eb548..7870ad886e34 100644 --- a/lib/std/json/stringify.zig +++ b/lib/std/json/stringify.zig @@ -1,73 +1,583 @@ const std = @import("std"); -const mem = std.mem; const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const BitStack = std.BitStack; + +const OBJECT_MODE = 0; +const ARRAY_MODE = 1; pub const StringifyOptions = struct { - pub const Whitespace = struct { - /// How many indentation levels deep are we? + /// Controls the whitespace emitted. + /// The default `.minified` is a compact encoding with no whitespace between tokens. + /// Any setting other than `.minified` will use newlines, indentation, and a space after each ':'. + /// `.indent_1` means 1 space for each indentation level, `.indent_2` means 2 spaces, etc. + /// `.indent_tab` uses a tab for each indentation level. + whitespace: enum { + minified, + indent_1, + indent_2, + indent_3, + indent_4, + indent_8, + indent_tab, + } = .minified, + + /// Should optional fields with null value be written? + emit_null_optional_fields: bool = true, + + /// Arrays/slices of u8 are typically encoded as JSON strings. + /// This option emits them as arrays of numbers instead. + /// Does not affect calls to `objectField()`. + emit_strings_as_arrays: bool = false, + + /// Should unicode characters be escaped in strings? + escape_unicode: bool = false, +}; + +/// Writes the given value to the `std.io.Writer` stream. +/// See `WriteStream` for how the given value is serialized into JSON. +/// The maximum nesting depth of the output JSON document is 256. +/// See also `stringifyMaxDepth` and `stringifyArbitraryDepth`. +pub fn stringify( + value: anytype, + options: StringifyOptions, + out_stream: anytype, +) @TypeOf(out_stream).Error!void { + var jw = writeStream(out_stream, options); + defer jw.deinit(); + try jw.write(value); +} + +/// Like `stringify` with configurable nesting depth. +/// `max_depth` is rounded up to the nearest multiple of 8. +/// Give `null` for `max_depth` to disable some safety checks and allow arbitrary nesting depth. +/// See `writeStreamMaxDepth` for more info. +pub fn stringifyMaxDepth( + value: anytype, + options: StringifyOptions, + out_stream: anytype, + comptime max_depth: ?usize, +) @TypeOf(out_stream).Error!void { + var jw = writeStreamMaxDepth(out_stream, options, max_depth); + try jw.write(value); +} + +/// Like `stringify` but takes an allocator to facilitate safety checks while allowing arbitrary nesting depth. +/// These safety checks can be helpful when debugging custom `jsonStringify` implementations; +/// See `WriteStream`. +pub fn stringifyArbitraryDepth( + allocator: Allocator, + value: anytype, + options: StringifyOptions, + out_stream: anytype, +) WriteStream(@TypeOf(out_stream), .checked_to_arbitrary_depth).Error!void { + var jw = writeStreamArbitraryDepth(allocator, out_stream, options); + defer jw.deinit(); + try jw.write(value); +} + +/// Calls `stringifyArbitraryDepth` and stores the result in dynamically allocated memory +/// instead of taking a `std.io.Writer`. +/// +/// Caller owns returned memory. +pub fn stringifyAlloc( + allocator: Allocator, + value: anytype, + options: StringifyOptions, +) error{OutOfMemory}![]const u8 { + var list = std.ArrayList(u8).init(allocator); + errdefer list.deinit(); + try stringifyArbitraryDepth(allocator, value, options, list.writer()); + return list.toOwnedSlice(); +} + +/// See `WriteStream` for documentation. +/// Equivalent to calling `writeStreamMaxDepth` with a depth of `256`. +/// +/// The caller does *not* need to call `deinit()` on the returned object. +pub fn writeStream( + out_stream: anytype, + options: StringifyOptions, +) WriteStream(@TypeOf(out_stream), .{ .checked_to_fixed_depth = 256 }) { + return writeStreamMaxDepth(out_stream, options, 256); +} + +/// See `WriteStream` for documentation. +/// The returned object includes 1 bit of size per `max_depth` to enable safety checks on the order of method calls; +/// see the grammar in the `WriteStream` documentation. +/// `max_depth` is rounded up to the nearest multiple of 8. +/// If the nesting depth exceeds `max_depth`, it is detectable illegal behavior. +/// Give `null` for `max_depth` to disable safety checks for the grammar and allow arbitrary nesting depth. +/// Alternatively, see `writeStreamArbitraryDepth` to do safety checks to arbitrary depth. +/// +/// The caller does *not* need to call `deinit()` on the returned object. +pub fn writeStreamMaxDepth( + out_stream: anytype, + options: StringifyOptions, + comptime max_depth: ?usize, +) WriteStream( + @TypeOf(out_stream), + if (max_depth) |d| .{ .checked_to_fixed_depth = d } else .assumed_correct, +) { + return WriteStream( + @TypeOf(out_stream), + if (max_depth) |d| .{ .checked_to_fixed_depth = d } else .assumed_correct, + ).init(undefined, out_stream, options); +} + +/// See `WriteStream` for documentation. +/// This version of the write stream enables safety checks to arbitrarily deep nesting levels +/// by using the given allocator. +/// The caller should call `deinit()` on the returned object to free allocated memory. +pub fn writeStreamArbitraryDepth( + allocator: Allocator, + out_stream: anytype, + options: StringifyOptions, +) WriteStream(@TypeOf(out_stream), .checked_to_arbitrary_depth) { + return WriteStream(@TypeOf(out_stream), .checked_to_arbitrary_depth).init(allocator, out_stream, options); +} + +/// Writes JSON ([RFC8259](https://tools.ietf.org/html/rfc8259)) formatted data +/// to a stream. +/// +/// The seqeunce of method calls to write JSON content must follow this grammar: +/// ``` +/// = +/// = +/// | +/// | +/// | write +/// | writePreformatted +/// = beginObject ( objectField )* endObject +/// = beginArray ( )* endArray +/// ``` +/// +/// Supported types: +/// * Zig `bool` -> JSON `true` or `false`. +/// * Zig `?T` -> `null` or the rendering of `T`. +/// * Zig `i32`, `u64`, etc. -> JSON number or string. +/// * If the value is outside the range `±1<<53` (the precise integer rage of f64), it is rendered as a JSON string in base 10. Otherwise, it is rendered as JSON number. +/// * Zig floats -> JSON number or string. +/// * If the value cannot be precisely represented by an f64, it is rendered as a JSON string. Otherwise, it is rendered as JSON number. +/// * TODO: Float rendering will likely change in the future, e.g. to remove the unnecessary "e+00". +/// * Zig `[]const u8`, `[]u8`, `*[N]u8`, `@Vector(N, u8)`, and similar -> JSON string. +/// * See `StringifyOptions.emit_strings_as_arrays`. +/// * If the content is not valid UTF-8, rendered as an array of numbers instead. +/// * Zig `[]T`, `[N]T`, `*[N]T`, `@Vector(N, T)`, and similar -> JSON array of the rendering of each item. +/// * Zig tuple -> JSON array of the rendering of each item. +/// * Zig `struct` -> JSON object with each field in declaration order. +/// * If the struct declares a method `pub fn jsonStringify(self: *@This(), jw: anytype) !void`, it is called to do the serialization instead of the default behavior. The given `jw` is a pointer to this `WriteStream`. See `std.json.Value` for an example. +/// * See `StringifyOptions.emit_null_optional_fields`. +/// * Zig `union(enum)` -> JSON object with one field named for the active tag and a value representing the payload. +/// * If the payload is `void`, then the emitted value is `{}`. +/// * If the union declares a method `pub fn jsonStringify(self: *@This(), jw: anytype) !void`, it is called to do the serialization instead of the default behavior. The given `jw` is a pointer to this `WriteStream`. +/// * Zig `enum` -> JSON string naming the active tag. +/// * If the enum declares a method `pub fn jsonStringify(self: *@This(), jw: anytype) !void`, it is called to do the serialization instead of the default behavior. The given `jw` is a pointer to this `WriteStream`. +/// * Zig error -> JSON string naming the error. +/// * Zig `*T` -> the rendering of `T`. Note there is no guard against circular-reference infinite recursion. +pub fn WriteStream( + comptime OutStream: type, + comptime safety_checks: union(enum) { + checked_to_arbitrary_depth, + checked_to_fixed_depth: usize, // Rounded up to the nearest multiple of 8. + assumed_correct, + }, +) type { + return struct { + const Self = @This(); + + pub const Stream = OutStream; + pub const Error = switch (safety_checks) { + .checked_to_arbitrary_depth => Stream.Error || error{OutOfMemory}, + .checked_to_fixed_depth, .assumed_correct => Stream.Error, + }; + + options: StringifyOptions, + + stream: OutStream, indent_level: usize = 0, + next_punctuation: enum { + the_beginning, + none, + comma, + colon, + } = .the_beginning, + + nesting_stack: switch (safety_checks) { + .checked_to_arbitrary_depth => BitStack, + .checked_to_fixed_depth => |fixed_buffer_size| [(fixed_buffer_size + 7) >> 3]u8, + .assumed_correct => void, + }, - /// What character(s) should be used for indentation? - indent: union(enum) { - space: u8, - tab: void, - none: void, - } = .{ .space = 4 }, - - /// After a colon, should whitespace be inserted? - separator: bool = true, - - pub fn outputIndent( - whitespace: @This(), - out_stream: anytype, - ) @TypeOf(out_stream).Error!void { - var char: u8 = undefined; - var n_chars: usize = undefined; - switch (whitespace.indent) { - .space => |n_spaces| { - char = ' '; - n_chars = n_spaces; + pub fn init(safety_allocator: Allocator, stream: OutStream, options: StringifyOptions) Self { + return .{ + .options = options, + .stream = stream, + .nesting_stack = switch (safety_checks) { + .checked_to_arbitrary_depth => BitStack.init(safety_allocator), + .checked_to_fixed_depth => |fixed_buffer_size| [_]u8{0} ** ((fixed_buffer_size + 7) >> 3), + .assumed_correct => {}, }, - .tab => { + }; + } + + pub fn deinit(self: *Self) void { + switch (safety_checks) { + .checked_to_arbitrary_depth => self.nesting_stack.deinit(), + .checked_to_fixed_depth, .assumed_correct => {}, + } + self.* = undefined; + } + + pub fn beginArray(self: *Self) Error!void { + try self.valueStart(); + try self.stream.writeByte('['); + try self.pushIndentation(ARRAY_MODE); + self.next_punctuation = .none; + } + + pub fn beginObject(self: *Self) Error!void { + try self.valueStart(); + try self.stream.writeByte('{'); + try self.pushIndentation(OBJECT_MODE); + self.next_punctuation = .none; + } + + pub fn endArray(self: *Self) Error!void { + self.popIndentation(ARRAY_MODE); + switch (self.next_punctuation) { + .none => {}, + .comma => { + try self.indent(); + }, + .the_beginning, .colon => unreachable, + } + try self.stream.writeByte(']'); + self.valueDone(); + } + + pub fn endObject(self: *Self) Error!void { + self.popIndentation(OBJECT_MODE); + switch (self.next_punctuation) { + .none => {}, + .comma => { + try self.indent(); + }, + .the_beginning, .colon => unreachable, + } + try self.stream.writeByte('}'); + self.valueDone(); + } + + fn pushIndentation(self: *Self, mode: u1) !void { + switch (safety_checks) { + .checked_to_arbitrary_depth => { + try self.nesting_stack.push(mode); + self.indent_level += 1; + }, + .checked_to_fixed_depth => { + BitStack.pushWithStateAssumeCapacity(&self.nesting_stack, &self.indent_level, mode); + }, + .assumed_correct => { + self.indent_level += 1; + }, + } + } + fn popIndentation(self: *Self, assert_its_this_one: u1) void { + switch (safety_checks) { + .checked_to_arbitrary_depth => { + assert(self.nesting_stack.pop() == assert_its_this_one); + self.indent_level -= 1; + }, + .checked_to_fixed_depth => { + assert(BitStack.popWithState(&self.nesting_stack, &self.indent_level) == assert_its_this_one); + }, + .assumed_correct => { + self.indent_level -= 1; + }, + } + } + + fn indent(self: *Self) !void { + var char: u8 = ' '; + const n_chars = switch (self.options.whitespace) { + .minified => return, + .indent_1 => 1 * self.indent_level, + .indent_2 => 2 * self.indent_level, + .indent_3 => 3 * self.indent_level, + .indent_4 => 4 * self.indent_level, + .indent_8 => 8 * self.indent_level, + .indent_tab => blk: { char = '\t'; - n_chars = 1; + break :blk self.indent_level; + }, + }; + try self.stream.writeByte('\n'); + try self.stream.writeByteNTimes(char, n_chars); + } + + fn valueStart(self: *Self) !void { + if (self.isObjectKeyExpected()) |is_it| assert(!is_it); // Call objectField(), not write(), for object keys. + return self.valueStartAssumeTypeOk(); + } + fn objectFieldStart(self: *Self) !void { + if (self.isObjectKeyExpected()) |is_it| assert(is_it); // Expected write(), not objectField(). + return self.valueStartAssumeTypeOk(); + } + fn valueStartAssumeTypeOk(self: *Self) !void { + assert(!self.isComplete()); // JSON document already complete. + switch (self.next_punctuation) { + .the_beginning => { + // No indentation for the very beginning. + }, + .none => { + // First item in a container. + try self.indent(); + }, + .comma => { + // Subsequent item in a container. + try self.stream.writeByte(','); + try self.indent(); + }, + .colon => { + try self.stream.writeByte(':'); + if (self.options.whitespace != .minified) { + try self.stream.writeByte(' '); + } }, - .none => return, } - try out_stream.writeByte('\n'); - n_chars *= whitespace.indent_level; - try out_stream.writeByteNTimes(char, n_chars); } - }; + fn valueDone(self: *Self) void { + self.next_punctuation = .comma; + } - /// Controls the whitespace emitted - whitespace: Whitespace = .{ .indent = .none, .separator = false }, + // Only when safety is enabled: + fn isObjectKeyExpected(self: *const Self) ?bool { + switch (safety_checks) { + .checked_to_arbitrary_depth => return self.indent_level > 0 and + self.nesting_stack.peek() == OBJECT_MODE and + self.next_punctuation != .colon, + .checked_to_fixed_depth => return self.indent_level > 0 and + BitStack.peekWithState(&self.nesting_stack, self.indent_level) == OBJECT_MODE and + self.next_punctuation != .colon, + .assumed_correct => return null, + } + } + fn isComplete(self: *const Self) bool { + return self.indent_level == 0 and self.next_punctuation == .comma; + } - /// Should optional fields with null value be written? - emit_null_optional_fields: bool = true, + /// An alternative to calling `write` that outputs the given bytes verbatim. + /// This function does the usual punctuation and indentation formatting + /// assuming the given slice represents a single complete value; + /// e.g. `"1"`, `"[]"`, `"[1,2]"`, not `"1,2"`. + pub fn writePreformatted(self: *Self, value_slice: []const u8) Error!void { + try self.valueStart(); + try self.stream.writeAll(value_slice); + self.valueDone(); + } - string: StringOptions = StringOptions{ .String = .{} }, + pub fn objectField(self: *Self, key: []const u8) Error!void { + try self.objectFieldStart(); + try encodeJsonString(key, self.options, self.stream); + self.next_punctuation = .colon; + } - /// Should []u8 be serialised as a string? or an array? - pub const StringOptions = union(enum) { - Array, - String: StringOutputOptions, + /// See `WriteStream`. + pub fn write(self: *Self, value: anytype) Error!void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .Int => |info| { + if (info.bits < 53) { + try self.valueStart(); + try self.stream.print("{}", .{value}); + self.valueDone(); + return; + } + if (value < 4503599627370496 and (info.signedness == .unsigned or value > -4503599627370496)) { + try self.valueStart(); + try self.stream.print("{}", .{value}); + self.valueDone(); + return; + } + try self.valueStart(); + try self.stream.print("\"{}\"", .{value}); + self.valueDone(); + return; + }, + .ComptimeInt => { + return self.write(@as(std.math.IntFittingRange(value, value), value)); + }, + .Float, .ComptimeFloat => { + if (@as(f64, @floatCast(value)) == value) { + try self.valueStart(); + try self.stream.print("{}", .{@as(f64, @floatCast(value))}); + self.valueDone(); + return; + } + try self.valueStart(); + try self.stream.print("\"{}\"", .{value}); + self.valueDone(); + return; + }, - /// String output options - const StringOutputOptions = struct { - /// Should '/' be escaped in strings? - escape_solidus: bool = false, + .Bool => { + try self.valueStart(); + try self.stream.writeAll(if (value) "true" else "false"); + self.valueDone(); + return; + }, + .Null => { + try self.valueStart(); + try self.stream.writeAll("null"); + self.valueDone(); + return; + }, + .Optional => { + if (value) |payload| { + return try self.write(payload); + } else { + return try self.write(null); + } + }, + .Enum => { + if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { + return value.jsonStringify(self); + } - /// Should unicode characters be escaped in strings? - escape_unicode: bool = false, - }; + return self.stringValue(@tagName(value)); + }, + .Union => { + if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { + return value.jsonStringify(self); + } + + const info = @typeInfo(T).Union; + if (info.tag_type) |UnionTagType| { + try self.beginObject(); + inline for (info.fields) |u_field| { + if (value == @field(UnionTagType, u_field.name)) { + try self.objectField(u_field.name); + if (u_field.type == void) { + // void value is {} + try self.beginObject(); + try self.endObject(); + } else { + try self.write(@field(value, u_field.name)); + } + break; + } + } else { + unreachable; // No active tag? + } + try self.endObject(); + return; + } else { + @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'"); + } + }, + .Struct => |S| { + if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { + return value.jsonStringify(self); + } + + if (S.is_tuple) { + try self.beginArray(); + } else { + try self.beginObject(); + } + inline for (S.fields) |Field| { + // don't include void fields + if (Field.type == void) continue; + + var emit_field = true; + + // don't include optional fields that are null when emit_null_optional_fields is set to false + if (@typeInfo(Field.type) == .Optional) { + if (self.options.emit_null_optional_fields == false) { + if (@field(value, Field.name) == null) { + emit_field = false; + } + } + } + + if (emit_field) { + if (!S.is_tuple) { + try self.objectField(Field.name); + } + try self.write(@field(value, Field.name)); + } + } + if (S.is_tuple) { + try self.endArray(); + } else { + try self.endObject(); + } + return; + }, + .ErrorSet => return self.stringValue(@errorName(value)), + .Pointer => |ptr_info| switch (ptr_info.size) { + .One => switch (@typeInfo(ptr_info.child)) { + .Array => { + // Coerce `*[N]T` to `[]const T`. + const Slice = []const std.meta.Elem(ptr_info.child); + return self.write(@as(Slice, value)); + }, + else => { + return self.write(value.*); + }, + }, + .Many, .Slice => { + if (ptr_info.size == .Many and ptr_info.sentinel == null) + @compileError("unable to stringify type '" ++ @typeName(T) ++ "' without sentinel"); + const slice = if (ptr_info.size == .Many) std.mem.span(value) else value; + + if (ptr_info.child == u8) { + // This is a []const u8, or some similar Zig string. + if (!self.options.emit_strings_as_arrays and std.unicode.utf8ValidateSlice(slice)) { + return self.stringValue(slice); + } + } + + try self.beginArray(); + for (slice) |x| { + try self.write(x); + } + try self.endArray(); + return; + }, + else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"), + }, + .Array => { + // Coerce `[N]T` to `*const [N]T` (and then to `[]const T`). + return self.write(&value); + }, + .Vector => |info| { + const array: [info.len]info.child = value; + return self.write(&array); + }, + else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"), + } + unreachable; + } + + fn stringValue(self: *Self, s: []const u8) !void { + try self.valueStart(); + try encodeJsonString(s, self.options, self.stream); + self.valueDone(); + } + + pub const arrayElem = @compileError("Deprecated; You don't need to call this anymore."); + pub const emitNull = @compileError("Deprecated; Use .write(null) instead."); + pub const emitBool = @compileError("Deprecated; Use .write() instead."); + pub const emitNumber = @compileError("Deprecated; Use .write() instead."); + pub const emitString = @compileError("Deprecated; Use .write() instead."); + pub const emitJson = @compileError("Deprecated; Use .write() instead."); }; -}; +} -fn outputUnicodeEscape( - codepoint: u21, - out_stream: anytype, -) !void { +fn outputUnicodeEscape(codepoint: u21, out_stream: anytype) !void { if (codepoint <= 0xFFFF) { // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF), // then it may be represented as a six-character sequence: a reverse solidus, followed @@ -87,6 +597,19 @@ fn outputUnicodeEscape( } } +fn outputSpecialEscape(c: u8, writer: anytype) !void { + switch (c) { + '\\' => try writer.writeAll("\\\\"), + '\"' => try writer.writeAll("\\\""), + 0x08 => try writer.writeAll("\\b"), + 0x0C => try writer.writeAll("\\f"), + '\n' => try writer.writeAll("\\n"), + '\r' => try writer.writeAll("\\r"), + '\t' => try writer.writeAll("\\t"), + else => try outputUnicodeEscape(c, writer), + } +} + /// Write `string` to `writer` as a JSON encoded string. pub fn encodeJsonString(string: []const u8, options: StringifyOptions, writer: anytype) !void { try writer.writeByte('\"'); @@ -96,218 +619,44 @@ pub fn encodeJsonString(string: []const u8, options: StringifyOptions, writer: a /// Write `chars` to `writer` as JSON encoded string characters. pub fn encodeJsonStringChars(chars: []const u8, options: StringifyOptions, writer: anytype) !void { + var write_cursor: usize = 0; var i: usize = 0; - while (i < chars.len) : (i += 1) { - switch (chars[i]) { - // normal ascii character - 0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => |c| try writer.writeByte(c), - // only 2 characters that *must* be escaped - '\\' => try writer.writeAll("\\\\"), - '\"' => try writer.writeAll("\\\""), - // solidus is optional to escape - '/' => { - if (options.string.String.escape_solidus) { - try writer.writeAll("\\/"); - } else { - try writer.writeByte('/'); - } - }, - // control characters with short escapes - // TODO: option to switch between unicode and 'short' forms? - 0x8 => try writer.writeAll("\\b"), - 0xC => try writer.writeAll("\\f"), - '\n' => try writer.writeAll("\\n"), - '\r' => try writer.writeAll("\\r"), - '\t' => try writer.writeAll("\\t"), - else => { - const ulen = std.unicode.utf8ByteSequenceLength(chars[i]) catch unreachable; - // control characters (only things left with 1 byte length) should always be printed as unicode escapes - if (ulen == 1 or options.string.String.escape_unicode) { + if (options.escape_unicode) { + while (i < chars.len) : (i += 1) { + switch (chars[i]) { + // normal ascii character + 0x20...0x21, 0x23...0x5B, 0x5D...0x7E => {}, + 0x00...0x1F, '\\', '\"' => { + // Always must escape these. + try writer.writeAll(chars[write_cursor..i]); + try outputSpecialEscape(chars[i], writer); + write_cursor = i + 1; + }, + 0x7F...0xFF => { + try writer.writeAll(chars[write_cursor..i]); + const ulen = std.unicode.utf8ByteSequenceLength(chars[i]) catch unreachable; const codepoint = std.unicode.utf8Decode(chars[i..][0..ulen]) catch unreachable; try outputUnicodeEscape(codepoint, writer); - } else { - try writer.writeAll(chars[i..][0..ulen]); - } - i += ulen - 1; - }, - } - } -} - -/// If `value` has a method called `jsonStringify`, this will call that method instead of the -/// default implementation, passing it the `options` and `out_stream` parameters. -pub fn stringify( - value: anytype, - options: StringifyOptions, - out_stream: anytype, -) @TypeOf(out_stream).Error!void { - const T = @TypeOf(value); - switch (@typeInfo(T)) { - .Float, .ComptimeFloat => { - return std.fmt.formatFloatScientific(value, std.fmt.FormatOptions{}, out_stream); - }, - .Int, .ComptimeInt => { - return std.fmt.formatIntValue(value, "", std.fmt.FormatOptions{}, out_stream); - }, - .Bool => { - return out_stream.writeAll(if (value) "true" else "false"); - }, - .Null => { - return out_stream.writeAll("null"); - }, - .Optional => { - if (value) |payload| { - return try stringify(payload, options, out_stream); - } else { - return try stringify(null, options, out_stream); - } - }, - .Enum => { - if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { - return value.jsonStringify(options, out_stream); - } - - return try encodeJsonString(@tagName(value), options, out_stream); - }, - .Union => { - if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { - return value.jsonStringify(options, out_stream); - } - - const info = @typeInfo(T).Union; - if (info.tag_type) |UnionTagType| { - try out_stream.writeByte('{'); - var child_options = options; - child_options.whitespace.indent_level += 1; - inline for (info.fields) |u_field| { - if (value == @field(UnionTagType, u_field.name)) { - try child_options.whitespace.outputIndent(out_stream); - try encodeJsonString(u_field.name, options, out_stream); - try out_stream.writeByte(':'); - if (child_options.whitespace.separator) { - try out_stream.writeByte(' '); - } - if (u_field.type == void) { - try out_stream.writeAll("{}"); - } else { - try stringify(@field(value, u_field.name), child_options, out_stream); - } - break; - } - } else { - unreachable; // No active tag? - } - try options.whitespace.outputIndent(out_stream); - try out_stream.writeByte('}'); - return; - } else { - @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'"); - } - }, - .Struct => |S| { - if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { - return value.jsonStringify(options, out_stream); - } - - try out_stream.writeByte(if (S.is_tuple) '[' else '{'); - var field_output = false; - var child_options = options; - child_options.whitespace.indent_level += 1; - inline for (S.fields) |Field| { - // don't include void fields - if (Field.type == void) continue; - - var emit_field = true; - - // don't include optional fields that are null when emit_null_optional_fields is set to false - if (@typeInfo(Field.type) == .Optional) { - if (options.emit_null_optional_fields == false) { - if (@field(value, Field.name) == null) { - emit_field = false; - } - } - } - - if (emit_field) { - if (!field_output) { - field_output = true; - } else { - try out_stream.writeByte(','); - } - try child_options.whitespace.outputIndent(out_stream); - if (!S.is_tuple) { - try encodeJsonString(Field.name, options, out_stream); - try out_stream.writeByte(':'); - if (child_options.whitespace.separator) { - try out_stream.writeByte(' '); - } - } - try stringify(@field(value, Field.name), child_options, out_stream); - } - } - if (field_output) { - try options.whitespace.outputIndent(out_stream); - } - try out_stream.writeByte(if (S.is_tuple) ']' else '}'); - return; - }, - .ErrorSet => return stringify(@as([]const u8, @errorName(value)), options, out_stream), - .Pointer => |ptr_info| switch (ptr_info.size) { - .One => switch (@typeInfo(ptr_info.child)) { - .Array => { - const Slice = []const std.meta.Elem(ptr_info.child); - return stringify(@as(Slice, value), options, out_stream); + i += ulen - 1; + write_cursor = i + 1; }, - else => { - // TODO: avoid loops? - return stringify(value.*, options, out_stream); + } + } + } else { + while (i < chars.len) : (i += 1) { + switch (chars[i]) { + // normal bytes + 0x20...0x21, 0x23...0x5B, 0x5D...0xFF => {}, + 0x00...0x1F, '\\', '\"' => { + // Always must escape these. + try writer.writeAll(chars[write_cursor..i]); + try outputSpecialEscape(chars[i], writer); + write_cursor = i + 1; }, - }, - .Many, .Slice => { - if (ptr_info.size == .Many and ptr_info.sentinel == null) - @compileError("unable to stringify type '" ++ @typeName(T) ++ "' without sentinel"); - const slice = if (ptr_info.size == .Many) mem.span(value) else value; - - if (ptr_info.child == u8 and options.string == .String and std.unicode.utf8ValidateSlice(slice)) { - try encodeJsonString(slice, options, out_stream); - return; - } - - try out_stream.writeByte('['); - var child_options = options; - child_options.whitespace.indent_level += 1; - for (slice, 0..) |x, i| { - if (i != 0) { - try out_stream.writeByte(','); - } - try child_options.whitespace.outputIndent(out_stream); - try stringify(x, child_options, out_stream); - } - if (slice.len != 0) { - try options.whitespace.outputIndent(out_stream); - } - try out_stream.writeByte(']'); - return; - }, - else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"), - }, - .Array => return stringify(&value, options, out_stream), - .Vector => |info| { - const array: [info.len]info.child = value; - return stringify(&array, options, out_stream); - }, - else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"), + } + } } - unreachable; -} - -// Same as `stringify` but accepts an Allocator and stores result in dynamically allocated memory instead of using a Writer. -// Caller owns returned memory. -pub fn stringifyAlloc(allocator: std.mem.Allocator, value: anytype, options: StringifyOptions) ![]const u8 { - var list = std.ArrayList(u8).init(allocator); - errdefer list.deinit(); - try stringify(value, options, list.writer()); - return list.toOwnedSlice(); + try writer.writeAll(chars[write_cursor..chars.len]); } test { diff --git a/lib/std/json/stringify_test.zig b/lib/std/json/stringify_test.zig index cb258eb24c6d..a9f74e6bce16 100644 --- a/lib/std/json/stringify_test.zig +++ b/lib/std/json/stringify_test.zig @@ -2,9 +2,99 @@ const std = @import("std"); const mem = std.mem; const testing = std.testing; +const ObjectMap = @import("dynamic.zig").ObjectMap; +const Value = @import("dynamic.zig").Value; + const StringifyOptions = @import("stringify.zig").StringifyOptions; const stringify = @import("stringify.zig").stringify; +const stringifyMaxDepth = @import("stringify.zig").stringifyMaxDepth; +const stringifyArbitraryDepth = @import("stringify.zig").stringifyArbitraryDepth; const stringifyAlloc = @import("stringify.zig").stringifyAlloc; +const writeStream = @import("stringify.zig").writeStream; +const writeStreamMaxDepth = @import("stringify.zig").writeStreamMaxDepth; +const writeStreamArbitraryDepth = @import("stringify.zig").writeStreamArbitraryDepth; + +test "json write stream" { + var out_buf: [1024]u8 = undefined; + var slice_stream = std.io.fixedBufferStream(&out_buf); + const out = slice_stream.writer(); + + { + var w = writeStream(out, .{ .whitespace = .indent_2 }); + try testBasicWriteStream(&w, &slice_stream); + } + + { + var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, 8); + try testBasicWriteStream(&w, &slice_stream); + } + + { + var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, null); + try testBasicWriteStream(&w, &slice_stream); + } + + { + var w = writeStreamArbitraryDepth(testing.allocator, out, .{ .whitespace = .indent_2 }); + defer w.deinit(); + try testBasicWriteStream(&w, &slice_stream); + } +} + +fn testBasicWriteStream(w: anytype, slice_stream: anytype) !void { + slice_stream.reset(); + + try w.beginObject(); + + try w.objectField("object"); + var arena_allocator = std.heap.ArenaAllocator.init(testing.allocator); + defer arena_allocator.deinit(); + try w.write(try getJsonObject(arena_allocator.allocator())); + + try w.objectField("string"); + try w.write("This is a string"); + + try w.objectField("array"); + try w.beginArray(); + try w.write("Another string"); + try w.write(@as(i32, 1)); + try w.write(@as(f32, 3.5)); + try w.endArray(); + + try w.objectField("int"); + try w.write(@as(i32, 10)); + + try w.objectField("float"); + try w.write(@as(f32, 3.5)); + + try w.endObject(); + + const result = slice_stream.getWritten(); + const expected = + \\{ + \\ "object": { + \\ "one": 1, + \\ "two": 2.0e+00 + \\ }, + \\ "string": "This is a string", + \\ "array": [ + \\ "Another string", + \\ 1, + \\ 3.5e+00 + \\ ], + \\ "int": 10, + \\ "float": 3.5e+00 + \\} + ; + try std.testing.expectEqualStrings(expected, result); +} + +fn getJsonObject(allocator: std.mem.Allocator) !Value { + var value = Value{ .object = ObjectMap.init(allocator) }; + try value.object.put("one", Value{ .integer = @as(i64, @intCast(1)) }); + try value.object.put("two", Value{ .float = 2.0 }); + return value; +} test "stringify null optional fields" { const MyStruct = struct { @@ -13,64 +103,63 @@ test "stringify null optional fields" { another_optional: ?[]const u8 = null, another_required: []const u8 = "something else", }; - try teststringify( + try testStringify( \\{"optional":null,"required":"something","another_optional":null,"another_required":"something else"} , MyStruct{}, - StringifyOptions{}, + .{}, ); - try teststringify( + try testStringify( \\{"required":"something","another_required":"something else"} , MyStruct{}, - StringifyOptions{ .emit_null_optional_fields = false }, + .{ .emit_null_optional_fields = false }, ); } test "stringify basic types" { - try teststringify("false", false, StringifyOptions{}); - try teststringify("true", true, StringifyOptions{}); - try teststringify("null", @as(?u8, null), StringifyOptions{}); - try teststringify("null", @as(?*u32, null), StringifyOptions{}); - try teststringify("42", 42, StringifyOptions{}); - try teststringify("4.2e+01", 42.0, StringifyOptions{}); - try teststringify("42", @as(u8, 42), StringifyOptions{}); - try teststringify("42", @as(u128, 42), StringifyOptions{}); - try teststringify("4.2e+01", @as(f32, 42), StringifyOptions{}); - try teststringify("4.2e+01", @as(f64, 42), StringifyOptions{}); - try teststringify("\"ItBroke\"", @as(anyerror, error.ItBroke), StringifyOptions{}); + try testStringify("false", false, .{}); + try testStringify("true", true, .{}); + try testStringify("null", @as(?u8, null), .{}); + try testStringify("null", @as(?*u32, null), .{}); + try testStringify("42", 42, .{}); + try testStringify("4.2e+01", 42.0, .{}); + try testStringify("42", @as(u8, 42), .{}); + try testStringify("42", @as(u128, 42), .{}); + try testStringify("4.2e+01", @as(f32, 42), .{}); + try testStringify("4.2e+01", @as(f64, 42), .{}); + try testStringify("\"ItBroke\"", @as(anyerror, error.ItBroke), .{}); + try testStringify("\"ItBroke\"", error.ItBroke, .{}); } test "stringify string" { - try teststringify("\"hello\"", "hello", StringifyOptions{}); - try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r", StringifyOptions{}); - try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); - try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}", StringifyOptions{}); - try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); - try teststringify("\"with unicode\u{80}\"", "with unicode\u{80}", StringifyOptions{}); - try teststringify("\"with unicode\\u0080\"", "with unicode\u{80}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); - try teststringify("\"with unicode\u{FF}\"", "with unicode\u{FF}", StringifyOptions{}); - try teststringify("\"with unicode\\u00ff\"", "with unicode\u{FF}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); - try teststringify("\"with unicode\u{100}\"", "with unicode\u{100}", StringifyOptions{}); - try teststringify("\"with unicode\\u0100\"", "with unicode\u{100}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); - try teststringify("\"with unicode\u{800}\"", "with unicode\u{800}", StringifyOptions{}); - try teststringify("\"with unicode\\u0800\"", "with unicode\u{800}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); - try teststringify("\"with unicode\u{8000}\"", "with unicode\u{8000}", StringifyOptions{}); - try teststringify("\"with unicode\\u8000\"", "with unicode\u{8000}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); - try teststringify("\"with unicode\u{D799}\"", "with unicode\u{D799}", StringifyOptions{}); - try teststringify("\"with unicode\\ud799\"", "with unicode\u{D799}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); - try teststringify("\"with unicode\u{10000}\"", "with unicode\u{10000}", StringifyOptions{}); - try teststringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); - try teststringify("\"with unicode\u{10FFFF}\"", "with unicode\u{10FFFF}", StringifyOptions{}); - try teststringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}", StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); - try teststringify("\"/\"", "/", StringifyOptions{}); - try teststringify("\"\\/\"", "/", StringifyOptions{ .string = .{ .String = .{ .escape_solidus = true } } }); + try testStringify("\"hello\"", "hello", .{}); + try testStringify("\"with\\nescapes\\r\"", "with\nescapes\r", .{}); + try testStringify("\"with\\nescapes\\r\"", "with\nescapes\r", .{ .escape_unicode = true }); + try testStringify("\"with unicode\\u0001\"", "with unicode\u{1}", .{}); + try testStringify("\"with unicode\\u0001\"", "with unicode\u{1}", .{ .escape_unicode = true }); + try testStringify("\"with unicode\u{80}\"", "with unicode\u{80}", .{}); + try testStringify("\"with unicode\\u0080\"", "with unicode\u{80}", .{ .escape_unicode = true }); + try testStringify("\"with unicode\u{FF}\"", "with unicode\u{FF}", .{}); + try testStringify("\"with unicode\\u00ff\"", "with unicode\u{FF}", .{ .escape_unicode = true }); + try testStringify("\"with unicode\u{100}\"", "with unicode\u{100}", .{}); + try testStringify("\"with unicode\\u0100\"", "with unicode\u{100}", .{ .escape_unicode = true }); + try testStringify("\"with unicode\u{800}\"", "with unicode\u{800}", .{}); + try testStringify("\"with unicode\\u0800\"", "with unicode\u{800}", .{ .escape_unicode = true }); + try testStringify("\"with unicode\u{8000}\"", "with unicode\u{8000}", .{}); + try testStringify("\"with unicode\\u8000\"", "with unicode\u{8000}", .{ .escape_unicode = true }); + try testStringify("\"with unicode\u{D799}\"", "with unicode\u{D799}", .{}); + try testStringify("\"with unicode\\ud799\"", "with unicode\u{D799}", .{ .escape_unicode = true }); + try testStringify("\"with unicode\u{10000}\"", "with unicode\u{10000}", .{}); + try testStringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}", .{ .escape_unicode = true }); + try testStringify("\"with unicode\u{10FFFF}\"", "with unicode\u{10FFFF}", .{}); + try testStringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}", .{ .escape_unicode = true }); } test "stringify many-item sentinel-terminated string" { - try teststringify("\"hello\"", @as([*:0]const u8, "hello"), StringifyOptions{}); - try teststringify("\"with\\nescapes\\r\"", @as([*:0]const u8, "with\nescapes\r"), StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); - try teststringify("\"with unicode\\u0001\"", @as([*:0]const u8, "with unicode\u{1}"), StringifyOptions{ .string = .{ .String = .{ .escape_unicode = true } } }); + try testStringify("\"hello\"", @as([*:0]const u8, "hello"), .{}); + try testStringify("\"with\\nescapes\\r\"", @as([*:0]const u8, "with\nescapes\r"), .{ .escape_unicode = true }); + try testStringify("\"with unicode\\u0001\"", @as([*:0]const u8, "with unicode\u{1}"), .{ .escape_unicode = true }); } test "stringify enums" { @@ -78,8 +167,8 @@ test "stringify enums" { foo, bar, }; - try teststringify("\"foo\"", E.foo, .{}); - try teststringify("\"bar\"", E.bar, .{}); + try testStringify("\"foo\"", E.foo, .{}); + try testStringify("\"bar\"", E.bar, .{}); } test "stringify tagged unions" { @@ -88,24 +177,33 @@ test "stringify tagged unions" { foo: u32, bar: bool, }; - try teststringify("{\"nothing\":{}}", T{ .nothing = {} }, StringifyOptions{}); - try teststringify("{\"foo\":42}", T{ .foo = 42 }, StringifyOptions{}); - try teststringify("{\"bar\":true}", T{ .bar = true }, StringifyOptions{}); + try testStringify("{\"nothing\":{}}", T{ .nothing = {} }, .{}); + try testStringify("{\"foo\":42}", T{ .foo = 42 }, .{}); + try testStringify("{\"bar\":true}", T{ .bar = true }, .{}); } test "stringify struct" { - try teststringify("{\"foo\":42}", struct { + try testStringify("{\"foo\":42}", struct { foo: u32, - }{ .foo = 42 }, StringifyOptions{}); + }{ .foo = 42 }, .{}); } -test "stringify struct with string as array" { - try teststringify("{\"foo\":\"bar\"}", .{ .foo = "bar" }, StringifyOptions{}); - try teststringify("{\"foo\":[98,97,114]}", .{ .foo = "bar" }, StringifyOptions{ .string = .Array }); +test "emit_strings_as_arrays" { + // Should only affect string values, not object keys. + try testStringify("{\"foo\":\"bar\"}", .{ .foo = "bar" }, .{}); + try testStringify("{\"foo\":[98,97,114]}", .{ .foo = "bar" }, .{ .emit_strings_as_arrays = true }); + // Should *not* affect these types: + try testStringify("\"foo\"", @as(enum { foo, bar }, .foo), .{ .emit_strings_as_arrays = true }); + try testStringify("\"ItBroke\"", error.ItBroke, .{ .emit_strings_as_arrays = true }); + // Should work on these: + try testStringify("\"bar\"", @Vector(3, u8){ 'b', 'a', 'r' }, .{}); + try testStringify("[98,97,114]", @Vector(3, u8){ 'b', 'a', 'r' }, .{ .emit_strings_as_arrays = true }); + try testStringify("\"bar\"", [3]u8{ 'b', 'a', 'r' }, .{}); + try testStringify("[98,97,114]", [3]u8{ 'b', 'a', 'r' }, .{ .emit_strings_as_arrays = true }); } test "stringify struct with indentation" { - try teststringify( + try testStringify( \\{ \\ "foo": 42, \\ "bar": [ @@ -122,12 +220,10 @@ test "stringify struct with indentation" { .foo = 42, .bar = .{ 1, 2, 3 }, }, - StringifyOptions{ - .whitespace = .{}, - }, + .{ .whitespace = .indent_4 }, ); - try teststringify( - "{\n\t\"foo\":42,\n\t\"bar\":[\n\t\t1,\n\t\t2,\n\t\t3\n\t]\n}", + try testStringify( + "{\n\t\"foo\": 42,\n\t\"bar\": [\n\t\t1,\n\t\t2,\n\t\t3\n\t]\n}", struct { foo: u32, bar: [3]u32, @@ -135,14 +231,9 @@ test "stringify struct with indentation" { .foo = 42, .bar = .{ 1, 2, 3 }, }, - StringifyOptions{ - .whitespace = .{ - .indent = .tab, - .separator = false, - }, - }, + .{ .whitespace = .indent_tab }, ); - try teststringify( + try testStringify( \\{"foo":42,"bar":[1,2,3]} , struct { @@ -152,59 +243,53 @@ test "stringify struct with indentation" { .foo = 42, .bar = .{ 1, 2, 3 }, }, - StringifyOptions{ - .whitespace = .{ - .indent = .none, - .separator = false, - }, - }, + .{ .whitespace = .minified }, ); } test "stringify struct with void field" { - try teststringify("{\"foo\":42}", struct { + try testStringify("{\"foo\":42}", struct { foo: u32, bar: void = {}, - }{ .foo = 42 }, StringifyOptions{}); + }{ .foo = 42 }, .{}); } test "stringify array of structs" { const MyStruct = struct { foo: u32, }; - try teststringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{ + try testStringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{ MyStruct{ .foo = 42 }, MyStruct{ .foo = 100 }, MyStruct{ .foo = 1000 }, - }, StringifyOptions{}); + }, .{}); } test "stringify struct with custom stringifier" { - try teststringify("[\"something special\",42]", struct { + try testStringify("[\"something special\",42]", struct { foo: u32, const Self = @This(); - pub fn jsonStringify( - value: Self, - options: StringifyOptions, - out_stream: anytype, - ) !void { + pub fn jsonStringify(value: @This(), jws: anytype) !void { _ = value; - try out_stream.writeAll("[\"something special\","); - try stringify(42, options, out_stream); - try out_stream.writeByte(']'); + try jws.beginArray(); + try jws.write("something special"); + try jws.write(42); + try jws.endArray(); } - }{ .foo = 42 }, StringifyOptions{}); + }{ .foo = 42 }, .{}); } test "stringify vector" { - try teststringify("[1,1]", @as(@Vector(2, u32), @splat(1)), StringifyOptions{}); + try testStringify("[1,1]", @as(@Vector(2, u32), @splat(1)), .{}); + try testStringify("\"AA\"", @as(@Vector(2, u8), @splat('A')), .{}); + try testStringify("[65,65]", @as(@Vector(2, u8), @splat('A')), .{ .emit_strings_as_arrays = true }); } test "stringify tuple" { - try teststringify("[\"foo\",42]", std.meta.Tuple(&.{ []const u8, usize }){ "foo", 42 }, StringifyOptions{}); + try testStringify("[\"foo\",42]", std.meta.Tuple(&.{ []const u8, usize }){ "foo", 42 }, .{}); } -fn teststringify(expected: []const u8, value: anytype, options: StringifyOptions) !void { +fn testStringify(expected: []const u8, value: anytype, options: StringifyOptions) !void { const ValidationWriter = struct { const Self = @This(); pub const Writer = std.io.Writer(*Self, Error, write); @@ -256,8 +341,34 @@ fn teststringify(expected: []const u8, value: anytype, options: StringifyOptions }; var vos = ValidationWriter.init(expected); - try stringify(value, options, vos.writer()); + try stringifyArbitraryDepth(testing.allocator, value, options, vos.writer()); if (vos.expected_remaining.len > 0) return error.NotEnoughData; + + // Also test with safety disabled. + try testStringifyMaxDepth(expected, value, options, null); + try testStringifyArbitraryDepth(expected, value, options); +} + +fn testStringifyMaxDepth(expected: []const u8, value: anytype, options: StringifyOptions, comptime max_depth: ?usize) !void { + var out_buf: [1024]u8 = undefined; + var slice_stream = std.io.fixedBufferStream(&out_buf); + const out = slice_stream.writer(); + + try stringifyMaxDepth(value, options, out, max_depth); + const got = slice_stream.getWritten(); + + try testing.expectEqualStrings(expected, got); +} + +fn testStringifyArbitraryDepth(expected: []const u8, value: anytype, options: StringifyOptions) !void { + var out_buf: [1024]u8 = undefined; + var slice_stream = std.io.fixedBufferStream(&out_buf); + const out = slice_stream.writer(); + + try stringifyArbitraryDepth(testing.allocator, value, options, out); + const got = slice_stream.getWritten(); + + try testing.expectEqualStrings(expected, got); } test "stringify alloc" { @@ -270,3 +381,54 @@ test "stringify alloc" { try std.testing.expectEqualStrings(expected, actual); } + +test "comptime stringify" { + comptime testStringifyMaxDepth("false", false, .{}, null) catch unreachable; + comptime testStringifyMaxDepth("false", false, .{}, 0) catch unreachable; + comptime testStringifyArbitraryDepth("false", false, .{}) catch unreachable; + + const MyStruct = struct { + foo: u32, + }; + comptime testStringifyMaxDepth("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{ + MyStruct{ .foo = 42 }, + MyStruct{ .foo = 100 }, + MyStruct{ .foo = 1000 }, + }, .{}, null) catch unreachable; + comptime testStringifyMaxDepth("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{ + MyStruct{ .foo = 42 }, + MyStruct{ .foo = 100 }, + MyStruct{ .foo = 1000 }, + }, .{}, 8) catch unreachable; +} + +test "writePreformatted" { + var out_buf: [1024]u8 = undefined; + var slice_stream = std.io.fixedBufferStream(&out_buf); + const out = slice_stream.writer(); + + var w = writeStream(out, .{ .whitespace = .indent_2 }); + defer w.deinit(); + + try w.beginObject(); + try w.objectField("a"); + try w.writePreformatted("[ ]"); + try w.objectField("b"); + try w.beginArray(); + try w.writePreformatted("[[]] "); + try w.writePreformatted(" {}"); + try w.endArray(); + try w.endObject(); + + const result = slice_stream.getWritten(); + const expected = + \\{ + \\ "a": [ ], + \\ "b": [ + \\ [[]] , + \\ {} + \\ ] + \\} + ; + try std.testing.expectEqualStrings(expected, result); +} diff --git a/lib/std/json/test.zig b/lib/std/json/test.zig index 939f2e194c18..9530ab37a6bf 100644 --- a/lib/std/json/test.zig +++ b/lib/std/json/test.zig @@ -4,6 +4,7 @@ const parseFromSlice = @import("./static.zig").parseFromSlice; const validate = @import("./scanner.zig").validate; const JsonScanner = @import("./scanner.zig").Scanner; const Value = @import("./dynamic.zig").Value; +const stringifyAlloc = @import("./stringify.zig").stringifyAlloc; // Support for JSONTestSuite.zig pub fn ok(s: []const u8) !void { @@ -49,11 +50,10 @@ fn roundTrip(s: []const u8) !void { var parsed = try parseFromSlice(Value, testing.allocator, s, .{}); defer parsed.deinit(); - var buf: [256]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - try parsed.value.jsonStringify(.{}, fbs.writer()); + const rendered = try stringifyAlloc(testing.allocator, parsed.value, .{}); + defer testing.allocator.free(rendered); - try testing.expectEqualStrings(s, fbs.getWritten()); + try testing.expectEqualStrings(s, rendered); } test "truncated UTF-8 sequence" { diff --git a/lib/std/json/write_stream.zig b/lib/std/json/write_stream.zig deleted file mode 100644 index 3a2750f5a150..000000000000 --- a/lib/std/json/write_stream.zig +++ /dev/null @@ -1,300 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; -const maxInt = std.math.maxInt; - -const StringifyOptions = @import("./stringify.zig").StringifyOptions; -const jsonStringify = @import("./stringify.zig").stringify; - -const Value = @import("./dynamic.zig").Value; - -const State = enum { - complete, - value, - array_start, - array, - object_start, - object, -}; - -/// Writes JSON ([RFC8259](https://tools.ietf.org/html/rfc8259)) formatted data -/// to a stream. `max_depth` is a comptime-known upper bound on the nesting depth. -/// TODO A future iteration of this API will allow passing `null` for this value, -/// and disable safety checks in release builds. -pub fn WriteStream(comptime OutStream: type, comptime max_depth: usize) type { - return struct { - const Self = @This(); - - pub const Stream = OutStream; - - whitespace: StringifyOptions.Whitespace = StringifyOptions.Whitespace{ - .indent_level = 0, - .indent = .{ .space = 1 }, - }, - - stream: OutStream, - state_index: usize, - state: [max_depth]State, - - pub fn init(stream: OutStream) Self { - var self = Self{ - .stream = stream, - .state_index = 1, - .state = undefined, - }; - self.state[0] = .complete; - self.state[1] = .value; - return self; - } - - pub fn beginArray(self: *Self) !void { - assert(self.state[self.state_index] == State.value); // need to call arrayElem or objectField - try self.stream.writeByte('['); - self.state[self.state_index] = State.array_start; - self.whitespace.indent_level += 1; - } - - pub fn beginObject(self: *Self) !void { - assert(self.state[self.state_index] == State.value); // need to call arrayElem or objectField - try self.stream.writeByte('{'); - self.state[self.state_index] = State.object_start; - self.whitespace.indent_level += 1; - } - - pub fn arrayElem(self: *Self) !void { - const state = self.state[self.state_index]; - switch (state) { - .complete => unreachable, - .value => unreachable, - .object_start => unreachable, - .object => unreachable, - .array, .array_start => { - if (state == .array) { - try self.stream.writeByte(','); - } - self.state[self.state_index] = .array; - self.pushState(.value); - try self.indent(); - }, - } - } - - pub fn objectField(self: *Self, name: []const u8) !void { - const state = self.state[self.state_index]; - switch (state) { - .complete => unreachable, - .value => unreachable, - .array_start => unreachable, - .array => unreachable, - .object, .object_start => { - if (state == .object) { - try self.stream.writeByte(','); - } - self.state[self.state_index] = .object; - self.pushState(.value); - try self.indent(); - try self.writeEscapedString(name); - try self.stream.writeByte(':'); - if (self.whitespace.separator) { - try self.stream.writeByte(' '); - } - }, - } - } - - pub fn endArray(self: *Self) !void { - switch (self.state[self.state_index]) { - .complete => unreachable, - .value => unreachable, - .object_start => unreachable, - .object => unreachable, - .array_start => { - self.whitespace.indent_level -= 1; - try self.stream.writeByte(']'); - self.popState(); - }, - .array => { - self.whitespace.indent_level -= 1; - try self.indent(); - self.popState(); - try self.stream.writeByte(']'); - }, - } - } - - pub fn endObject(self: *Self) !void { - switch (self.state[self.state_index]) { - .complete => unreachable, - .value => unreachable, - .array_start => unreachable, - .array => unreachable, - .object_start => { - self.whitespace.indent_level -= 1; - try self.stream.writeByte('}'); - self.popState(); - }, - .object => { - self.whitespace.indent_level -= 1; - try self.indent(); - self.popState(); - try self.stream.writeByte('}'); - }, - } - } - - pub fn emitNull(self: *Self) !void { - assert(self.state[self.state_index] == State.value); - try self.stringify(null); - self.popState(); - } - - pub fn emitBool(self: *Self, value: bool) !void { - assert(self.state[self.state_index] == State.value); - try self.stringify(value); - self.popState(); - } - - pub fn emitNumber( - self: *Self, - /// An integer, float, or `std.math.BigInt`. Emitted as a bare number if it fits losslessly - /// in a IEEE 754 double float, otherwise emitted as a string to the full precision. - value: anytype, - ) !void { - assert(self.state[self.state_index] == State.value); - switch (@typeInfo(@TypeOf(value))) { - .Int => |info| { - if (info.bits < 53) { - try self.stream.print("{}", .{value}); - self.popState(); - return; - } - if (value < 4503599627370496 and (info.signedness == .unsigned or value > -4503599627370496)) { - try self.stream.print("{}", .{value}); - self.popState(); - return; - } - }, - .ComptimeInt => { - return self.emitNumber(@as(std.math.IntFittingRange(value, value), value)); - }, - .Float, .ComptimeFloat => if (@as(f64, @floatCast(value)) == value) { - try self.stream.print("{}", .{@as(f64, @floatCast(value))}); - self.popState(); - return; - }, - else => {}, - } - try self.stream.print("\"{}\"", .{value}); - self.popState(); - } - - pub fn emitString(self: *Self, string: []const u8) !void { - assert(self.state[self.state_index] == State.value); - try self.writeEscapedString(string); - self.popState(); - } - - fn writeEscapedString(self: *Self, string: []const u8) !void { - assert(std.unicode.utf8ValidateSlice(string)); - try self.stringify(string); - } - - /// Writes the complete json into the output stream - pub fn emitJson(self: *Self, value: Value) Stream.Error!void { - assert(self.state[self.state_index] == State.value); - try self.stringify(value); - self.popState(); - } - - fn indent(self: *Self) !void { - assert(self.state_index >= 1); - try self.whitespace.outputIndent(self.stream); - } - - fn pushState(self: *Self, state: State) void { - self.state_index += 1; - self.state[self.state_index] = state; - } - - fn popState(self: *Self) void { - self.state_index -= 1; - } - - fn stringify(self: *Self, value: anytype) !void { - try jsonStringify(value, StringifyOptions{ - .whitespace = self.whitespace, - }, self.stream); - } - }; -} - -pub fn writeStream( - out_stream: anytype, - comptime max_depth: usize, -) WriteStream(@TypeOf(out_stream), max_depth) { - return WriteStream(@TypeOf(out_stream), max_depth).init(out_stream); -} - -const ObjectMap = @import("./dynamic.zig").ObjectMap; - -test "json write stream" { - var out_buf: [1024]u8 = undefined; - var slice_stream = std.io.fixedBufferStream(&out_buf); - const out = slice_stream.writer(); - - var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena_allocator.deinit(); - - var w = writeStream(out, 10); - - try w.beginObject(); - - try w.objectField("object"); - try w.emitJson(try getJsonObject(arena_allocator.allocator())); - - try w.objectField("string"); - try w.emitString("This is a string"); - - try w.objectField("array"); - try w.beginArray(); - try w.arrayElem(); - try w.emitString("Another string"); - try w.arrayElem(); - try w.emitNumber(@as(i32, 1)); - try w.arrayElem(); - try w.emitNumber(@as(f32, 3.5)); - try w.endArray(); - - try w.objectField("int"); - try w.emitNumber(@as(i32, 10)); - - try w.objectField("float"); - try w.emitNumber(@as(f32, 3.5)); - - try w.endObject(); - - const result = slice_stream.getWritten(); - const expected = - \\{ - \\ "object": { - \\ "one": 1, - \\ "two": 2.0e+00 - \\ }, - \\ "string": "This is a string", - \\ "array": [ - \\ "Another string", - \\ 1, - \\ 3.5e+00 - \\ ], - \\ "int": 10, - \\ "float": 3.5e+00 - \\} - ; - try std.testing.expect(std.mem.eql(u8, expected, result)); -} - -fn getJsonObject(allocator: std.mem.Allocator) !Value { - var value = Value{ .object = ObjectMap.init(allocator) }; - try value.object.put("one", Value{ .integer = @as(i64, @intCast(1)) }); - try value.object.put("two", Value{ .float = 2.0 }); - return value; -} diff --git a/lib/std/std.zig b/lib/std/std.zig index ef4a6831d0a1..c0890c50381d 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -8,6 +8,7 @@ pub const AutoArrayHashMap = array_hash_map.AutoArrayHashMap; pub const AutoArrayHashMapUnmanaged = array_hash_map.AutoArrayHashMapUnmanaged; pub const AutoHashMap = hash_map.AutoHashMap; pub const AutoHashMapUnmanaged = hash_map.AutoHashMapUnmanaged; +pub const BitStack = @import("BitStack.zig"); pub const BoundedArray = @import("bounded_array.zig").BoundedArray; pub const BoundedArrayAligned = @import("bounded_array.zig").BoundedArrayAligned; pub const Build = @import("Build.zig"); diff --git a/src/Autodoc.zig b/src/Autodoc.zig index 2a4fb1d15148..ae4c859934d2 100644 --- a/src/Autodoc.zig +++ b/src/Autodoc.zig @@ -385,10 +385,11 @@ pub fn generateZirData(self: *Autodoc) !void { \\ /** @type {{DocData}} */ \\ var zigAnalysis= , .{}); - try std.json.stringify( + try std.json.stringifyArbitraryDepth( + arena_allocator.allocator(), data, .{ - .whitespace = .{ .indent = .none, .separator = false }, + .whitespace = .minified, .emit_null_optional_fields = true, }, out, @@ -532,28 +533,16 @@ const DocData = struct { ret: Expr, }; - pub fn jsonStringify( - self: DocData, - opts: std.json.StringifyOptions, - w: anytype, - ) !void { - var jsw = std.json.writeStream(w, 15); - jsw.whitespace = opts.whitespace; + pub fn jsonStringify(self: DocData, jsw: anytype) !void { try jsw.beginObject(); inline for (comptime std.meta.tags(std.meta.FieldEnum(DocData))) |f| { const f_name = @tagName(f); try jsw.objectField(f_name); switch (f) { - .files => try writeFileTableToJson(self.files, self.modules, &jsw), - .guide_sections => try writeGuidesToJson(self.guide_sections, &jsw), - .modules => { - try std.json.stringify(self.modules.values(), opts, w); - jsw.state_index -= 1; - }, - else => { - try std.json.stringify(@field(self, f_name), opts, w); - jsw.state_index -= 1; - }, + .files => try writeFileTableToJson(self.files, self.modules, jsw), + .guide_sections => try writeGuidesToJson(self.guide_sections, jsw), + .modules => try jsw.write(self.modules.values()), + else => try jsw.write(@field(self, f_name)), } } try jsw.endObject(); @@ -583,24 +572,14 @@ const DocData = struct { value: usize, }; - pub fn jsonStringify( - self: DocModule, - opts: std.json.StringifyOptions, - w: anytype, - ) !void { - var jsw = std.json.writeStream(w, 15); - jsw.whitespace = opts.whitespace; - + pub fn jsonStringify(self: DocModule, jsw: anytype) !void { try jsw.beginObject(); inline for (comptime std.meta.tags(std.meta.FieldEnum(DocModule))) |f| { const f_name = @tagName(f); try jsw.objectField(f_name); switch (f) { - .table => try writeModuleTableToJson(self.table, &jsw), - else => { - try std.json.stringify(@field(self, f_name), opts, w); - jsw.state_index -= 1; - }, + .table => try writeModuleTableToJson(self.table, jsw), + else => try jsw.write(@field(self, f_name)), } } try jsw.endObject(); @@ -617,18 +596,10 @@ const DocData = struct { is_uns: bool = false, // usingnamespace parent_container: ?usize, // index into `types` - pub fn jsonStringify( - self: Decl, - opts: std.json.StringifyOptions, - w: anytype, - ) !void { - var jsw = std.json.writeStream(w, 15); - jsw.whitespace = opts.whitespace; + pub fn jsonStringify(self: Decl, jsw: anytype) !void { try jsw.beginArray(); inline for (comptime std.meta.fields(Decl)) |f| { - try jsw.arrayElem(); - try std.json.stringify(@field(self, f.name), opts, w); - jsw.state_index -= 1; + try jsw.write(@field(self, f.name)); } try jsw.endArray(); } @@ -644,18 +615,10 @@ const DocData = struct { fields: ?[]usize = null, // index into astNodes @"comptime": bool = false, - pub fn jsonStringify( - self: AstNode, - opts: std.json.StringifyOptions, - w: anytype, - ) !void { - var jsw = std.json.writeStream(w, 15); - jsw.whitespace = opts.whitespace; + pub fn jsonStringify(self: AstNode, jsw: anytype) !void { try jsw.beginArray(); inline for (comptime std.meta.fields(AstNode)) |f| { - try jsw.arrayElem(); - try std.json.stringify(@field(self, f.name), opts, w); - jsw.state_index -= 1; + try jsw.write(@field(self, f.name)); } try jsw.endArray(); } @@ -776,27 +739,18 @@ const DocData = struct { docs: []const u8, }; - pub fn jsonStringify( - self: Type, - opts: std.json.StringifyOptions, - w: anytype, - ) !void { + pub fn jsonStringify(self: Type, jsw: anytype) !void { const active_tag = std.meta.activeTag(self); - var jsw = std.json.writeStream(w, 15); - jsw.whitespace = opts.whitespace; try jsw.beginArray(); - try jsw.arrayElem(); - try jsw.emitNumber(@intFromEnum(active_tag)); + try jsw.write(@intFromEnum(active_tag)); inline for (comptime std.meta.fields(Type)) |case| { if (@field(Type, case.name) == active_tag) { const current_value = @field(self, case.name); inline for (comptime std.meta.fields(case.type)) |f| { - try jsw.arrayElem(); if (f.type == std.builtin.Type.Pointer.Size) { - try jsw.emitNumber(@intFromEnum(@field(current_value, f.name))); + try jsw.write(@intFromEnum(@field(current_value, f.name))); } else { - try std.json.stringify(@field(current_value, f.name), opts, w); - jsw.state_index -= 1; + try jsw.write(@field(current_value, f.name)); } } } @@ -919,14 +873,8 @@ const DocData = struct { val: WalkResult, }; - pub fn jsonStringify( - self: Expr, - opts: std.json.StringifyOptions, - w: anytype, - ) @TypeOf(w).Error!void { + pub fn jsonStringify(self: Expr, jsw: anytype) !void { const active_tag = std.meta.activeTag(self); - var jsw = std.json.writeStream(w, 15); - jsw.whitespace = opts.whitespace; try jsw.beginObject(); if (active_tag == .declIndex) { try jsw.objectField("declRef"); @@ -935,14 +883,17 @@ const DocData = struct { } switch (self) { .int => { - if (self.int.negated) try w.writeAll("-"); - try jsw.emitNumber(self.int.value); + if (self.int.negated) { + try jsw.write(-@as(i65, self.int.value)); + } else { + try jsw.write(self.int.value); + } }, .builtinField => { - try jsw.emitString(@tagName(self.builtinField)); + try jsw.write(@tagName(self.builtinField)); }, .declRef => { - try jsw.emitNumber(self.declRef.Analyzed); + try jsw.write(self.declRef.Analyzed); }, else => { inline for (comptime std.meta.fields(Expr)) |case| { @@ -952,14 +903,7 @@ const DocData = struct { if (comptime std.mem.eql(u8, case.name, "declRef")) continue; if (@field(Expr, case.name) == active_tag) { - try std.json.stringify(@field(self, case.name), opts, w); - jsw.state_index -= 1; - // TODO: we should not reach into the state of the - // json writer, but alas, this is what's - // necessary with the current api. - // would be nice to have a proper integration - // between the json writer and the generic - // std.json.stringify implementation + try jsw.write(@field(self, case.name)); } } }, @@ -5440,12 +5384,9 @@ fn writeFileTableToJson( try jsw.beginArray(); var it = map.iterator(); while (it.next()) |entry| { - try jsw.arrayElem(); try jsw.beginArray(); - try jsw.arrayElem(); - try jsw.emitString(entry.key_ptr.*.sub_file_path); - try jsw.arrayElem(); - try jsw.emitNumber(mods.getIndex(entry.key_ptr.*.pkg) orelse 0); + try jsw.write(entry.key_ptr.*.sub_file_path); + try jsw.write(mods.getIndex(entry.key_ptr.*.pkg) orelse 0); try jsw.endArray(); } try jsw.endArray(); @@ -5462,21 +5403,19 @@ fn writeGuidesToJson(sections: std.ArrayListUnmanaged(Section), jsw: anytype) !v for (sections.items) |s| { // section name - try jsw.arrayElem(); try jsw.beginObject(); try jsw.objectField("name"); - try jsw.emitString(s.name); + try jsw.write(s.name); try jsw.objectField("guides"); // section value try jsw.beginArray(); for (s.guides.items) |g| { - try jsw.arrayElem(); try jsw.beginObject(); try jsw.objectField("name"); - try jsw.emitString(g.name); + try jsw.write(g.name); try jsw.objectField("body"); - try jsw.emitString(g.body); + try jsw.write(g.body); try jsw.endObject(); } try jsw.endArray(); @@ -5494,7 +5433,7 @@ fn writeModuleTableToJson( var it = map.valueIterator(); while (it.next()) |entry| { try jsw.objectField(entry.name); - try jsw.emitNumber(entry.value); + try jsw.write(entry.value); } try jsw.endObject(); } diff --git a/src/print_env.zig b/src/print_env.zig index 58da854989cb..cf4720c5f278 100644 --- a/src/print_env.zig +++ b/src/print_env.zig @@ -28,26 +28,27 @@ pub fn cmdEnv(gpa: Allocator, args: []const []const u8, stdout: std.fs.File.Writ var bw = std.io.bufferedWriter(stdout); const w = bw.writer(); - var jws = std.json.writeStream(w, 6); + var jws = std.json.writeStream(w, .{ .whitespace = .indent_1 }); + try jws.beginObject(); try jws.objectField("zig_exe"); - try jws.emitString(self_exe_path); + try jws.write(self_exe_path); try jws.objectField("lib_dir"); - try jws.emitString(zig_lib_directory.path.?); + try jws.write(zig_lib_directory.path.?); try jws.objectField("std_dir"); - try jws.emitString(zig_std_dir); + try jws.write(zig_std_dir); try jws.objectField("global_cache_dir"); - try jws.emitString(global_cache_dir); + try jws.write(global_cache_dir); try jws.objectField("version"); - try jws.emitString(build_options.version); + try jws.write(build_options.version); try jws.objectField("target"); - try jws.emitString(triple); + try jws.write(triple); try jws.endObject(); try w.writeByte('\n'); diff --git a/src/print_targets.zig b/src/print_targets.zig index 62e1d3b1581f..bfb2ac017702 100644 --- a/src/print_targets.zig +++ b/src/print_targets.zig @@ -40,31 +40,28 @@ pub fn cmdTargets( var bw = io.bufferedWriter(stdout); const w = bw.writer(); - var jws = std.json.writeStream(w, 6); + var jws = std.json.writeStream(w, .{ .whitespace = .indent_1 }); try jws.beginObject(); try jws.objectField("arch"); try jws.beginArray(); for (meta.fieldNames(Target.Cpu.Arch)) |field| { - try jws.arrayElem(); - try jws.emitString(field); + try jws.write(field); } try jws.endArray(); try jws.objectField("os"); try jws.beginArray(); for (meta.fieldNames(Target.Os.Tag)) |field| { - try jws.arrayElem(); - try jws.emitString(field); + try jws.write(field); } try jws.endArray(); try jws.objectField("abi"); try jws.beginArray(); for (meta.fieldNames(Target.Abi)) |field| { - try jws.arrayElem(); - try jws.emitString(field); + try jws.write(field); } try jws.endArray(); @@ -75,19 +72,16 @@ pub fn cmdTargets( @tagName(libc.arch), @tagName(libc.os), @tagName(libc.abi), }); defer allocator.free(tmp); - try jws.arrayElem(); - try jws.emitString(tmp); + try jws.write(tmp); } try jws.endArray(); try jws.objectField("glibc"); try jws.beginArray(); for (glibc_abi.all_versions) |ver| { - try jws.arrayElem(); - const tmp = try std.fmt.allocPrint(allocator, "{}", .{ver}); defer allocator.free(tmp); - try jws.emitString(tmp); + try jws.write(tmp); } try jws.endArray(); @@ -102,8 +96,7 @@ pub fn cmdTargets( for (arch.allFeaturesList(), 0..) |feature, i_usize| { const index = @as(Target.Cpu.Feature.Set.Index, @intCast(i_usize)); if (model.features.isEnabled(index)) { - try jws.arrayElem(); - try jws.emitString(feature.name); + try jws.write(feature.name); } } try jws.endArray(); @@ -118,8 +111,7 @@ pub fn cmdTargets( try jws.objectField(@tagName(arch)); try jws.beginArray(); for (arch.allFeaturesList()) |feature| { - try jws.arrayElem(); - try jws.emitString(feature.name); + try jws.write(feature.name); } try jws.endArray(); } @@ -131,17 +123,17 @@ pub fn cmdTargets( const triple = try native_target.zigTriple(allocator); defer allocator.free(triple); try jws.objectField("triple"); - try jws.emitString(triple); + try jws.write(triple); } { try jws.objectField("cpu"); try jws.beginObject(); try jws.objectField("arch"); - try jws.emitString(@tagName(native_target.cpu.arch)); + try jws.write(@tagName(native_target.cpu.arch)); try jws.objectField("name"); const cpu = native_target.cpu; - try jws.emitString(cpu.model.name); + try jws.write(cpu.model.name); { try jws.objectField("features"); @@ -149,8 +141,7 @@ pub fn cmdTargets( for (native_target.cpu.arch.allFeaturesList(), 0..) |feature, i_usize| { const index = @as(Target.Cpu.Feature.Set.Index, @intCast(i_usize)); if (cpu.features.isEnabled(index)) { - try jws.arrayElem(); - try jws.emitString(feature.name); + try jws.write(feature.name); } } try jws.endArray(); @@ -158,9 +149,9 @@ pub fn cmdTargets( try jws.endObject(); } try jws.objectField("os"); - try jws.emitString(@tagName(native_target.os.tag)); + try jws.write(@tagName(native_target.os.tag)); try jws.objectField("abi"); - try jws.emitString(@tagName(native_target.abi)); + try jws.write(@tagName(native_target.abi)); try jws.endObject(); try jws.endObject();