Skip to content

Commit febfcbd

Browse files
authored
std.json.WriteStream supports streaming long values directly to the underlying stream (#21155)
1 parent 31220b5 commit febfcbd

File tree

2 files changed

+155
-30
lines changed

2 files changed

+155
-30
lines changed

lib/std/json/stringify.zig

Lines changed: 105 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -156,36 +156,23 @@ pub fn writeStreamArbitraryDepth(
156156
/// | <array>
157157
/// | write
158158
/// | print
159+
/// | <writeRawStream>
159160
/// <object> = beginObject ( <field> <value> )* endObject
160-
/// <field> = objectField | objectFieldRaw
161+
/// <field> = objectField | objectFieldRaw | <objectFieldRawStream>
161162
/// <array> = beginArray ( <value> )* endArray
163+
/// <writeRawStream> = beginWriteRaw ( stream.writeAll )* endWriteRaw
164+
/// <objectFieldRawStream> = beginObjectFieldRaw ( stream.writeAll )* endObjectFieldRaw
162165
/// ```
163166
///
164-
/// Supported types:
165-
/// * Zig `bool` -> JSON `true` or `false`.
166-
/// * Zig `?T` -> `null` or the rendering of `T`.
167-
/// * Zig `i32`, `u64`, etc. -> JSON number or string.
168-
/// * 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.
169-
/// * Zig floats -> JSON number or string.
170-
/// * If the value cannot be precisely represented by an f64, it is rendered as a JSON string. Otherwise, it is rendered as JSON number.
171-
/// * TODO: Float rendering will likely change in the future, e.g. to remove the unnecessary "e+00".
172-
/// * Zig `[]const u8`, `[]u8`, `*[N]u8`, `@Vector(N, u8)`, and similar -> JSON string.
173-
/// * See `StringifyOptions.emit_strings_as_arrays`.
174-
/// * If the content is not valid UTF-8, rendered as an array of numbers instead.
175-
/// * Zig `[]T`, `[N]T`, `*[N]T`, `@Vector(N, T)`, and similar -> JSON array of the rendering of each item.
176-
/// * Zig tuple -> JSON array of the rendering of each item.
177-
/// * Zig `struct` -> JSON object with each field in declaration order.
178-
/// * 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.
179-
/// * See `StringifyOptions.emit_null_optional_fields`.
180-
/// * Zig `union(enum)` -> JSON object with one field named for the active tag and a value representing the payload.
181-
/// * If the payload is `void`, then the emitted value is `{}`.
182-
/// * 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`.
183-
/// * Zig `enum` -> JSON string naming the active tag.
184-
/// * 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`.
185-
/// * Zig untyped enum literal -> JSON string naming the active tag.
186-
/// * Zig error -> JSON string naming the error.
187-
/// * Zig `*T` -> the rendering of `T`. Note there is no guard against circular-reference infinite recursion.
188-
///
167+
/// The `safety_checks_hint` parameter determines how much memory is used to enable assertions that the above grammar is being followed,
168+
/// e.g. tripping an assertion rather than allowing `endObject` to emit the final `}` in `[[[]]}`.
169+
/// "Depth" in this context means the depth of nested `[]` or `{}` expressions
170+
/// (or equivalently the amount of recursion on the `<value>` grammar expression above).
171+
/// For example, emitting the JSON `[[[]]]` requires a depth of 3.
172+
/// If `.checked_to_fixed_depth` is used, there is additionally an assertion that the nesting depth never exceeds the given limit.
173+
/// `.checked_to_arbitrary_depth` requires a runtime allocator for the memory.
174+
/// `.checked_to_fixed_depth` embeds the storage required in the `WriteStream` struct.
175+
/// `.assumed_correct` requires no space and performs none of these assertions.
189176
/// In `ReleaseFast` and `ReleaseSmall` mode, the given `safety_checks_hint` is ignored and is always treated as `.assumed_correct`.
190177
pub fn WriteStream(
191178
comptime OutStream: type,
@@ -197,10 +184,14 @@ pub fn WriteStream(
197184
) type {
198185
return struct {
199186
const Self = @This();
200-
const safety_checks: @TypeOf(safety_checks_hint) = switch (@import("builtin").mode) {
201-
.Debug, .ReleaseSafe => safety_checks_hint,
202-
.ReleaseFast, .ReleaseSmall => .assumed_correct,
187+
const build_mode_has_safety = switch (@import("builtin").mode) {
188+
.Debug, .ReleaseSafe => true,
189+
.ReleaseFast, .ReleaseSmall => false,
203190
};
191+
const safety_checks: @TypeOf(safety_checks_hint) = if (build_mode_has_safety)
192+
safety_checks_hint
193+
else
194+
.assumed_correct;
204195

205196
pub const Stream = OutStream;
206197
pub const Error = switch (safety_checks) {
@@ -225,6 +216,11 @@ pub fn WriteStream(
225216
.assumed_correct => void,
226217
},
227218

219+
raw_streaming_mode: if (build_mode_has_safety)
220+
enum { none, value, objectField }
221+
else
222+
void = if (build_mode_has_safety) .none else {},
223+
228224
pub fn init(safety_allocator: Allocator, stream: OutStream, options: StringifyOptions) Self {
229225
return .{
230226
.options = options,
@@ -237,6 +233,7 @@ pub fn WriteStream(
237233
};
238234
}
239235

236+
/// Only necessary with .checked_to_arbitrary_depth.
240237
pub fn deinit(self: *Self) void {
241238
switch (safety_checks) {
242239
.checked_to_arbitrary_depth => self.nesting_stack.deinit(),
@@ -246,20 +243,23 @@ pub fn WriteStream(
246243
}
247244

248245
pub fn beginArray(self: *Self) Error!void {
246+
if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
249247
try self.valueStart();
250248
try self.stream.writeByte('[');
251249
try self.pushIndentation(ARRAY_MODE);
252250
self.next_punctuation = .none;
253251
}
254252

255253
pub fn beginObject(self: *Self) Error!void {
254+
if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
256255
try self.valueStart();
257256
try self.stream.writeByte('{');
258257
try self.pushIndentation(OBJECT_MODE);
259258
self.next_punctuation = .none;
260259
}
261260

262261
pub fn endArray(self: *Self) Error!void {
262+
if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
263263
self.popIndentation(ARRAY_MODE);
264264
switch (self.next_punctuation) {
265265
.none => {},
@@ -273,6 +273,7 @@ pub fn WriteStream(
273273
}
274274

275275
pub fn endObject(self: *Self) Error!void {
276+
if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
276277
self.popIndentation(OBJECT_MODE);
277278
switch (self.next_punctuation) {
278279
.none => {},
@@ -389,16 +390,39 @@ pub fn WriteStream(
389390
/// e.g. `"1"`, `"[]"`, `"[1,2]"`, not `"1,2"`.
390391
/// This function may be useful for doing your own number formatting.
391392
pub fn print(self: *Self, comptime fmt: []const u8, args: anytype) Error!void {
393+
if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
392394
try self.valueStart();
393395
try self.stream.print(fmt, args);
394396
self.valueDone();
395397
}
396398

399+
/// An alternative to calling `write` that allows you to write directly to the `.stream` field, e.g. with `.stream.writeAll()`.
400+
/// Call `beginWriteRaw()`, then write a complete value (including any quotes if necessary) directly to the `.stream` field,
401+
/// then call `endWriteRaw()`.
402+
/// This can be useful for streaming very long strings into the output without needing it all buffered in memory.
403+
pub fn beginWriteRaw(self: *Self) !void {
404+
if (build_mode_has_safety) {
405+
assert(self.raw_streaming_mode == .none);
406+
self.raw_streaming_mode = .value;
407+
}
408+
try self.valueStart();
409+
}
410+
411+
/// See `beginWriteRaw`.
412+
pub fn endWriteRaw(self: *Self) void {
413+
if (build_mode_has_safety) {
414+
assert(self.raw_streaming_mode == .value);
415+
self.raw_streaming_mode = .none;
416+
}
417+
self.valueDone();
418+
}
419+
397420
/// See `WriteStream` for when to call this method.
398421
/// `key` is the string content of the property name.
399422
/// Surrounding quotes will be added and any special characters will be escaped.
400423
/// See also `objectFieldRaw`.
401424
pub fn objectField(self: *Self, key: []const u8) Error!void {
425+
if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
402426
try self.objectFieldStart();
403427
try encodeJsonString(key, self.options, self.stream);
404428
self.next_punctuation = .colon;
@@ -408,14 +432,65 @@ pub fn WriteStream(
408432
/// A few assertions are performed on the given value to ensure that the caller of this function understands the API contract.
409433
/// See also `objectField`.
410434
pub fn objectFieldRaw(self: *Self, quoted_key: []const u8) Error!void {
435+
if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
411436
assert(quoted_key.len >= 2 and quoted_key[0] == '"' and quoted_key[quoted_key.len - 1] == '"'); // quoted_key should be "quoted".
412437
try self.objectFieldStart();
413438
try self.stream.writeAll(quoted_key);
414439
self.next_punctuation = .colon;
415440
}
416441

417-
/// See `WriteStream`.
442+
/// In the rare case that you need to write very long object field names,
443+
/// this is an alternative to `objectField` and `objectFieldRaw` that allows you to write directly to the `.stream` field
444+
/// similar to `beginWriteRaw`.
445+
/// Call `endObjectFieldRaw()` when you're done.
446+
pub fn beginObjectFieldRaw(self: *Self) !void {
447+
if (build_mode_has_safety) {
448+
assert(self.raw_streaming_mode == .none);
449+
self.raw_streaming_mode = .objectField;
450+
}
451+
try self.objectFieldStart();
452+
}
453+
454+
/// See `beginObjectFieldRaw`.
455+
pub fn endObjectFieldRaw(self: *Self) void {
456+
if (build_mode_has_safety) {
457+
assert(self.raw_streaming_mode == .objectField);
458+
self.raw_streaming_mode = .none;
459+
}
460+
self.next_punctuation = .colon;
461+
}
462+
463+
/// Renders the given Zig value as JSON.
464+
///
465+
/// Supported types:
466+
/// * Zig `bool` -> JSON `true` or `false`.
467+
/// * Zig `?T` -> `null` or the rendering of `T`.
468+
/// * Zig `i32`, `u64`, etc. -> JSON number or string.
469+
/// * 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.
470+
/// * Zig floats -> JSON number or string.
471+
/// * If the value cannot be precisely represented by an f64, it is rendered as a JSON string. Otherwise, it is rendered as JSON number.
472+
/// * TODO: Float rendering will likely change in the future, e.g. to remove the unnecessary "e+00".
473+
/// * Zig `[]const u8`, `[]u8`, `*[N]u8`, `@Vector(N, u8)`, and similar -> JSON string.
474+
/// * See `StringifyOptions.emit_strings_as_arrays`.
475+
/// * If the content is not valid UTF-8, rendered as an array of numbers instead.
476+
/// * Zig `[]T`, `[N]T`, `*[N]T`, `@Vector(N, T)`, and similar -> JSON array of the rendering of each item.
477+
/// * Zig tuple -> JSON array of the rendering of each item.
478+
/// * Zig `struct` -> JSON object with each field in declaration order.
479+
/// * 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.
480+
/// * See `StringifyOptions.emit_null_optional_fields`.
481+
/// * Zig `union(enum)` -> JSON object with one field named for the active tag and a value representing the payload.
482+
/// * If the payload is `void`, then the emitted value is `{}`.
483+
/// * 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`.
484+
/// * Zig `enum` -> JSON string naming the active tag.
485+
/// * 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`.
486+
/// * Zig untyped enum literal -> JSON string naming the active tag.
487+
/// * Zig error -> JSON string naming the error.
488+
/// * Zig `*T` -> the rendering of `T`. Note there is no guard against circular-reference infinite recursion.
489+
///
490+
/// See also alternative functions `print` and `beginWriteRaw`.
491+
/// For writing object field names, use `objectField` instead.
418492
pub fn write(self: *Self, value: anytype) Error!void {
493+
if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
419494
const T = @TypeOf(value);
420495
switch (@typeInfo(T)) {
421496
.Int => {

lib/std/json/stringify_test.zig

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,3 +443,53 @@ test "nonportable numbers" {
443443
try testStringify("9999999999999999", 9999999999999999, .{});
444444
try testStringify("\"9999999999999999\"", 9999999999999999, .{ .emit_nonportable_numbers_as_strings = true });
445445
}
446+
447+
test "stringify raw streaming" {
448+
var out_buf: [1024]u8 = undefined;
449+
var slice_stream = std.io.fixedBufferStream(&out_buf);
450+
const out = slice_stream.writer();
451+
452+
{
453+
var w = writeStream(out, .{ .whitespace = .indent_2 });
454+
try testRawStreaming(&w, &slice_stream);
455+
}
456+
457+
{
458+
var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, 8);
459+
try testRawStreaming(&w, &slice_stream);
460+
}
461+
462+
{
463+
var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, null);
464+
try testRawStreaming(&w, &slice_stream);
465+
}
466+
467+
{
468+
var w = writeStreamArbitraryDepth(testing.allocator, out, .{ .whitespace = .indent_2 });
469+
defer w.deinit();
470+
try testRawStreaming(&w, &slice_stream);
471+
}
472+
}
473+
474+
fn testRawStreaming(w: anytype, slice_stream: anytype) !void {
475+
slice_stream.reset();
476+
477+
try w.beginObject();
478+
try w.beginObjectFieldRaw();
479+
try w.stream.writeAll("\"long");
480+
try w.stream.writeAll(" key\"");
481+
w.endObjectFieldRaw();
482+
try w.beginWriteRaw();
483+
try w.stream.writeAll("\"long");
484+
try w.stream.writeAll(" value\"");
485+
w.endWriteRaw();
486+
try w.endObject();
487+
488+
const result = slice_stream.getWritten();
489+
const expected =
490+
\\{
491+
\\ "long key": "long value"
492+
\\}
493+
;
494+
try std.testing.expectEqualStrings(expected, result);
495+
}

0 commit comments

Comments
 (0)