diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a0f70836c..5158f50cc0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -459,7 +459,6 @@ set(ZIG_STAGE2_SOURCES lib/std/io/limited_reader.zig lib/std/io/seekable_stream.zig lib/std/json.zig - lib/std/json/stringify.zig lib/std/leb128.zig lib/std/log.zig lib/std/macho.zig diff --git a/lib/std/json.zig b/lib/std/json.zig index d977c34577..321c1c1bd8 100644 --- a/lib/std/json.zig +++ b/lib/std/json.zig @@ -10,8 +10,8 @@ //! The high-level `stringify` serializes a Zig or `Value` type into JSON. const builtin = @import("builtin"); -const testing = @import("std").testing; -const ArrayList = @import("std").ArrayList; +const std = @import("std"); +const testing = std.testing; test Scanner { var scanner = Scanner.initCompleteInput(testing.allocator, "{\"foo\": 123}\n"); @@ -41,11 +41,13 @@ test Value { try testing.expectEqualSlices(u8, "goes", parsed.value.object.get("anything").?.string); } -test writeStream { - var out = ArrayList(u8).init(testing.allocator); +test Stringify { + var out: std.io.AllocatingWriter = undefined; + var write_stream: Stringify = .{ + .writer = out.init(testing.allocator), + .options = .{ .whitespace = .indent_2 }, + }; defer out.deinit(); - 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.write(123); @@ -55,16 +57,7 @@ test writeStream { \\ "foo": 123 \\} ; - try testing.expectEqualSlices(u8, expected, out.items); -} - -test stringify { - var out = ArrayList(u8).init(testing.allocator); - defer out.deinit(); - - const T = struct { a: i32, b: []const u8 }; - try stringify(T{ .a = 123, .b = "xy" }, .{}, out.writer()); - try testing.expectEqualSlices(u8, "{\"a\":123,\"b\":\"xy\"}", out.items); + try testing.expectEqualSlices(u8, expected, out.getWritten()); } pub const ObjectMap = @import("json/dynamic.zig").ObjectMap; @@ -99,20 +92,49 @@ pub const innerParseFromValue = @import("json/static.zig").innerParseFromValue; 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 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/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; +pub const Stringify = @import("json/Stringify.zig"); -pub const Formatter = @import("json/fmt.zig").Formatter; -pub const fmt = @import("json/fmt.zig").fmt; +/// Returns a formatter that formats the given value using stringify. +pub fn fmt(value: anytype, options: Stringify.Options) Formatter(@TypeOf(value)) { + return Formatter(@TypeOf(value)){ .value = value, .options = options }; +} + +test fmt { + const expectFmt = std.testing.expectFmt; + try expectFmt("123", "{}", .{fmt(@as(u32, 123), .{})}); + try expectFmt( + \\{"num":927,"msg":"hello","sub":{"mybool":true}} + , "{}", .{fmt(struct { + num: u32, + msg: []const u8, + sub: struct { + mybool: bool, + }, + }{ + .num = 927, + .msg = "hello", + .sub = .{ .mybool = true }, + }, .{})}); +} + +/// Formats the given value using stringify. +pub fn Formatter(comptime T: type) type { + return struct { + value: T, + options: Stringify.Options, + + pub fn format( + self: @This(), + comptime fmt_spec: []const u8, + options: std.fmt.FormatOptions, + writer: *std.io.BufferedWriter, + ) !void { + _ = fmt_spec; + _ = options; + try Stringify.value(self.value, self.options, writer); + } + }; +} test { _ = @import("json/test.zig"); @@ -120,6 +142,6 @@ test { _ = @import("json/dynamic.zig"); _ = @import("json/hashmap.zig"); _ = @import("json/static.zig"); - _ = @import("json/stringify.zig"); + _ = Stringify; _ = @import("json/JSONTestSuite_test.zig"); } diff --git a/lib/std/json/Stringify.zig b/lib/std/json/Stringify.zig new file mode 100644 index 0000000000..4df62dfed9 --- /dev/null +++ b/lib/std/json/Stringify.zig @@ -0,0 +1,1048 @@ +//! Writes JSON ([RFC8259](https://tools.ietf.org/html/rfc8259)) formatted data +//! to a stream. +//! +//! The sequence of method calls to write JSON content must follow this grammar: +//! ``` +//! = +//! = +//! | +//! | +//! | write +//! | print +//! | +//! = beginObject ( )* endObject +//! = objectField | objectFieldRaw | +//! = beginArray ( )* endArray +//! = beginWriteRaw ( stream.writeAll )* endWriteRaw +//! = beginObjectFieldRaw ( stream.writeAll )* endObjectFieldRaw +//! ``` + +const std = @import("../std.zig"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const BitStack = std.BitStack; +const Stringify = @This(); + +const OBJECT_MODE = 0; +const ARRAY_MODE = 1; + +writer: *std.io.BufferedWriter, +options: Options = .{}, +indent_level: usize = 0, +next_punctuation: enum { + the_beginning, + none, + comma, + colon, +} = .the_beginning, + +nesting_stack: switch (safety_checks) { + .checked_to_fixed_depth => |fixed_buffer_size| [(fixed_buffer_size + 7) >> 3]u8, + .assumed_correct => void, +} = switch (safety_checks) { + .checked_to_fixed_depth => @splat(0), + .assumed_correct => {}, +}, + +raw_streaming_mode: if (build_mode_has_safety) + enum { none, value, objectField } +else + void = if (build_mode_has_safety) .none else {}, + +const build_mode_has_safety = switch (@import("builtin").mode) { + .Debug, .ReleaseSafe => true, + .ReleaseFast, .ReleaseSmall => false, +}; + +/// The `safety_checks_hint` parameter determines how much memory is used to enable assertions that the above grammar is being followed, +/// e.g. tripping an assertion rather than allowing `endObject` to emit the final `}` in `[[[]]}`. +/// "Depth" in this context means the depth of nested `[]` or `{}` expressions +/// (or equivalently the amount of recursion on the `` grammar expression above). +/// For example, emitting the JSON `[[[]]]` requires a depth of 3. +/// If `.checked_to_fixed_depth` is used, there is additionally an assertion that the nesting depth never exceeds the given limit. +/// `.checked_to_fixed_depth` embeds the storage required in the `Stringify` struct. +/// `.assumed_correct` requires no space and performs none of these assertions. +/// In `ReleaseFast` and `ReleaseSmall` mode, the given `safety_checks_hint` is ignored and is always treated as `.assumed_correct`. +const safety_checks_hint: union(enum) { + /// Rounded up to the nearest multiple of 8. + checked_to_fixed_depth: usize, + assumed_correct, +} = .{ .checked_to_fixed_depth = 256 }; + +const safety_checks: @TypeOf(safety_checks_hint) = if (build_mode_has_safety) + safety_checks_hint +else + .assumed_correct; + +pub fn beginArray(self: *Stringify) anyerror!void { + if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); + try self.valueStart(); + try self.stream.writeByte('['); + try self.pushIndentation(ARRAY_MODE); + self.next_punctuation = .none; +} + +pub fn beginObject(self: *Stringify) anyerror!void { + if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); + try self.valueStart(); + try self.stream.writeByte('{'); + try self.pushIndentation(OBJECT_MODE); + self.next_punctuation = .none; +} + +pub fn endArray(self: *Stringify) anyerror!void { + if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); + 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: *Stringify) anyerror!void { + if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); + 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: *Stringify, mode: u1) !void { + switch (safety_checks) { + .checked_to_fixed_depth => { + BitStack.pushWithStateAssumeCapacity(&self.nesting_stack, &self.indent_level, mode); + }, + .assumed_correct => { + self.indent_level += 1; + }, + } +} +fn popIndentation(self: *Stringify, assert_its_this_one: u1) void { + switch (safety_checks) { + .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: *Stringify) !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'; + break :blk self.indent_level; + }, + }; + try self.stream.writeByte('\n'); + try self.stream.writeByteNTimes(char, n_chars); +} + +fn valueStart(self: *Stringify) !void { + if (self.isObjectKeyExpected()) |is_it| assert(!is_it); // Call objectField*(), not write(), for object keys. + return self.valueStartAssumeTypeOk(); +} +fn objectFieldStart(self: *Stringify) !void { + if (self.isObjectKeyExpected()) |is_it| assert(is_it); // Expected write(), not objectField*(). + return self.valueStartAssumeTypeOk(); +} +fn valueStartAssumeTypeOk(self: *Stringify) !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(' '); + } + }, + } +} +fn valueDone(self: *Stringify) void { + self.next_punctuation = .comma; +} + +// Only when safety is enabled: +fn isObjectKeyExpected(self: *const Stringify) ?bool { + switch (safety_checks) { + .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 Stringify) bool { + return self.indent_level == 0 and self.next_punctuation == .comma; +} + +/// An alternative to calling `write` that formats a value with `std.fmt`. +/// This function does the usual punctuation and indentation formatting +/// assuming the resulting formatted string represents a single complete value; +/// e.g. `"1"`, `"[]"`, `"[1,2]"`, not `"1,2"`. +/// This function may be useful for doing your own number formatting. +pub fn print(self: *Stringify, comptime fmt: []const u8, args: anytype) anyerror!void { + if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); + try self.valueStart(); + try self.stream.print(fmt, args); + self.valueDone(); +} + +test print { + var out_buf: [1024]u8 = undefined; + var out: std.io.BufferedWriter = undefined; + out.initFixed(&out_buf); + + var w: Stringify = .{ .writer = &out, .options = .{ .whitespace = .indent_2 } }; + defer w.deinit(); + + try w.beginObject(); + try w.objectField("a"); + try w.print("[ ]", .{}); + try w.objectField("b"); + try w.beginArray(); + try w.print("[{s}] ", .{"[]"}); + try w.print(" {}", .{12345}); + try w.endArray(); + try w.endObject(); + + const expected = + \\{ + \\ "a": [ ], + \\ "b": [ + \\ [[]] , + \\ 12345 + \\ ] + \\} + ; + try std.testing.expectEqualStrings(expected, out.getWritten()); +} + +/// An alternative to calling `write` that allows you to write directly to the `.stream` field, e.g. with `.stream.writeAll()`. +/// Call `beginWriteRaw()`, then write a complete value (including any quotes if necessary) directly to the `.stream` field, +/// then call `endWriteRaw()`. +/// This can be useful for streaming very long strings into the output without needing it all buffered in memory. +pub fn beginWriteRaw(self: *Stringify) !void { + if (build_mode_has_safety) { + assert(self.raw_streaming_mode == .none); + self.raw_streaming_mode = .value; + } + try self.valueStart(); +} + +/// See `beginWriteRaw`. +pub fn endWriteRaw(self: *Stringify) void { + if (build_mode_has_safety) { + assert(self.raw_streaming_mode == .value); + self.raw_streaming_mode = .none; + } + self.valueDone(); +} + +/// See `Stringify` for when to call this method. +/// `key` is the string content of the property name. +/// Surrounding quotes will be added and any special characters will be escaped. +/// See also `objectFieldRaw`. +pub fn objectField(self: *Stringify, key: []const u8) anyerror!void { + if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); + try self.objectFieldStart(); + try encodeJsonString(key, self.options, self.stream); + self.next_punctuation = .colon; +} +/// See `Stringify` for when to call this method. +/// `quoted_key` is the complete bytes of the key including quotes and any necessary escape sequences. +/// A few assertions are performed on the given value to ensure that the caller of this function understands the API contract. +/// See also `objectField`. +pub fn objectFieldRaw(self: *Stringify, quoted_key: []const u8) anyerror!void { + if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); + assert(quoted_key.len >= 2 and quoted_key[0] == '"' and quoted_key[quoted_key.len - 1] == '"'); // quoted_key should be "quoted". + try self.objectFieldStart(); + try self.stream.writeAll(quoted_key); + self.next_punctuation = .colon; +} + +/// In the rare case that you need to write very long object field names, +/// this is an alternative to `objectField` and `objectFieldRaw` that allows you to write directly to the `.stream` field +/// similar to `beginWriteRaw`. +/// Call `endObjectFieldRaw()` when you're done. +pub fn beginObjectFieldRaw(self: *Stringify) !void { + if (build_mode_has_safety) { + assert(self.raw_streaming_mode == .none); + self.raw_streaming_mode = .objectField; + } + try self.objectFieldStart(); +} + +/// See `beginObjectFieldRaw`. +pub fn endObjectFieldRaw(self: *Stringify) void { + if (build_mode_has_safety) { + assert(self.raw_streaming_mode == .objectField); + self.raw_streaming_mode = .none; + } + self.next_punctuation = .colon; +} + +/// Renders the given Zig value as JSON. +/// +/// Supported types: +/// * Zig `bool` -> JSON `true` or `false`. +/// * Zig `?T` -> `null` or the rendering of `T`. +/// * Zig `i32`, `u64`, etc. -> JSON number or string. +/// * When option `emit_nonportable_numbers_as_strings` is true, if the value is outside the range `+-1<<53` (the precise integer range 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 `Options.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 `Stringify`. See `std.json.Value` for an example. +/// * See `Options.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 `Stringify`. +/// * 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 `Stringify`. +/// * If the enum is non-exhaustive, unnamed values are rendered as integers. +/// * Zig untyped enum literal -> JSON string naming the active tag. +/// * Zig error -> JSON string naming the error. +/// * Zig `*T` -> the rendering of `T`. Note there is no guard against circular-reference infinite recursion. +/// +/// See also alternative functions `print` and `beginWriteRaw`. +/// For writing object field names, use `objectField` instead. +pub fn write(self: *Stringify, v: anytype) anyerror!void { + if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); + const T = @TypeOf(v); + switch (@typeInfo(T)) { + .int => { + try self.valueStart(); + if (self.options.emit_nonportable_numbers_as_strings and + (v <= -(1 << 53) or v >= (1 << 53))) + { + try self.stream.print("\"{}\"", .{v}); + } else { + try self.stream.print("{}", .{v}); + } + self.valueDone(); + return; + }, + .comptime_int => { + return self.write(@as(std.math.IntFittingRange(v, v), v)); + }, + .float, .comptime_float => { + if (@as(f64, @floatCast(v)) == v) { + try self.valueStart(); + try self.stream.print("{}", .{@as(f64, @floatCast(v))}); + self.valueDone(); + return; + } + try self.valueStart(); + try self.stream.print("\"{}\"", .{v}); + self.valueDone(); + return; + }, + + .bool => { + try self.valueStart(); + try self.stream.writeAll(if (v) "true" else "false"); + self.valueDone(); + return; + }, + .null => { + try self.valueStart(); + try self.stream.writeAll("null"); + self.valueDone(); + return; + }, + .optional => { + if (v) |payload| { + return try self.write(payload); + } else { + return try self.write(null); + } + }, + .@"enum" => |enum_info| { + if (std.meta.hasFn(T, "jsonStringify")) { + return v.jsonStringify(self); + } + + if (!enum_info.is_exhaustive) { + inline for (enum_info.fields) |field| { + if (v == @field(T, field.name)) { + break; + } + } else { + return self.write(@intFromEnum(v)); + } + } + + return self.stringValue(@tagName(v)); + }, + .enum_literal => { + return self.stringValue(@tagName(v)); + }, + .@"union" => { + if (std.meta.hasFn(T, "jsonStringify")) { + return v.jsonStringify(self); + } + + const info = @typeInfo(T).@"union"; + if (info.tag_type) |UnionTagType| { + try self.beginObject(); + inline for (info.fields) |u_field| { + if (v == @field(UnionTagType, u_field.name)) { + try self.objectField(u_field.name); + if (u_field.type == void) { + // void v is {} + try self.beginObject(); + try self.endObject(); + } else { + try self.write(@field(v, 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 (std.meta.hasFn(T, "jsonStringify")) { + return v.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(v, Field.name) == null) { + emit_field = false; + } + } + } + + if (emit_field) { + if (!S.is_tuple) { + try self.objectField(Field.name); + } + try self.write(@field(v, Field.name)); + } + } + if (S.is_tuple) { + try self.endArray(); + } else { + try self.endObject(); + } + return; + }, + .error_set => return self.stringValue(@errorName(v)), + .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, v)); + }, + else => { + return self.write(v.*); + }, + }, + .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(v) else v; + + 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(&v); + }, + .vector => |info| { + const array: [info.len]info.child = v; + return self.write(&array); + }, + else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"), + } + unreachable; +} + +fn stringValue(self: *Stringify, s: []const u8) !void { + try self.valueStart(); + try encodeJsonString(s, self.options, self.stream); + self.valueDone(); +} + +pub const Options = struct { + /// 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, + + /// When true, renders numbers outside the range `+-1<<53` (the precise integer range of f64) as JSON strings in base 10. + emit_nonportable_numbers_as_strings: bool = false, +}; + +/// Writes the given value to the `std.io.Writer` stream. +/// See `Stringify` for how the given value is serialized into JSON. +/// The maximum nesting depth of the output JSON document is 256. +pub fn value(v: anytype, options: Options, writer: *std.io.BufferedWriter) anyerror!void { + var s: Stringify = .{ .writer = writer, .options = options }; + try s.write(v); +} + +test value { + var out: std.io.AllocatingWriter = undefined; + const writer = out.init(std.testing.allocator); + defer out.deinit(); + + const T = struct { a: i32, b: []const u8 }; + try value(T{ .a = 123, .b = "xy" }, .{}, writer); + try std.testing.expectEqualSlices(u8, "{\"a\":123,\"b\":\"xy\"}", out.getWritten()); + + try testStringify("9999999999999999", 9999999999999999, .{}); + try testStringify("\"9999999999999999\"", 9999999999999999, .{ .emit_nonportable_numbers_as_strings = true }); + + 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 }); + + // void field + try testStringify("{\"foo\":42}", struct { + foo: u32, + bar: void = {}, + }{ .foo = 42 }, .{}); + + const Tuple = struct { []const u8, usize }; + try testStringify("[\"foo\",42]", Tuple{ "foo", 42 }, .{}); + + comptime { + testStringify("false", false, .{}) catch unreachable; + const MyStruct = struct { foo: u32 }; + testStringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{ + MyStruct{ .foo = 42 }, + MyStruct{ .foo = 100 }, + MyStruct{ .foo = 1000 }, + }, .{}) catch unreachable; + } +} + +/// Calls `value` and stores the result in dynamically allocated memory instead +/// of taking a writer. +/// +/// Caller owns returned memory. +pub fn valueAlloc(gpa: Allocator, v: anytype, options: Options) error{OutOfMemory}![]u8 { + var aw: std.io.AllocatingWriter = undefined; + const writer = aw.init(gpa); + defer aw.deinit(); + try value(v, options, writer); + return aw.toOwnedSlice(); +} + +test valueAlloc { + const allocator = std.testing.allocator; + const expected = + \\{"foo":"bar","answer":42,"my_friend":"sammy"} + ; + const actual = try valueAlloc(allocator, .{ .foo = "bar", .answer = 42, .my_friend = "sammy" }, .{}); + defer allocator.free(actual); + + try std.testing.expectEqualStrings(expected, actual); +} + +fn outputUnicodeEscape(codepoint: u21, out_stream: *std.io.BufferedWriter) !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 + // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point. + try out_stream.writeAll("\\u"); + try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); + } else { + assert(codepoint <= 0x10FFFF); + // To escape an extended character that is not in the Basic Multilingual Plane, + // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair. + const high = @as(u16, @intCast((codepoint - 0x10000) >> 10)) + 0xD800; + const low = @as(u16, @intCast(codepoint & 0x3FF)) + 0xDC00; + try out_stream.writeAll("\\u"); + try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); + try out_stream.writeAll("\\u"); + try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); + } +} + +fn outputSpecialEscape(c: u8, writer: *std.io.BufferedWriter) !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: Options, writer: *std.io.BufferedWriter) !void { + try writer.writeByte('\"'); + try encodeJsonStringChars(string, options, writer); + try writer.writeByte('\"'); +} + +/// Write `chars` to `writer` as JSON encoded string characters. +pub fn encodeJsonStringChars(chars: []const u8, options: Options, writer: *std.io.BufferedWriter) !void { + var write_cursor: usize = 0; + var i: usize = 0; + 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); + i += ulen - 1; + write_cursor = i + 1; + }, + } + } + } 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; + }, + } + } + } + try writer.writeAll(chars[write_cursor..chars.len]); +} + +test "json write stream" { + var out_buf: [1024]u8 = undefined; + var out: std.io.BufferedWriter = undefined; + out.initFixed(&out_buf); + var w: Stringify = .{ .writer = &out, .options = .{ .whitespace = .indent_2 } }; + try testBasicWriteStream(&w); +} + +fn testBasicWriteStream(w: *Stringify, out: *std.io.BufferedWriter) !void { + out.reset(); + + try w.beginObject(); + + try w.objectField("object"); + var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_allocator.deinit(); + try w.write(try getJsonObject(arena_allocator.allocator())); + + try w.objectFieldRaw("\"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 expected = + \\{ + \\ "object": { + \\ "one": 1, + \\ "two": 2e0 + \\ }, + \\ "string": "This is a string", + \\ "array": [ + \\ "Another string", + \\ 1, + \\ 3.5e0 + \\ ], + \\ "int": 10, + \\ "float": 3.5e0 + \\} + ; + try std.testing.expectEqualStrings(expected, out.getWritten()); +} + +fn getJsonObject(allocator: std.mem.Allocator) !std.json.Value { + var v: std.json.Value = .{ .object = std.json.ObjectMap.init(allocator) }; + try v.object.put("one", std.json.Value{ .integer = @as(i64, @intCast(1)) }); + try v.object.put("two", std.json.Value{ .float = 2.0 }); + return v; +} + +test "stringify null optional fields" { + const MyStruct = struct { + optional: ?[]const u8 = null, + required: []const u8 = "something", + another_optional: ?[]const u8 = null, + another_required: []const u8 = "something else", + }; + try testStringify( + \\{"optional":null,"required":"something","another_optional":null,"another_required":"something else"} + , + MyStruct{}, + .{}, + ); + try testStringify( + \\{"required":"something","another_required":"something else"} + , + MyStruct{}, + .{ .emit_null_optional_fields = false }, + ); +} + +test "stringify basic types" { + 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.2e1", 42.0, .{}); + try testStringify("42", @as(u8, 42), .{}); + try testStringify("42", @as(u128, 42), .{}); + try testStringify("9999999999999999", 9999999999999999, .{}); + try testStringify("4.2e1", @as(f32, 42), .{}); + try testStringify("4.2e1", @as(f64, 42), .{}); + try testStringify("\"ItBroke\"", @as(anyerror, error.ItBroke), .{}); + try testStringify("\"ItBroke\"", error.ItBroke, .{}); +} + +test "stringify string" { + 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"), .{}); + 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" { + const E = enum { + foo, + bar, + }; + try testStringify("\"foo\"", E.foo, .{}); + try testStringify("\"bar\"", E.bar, .{}); +} + +test "stringify non-exhaustive enum" { + const E = enum(u8) { + foo = 0, + _, + }; + try testStringify("\"foo\"", E.foo, .{}); + try testStringify("1", @as(E, @enumFromInt(1)), .{}); +} + +test "stringify enum literals" { + try testStringify("\"foo\"", .foo, .{}); + try testStringify("\"bar\"", .bar, .{}); +} + +test "stringify tagged unions" { + const T = union(enum) { + nothing, + foo: u32, + bar: bool, + }; + 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 { + foo: u32, + }{ .foo = 42 }, .{}); +} + +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( + \\{ + \\ "foo": 42, + \\ "bar": [ + \\ 1, + \\ 2, + \\ 3 + \\ ] + \\} + , + struct { + foo: u32, + bar: [3]u32, + }{ + .foo = 42, + .bar = .{ 1, 2, 3 }, + }, + .{ .whitespace = .indent_4 }, + ); + 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, + }{ + .foo = 42, + .bar = .{ 1, 2, 3 }, + }, + .{ .whitespace = .indent_tab }, + ); + try testStringify( + \\{"foo":42,"bar":[1,2,3]} + , + struct { + foo: u32, + bar: [3]u32, + }{ + .foo = 42, + .bar = .{ 1, 2, 3 }, + }, + .{ .whitespace = .minified }, + ); +} + +test "stringify array of structs" { + const MyStruct = struct { + foo: u32, + }; + try testStringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{ + MyStruct{ .foo = 42 }, + MyStruct{ .foo = 100 }, + MyStruct{ .foo = 1000 }, + }, .{}); +} + +test "stringify struct with custom stringifier" { + try testStringify("[\"something special\",42]", struct { + foo: u32, + const Self = @This(); + pub fn jsonStringify(v: @This(), jws: anytype) !void { + _ = v; + try jws.beginArray(); + try jws.write("something special"); + try jws.write(42); + try jws.endArray(); + } + }{ .foo = 42 }, .{}); +} + +fn testStringify(expected: []const u8, v: anytype, options: Options) !void { + const ValidationWriter = struct { + const Self = @This(); + pub const Writer = std.io.Writer(*Self, Error, Self.write); + pub const Error = error{ + TooMuchData, + DifferentData, + }; + + expected_remaining: []const u8, + + fn init(exp: []const u8) Self { + return .{ .expected_remaining = exp }; + } + + pub fn writer(self: *Self) Writer { + return .{ .context = self }; + } + + fn write(self: *Self, bytes: []const u8) Error!usize { + if (self.expected_remaining.len < bytes.len) { + std.debug.print( + \\====== expected this output: ========= + \\{s} + \\======== instead found this: ========= + \\{s} + \\====================================== + , .{ + self.expected_remaining, + bytes, + }); + return error.TooMuchData; + } + if (!std.mem.eql(u8, self.expected_remaining[0..bytes.len], bytes)) { + std.debug.print( + \\====== expected this output: ========= + \\{s} + \\======== instead found this: ========= + \\{s} + \\====================================== + , .{ + self.expected_remaining[0..bytes.len], + bytes, + }); + return error.DifferentData; + } + self.expected_remaining = self.expected_remaining[bytes.len..]; + return bytes.len; + } + }; + + var vos = ValidationWriter.init(expected); + try value(v, options, vos.writer()); + if (vos.expected_remaining.len > 0) return error.NotEnoughData; +} + +test "raw streaming" { + var out_buf: [1024]u8 = undefined; + var out: std.io.BufferedWriter = undefined; + out.initFixed(&out_buf); + + var w: Stringify = .{ .writer = &out, .options = .{ .whitespace = .indent_2 } }; + try w.beginObject(); + try w.beginObjectFieldRaw(); + try w.stream.writeAll("\"long"); + try w.stream.writeAll(" key\""); + w.endObjectFieldRaw(); + try w.beginWriteRaw(); + try w.stream.writeAll("\"long"); + try w.stream.writeAll(" value\""); + w.endWriteRaw(); + try w.endObject(); + + const expected = + \\{ + \\ "long key": "long value" + \\} + ; + try std.testing.expectEqualStrings(expected, w.writer.getWritten()); +} diff --git a/lib/std/json/dynamic.zig b/lib/std/json/dynamic.zig index a5aa80a888..a17541c0ef 100644 --- a/lib/std/json/dynamic.zig +++ b/lib/std/json/dynamic.zig @@ -4,9 +4,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const ArrayList = std.ArrayList; const StringArrayHashMap = std.StringArrayHashMap; const Allocator = std.mem.Allocator; - -const StringifyOptions = @import("./stringify.zig").StringifyOptions; -const stringify = @import("./stringify.zig").stringify; +const json = std.json; const ParseOptions = @import("./static.zig").ParseOptions; const ParseError = @import("./static.zig").ParseError; @@ -52,12 +50,11 @@ pub const Value = union(enum) { } } - pub fn dump(self: Value) void { - std.debug.lockStdErr(); + pub fn dump(v: Value) void { + var bw = std.debug.lockStdErr2(); defer std.debug.unlockStdErr(); - const stderr = std.io.getStdErr().writer(); - stringify(self, .{}, stderr) catch return; + json.Stringify.value(v, .{}, &bw) catch return; } pub fn jsonStringify(value: @This(), jws: anytype) !void { diff --git a/lib/std/json/dynamic_test.zig b/lib/std/json/dynamic_test.zig index 45cdc0d0c7..3f117db753 100644 --- a/lib/std/json/dynamic_test.zig +++ b/lib/std/json/dynamic_test.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const json = std.json; const mem = std.mem; const testing = std.testing; const ArenaAllocator = std.heap.ArenaAllocator; @@ -70,13 +71,11 @@ test "json.parser.dynamic" { try testing.expect(mem.eql(u8, large_int.number_string, "18446744073709551615")); } -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, .{}); + var fixed_writer: std.io.BufferedWriter = undefined; + fixed_writer.initFixed(&out_buffer); + var jw: json.Stringify = .{ .writer = &fixed_writer, .options = .{} }; defer jw.deinit(); try jw.beginObject(); @@ -101,8 +100,8 @@ test "write json then parse it" { try jw.endObject(); - fixed_buffer_stream = std.io.fixedBufferStream(fixed_buffer_stream.getWritten()); - var json_reader = jsonReader(testing.allocator, fixed_buffer_stream.reader()); + var fbs: std.io.FixedBufferStream = .{ .buffer = fixed_writer.getWritten() }; + var json_reader = jsonReader(testing.allocator, fbs.reader()); defer json_reader.deinit(); var parsed = try parseFromTokenSource(Value, testing.allocator, &json_reader, .{}); defer parsed.deinit(); @@ -242,9 +241,10 @@ test "Value.jsonStringify" { .{ .object = obj }, }; var buffer: [0x1000]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buffer); + var fixed_writer: std.io.BufferedWriter = undefined; + fixed_writer.initFixed(&buffer); - var jw = writeStream(fbs.writer(), .{ .whitespace = .indent_1 }); + var jw: json.Stringify = .{ .writer = &fixed_writer, .options = .{ .whitespace = .indent_1 } }; defer jw.deinit(); try jw.write(array); @@ -266,7 +266,7 @@ test "Value.jsonStringify" { \\ } \\] ; - try testing.expectEqualSlices(u8, expected, fbs.getWritten()); + try testing.expectEqualStrings(expected, fixed_writer.getWritten()); } test "parseFromValue(std.json.Value,...)" { diff --git a/lib/std/json/fmt.zig b/lib/std/json/fmt.zig deleted file mode 100644 index 09f58b3be6..0000000000 --- a/lib/std/json/fmt.zig +++ /dev/null @@ -1,46 +0,0 @@ -const std = @import("std"); - -const stringify = @import("stringify.zig").stringify; -const StringifyOptions = @import("stringify.zig").StringifyOptions; - -/// Returns a formatter that formats the given value using stringify. -pub fn fmt(value: anytype, options: StringifyOptions) Formatter(@TypeOf(value)) { - return Formatter(@TypeOf(value)){ .value = value, .options = options }; -} - -/// Formats the given value using stringify. -pub fn Formatter(comptime T: type) type { - return struct { - value: T, - options: StringifyOptions, - - pub fn format( - self: @This(), - comptime fmt_spec: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - _ = fmt_spec; - _ = options; - try stringify(self.value, self.options, writer); - } - }; -} - -test fmt { - const expectFmt = std.testing.expectFmt; - try expectFmt("123", "{}", .{fmt(@as(u32, 123), .{})}); - try expectFmt( - \\{"num":927,"msg":"hello","sub":{"mybool":true}} - , "{}", .{fmt(struct { - num: u32, - msg: []const u8, - sub: struct { - mybool: bool, - }, - }{ - .num = 927, - .msg = "hello", - .sub = .{ .mybool = true }, - }, .{})}); -} diff --git a/lib/std/json/hashmap_test.zig b/lib/std/json/hashmap_test.zig index 49d8caffae..576e486876 100644 --- a/lib/std/json/hashmap_test.zig +++ b/lib/std/json/hashmap_test.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const json = std.json; const testing = std.testing; const ArrayHashMap = @import("hashmap.zig").ArrayHashMap; @@ -7,7 +8,6 @@ const parseFromSlice = @import("static.zig").parseFromSlice; const parseFromSliceLeaky = @import("static.zig").parseFromSliceLeaky; const parseFromTokenSource = @import("static.zig").parseFromTokenSource; const parseFromValue = @import("static.zig").parseFromValue; -const stringifyAlloc = @import("stringify.zig").stringifyAlloc; const Value = @import("dynamic.zig").Value; const jsonReader = @import("./scanner.zig").reader; @@ -89,7 +89,7 @@ test "stringify json hashmap" { var value = ArrayHashMap(T){}; defer value.deinit(testing.allocator); { - const doc = try stringifyAlloc(testing.allocator, value, .{}); + const doc = try json.Stringify.valueAlloc(testing.allocator, value, .{}); defer testing.allocator.free(doc); try testing.expectEqualStrings("{}", doc); } @@ -98,7 +98,7 @@ test "stringify json hashmap" { try value.map.put(testing.allocator, "xyz", .{ .i = 1, .s = "w" }); { - const doc = try stringifyAlloc(testing.allocator, value, .{}); + const doc = try json.Stringify.valueAlloc(testing.allocator, value, .{}); defer testing.allocator.free(doc); try testing.expectEqualStrings( \\{"abc":{"i":0,"s":"d"},"xyz":{"i":1,"s":"w"}} @@ -107,7 +107,7 @@ test "stringify json hashmap" { try testing.expect(value.map.swapRemove("abc")); { - const doc = try stringifyAlloc(testing.allocator, value, .{}); + const doc = try json.Stringify.valueAlloc(testing.allocator, value, .{}); defer testing.allocator.free(doc); try testing.expectEqualStrings( \\{"xyz":{"i":1,"s":"w"}} @@ -116,7 +116,7 @@ test "stringify json hashmap" { try testing.expect(value.map.swapRemove("xyz")); { - const doc = try stringifyAlloc(testing.allocator, value, .{}); + const doc = try json.Stringify.valueAlloc(testing.allocator, value, .{}); defer testing.allocator.free(doc); try testing.expectEqualStrings("{}", doc); } @@ -129,7 +129,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_2 }); + const doc = try json.Stringify.valueAlloc(testing.allocator, value, .{ .whitespace = .indent_2 }); defer testing.allocator.free(doc); try testing.expectEqualStrings( \\{ diff --git a/lib/std/json/stringify.zig b/lib/std/json/stringify.zig deleted file mode 100644 index db2ba85318..0000000000 --- a/lib/std/json/stringify.zig +++ /dev/null @@ -1,770 +0,0 @@ -const std = @import("std"); -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 { - /// 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, - - /// When true, renders numbers outside the range `+-1<<53` (the precise integer range of f64) as JSON strings in base 10. - emit_nonportable_numbers_as_strings: 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}![]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. -/// In `ReleaseFast` and `ReleaseSmall`, `max_depth` is ignored, effectively equivalent to passing `null`. -/// 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. -/// -/// In `ReleaseFast` and `ReleaseSmall` mode, this function is effectively equivalent to calling `writeStreamMaxDepth(..., null)`; -/// in those build modes, the allocator is *not used*. -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 sequence of method calls to write JSON content must follow this grammar: -/// ``` -/// = -/// = -/// | -/// | -/// | write -/// | print -/// | -/// = beginObject ( )* endObject -/// = objectField | objectFieldRaw | -/// = beginArray ( )* endArray -/// = beginWriteRaw ( stream.writeAll )* endWriteRaw -/// = beginObjectFieldRaw ( stream.writeAll )* endObjectFieldRaw -/// ``` -/// -/// The `safety_checks_hint` parameter determines how much memory is used to enable assertions that the above grammar is being followed, -/// e.g. tripping an assertion rather than allowing `endObject` to emit the final `}` in `[[[]]}`. -/// "Depth" in this context means the depth of nested `[]` or `{}` expressions -/// (or equivalently the amount of recursion on the `` grammar expression above). -/// For example, emitting the JSON `[[[]]]` requires a depth of 3. -/// If `.checked_to_fixed_depth` is used, there is additionally an assertion that the nesting depth never exceeds the given limit. -/// `.checked_to_arbitrary_depth` requires a runtime allocator for the memory. -/// `.checked_to_fixed_depth` embeds the storage required in the `WriteStream` struct. -/// `.assumed_correct` requires no space and performs none of these assertions. -/// In `ReleaseFast` and `ReleaseSmall` mode, the given `safety_checks_hint` is ignored and is always treated as `.assumed_correct`. -pub fn WriteStream( - comptime OutStream: type, - comptime safety_checks_hint: 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(); - const build_mode_has_safety = switch (@import("builtin").mode) { - .Debug, .ReleaseSafe => true, - .ReleaseFast, .ReleaseSmall => false, - }; - const safety_checks: @TypeOf(safety_checks_hint) = if (build_mode_has_safety) - safety_checks_hint - else - .assumed_correct; - - 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, - }, - - raw_streaming_mode: if (build_mode_has_safety) - enum { none, value, objectField } - else - void = if (build_mode_has_safety) .none else {}, - - 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 => {}, - }, - }; - } - - /// Only necessary with .checked_to_arbitrary_depth. - 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 { - if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); - try self.valueStart(); - try self.stream.writeByte('['); - try self.pushIndentation(ARRAY_MODE); - self.next_punctuation = .none; - } - - pub fn beginObject(self: *Self) Error!void { - if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); - try self.valueStart(); - try self.stream.writeByte('{'); - try self.pushIndentation(OBJECT_MODE); - self.next_punctuation = .none; - } - - pub fn endArray(self: *Self) Error!void { - if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); - 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 { - if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); - 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'; - 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(' '); - } - }, - } - } - fn valueDone(self: *Self) void { - self.next_punctuation = .comma; - } - - // 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; - } - - /// An alternative to calling `write` that formats a value with `std.fmt`. - /// This function does the usual punctuation and indentation formatting - /// assuming the resulting formatted string represents a single complete value; - /// e.g. `"1"`, `"[]"`, `"[1,2]"`, not `"1,2"`. - /// This function may be useful for doing your own number formatting. - pub fn print(self: *Self, comptime fmt: []const u8, args: anytype) Error!void { - if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); - try self.valueStart(); - try self.stream.print(fmt, args); - self.valueDone(); - } - - /// An alternative to calling `write` that allows you to write directly to the `.stream` field, e.g. with `.stream.writeAll()`. - /// Call `beginWriteRaw()`, then write a complete value (including any quotes if necessary) directly to the `.stream` field, - /// then call `endWriteRaw()`. - /// This can be useful for streaming very long strings into the output without needing it all buffered in memory. - pub fn beginWriteRaw(self: *Self) !void { - if (build_mode_has_safety) { - assert(self.raw_streaming_mode == .none); - self.raw_streaming_mode = .value; - } - try self.valueStart(); - } - - /// See `beginWriteRaw`. - pub fn endWriteRaw(self: *Self) void { - if (build_mode_has_safety) { - assert(self.raw_streaming_mode == .value); - self.raw_streaming_mode = .none; - } - self.valueDone(); - } - - /// See `WriteStream` for when to call this method. - /// `key` is the string content of the property name. - /// Surrounding quotes will be added and any special characters will be escaped. - /// See also `objectFieldRaw`. - pub fn objectField(self: *Self, key: []const u8) Error!void { - if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); - try self.objectFieldStart(); - try encodeJsonString(key, self.options, self.stream); - self.next_punctuation = .colon; - } - /// See `WriteStream` for when to call this method. - /// `quoted_key` is the complete bytes of the key including quotes and any necessary escape sequences. - /// A few assertions are performed on the given value to ensure that the caller of this function understands the API contract. - /// See also `objectField`. - pub fn objectFieldRaw(self: *Self, quoted_key: []const u8) Error!void { - if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); - assert(quoted_key.len >= 2 and quoted_key[0] == '"' and quoted_key[quoted_key.len - 1] == '"'); // quoted_key should be "quoted". - try self.objectFieldStart(); - try self.stream.writeAll(quoted_key); - self.next_punctuation = .colon; - } - - /// In the rare case that you need to write very long object field names, - /// this is an alternative to `objectField` and `objectFieldRaw` that allows you to write directly to the `.stream` field - /// similar to `beginWriteRaw`. - /// Call `endObjectFieldRaw()` when you're done. - pub fn beginObjectFieldRaw(self: *Self) !void { - if (build_mode_has_safety) { - assert(self.raw_streaming_mode == .none); - self.raw_streaming_mode = .objectField; - } - try self.objectFieldStart(); - } - - /// See `beginObjectFieldRaw`. - pub fn endObjectFieldRaw(self: *Self) void { - if (build_mode_has_safety) { - assert(self.raw_streaming_mode == .objectField); - self.raw_streaming_mode = .none; - } - self.next_punctuation = .colon; - } - - /// Renders the given Zig value as JSON. - /// - /// Supported types: - /// * Zig `bool` -> JSON `true` or `false`. - /// * Zig `?T` -> `null` or the rendering of `T`. - /// * Zig `i32`, `u64`, etc. -> JSON number or string. - /// * When option `emit_nonportable_numbers_as_strings` is true, if the value is outside the range `+-1<<53` (the precise integer range 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`. - /// * If the enum is non-exhaustive, unnamed values are rendered as integers. - /// * Zig untyped enum literal -> JSON string naming the active tag. - /// * Zig error -> JSON string naming the error. - /// * Zig `*T` -> the rendering of `T`. Note there is no guard against circular-reference infinite recursion. - /// - /// See also alternative functions `print` and `beginWriteRaw`. - /// For writing object field names, use `objectField` instead. - pub fn write(self: *Self, value: anytype) Error!void { - if (build_mode_has_safety) assert(self.raw_streaming_mode == .none); - const T = @TypeOf(value); - switch (@typeInfo(T)) { - .int => { - try self.valueStart(); - if (self.options.emit_nonportable_numbers_as_strings and - (value <= -(1 << 53) or value >= (1 << 53))) - { - try self.stream.print("\"{}\"", .{value}); - } else { - try self.stream.print("{}", .{value}); - } - self.valueDone(); - return; - }, - .comptime_int => { - return self.write(@as(std.math.IntFittingRange(value, value), value)); - }, - .float, .comptime_float => { - 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; - }, - - .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" => |enum_info| { - if (std.meta.hasFn(T, "jsonStringify")) { - return value.jsonStringify(self); - } - - if (!enum_info.is_exhaustive) { - inline for (enum_info.fields) |field| { - if (value == @field(T, field.name)) { - break; - } - } else { - return self.write(@intFromEnum(value)); - } - } - - return self.stringValue(@tagName(value)); - }, - .enum_literal => { - return self.stringValue(@tagName(value)); - }, - .@"union" => { - if (std.meta.hasFn(T, "jsonStringify")) { - 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 (std.meta.hasFn(T, "jsonStringify")) { - 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; - }, - .error_set => 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(); - } - }; -} - -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 - // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point. - try out_stream.writeAll("\\u"); - try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); - } else { - assert(codepoint <= 0x10FFFF); - // To escape an extended character that is not in the Basic Multilingual Plane, - // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair. - const high = @as(u16, @intCast((codepoint - 0x10000) >> 10)) + 0xD800; - const low = @as(u16, @intCast(codepoint & 0x3FF)) + 0xDC00; - try out_stream.writeAll("\\u"); - try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); - try out_stream.writeAll("\\u"); - try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); - } -} - -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('\"'); - try encodeJsonStringChars(string, options, writer); - try writer.writeByte('\"'); -} - -/// 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; - 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); - i += ulen - 1; - write_cursor = i + 1; - }, - } - } - } 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; - }, - } - } - } - try writer.writeAll(chars[write_cursor..chars.len]); -} - -test { - _ = @import("./stringify_test.zig"); -} diff --git a/lib/std/json/stringify_test.zig b/lib/std/json/stringify_test.zig deleted file mode 100644 index 52e38d1e30..0000000000 --- a/lib/std/json/stringify_test.zig +++ /dev/null @@ -1,504 +0,0 @@ -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.objectFieldRaw("\"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": 2e0 - \\ }, - \\ "string": "This is a string", - \\ "array": [ - \\ "Another string", - \\ 1, - \\ 3.5e0 - \\ ], - \\ "int": 10, - \\ "float": 3.5e0 - \\} - ; - 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 { - optional: ?[]const u8 = null, - required: []const u8 = "something", - another_optional: ?[]const u8 = null, - another_required: []const u8 = "something else", - }; - try testStringify( - \\{"optional":null,"required":"something","another_optional":null,"another_required":"something else"} - , - MyStruct{}, - .{}, - ); - try testStringify( - \\{"required":"something","another_required":"something else"} - , - MyStruct{}, - .{ .emit_null_optional_fields = false }, - ); -} - -test "stringify basic types" { - 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.2e1", 42.0, .{}); - try testStringify("42", @as(u8, 42), .{}); - try testStringify("42", @as(u128, 42), .{}); - try testStringify("9999999999999999", 9999999999999999, .{}); - try testStringify("4.2e1", @as(f32, 42), .{}); - try testStringify("4.2e1", @as(f64, 42), .{}); - try testStringify("\"ItBroke\"", @as(anyerror, error.ItBroke), .{}); - try testStringify("\"ItBroke\"", error.ItBroke, .{}); -} - -test "stringify string" { - 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"), .{}); - 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" { - const E = enum { - foo, - bar, - }; - try testStringify("\"foo\"", E.foo, .{}); - try testStringify("\"bar\"", E.bar, .{}); -} - -test "stringify non-exhaustive enum" { - const E = enum(u8) { - foo = 0, - _, - }; - try testStringify("\"foo\"", E.foo, .{}); - try testStringify("1", @as(E, @enumFromInt(1)), .{}); -} - -test "stringify enum literals" { - try testStringify("\"foo\"", .foo, .{}); - try testStringify("\"bar\"", .bar, .{}); -} - -test "stringify tagged unions" { - const T = union(enum) { - nothing, - foo: u32, - bar: bool, - }; - 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 { - foo: u32, - }{ .foo = 42 }, .{}); -} - -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( - \\{ - \\ "foo": 42, - \\ "bar": [ - \\ 1, - \\ 2, - \\ 3 - \\ ] - \\} - , - struct { - foo: u32, - bar: [3]u32, - }{ - .foo = 42, - .bar = .{ 1, 2, 3 }, - }, - .{ .whitespace = .indent_4 }, - ); - 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, - }{ - .foo = 42, - .bar = .{ 1, 2, 3 }, - }, - .{ .whitespace = .indent_tab }, - ); - try testStringify( - \\{"foo":42,"bar":[1,2,3]} - , - struct { - foo: u32, - bar: [3]u32, - }{ - .foo = 42, - .bar = .{ 1, 2, 3 }, - }, - .{ .whitespace = .minified }, - ); -} - -test "stringify struct with void field" { - try testStringify("{\"foo\":42}", struct { - foo: u32, - bar: void = {}, - }{ .foo = 42 }, .{}); -} - -test "stringify array of structs" { - const MyStruct = struct { - foo: u32, - }; - try testStringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{ - MyStruct{ .foo = 42 }, - MyStruct{ .foo = 100 }, - MyStruct{ .foo = 1000 }, - }, .{}); -} - -test "stringify struct with custom stringifier" { - try testStringify("[\"something special\",42]", struct { - foo: u32, - const Self = @This(); - pub fn jsonStringify(value: @This(), jws: anytype) !void { - _ = value; - try jws.beginArray(); - try jws.write("something special"); - try jws.write(42); - try jws.endArray(); - } - }{ .foo = 42 }, .{}); -} - -test "stringify vector" { - 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 }, .{}); -} - -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); - pub const Error = error{ - TooMuchData, - DifferentData, - }; - - expected_remaining: []const u8, - - fn init(exp: []const u8) Self { - return .{ .expected_remaining = exp }; - } - - pub fn writer(self: *Self) Writer { - return .{ .context = self }; - } - - fn write(self: *Self, bytes: []const u8) Error!usize { - if (self.expected_remaining.len < bytes.len) { - std.debug.print( - \\====== expected this output: ========= - \\{s} - \\======== instead found this: ========= - \\{s} - \\====================================== - , .{ - self.expected_remaining, - bytes, - }); - return error.TooMuchData; - } - if (!mem.eql(u8, self.expected_remaining[0..bytes.len], bytes)) { - std.debug.print( - \\====== expected this output: ========= - \\{s} - \\======== instead found this: ========= - \\{s} - \\====================================== - , .{ - self.expected_remaining[0..bytes.len], - bytes, - }); - return error.DifferentData; - } - self.expected_remaining = self.expected_remaining[bytes.len..]; - return bytes.len; - } - }; - - var vos = ValidationWriter.init(expected); - 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" { - const allocator = std.testing.allocator; - const expected = - \\{"foo":"bar","answer":42,"my_friend":"sammy"} - ; - const actual = try stringifyAlloc(allocator, .{ .foo = "bar", .answer = 42, .my_friend = "sammy" }, .{}); - defer allocator.free(actual); - - 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 "print" { - 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.print("[ ]", .{}); - try w.objectField("b"); - try w.beginArray(); - try w.print("[{s}] ", .{"[]"}); - try w.print(" {}", .{12345}); - try w.endArray(); - try w.endObject(); - - const result = slice_stream.getWritten(); - const expected = - \\{ - \\ "a": [ ], - \\ "b": [ - \\ [[]] , - \\ 12345 - \\ ] - \\} - ; - try std.testing.expectEqualStrings(expected, result); -} - -test "nonportable numbers" { - try testStringify("9999999999999999", 9999999999999999, .{}); - try testStringify("\"9999999999999999\"", 9999999999999999, .{ .emit_nonportable_numbers_as_strings = true }); -} - -test "stringify raw streaming" { - 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 testRawStreaming(&w, &slice_stream); - } - - { - var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, 8); - try testRawStreaming(&w, &slice_stream); - } - - { - var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, null); - try testRawStreaming(&w, &slice_stream); - } - - { - var w = writeStreamArbitraryDepth(testing.allocator, out, .{ .whitespace = .indent_2 }); - defer w.deinit(); - try testRawStreaming(&w, &slice_stream); - } -} - -fn testRawStreaming(w: anytype, slice_stream: anytype) !void { - slice_stream.reset(); - - try w.beginObject(); - try w.beginObjectFieldRaw(); - try w.stream.writeAll("\"long"); - try w.stream.writeAll(" key\""); - w.endObjectFieldRaw(); - try w.beginWriteRaw(); - try w.stream.writeAll("\"long"); - try w.stream.writeAll(" value\""); - w.endWriteRaw(); - try w.endObject(); - - const result = slice_stream.getWritten(); - const expected = - \\{ - \\ "long key": "long value" - \\} - ; - try std.testing.expectEqualStrings(expected, result); -} diff --git a/lib/std/json/test.zig b/lib/std/json/test.zig index 136e8e34d1..6d1886f9ff 100644 --- a/lib/std/json/test.zig +++ b/lib/std/json/test.zig @@ -1,10 +1,10 @@ const std = @import("std"); +const json = std.json; const testing = std.testing; 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 { @@ -52,7 +52,7 @@ fn roundTrip(s: []const u8) !void { var parsed = try parseFromSlice(Value, testing.allocator, s, .{}); defer parsed.deinit(); - const rendered = try stringifyAlloc(testing.allocator, parsed.value, .{}); + const rendered = try json.Stringify.valueAlloc(testing.allocator, parsed.value, .{}); defer testing.allocator.free(rendered); try testing.expectEqualStrings(s, rendered);