lib/pretty/core/src/json/stream.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const escape = @import("escape.zig");
3 const format = @import("format.zig");
4
5 /// An open object scope on one `Writer`.
6 /// Nested scopes share the writer and must end before their parent continues.
7 pub const Object = struct {
8 writer: *Writer,
9
10 pub fn field(self: Object, name: []const u8, value: anytype) !void {
11 try self.writer.objectField(name);
12 try self.writer.write(value);
13 }
14
15 pub fn fields(self: Object, values: anytype) !void {
16 const info = switch (@typeInfo(@TypeOf(values))) {
17 .@"struct" => |value| value,
18 else => @compileError("JSON object fields require a named struct"),
19 };
20 if (info.is_tuple) {
21 @compileError("JSON object fields require a named struct");
22 }
23 inline for (info.field_names) |name| {
24 try self.field(name, @field(values, name));
25 }
26 }
27
28 pub fn fieldParts(
29 self: Object,
30 name_parts: []const []const u8,
31 value: anytype,
32 ) !void {
33 try self.writer.objectFieldParts(name_parts);
34 try self.writer.write(value);
35 }
36
37 pub fn formattedField(
38 self: Object,
39 comptime capacity: usize,
40 comptime fmt: []const u8,
41 args: anytype,
42 value: anytype,
43 ) !void {
44 var buffer: [capacity]u8 = undefined;
45 const name = try std.fmt.bufPrint(&buffer, fmt, args);
46 try self.field(name, value);
47 }
48
49 pub fn object(self: Object, name: []const u8) !Object {
50 try self.writer.objectField(name);
51 return try self.writer.object();
52 }
53
54 pub fn formattedObject(
55 self: Object,
56 comptime capacity: usize,
57 comptime fmt: []const u8,
58 args: anytype,
59 ) !Object {
60 var buffer: [capacity]u8 = undefined;
61 const name = try std.fmt.bufPrint(&buffer, fmt, args);
62 return try self.object(name);
63 }
64
65 pub fn array(self: Object, name: []const u8) !Array {
66 try self.writer.objectField(name);
67 return try self.writer.array();
68 }
69
70 pub fn formattedArray(
71 self: Object,
72 comptime capacity: usize,
73 comptime fmt: []const u8,
74 args: anytype,
75 ) !Array {
76 var buffer: [capacity]u8 = undefined;
77 const name = try std.fmt.bufPrint(&buffer, fmt, args);
78 return try self.array(name);
79 }
80
81 pub fn stringParts(
82 self: Object,
83 name: []const u8,
84 parts: []const []const u8,
85 ) !void {
86 try self.writer.objectField(name);
87 try self.writer.stringParts(parts);
88 }
89
90 pub fn byteString(self: Object, name: []const u8, bytes: []const u8) !void {
91 try self.writer.objectField(name);
92 try self.writer.byteString(bytes);
93 }
94
95 pub fn hexString(self: Object, name: []const u8, bytes: []const u8) !void {
96 try self.writer.objectField(name);
97 try self.writer.hexString(bytes);
98 }
99
100 pub fn base64String(self: Object, name: []const u8, bytes: []const u8) !void {
101 try self.writer.objectField(name);
102 try self.writer.base64String(bytes);
103 }
104
105 pub fn formattedString(
106 self: Object,
107 name: []const u8,
108 comptime capacity: usize,
109 comptime fmt: []const u8,
110 args: anytype,
111 ) !void {
112 try self.writer.objectField(name);
113 try self.writer.formattedString(capacity, fmt, args);
114 }
115
116 pub fn raw(self: Object, name: []const u8, value: []const u8) !void {
117 try self.writer.objectField(name);
118 try self.writer.raw(value);
119 }
120
121 pub fn print(
122 self: Object,
123 name: []const u8,
124 comptime fmt: []const u8,
125 args: anytype,
126 ) !void {
127 try self.writer.objectField(name);
128 try self.writer.print(fmt, args);
129 }
130
131 pub fn end(self: Object) !void {
132 try self.writer.endObject();
133 }
134
135 pub fn endLine(self: Object) !void {
136 try self.end();
137 try self.writer.newline();
138 }
139 };
140
141 /// An open array scope on one `Writer`.
142 /// Nested scopes share the writer and must end before their parent continues.
143 pub const Array = struct {
144 writer: *Writer,
145
146 pub fn element(self: Array, value: anytype) !void {
147 try self.writer.write(value);
148 }
149
150 pub fn object(self: Array) !Object {
151 return try self.writer.object();
152 }
153
154 pub fn array(self: Array) !Array {
155 return try self.writer.array();
156 }
157
158 pub fn stringParts(self: Array, parts: []const []const u8) !void {
159 try self.writer.stringParts(parts);
160 }
161
162 pub fn byteString(self: Array, bytes: []const u8) !void {
163 try self.writer.byteString(bytes);
164 }
165
166 pub fn hexString(self: Array, bytes: []const u8) !void {
167 try self.writer.hexString(bytes);
168 }
169
170 pub fn base64String(self: Array, bytes: []const u8) !void {
171 try self.writer.base64String(bytes);
172 }
173
174 pub fn formattedString(
175 self: Array,
176 comptime capacity: usize,
177 comptime fmt: []const u8,
178 args: anytype,
179 ) !void {
180 try self.writer.formattedString(capacity, fmt, args);
181 }
182
183 pub fn raw(self: Array, value: []const u8) !void {
184 try self.writer.raw(value);
185 }
186
187 pub fn print(self: Array, comptime fmt: []const u8, args: anytype) !void {
188 try self.writer.print(fmt, args);
189 }
190
191 pub fn end(self: Array) !void {
192 try self.writer.endArray();
193 }
194
195 pub fn endLine(self: Array) !void {
196 try self.end();
197 try self.writer.newline();
198 }
199 };
200
201 /// Streams one JSON value at a time without intermediate string allocations.
202 /// `newline` resets completed top-level state so the same writer can emit JSONL.
203 pub const Writer = struct {
204 stringify: std.json.Stringify,
205
206 pub fn init(writer: *std.Io.Writer, style: format.Style) Writer {
207 return .{ .stringify = .{
208 .writer = writer,
209 .options = format.stringifyOptions(style),
210 } };
211 }
212
213 pub fn beginObject(self: *Writer) !void {
214 try self.stringify.beginObject();
215 }
216
217 pub fn object(self: *Writer) !Object {
218 try self.beginObject();
219 return .{ .writer = self };
220 }
221
222 pub fn endObject(self: *Writer) !void {
223 try self.stringify.endObject();
224 }
225
226 pub fn objectField(self: *Writer, name: []const u8) !void {
227 try self.stringify.objectField(name);
228 }
229
230 fn objectFieldParts(self: *Writer, parts: []const []const u8) !void {
231 try self.stringify.beginObjectFieldRaw();
232 try escape.writeStringParts(self.stringify.writer, parts);
233 self.stringify.endObjectFieldRaw();
234 }
235
236 pub fn beginArray(self: *Writer) !void {
237 try self.stringify.beginArray();
238 }
239
240 pub fn array(self: *Writer) !Array {
241 try self.beginArray();
242 return .{ .writer = self };
243 }
244
245 pub fn endArray(self: *Writer) !void {
246 try self.stringify.endArray();
247 }
248
249 pub fn write(self: *Writer, value: anytype) !void {
250 try self.stringify.write(value);
251 }
252
253 pub fn stringParts(self: *Writer, parts: []const []const u8) !void {
254 const raw_writer = try self.beginRaw();
255 try escape.writeStringParts(raw_writer, parts);
256 self.endRaw();
257 }
258
259 pub fn byteString(self: *Writer, bytes: []const u8) !void {
260 const raw_writer = try self.beginRaw();
261 try escape.writeByteString(raw_writer, bytes);
262 self.endRaw();
263 }
264
265 pub fn hexString(self: *Writer, bytes: []const u8) !void {
266 const hex = "0123456789abcdef";
267 const raw_writer = try self.beginRaw();
268 try raw_writer.writeByte('"');
269 for (bytes) |byte| {
270 try raw_writer.writeByte(hex[byte >> 4]);
271 try raw_writer.writeByte(hex[byte & 0x0f]);
272 }
273 try raw_writer.writeByte('"');
274 self.endRaw();
275 }
276
277 pub fn base64String(self: *Writer, bytes: []const u8) !void {
278 const raw_writer = try self.beginRaw();
279 try raw_writer.writeByte('"');
280 try std.base64.standard.Encoder.encodeWriter(raw_writer, bytes);
281 try raw_writer.writeByte('"');
282 self.endRaw();
283 }
284
285 pub fn formattedString(
286 self: *Writer,
287 comptime capacity: usize,
288 comptime fmt: []const u8,
289 args: anytype,
290 ) !void {
291 if (capacity == 0) @compileError("formatted JSON string capacity must be positive");
292 var buffer: [capacity]u8 = undefined;
293 try self.write(try std.fmt.bufPrint(&buffer, fmt, args));
294 }
295
296 pub fn raw(self: *Writer, value: []const u8) !void {
297 try self.stringify.beginWriteRaw();
298 try self.stringify.writer.writeAll(value);
299 self.stringify.endWriteRaw();
300 }
301
302 pub fn beginRaw(self: *Writer) !*std.Io.Writer {
303 try self.stringify.beginWriteRaw();
304 return self.stringify.writer;
305 }
306
307 pub fn endRaw(self: *Writer) void {
308 self.stringify.endWriteRaw();
309 }
310
311 pub fn print(self: *Writer, comptime fmt: []const u8, args: anytype) !void {
312 try self.stringify.print(fmt, args);
313 }
314
315 pub fn newline(self: *Writer) !void {
316 try self.stringify.writer.writeByte('\n');
317 if (self.stringify.indent_level != 0) return;
318 if (self.stringify.next_punctuation != .comma) return;
319 self.stringify = .{
320 .writer = self.stringify.writer,
321 .options = self.stringify.options,
322 };
323 }
324 };
325
326 test "Writer streams one JSON document per JSONL line" {
327 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
328 defer out.deinit();
329 var writer = Writer.init(&out.writer, .minified);
330 const first = try writer.object();
331 try first.field("line", @as(u8, 1));
332 try first.endLine();
333 const second = try writer.object();
334 try second.field("line", @as(u8, 2));
335 try second.endLine();
336 try std.testing.expectEqualStrings(
337 "{\"line\":1}\n{\"line\":2}\n",
338 out.written(),
339 );
340 }
341
342 test "Writer keeps document state across a newline inside a value" {
343 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
344 defer out.deinit();
345 var writer = Writer.init(&out.writer, .minified);
346 const root = try writer.object();
347 const nested = try root.object("nested");
348 try nested.field("kept", true);
349 try nested.endLine();
350 try root.field("after", @as(u8, 7));
351 try root.endLine();
352 try std.testing.expectEqualStrings(
353 "{\"nested\":{\"kept\":true}\n,\"after\":7}\n",
354 out.written(),
355 );
356 }
357
358 test "Writer streams minified machine JSON" {
359 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
360 defer out.deinit();
361 var writer = Writer.init(&out.writer, .minified);
362 try writer.beginObject();
363 try writer.objectField("event");
364 try writer.write("sample");
365 try writer.objectField("values");
366 try writer.beginArray();
367 try writer.write(@as(u8, 1));
368 try writer.write(@as(u8, 2));
369 try writer.endArray();
370 try writer.endObject();
371 try std.testing.expectEqualStrings("{\"event\":\"sample\",\"values\":[1,2]}", out.written());
372 }
373
374 test "Writer prints raw formatted JSON values" {
375 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
376 defer out.deinit();
377 var writer = Writer.init(&out.writer, .minified);
378 try writer.beginObject();
379 try writer.objectField("checksum");
380 try writer.print("{s}", .{"201123673782136532013988583703766643259"});
381 try writer.endObject();
382 try std.testing.expectEqualStrings("{\"checksum\":201123673782136532013988583703766643259}", out.written());
383 }
384
385 test "Writer inserts raw values and string parts" {
386 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
387 defer out.deinit();
388 var writer = Writer.init(&out.writer, .minified);
389 try writer.beginObject();
390 try writer.objectField("metadata");
391 try writer.raw("{\"count\":2}");
392 try writer.objectField("ref");
393 try writer.stringParts(&.{ "forum:", "post\"id", "#", "reply\nid" });
394 try writer.endObject();
395 try std.testing.expectEqualStrings("{\"metadata\":{\"count\":2},\"ref\":\"forum:post\\\"id#reply\\nid\"}", out.written());
396 }
397
398 test "Writer streams arbitrary byte strings through every container" {
399 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
400 defer out.deinit();
401 var writer = Writer.init(&out.writer, .minified);
402 const root = try writer.object();
403 try root.byteString("payload", "quote\"\n\x80");
404 const values = try root.array("values");
405 try values.byteString("\x01\xff");
406 try values.end();
407 try root.end();
408 try std.testing.expectEqualStrings(
409 "{\"payload\":\"quote\\\"\\n\\u0080\",\"values\":[\"\\u0001\\u00ff\"]}",
410 out.written(),
411 );
412 }
413
414 test "Writer streams byte slices as lowercase hexadecimal strings" {
415 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
416 defer out.deinit();
417 var writer = Writer.init(&out.writer, .minified);
418 const root = try writer.object();
419 try root.hexString("payload", &.{ 0x00, 0x7f, 0xa5, 0xff });
420 const values = try root.array("values");
421 try values.hexString(&.{ 0x12, 0x34 });
422 try values.end();
423 try root.end();
424 try std.testing.expectEqualStrings(
425 "{\"payload\":\"007fa5ff\",\"values\":[\"1234\"]}",
426 out.written(),
427 );
428 }
429
430 test "Writer streams byte slices as base64 strings" {
431 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
432 defer out.deinit();
433 var writer = Writer.init(&out.writer, .minified);
434 const root = try writer.object();
435 try root.base64String("payload", &.{ 0x00, 0xfb, 0xff });
436 const values = try root.array("values");
437 try values.base64String("tiny");
438 try values.end();
439 try root.end();
440 try std.testing.expectEqualStrings(
441 "{\"payload\":\"APv/\",\"values\":[\"dGlueQ==\"]}",
442 out.written(),
443 );
444 }
445
446 test "Writer formats bounded string values without allocating" {
447 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
448 defer out.deinit();
449 var writer = Writer.init(&out.writer, .minified);
450 const root = try writer.object();
451 try root.formattedString("timepoint", 54, "{d}:{d}:{d}", .{ 7, 1, 42 });
452 const values = try root.array("values");
453 try values.formattedString(8, "item-{d}", .{3});
454 try values.end();
455 try root.end();
456 try std.testing.expectEqualStrings(
457 "{\"timepoint\":\"7:1:42\",\"values\":[\"item-3\"]}",
458 out.written(),
459 );
460 }
461
462 test "Writer composes large nested records through object and array scopes" {
463 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
464 defer out.deinit();
465 var writer = Writer.init(&out.writer, .minified);
466 const root = try writer.object();
467 try root.field("schema", "sample/v1");
468 const run = try root.object("run");
469 try run.field("id", @as(u32, 42));
470 try run.stringParts("ref", &.{ "run:", "a\"b" });
471 const samples = try run.array("samples");
472 const first = try samples.object();
473 try first.field("ok", true);
474 try first.print("ratio", "{d:.2}", .{@as(f64, 0.5)});
475 try first.end();
476 try samples.raw("null");
477 try samples.end();
478 try run.end();
479 try root.endLine();
480 try std.testing.expectEqualStrings(
481 "{\"schema\":\"sample/v1\",\"run\":{\"id\":42,\"ref\":\"run:a\\\"b\"," ++
482 "\"samples\":[{\"ok\":true,\"ratio\":0.50},null]}}\n",
483 out.written(),
484 );
485 }
486
487 test "Object writes named struct fields in declaration order" {
488 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
489 defer out.deinit();
490 var writer = Writer.init(&out.writer, .minified);
491 const root = try writer.object();
492 try root.fields(.{
493 .status = "ready",
494 .count = @as(u8, 2),
495 .label = "quoted\"\n",
496 });
497 try root.end();
498 try std.testing.expectEqualStrings(
499 "{\"status\":\"ready\",\"count\":2,\"label\":\"quoted\\\"\\n\"}",
500 out.written(),
501 );
502 }
503
504 test "Object formats and escapes field names without allocation" {
505 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
506 defer out.deinit();
507 var writer = Writer.init(&out.writer, .minified);
508 const root = try writer.object();
509 try root.formattedField(32, "field-\"{d}", .{1}, true);
510 const child = try root.formattedObject(32, "object-\"{d}", .{2});
511 try child.field("ok", true);
512 try child.end();
513 const values = try root.formattedArray(32, "array-\"{d}", .{3});
514 try values.element(@as(u8, 7));
515 try values.end();
516 try root.end();
517 try std.testing.expectEqualStrings(
518 "{\"field-\\\"1\":true,\"object-\\\"2\":{\"ok\":true},\"array-\\\"3\":[7]}",
519 out.written(),
520 );
521 }
522
523 test "Writer scope formatting indents nested records" {
524 var out = std.Io.Writer.Allocating.init(std.testing.allocator);
525 defer out.deinit();
526 var writer = Writer.init(&out.writer, .indent_2);
527 const root = try writer.object();
528 try root.field("status", "ok");
529 try root.fieldParts(&.{ "mean_", "ns", "_per_", "source\"op" }, 30);
530 const values = try root.array("values");
531 try values.element(@as(u8, 1));
532 try values.element(@as(u8, 2));
533 try values.end();
534 try root.end();
535 try std.testing.expectEqualStrings(
536 "{\n \"status\": \"ok\",\n \"mean_ns_per_source\\\"op\": 30," ++
537 "\n \"values\": [\n 1,\n 2\n ]\n}",
538 out.written(),
539 );
540 }