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 }