authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-17 18:03:31-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-01 16:35:26-07:00
loge154b62d63539cdd0f281dd4eaadc5ab229d2e24
treed1f8267a580892f1f0447c8fd6009bb2a7108a4e
parent7569494b454aebeff67825fbbbe0cfe45e6e60d7

give std.json an API spanking

Avoid Redundant Names in Fully-Qualified Namespaces

10 files changed, 1123 insertions(+), 1377 deletions(-)

CMakeLists.txt-1
...@@ -459,7 +459,6 @@ set(ZIG_STAGE2_SOURCES...@@ -459,7 +459,6 @@ set(ZIG_STAGE2_SOURCES
459 lib/std/io/limited_reader.zig459 lib/std/io/limited_reader.zig
460 lib/std/io/seekable_stream.zig460 lib/std/io/seekable_stream.zig
461 lib/std/json.zig461 lib/std/json.zig
462 lib/std/json/stringify.zig
463 lib/std/leb128.zig462 lib/std/leb128.zig
464 lib/std/log.zig463 lib/std/log.zig
465 lib/std/macho.zig464 lib/std/macho.zig
lib/std/json.zig+53-31
...@@ -10,8 +10,8 @@...@@ -10,8 +10,8 @@
10//! The high-level `stringify` serializes a Zig or `Value` type into JSON.10//! The high-level `stringify` serializes a Zig or `Value` type into JSON.
1111
12const builtin = @import("builtin");12const builtin = @import("builtin");
13const testing = @import("std").testing;13const std = @import("std");
14const ArrayList = @import("std").ArrayList;14const testing = std.testing;
1515
16test Scanner {16test Scanner {
17 var scanner = Scanner.initCompleteInput(testing.allocator, "{\"foo\": 123}\n");17 var scanner = Scanner.initCompleteInput(testing.allocator, "{\"foo\": 123}\n");
...@@ -41,11 +41,13 @@ test Value {...@@ -41,11 +41,13 @@ test Value {
41 try testing.expectEqualSlices(u8, "goes", parsed.value.object.get("anything").?.string);41 try testing.expectEqualSlices(u8, "goes", parsed.value.object.get("anything").?.string);
42}42}
4343
44test writeStream {44test Stringify {
45 var out = ArrayList(u8).init(testing.allocator);45 var out: std.io.AllocatingWriter = undefined;
46 var write_stream: Stringify = .{
47 .writer = out.init(testing.allocator),
48 .options = .{ .whitespace = .indent_2 },
49 };
46 defer out.deinit();50 defer out.deinit();
47 var write_stream = writeStream(out.writer(), .{ .whitespace = .indent_2 });
48 defer write_stream.deinit();
49 try write_stream.beginObject();51 try write_stream.beginObject();
50 try write_stream.objectField("foo");52 try write_stream.objectField("foo");
51 try write_stream.write(123);53 try write_stream.write(123);
...@@ -55,16 +57,7 @@ test writeStream {...@@ -55,16 +57,7 @@ test writeStream {
55 \\ "foo": 12357 \\ "foo": 123
56 \\}58 \\}
57 ;59 ;
58 try testing.expectEqualSlices(u8, expected, out.items);60 try testing.expectEqualSlices(u8, expected, out.getWritten());
59}
60
61test stringify {
62 var out = ArrayList(u8).init(testing.allocator);
63 defer out.deinit();
64
65 const T = struct { a: i32, b: []const u8 };
66 try stringify(T{ .a = 123, .b = "xy" }, .{}, out.writer());
67 try testing.expectEqualSlices(u8, "{\"a\":123,\"b\":\"xy\"}", out.items);
68}61}
6962
70pub const ObjectMap = @import("json/dynamic.zig").ObjectMap;63pub const ObjectMap = @import("json/dynamic.zig").ObjectMap;
...@@ -99,20 +92,49 @@ pub const innerParseFromValue = @import("json/static.zig").innerParseFromValue;...@@ -99,20 +92,49 @@ pub const innerParseFromValue = @import("json/static.zig").innerParseFromValue;
99pub const ParseError = @import("json/static.zig").ParseError;92pub const ParseError = @import("json/static.zig").ParseError;
100pub const ParseFromValueError = @import("json/static.zig").ParseFromValueError;93pub const ParseFromValueError = @import("json/static.zig").ParseFromValueError;
10194
102pub const StringifyOptions = @import("json/stringify.zig").StringifyOptions;95pub const Stringify = @import("json/Stringify.zig");
103pub const stringify = @import("json/stringify.zig").stringify;96
104pub const stringifyMaxDepth = @import("json/stringify.zig").stringifyMaxDepth;97/// Returns a formatter that formats the given value using stringify.
105pub const stringifyArbitraryDepth = @import("json/stringify.zig").stringifyArbitraryDepth;98pub fn fmt(value: anytype, options: Stringify.Options) Formatter(@TypeOf(value)) {
106pub const stringifyAlloc = @import("json/stringify.zig").stringifyAlloc;99 return Formatter(@TypeOf(value)){ .value = value, .options = options };
107pub const writeStream = @import("json/stringify.zig").writeStream;100}
108pub const writeStreamMaxDepth = @import("json/stringify.zig").writeStreamMaxDepth;101
109pub const writeStreamArbitraryDepth = @import("json/stringify.zig").writeStreamArbitraryDepth;102test fmt {
110pub const WriteStream = @import("json/stringify.zig").WriteStream;103 const expectFmt = std.testing.expectFmt;
111pub const encodeJsonString = @import("json/stringify.zig").encodeJsonString;104 try expectFmt("123", "{}", .{fmt(@as(u32, 123), .{})});
112pub const encodeJsonStringChars = @import("json/stringify.zig").encodeJsonStringChars;105 try expectFmt(
113106 \\{"num":927,"msg":"hello","sub":{"mybool":true}}
114pub const Formatter = @import("json/fmt.zig").Formatter;107 , "{}", .{fmt(struct {
115pub const fmt = @import("json/fmt.zig").fmt;108 num: u32,
109 msg: []const u8,
110 sub: struct {
111 mybool: bool,
112 },
113 }{
114 .num = 927,
115 .msg = "hello",
116 .sub = .{ .mybool = true },
117 }, .{})});
118}
119
120/// Formats the given value using stringify.
121pub fn Formatter(comptime T: type) type {
122 return struct {
123 value: T,
124 options: Stringify.Options,
125
126 pub fn format(
127 self: @This(),
128 comptime fmt_spec: []const u8,
129 options: std.fmt.FormatOptions,
130 writer: *std.io.BufferedWriter,
131 ) !void {
132 _ = fmt_spec;
133 _ = options;
134 try Stringify.value(self.value, self.options, writer);
135 }
136 };
137}
116138
117test {139test {
118 _ = @import("json/test.zig");140 _ = @import("json/test.zig");
...@@ -120,6 +142,6 @@ test {...@@ -120,6 +142,6 @@ test {
120 _ = @import("json/dynamic.zig");142 _ = @import("json/dynamic.zig");
121 _ = @import("json/hashmap.zig");143 _ = @import("json/hashmap.zig");
122 _ = @import("json/static.zig");144 _ = @import("json/static.zig");
123 _ = @import("json/stringify.zig");145 _ = Stringify;
124 _ = @import("json/JSONTestSuite_test.zig");146 _ = @import("json/JSONTestSuite_test.zig");
125}147}
lib/std/json/Stringify.zig created+1048
...@@ -0,0 +1,1048 @@
1//! Writes JSON ([RFC8259](https://tools.ietf.org/html/rfc8259)) formatted data
2//! to a stream.
3//!
4//! The sequence of method calls to write JSON content must follow this grammar:
5//! ```
6//! <once> = <value>
7//! <value> =
8//! | <object>
9//! | <array>
10//! | write
11//! | print
12//! | <writeRawStream>
13//! <object> = beginObject ( <field> <value> )* endObject
14//! <field> = objectField | objectFieldRaw | <objectFieldRawStream>
15//! <array> = beginArray ( <value> )* endArray
16//! <writeRawStream> = beginWriteRaw ( stream.writeAll )* endWriteRaw
17//! <objectFieldRawStream> = beginObjectFieldRaw ( stream.writeAll )* endObjectFieldRaw
18//! ```
19
20const std = @import("../std.zig");
21const assert = std.debug.assert;
22const Allocator = std.mem.Allocator;
23const ArrayList = std.ArrayList;
24const BitStack = std.BitStack;
25const Stringify = @This();
26
27const OBJECT_MODE = 0;
28const ARRAY_MODE = 1;
29
30writer: *std.io.BufferedWriter,
31options: Options = .{},
32indent_level: usize = 0,
33next_punctuation: enum {
34 the_beginning,
35 none,
36 comma,
37 colon,
38} = .the_beginning,
39
40nesting_stack: switch (safety_checks) {
41 .checked_to_fixed_depth => |fixed_buffer_size| [(fixed_buffer_size + 7) >> 3]u8,
42 .assumed_correct => void,
43} = switch (safety_checks) {
44 .checked_to_fixed_depth => @splat(0),
45 .assumed_correct => {},
46},
47
48raw_streaming_mode: if (build_mode_has_safety)
49 enum { none, value, objectField }
50else
51 void = if (build_mode_has_safety) .none else {},
52
53const build_mode_has_safety = switch (@import("builtin").mode) {
54 .Debug, .ReleaseSafe => true,
55 .ReleaseFast, .ReleaseSmall => false,
56};
57
58/// The `safety_checks_hint` parameter determines how much memory is used to enable assertions that the above grammar is being followed,
59/// e.g. tripping an assertion rather than allowing `endObject` to emit the final `}` in `[[[]]}`.
60/// "Depth" in this context means the depth of nested `[]` or `{}` expressions
61/// (or equivalently the amount of recursion on the `<value>` grammar expression above).
62/// For example, emitting the JSON `[[[]]]` requires a depth of 3.
63/// If `.checked_to_fixed_depth` is used, there is additionally an assertion that the nesting depth never exceeds the given limit.
64/// `.checked_to_fixed_depth` embeds the storage required in the `Stringify` struct.
65/// `.assumed_correct` requires no space and performs none of these assertions.
66/// In `ReleaseFast` and `ReleaseSmall` mode, the given `safety_checks_hint` is ignored and is always treated as `.assumed_correct`.
67const safety_checks_hint: union(enum) {
68 /// Rounded up to the nearest multiple of 8.
69 checked_to_fixed_depth: usize,
70 assumed_correct,
71} = .{ .checked_to_fixed_depth = 256 };
72
73const safety_checks: @TypeOf(safety_checks_hint) = if (build_mode_has_safety)
74 safety_checks_hint
75else
76 .assumed_correct;
77
78pub fn beginArray(self: *Stringify) anyerror!void {
79 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
80 try self.valueStart();
81 try self.stream.writeByte('[');
82 try self.pushIndentation(ARRAY_MODE);
83 self.next_punctuation = .none;
84}
85
86pub fn beginObject(self: *Stringify) anyerror!void {
87 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
88 try self.valueStart();
89 try self.stream.writeByte('{');
90 try self.pushIndentation(OBJECT_MODE);
91 self.next_punctuation = .none;
92}
93
94pub fn endArray(self: *Stringify) anyerror!void {
95 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
96 self.popIndentation(ARRAY_MODE);
97 switch (self.next_punctuation) {
98 .none => {},
99 .comma => {
100 try self.indent();
101 },
102 .the_beginning, .colon => unreachable,
103 }
104 try self.stream.writeByte(']');
105 self.valueDone();
106}
107
108pub fn endObject(self: *Stringify) anyerror!void {
109 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
110 self.popIndentation(OBJECT_MODE);
111 switch (self.next_punctuation) {
112 .none => {},
113 .comma => {
114 try self.indent();
115 },
116 .the_beginning, .colon => unreachable,
117 }
118 try self.stream.writeByte('}');
119 self.valueDone();
120}
121
122fn pushIndentation(self: *Stringify, mode: u1) !void {
123 switch (safety_checks) {
124 .checked_to_fixed_depth => {
125 BitStack.pushWithStateAssumeCapacity(&self.nesting_stack, &self.indent_level, mode);
126 },
127 .assumed_correct => {
128 self.indent_level += 1;
129 },
130 }
131}
132fn popIndentation(self: *Stringify, assert_its_this_one: u1) void {
133 switch (safety_checks) {
134 .checked_to_fixed_depth => {
135 assert(BitStack.popWithState(&self.nesting_stack, &self.indent_level) == assert_its_this_one);
136 },
137 .assumed_correct => {
138 self.indent_level -= 1;
139 },
140 }
141}
142
143fn indent(self: *Stringify) !void {
144 var char: u8 = ' ';
145 const n_chars = switch (self.options.whitespace) {
146 .minified => return,
147 .indent_1 => 1 * self.indent_level,
148 .indent_2 => 2 * self.indent_level,
149 .indent_3 => 3 * self.indent_level,
150 .indent_4 => 4 * self.indent_level,
151 .indent_8 => 8 * self.indent_level,
152 .indent_tab => blk: {
153 char = '\t';
154 break :blk self.indent_level;
155 },
156 };
157 try self.stream.writeByte('\n');
158 try self.stream.writeByteNTimes(char, n_chars);
159}
160
161fn valueStart(self: *Stringify) !void {
162 if (self.isObjectKeyExpected()) |is_it| assert(!is_it); // Call objectField*(), not write(), for object keys.
163 return self.valueStartAssumeTypeOk();
164}
165fn objectFieldStart(self: *Stringify) !void {
166 if (self.isObjectKeyExpected()) |is_it| assert(is_it); // Expected write(), not objectField*().
167 return self.valueStartAssumeTypeOk();
168}
169fn valueStartAssumeTypeOk(self: *Stringify) !void {
170 assert(!self.isComplete()); // JSON document already complete.
171 switch (self.next_punctuation) {
172 .the_beginning => {
173 // No indentation for the very beginning.
174 },
175 .none => {
176 // First item in a container.
177 try self.indent();
178 },
179 .comma => {
180 // Subsequent item in a container.
181 try self.stream.writeByte(',');
182 try self.indent();
183 },
184 .colon => {
185 try self.stream.writeByte(':');
186 if (self.options.whitespace != .minified) {
187 try self.stream.writeByte(' ');
188 }
189 },
190 }
191}
192fn valueDone(self: *Stringify) void {
193 self.next_punctuation = .comma;
194}
195
196// Only when safety is enabled:
197fn isObjectKeyExpected(self: *const Stringify) ?bool {
198 switch (safety_checks) {
199 .checked_to_fixed_depth => return self.indent_level > 0 and
200 BitStack.peekWithState(&self.nesting_stack, self.indent_level) == OBJECT_MODE and
201 self.next_punctuation != .colon,
202 .assumed_correct => return null,
203 }
204}
205fn isComplete(self: *const Stringify) bool {
206 return self.indent_level == 0 and self.next_punctuation == .comma;
207}
208
209/// An alternative to calling `write` that formats a value with `std.fmt`.
210/// This function does the usual punctuation and indentation formatting
211/// assuming the resulting formatted string represents a single complete value;
212/// e.g. `"1"`, `"[]"`, `"[1,2]"`, not `"1,2"`.
213/// This function may be useful for doing your own number formatting.
214pub fn print(self: *Stringify, comptime fmt: []const u8, args: anytype) anyerror!void {
215 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
216 try self.valueStart();
217 try self.stream.print(fmt, args);
218 self.valueDone();
219}
220
221test print {
222 var out_buf: [1024]u8 = undefined;
223 var out: std.io.BufferedWriter = undefined;
224 out.initFixed(&out_buf);
225
226 var w: Stringify = .{ .writer = &out, .options = .{ .whitespace = .indent_2 } };
227 defer w.deinit();
228
229 try w.beginObject();
230 try w.objectField("a");
231 try w.print("[ ]", .{});
232 try w.objectField("b");
233 try w.beginArray();
234 try w.print("[{s}] ", .{"[]"});
235 try w.print(" {}", .{12345});
236 try w.endArray();
237 try w.endObject();
238
239 const expected =
240 \\{
241 \\ "a": [ ],
242 \\ "b": [
243 \\ [[]] ,
244 \\ 12345
245 \\ ]
246 \\}
247 ;
248 try std.testing.expectEqualStrings(expected, out.getWritten());
249}
250
251/// An alternative to calling `write` that allows you to write directly to the `.stream` field, e.g. with `.stream.writeAll()`.
252/// Call `beginWriteRaw()`, then write a complete value (including any quotes if necessary) directly to the `.stream` field,
253/// then call `endWriteRaw()`.
254/// This can be useful for streaming very long strings into the output without needing it all buffered in memory.
255pub fn beginWriteRaw(self: *Stringify) !void {
256 if (build_mode_has_safety) {
257 assert(self.raw_streaming_mode == .none);
258 self.raw_streaming_mode = .value;
259 }
260 try self.valueStart();
261}
262
263/// See `beginWriteRaw`.
264pub fn endWriteRaw(self: *Stringify) void {
265 if (build_mode_has_safety) {
266 assert(self.raw_streaming_mode == .value);
267 self.raw_streaming_mode = .none;
268 }
269 self.valueDone();
270}
271
272/// See `Stringify` for when to call this method.
273/// `key` is the string content of the property name.
274/// Surrounding quotes will be added and any special characters will be escaped.
275/// See also `objectFieldRaw`.
276pub fn objectField(self: *Stringify, key: []const u8) anyerror!void {
277 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
278 try self.objectFieldStart();
279 try encodeJsonString(key, self.options, self.stream);
280 self.next_punctuation = .colon;
281}
282/// See `Stringify` for when to call this method.
283/// `quoted_key` is the complete bytes of the key including quotes and any necessary escape sequences.
284/// A few assertions are performed on the given value to ensure that the caller of this function understands the API contract.
285/// See also `objectField`.
286pub fn objectFieldRaw(self: *Stringify, quoted_key: []const u8) anyerror!void {
287 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
288 assert(quoted_key.len >= 2 and quoted_key[0] == '"' and quoted_key[quoted_key.len - 1] == '"'); // quoted_key should be "quoted".
289 try self.objectFieldStart();
290 try self.stream.writeAll(quoted_key);
291 self.next_punctuation = .colon;
292}
293
294/// In the rare case that you need to write very long object field names,
295/// this is an alternative to `objectField` and `objectFieldRaw` that allows you to write directly to the `.stream` field
296/// similar to `beginWriteRaw`.
297/// Call `endObjectFieldRaw()` when you're done.
298pub fn beginObjectFieldRaw(self: *Stringify) !void {
299 if (build_mode_has_safety) {
300 assert(self.raw_streaming_mode == .none);
301 self.raw_streaming_mode = .objectField;
302 }
303 try self.objectFieldStart();
304}
305
306/// See `beginObjectFieldRaw`.
307pub fn endObjectFieldRaw(self: *Stringify) void {
308 if (build_mode_has_safety) {
309 assert(self.raw_streaming_mode == .objectField);
310 self.raw_streaming_mode = .none;
311 }
312 self.next_punctuation = .colon;
313}
314
315/// Renders the given Zig value as JSON.
316///
317/// Supported types:
318/// * Zig `bool` -> JSON `true` or `false`.
319/// * Zig `?T` -> `null` or the rendering of `T`.
320/// * Zig `i32`, `u64`, etc. -> JSON number or string.
321/// * 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.
322/// * Zig floats -> JSON number or string.
323/// * If the value cannot be precisely represented by an f64, it is rendered as a JSON string. Otherwise, it is rendered as JSON number.
324/// * TODO: Float rendering will likely change in the future, e.g. to remove the unnecessary "e+00".
325/// * Zig `[]const u8`, `[]u8`, `*[N]u8`, `@Vector(N, u8)`, and similar -> JSON string.
326/// * See `Options.emit_strings_as_arrays`.
327/// * If the content is not valid UTF-8, rendered as an array of numbers instead.
328/// * Zig `[]T`, `[N]T`, `*[N]T`, `@Vector(N, T)`, and similar -> JSON array of the rendering of each item.
329/// * Zig tuple -> JSON array of the rendering of each item.
330/// * Zig `struct` -> JSON object with each field in declaration order.
331/// * 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.
332/// * See `Options.emit_null_optional_fields`.
333/// * Zig `union(enum)` -> JSON object with one field named for the active tag and a value representing the payload.
334/// * If the payload is `void`, then the emitted value is `{}`.
335/// * 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`.
336/// * Zig `enum` -> JSON string naming the active tag.
337/// * 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`.
338/// * If the enum is non-exhaustive, unnamed values are rendered as integers.
339/// * Zig untyped enum literal -> JSON string naming the active tag.
340/// * Zig error -> JSON string naming the error.
341/// * Zig `*T` -> the rendering of `T`. Note there is no guard against circular-reference infinite recursion.
342///
343/// See also alternative functions `print` and `beginWriteRaw`.
344/// For writing object field names, use `objectField` instead.
345pub fn write(self: *Stringify, v: anytype) anyerror!void {
346 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
347 const T = @TypeOf(v);
348 switch (@typeInfo(T)) {
349 .int => {
350 try self.valueStart();
351 if (self.options.emit_nonportable_numbers_as_strings and
352 (v <= -(1 << 53) or v >= (1 << 53)))
353 {
354 try self.stream.print("\"{}\"", .{v});
355 } else {
356 try self.stream.print("{}", .{v});
357 }
358 self.valueDone();
359 return;
360 },
361 .comptime_int => {
362 return self.write(@as(std.math.IntFittingRange(v, v), v));
363 },
364 .float, .comptime_float => {
365 if (@as(f64, @floatCast(v)) == v) {
366 try self.valueStart();
367 try self.stream.print("{}", .{@as(f64, @floatCast(v))});
368 self.valueDone();
369 return;
370 }
371 try self.valueStart();
372 try self.stream.print("\"{}\"", .{v});
373 self.valueDone();
374 return;
375 },
376
377 .bool => {
378 try self.valueStart();
379 try self.stream.writeAll(if (v) "true" else "false");
380 self.valueDone();
381 return;
382 },
383 .null => {
384 try self.valueStart();
385 try self.stream.writeAll("null");
386 self.valueDone();
387 return;
388 },
389 .optional => {
390 if (v) |payload| {
391 return try self.write(payload);
392 } else {
393 return try self.write(null);
394 }
395 },
396 .@"enum" => |enum_info| {
397 if (std.meta.hasFn(T, "jsonStringify")) {
398 return v.jsonStringify(self);
399 }
400
401 if (!enum_info.is_exhaustive) {
402 inline for (enum_info.fields) |field| {
403 if (v == @field(T, field.name)) {
404 break;
405 }
406 } else {
407 return self.write(@intFromEnum(v));
408 }
409 }
410
411 return self.stringValue(@tagName(v));
412 },
413 .enum_literal => {
414 return self.stringValue(@tagName(v));
415 },
416 .@"union" => {
417 if (std.meta.hasFn(T, "jsonStringify")) {
418 return v.jsonStringify(self);
419 }
420
421 const info = @typeInfo(T).@"union";
422 if (info.tag_type) |UnionTagType| {
423 try self.beginObject();
424 inline for (info.fields) |u_field| {
425 if (v == @field(UnionTagType, u_field.name)) {
426 try self.objectField(u_field.name);
427 if (u_field.type == void) {
428 // void v is {}
429 try self.beginObject();
430 try self.endObject();
431 } else {
432 try self.write(@field(v, u_field.name));
433 }
434 break;
435 }
436 } else {
437 unreachable; // No active tag?
438 }
439 try self.endObject();
440 return;
441 } else {
442 @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'");
443 }
444 },
445 .@"struct" => |S| {
446 if (std.meta.hasFn(T, "jsonStringify")) {
447 return v.jsonStringify(self);
448 }
449
450 if (S.is_tuple) {
451 try self.beginArray();
452 } else {
453 try self.beginObject();
454 }
455 inline for (S.fields) |Field| {
456 // don't include void fields
457 if (Field.type == void) continue;
458
459 var emit_field = true;
460
461 // don't include optional fields that are null when emit_null_optional_fields is set to false
462 if (@typeInfo(Field.type) == .optional) {
463 if (self.options.emit_null_optional_fields == false) {
464 if (@field(v, Field.name) == null) {
465 emit_field = false;
466 }
467 }
468 }
469
470 if (emit_field) {
471 if (!S.is_tuple) {
472 try self.objectField(Field.name);
473 }
474 try self.write(@field(v, Field.name));
475 }
476 }
477 if (S.is_tuple) {
478 try self.endArray();
479 } else {
480 try self.endObject();
481 }
482 return;
483 },
484 .error_set => return self.stringValue(@errorName(v)),
485 .pointer => |ptr_info| switch (ptr_info.size) {
486 .one => switch (@typeInfo(ptr_info.child)) {
487 .array => {
488 // Coerce `*[N]T` to `[]const T`.
489 const Slice = []const std.meta.Elem(ptr_info.child);
490 return self.write(@as(Slice, v));
491 },
492 else => {
493 return self.write(v.*);
494 },
495 },
496 .many, .slice => {
497 if (ptr_info.size == .many and ptr_info.sentinel() == null)
498 @compileError("unable to stringify type '" ++ @typeName(T) ++ "' without sentinel");
499 const slice = if (ptr_info.size == .many) std.mem.span(v) else v;
500
501 if (ptr_info.child == u8) {
502 // This is a []const u8, or some similar Zig string.
503 if (!self.options.emit_strings_as_arrays and std.unicode.utf8ValidateSlice(slice)) {
504 return self.stringValue(slice);
505 }
506 }
507
508 try self.beginArray();
509 for (slice) |x| {
510 try self.write(x);
511 }
512 try self.endArray();
513 return;
514 },
515 else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
516 },
517 .array => {
518 // Coerce `[N]T` to `*const [N]T` (and then to `[]const T`).
519 return self.write(&v);
520 },
521 .vector => |info| {
522 const array: [info.len]info.child = v;
523 return self.write(&array);
524 },
525 else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
526 }
527 unreachable;
528}
529
530fn stringValue(self: *Stringify, s: []const u8) !void {
531 try self.valueStart();
532 try encodeJsonString(s, self.options, self.stream);
533 self.valueDone();
534}
535
536pub const Options = struct {
537 /// Controls the whitespace emitted.
538 /// The default `.minified` is a compact encoding with no whitespace between tokens.
539 /// Any setting other than `.minified` will use newlines, indentation, and a space after each ':'.
540 /// `.indent_1` means 1 space for each indentation level, `.indent_2` means 2 spaces, etc.
541 /// `.indent_tab` uses a tab for each indentation level.
542 whitespace: enum {
543 minified,
544 indent_1,
545 indent_2,
546 indent_3,
547 indent_4,
548 indent_8,
549 indent_tab,
550 } = .minified,
551
552 /// Should optional fields with null value be written?
553 emit_null_optional_fields: bool = true,
554
555 /// Arrays/slices of u8 are typically encoded as JSON strings.
556 /// This option emits them as arrays of numbers instead.
557 /// Does not affect calls to `objectField*()`.
558 emit_strings_as_arrays: bool = false,
559
560 /// Should unicode characters be escaped in strings?
561 escape_unicode: bool = false,
562
563 /// When true, renders numbers outside the range `+-1<<53` (the precise integer range of f64) as JSON strings in base 10.
564 emit_nonportable_numbers_as_strings: bool = false,
565};
566
567/// Writes the given value to the `std.io.Writer` stream.
568/// See `Stringify` for how the given value is serialized into JSON.
569/// The maximum nesting depth of the output JSON document is 256.
570pub fn value(v: anytype, options: Options, writer: *std.io.BufferedWriter) anyerror!void {
571 var s: Stringify = .{ .writer = writer, .options = options };
572 try s.write(v);
573}
574
575test value {
576 var out: std.io.AllocatingWriter = undefined;
577 const writer = out.init(std.testing.allocator);
578 defer out.deinit();
579
580 const T = struct { a: i32, b: []const u8 };
581 try value(T{ .a = 123, .b = "xy" }, .{}, writer);
582 try std.testing.expectEqualSlices(u8, "{\"a\":123,\"b\":\"xy\"}", out.getWritten());
583
584 try testStringify("9999999999999999", 9999999999999999, .{});
585 try testStringify("\"9999999999999999\"", 9999999999999999, .{ .emit_nonportable_numbers_as_strings = true });
586
587 try testStringify("[1,1]", @as(@Vector(2, u32), @splat(1)), .{});
588 try testStringify("\"AA\"", @as(@Vector(2, u8), @splat('A')), .{});
589 try testStringify("[65,65]", @as(@Vector(2, u8), @splat('A')), .{ .emit_strings_as_arrays = true });
590
591 // void field
592 try testStringify("{\"foo\":42}", struct {
593 foo: u32,
594 bar: void = {},
595 }{ .foo = 42 }, .{});
596
597 const Tuple = struct { []const u8, usize };
598 try testStringify("[\"foo\",42]", Tuple{ "foo", 42 }, .{});
599
600 comptime {
601 testStringify("false", false, .{}) catch unreachable;
602 const MyStruct = struct { foo: u32 };
603 testStringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
604 MyStruct{ .foo = 42 },
605 MyStruct{ .foo = 100 },
606 MyStruct{ .foo = 1000 },
607 }, .{}) catch unreachable;
608 }
609}
610
611/// Calls `value` and stores the result in dynamically allocated memory instead
612/// of taking a writer.
613///
614/// Caller owns returned memory.
615pub fn valueAlloc(gpa: Allocator, v: anytype, options: Options) error{OutOfMemory}![]u8 {
616 var aw: std.io.AllocatingWriter = undefined;
617 const writer = aw.init(gpa);
618 defer aw.deinit();
619 try value(v, options, writer);
620 return aw.toOwnedSlice();
621}
622
623test valueAlloc {
624 const allocator = std.testing.allocator;
625 const expected =
626 \\{"foo":"bar","answer":42,"my_friend":"sammy"}
627 ;
628 const actual = try valueAlloc(allocator, .{ .foo = "bar", .answer = 42, .my_friend = "sammy" }, .{});
629 defer allocator.free(actual);
630
631 try std.testing.expectEqualStrings(expected, actual);
632}
633
634fn outputUnicodeEscape(codepoint: u21, out_stream: *std.io.BufferedWriter) !void {
635 if (codepoint <= 0xFFFF) {
636 // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF),
637 // then it may be represented as a six-character sequence: a reverse solidus, followed
638 // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point.
639 try out_stream.writeAll("\\u");
640 try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream);
641 } else {
642 assert(codepoint <= 0x10FFFF);
643 // To escape an extended character that is not in the Basic Multilingual Plane,
644 // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair.
645 const high = @as(u16, @intCast((codepoint - 0x10000) >> 10)) + 0xD800;
646 const low = @as(u16, @intCast(codepoint & 0x3FF)) + 0xDC00;
647 try out_stream.writeAll("\\u");
648 try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream);
649 try out_stream.writeAll("\\u");
650 try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream);
651 }
652}
653
654fn outputSpecialEscape(c: u8, writer: *std.io.BufferedWriter) !void {
655 switch (c) {
656 '\\' => try writer.writeAll("\\\\"),
657 '\"' => try writer.writeAll("\\\""),
658 0x08 => try writer.writeAll("\\b"),
659 0x0C => try writer.writeAll("\\f"),
660 '\n' => try writer.writeAll("\\n"),
661 '\r' => try writer.writeAll("\\r"),
662 '\t' => try writer.writeAll("\\t"),
663 else => try outputUnicodeEscape(c, writer),
664 }
665}
666
667/// Write `string` to `writer` as a JSON encoded string.
668pub fn encodeJsonString(string: []const u8, options: Options, writer: *std.io.BufferedWriter) !void {
669 try writer.writeByte('\"');
670 try encodeJsonStringChars(string, options, writer);
671 try writer.writeByte('\"');
672}
673
674/// Write `chars` to `writer` as JSON encoded string characters.
675pub fn encodeJsonStringChars(chars: []const u8, options: Options, writer: *std.io.BufferedWriter) !void {
676 var write_cursor: usize = 0;
677 var i: usize = 0;
678 if (options.escape_unicode) {
679 while (i < chars.len) : (i += 1) {
680 switch (chars[i]) {
681 // normal ascii character
682 0x20...0x21, 0x23...0x5B, 0x5D...0x7E => {},
683 0x00...0x1F, '\\', '\"' => {
684 // Always must escape these.
685 try writer.writeAll(chars[write_cursor..i]);
686 try outputSpecialEscape(chars[i], writer);
687 write_cursor = i + 1;
688 },
689 0x7F...0xFF => {
690 try writer.writeAll(chars[write_cursor..i]);
691 const ulen = std.unicode.utf8ByteSequenceLength(chars[i]) catch unreachable;
692 const codepoint = std.unicode.utf8Decode(chars[i..][0..ulen]) catch unreachable;
693 try outputUnicodeEscape(codepoint, writer);
694 i += ulen - 1;
695 write_cursor = i + 1;
696 },
697 }
698 }
699 } else {
700 while (i < chars.len) : (i += 1) {
701 switch (chars[i]) {
702 // normal bytes
703 0x20...0x21, 0x23...0x5B, 0x5D...0xFF => {},
704 0x00...0x1F, '\\', '\"' => {
705 // Always must escape these.
706 try writer.writeAll(chars[write_cursor..i]);
707 try outputSpecialEscape(chars[i], writer);
708 write_cursor = i + 1;
709 },
710 }
711 }
712 }
713 try writer.writeAll(chars[write_cursor..chars.len]);
714}
715
716test "json write stream" {
717 var out_buf: [1024]u8 = undefined;
718 var out: std.io.BufferedWriter = undefined;
719 out.initFixed(&out_buf);
720 var w: Stringify = .{ .writer = &out, .options = .{ .whitespace = .indent_2 } };
721 try testBasicWriteStream(&w);
722}
723
724fn testBasicWriteStream(w: *Stringify, out: *std.io.BufferedWriter) !void {
725 out.reset();
726
727 try w.beginObject();
728
729 try w.objectField("object");
730 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
731 defer arena_allocator.deinit();
732 try w.write(try getJsonObject(arena_allocator.allocator()));
733
734 try w.objectFieldRaw("\"string\"");
735 try w.write("This is a string");
736
737 try w.objectField("array");
738 try w.beginArray();
739 try w.write("Another string");
740 try w.write(@as(i32, 1));
741 try w.write(@as(f32, 3.5));
742 try w.endArray();
743
744 try w.objectField("int");
745 try w.write(@as(i32, 10));
746
747 try w.objectField("float");
748 try w.write(@as(f32, 3.5));
749
750 try w.endObject();
751
752 const expected =
753 \\{
754 \\ "object": {
755 \\ "one": 1,
756 \\ "two": 2e0
757 \\ },
758 \\ "string": "This is a string",
759 \\ "array": [
760 \\ "Another string",
761 \\ 1,
762 \\ 3.5e0
763 \\ ],
764 \\ "int": 10,
765 \\ "float": 3.5e0
766 \\}
767 ;
768 try std.testing.expectEqualStrings(expected, out.getWritten());
769}
770
771fn getJsonObject(allocator: std.mem.Allocator) !std.json.Value {
772 var v: std.json.Value = .{ .object = std.json.ObjectMap.init(allocator) };
773 try v.object.put("one", std.json.Value{ .integer = @as(i64, @intCast(1)) });
774 try v.object.put("two", std.json.Value{ .float = 2.0 });
775 return v;
776}
777
778test "stringify null optional fields" {
779 const MyStruct = struct {
780 optional: ?[]const u8 = null,
781 required: []const u8 = "something",
782 another_optional: ?[]const u8 = null,
783 another_required: []const u8 = "something else",
784 };
785 try testStringify(
786 \\{"optional":null,"required":"something","another_optional":null,"another_required":"something else"}
787 ,
788 MyStruct{},
789 .{},
790 );
791 try testStringify(
792 \\{"required":"something","another_required":"something else"}
793 ,
794 MyStruct{},
795 .{ .emit_null_optional_fields = false },
796 );
797}
798
799test "stringify basic types" {
800 try testStringify("false", false, .{});
801 try testStringify("true", true, .{});
802 try testStringify("null", @as(?u8, null), .{});
803 try testStringify("null", @as(?*u32, null), .{});
804 try testStringify("42", 42, .{});
805 try testStringify("4.2e1", 42.0, .{});
806 try testStringify("42", @as(u8, 42), .{});
807 try testStringify("42", @as(u128, 42), .{});
808 try testStringify("9999999999999999", 9999999999999999, .{});
809 try testStringify("4.2e1", @as(f32, 42), .{});
810 try testStringify("4.2e1", @as(f64, 42), .{});
811 try testStringify("\"ItBroke\"", @as(anyerror, error.ItBroke), .{});
812 try testStringify("\"ItBroke\"", error.ItBroke, .{});
813}
814
815test "stringify string" {
816 try testStringify("\"hello\"", "hello", .{});
817 try testStringify("\"with\\nescapes\\r\"", "with\nescapes\r", .{});
818 try testStringify("\"with\\nescapes\\r\"", "with\nescapes\r", .{ .escape_unicode = true });
819 try testStringify("\"with unicode\\u0001\"", "with unicode\u{1}", .{});
820 try testStringify("\"with unicode\\u0001\"", "with unicode\u{1}", .{ .escape_unicode = true });
821 try testStringify("\"with unicode\u{80}\"", "with unicode\u{80}", .{});
822 try testStringify("\"with unicode\\u0080\"", "with unicode\u{80}", .{ .escape_unicode = true });
823 try testStringify("\"with unicode\u{FF}\"", "with unicode\u{FF}", .{});
824 try testStringify("\"with unicode\\u00ff\"", "with unicode\u{FF}", .{ .escape_unicode = true });
825 try testStringify("\"with unicode\u{100}\"", "with unicode\u{100}", .{});
826 try testStringify("\"with unicode\\u0100\"", "with unicode\u{100}", .{ .escape_unicode = true });
827 try testStringify("\"with unicode\u{800}\"", "with unicode\u{800}", .{});
828 try testStringify("\"with unicode\\u0800\"", "with unicode\u{800}", .{ .escape_unicode = true });
829 try testStringify("\"with unicode\u{8000}\"", "with unicode\u{8000}", .{});
830 try testStringify("\"with unicode\\u8000\"", "with unicode\u{8000}", .{ .escape_unicode = true });
831 try testStringify("\"with unicode\u{D799}\"", "with unicode\u{D799}", .{});
832 try testStringify("\"with unicode\\ud799\"", "with unicode\u{D799}", .{ .escape_unicode = true });
833 try testStringify("\"with unicode\u{10000}\"", "with unicode\u{10000}", .{});
834 try testStringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}", .{ .escape_unicode = true });
835 try testStringify("\"with unicode\u{10FFFF}\"", "with unicode\u{10FFFF}", .{});
836 try testStringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}", .{ .escape_unicode = true });
837}
838
839test "stringify many-item sentinel-terminated string" {
840 try testStringify("\"hello\"", @as([*:0]const u8, "hello"), .{});
841 try testStringify("\"with\\nescapes\\r\"", @as([*:0]const u8, "with\nescapes\r"), .{ .escape_unicode = true });
842 try testStringify("\"with unicode\\u0001\"", @as([*:0]const u8, "with unicode\u{1}"), .{ .escape_unicode = true });
843}
844
845test "stringify enums" {
846 const E = enum {
847 foo,
848 bar,
849 };
850 try testStringify("\"foo\"", E.foo, .{});
851 try testStringify("\"bar\"", E.bar, .{});
852}
853
854test "stringify non-exhaustive enum" {
855 const E = enum(u8) {
856 foo = 0,
857 _,
858 };
859 try testStringify("\"foo\"", E.foo, .{});
860 try testStringify("1", @as(E, @enumFromInt(1)), .{});
861}
862
863test "stringify enum literals" {
864 try testStringify("\"foo\"", .foo, .{});
865 try testStringify("\"bar\"", .bar, .{});
866}
867
868test "stringify tagged unions" {
869 const T = union(enum) {
870 nothing,
871 foo: u32,
872 bar: bool,
873 };
874 try testStringify("{\"nothing\":{}}", T{ .nothing = {} }, .{});
875 try testStringify("{\"foo\":42}", T{ .foo = 42 }, .{});
876 try testStringify("{\"bar\":true}", T{ .bar = true }, .{});
877}
878
879test "stringify struct" {
880 try testStringify("{\"foo\":42}", struct {
881 foo: u32,
882 }{ .foo = 42 }, .{});
883}
884
885test "emit_strings_as_arrays" {
886 // Should only affect string values, not object keys.
887 try testStringify("{\"foo\":\"bar\"}", .{ .foo = "bar" }, .{});
888 try testStringify("{\"foo\":[98,97,114]}", .{ .foo = "bar" }, .{ .emit_strings_as_arrays = true });
889 // Should *not* affect these types:
890 try testStringify("\"foo\"", @as(enum { foo, bar }, .foo), .{ .emit_strings_as_arrays = true });
891 try testStringify("\"ItBroke\"", error.ItBroke, .{ .emit_strings_as_arrays = true });
892 // Should work on these:
893 try testStringify("\"bar\"", @Vector(3, u8){ 'b', 'a', 'r' }, .{});
894 try testStringify("[98,97,114]", @Vector(3, u8){ 'b', 'a', 'r' }, .{ .emit_strings_as_arrays = true });
895 try testStringify("\"bar\"", [3]u8{ 'b', 'a', 'r' }, .{});
896 try testStringify("[98,97,114]", [3]u8{ 'b', 'a', 'r' }, .{ .emit_strings_as_arrays = true });
897}
898
899test "stringify struct with indentation" {
900 try testStringify(
901 \\{
902 \\ "foo": 42,
903 \\ "bar": [
904 \\ 1,
905 \\ 2,
906 \\ 3
907 \\ ]
908 \\}
909 ,
910 struct {
911 foo: u32,
912 bar: [3]u32,
913 }{
914 .foo = 42,
915 .bar = .{ 1, 2, 3 },
916 },
917 .{ .whitespace = .indent_4 },
918 );
919 try testStringify(
920 "{\n\t\"foo\": 42,\n\t\"bar\": [\n\t\t1,\n\t\t2,\n\t\t3\n\t]\n}",
921 struct {
922 foo: u32,
923 bar: [3]u32,
924 }{
925 .foo = 42,
926 .bar = .{ 1, 2, 3 },
927 },
928 .{ .whitespace = .indent_tab },
929 );
930 try testStringify(
931 \\{"foo":42,"bar":[1,2,3]}
932 ,
933 struct {
934 foo: u32,
935 bar: [3]u32,
936 }{
937 .foo = 42,
938 .bar = .{ 1, 2, 3 },
939 },
940 .{ .whitespace = .minified },
941 );
942}
943
944test "stringify array of structs" {
945 const MyStruct = struct {
946 foo: u32,
947 };
948 try testStringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
949 MyStruct{ .foo = 42 },
950 MyStruct{ .foo = 100 },
951 MyStruct{ .foo = 1000 },
952 }, .{});
953}
954
955test "stringify struct with custom stringifier" {
956 try testStringify("[\"something special\",42]", struct {
957 foo: u32,
958 const Self = @This();
959 pub fn jsonStringify(v: @This(), jws: anytype) !void {
960 _ = v;
961 try jws.beginArray();
962 try jws.write("something special");
963 try jws.write(42);
964 try jws.endArray();
965 }
966 }{ .foo = 42 }, .{});
967}
968
969fn testStringify(expected: []const u8, v: anytype, options: Options) !void {
970 const ValidationWriter = struct {
971 const Self = @This();
972 pub const Writer = std.io.Writer(*Self, Error, Self.write);
973 pub const Error = error{
974 TooMuchData,
975 DifferentData,
976 };
977
978 expected_remaining: []const u8,
979
980 fn init(exp: []const u8) Self {
981 return .{ .expected_remaining = exp };
982 }
983
984 pub fn writer(self: *Self) Writer {
985 return .{ .context = self };
986 }
987
988 fn write(self: *Self, bytes: []const u8) Error!usize {
989 if (self.expected_remaining.len < bytes.len) {
990 std.debug.print(
991 \\====== expected this output: =========
992 \\{s}
993 \\======== instead found this: =========
994 \\{s}
995 \\======================================
996 , .{
997 self.expected_remaining,
998 bytes,
999 });
1000 return error.TooMuchData;
1001 }
1002 if (!std.mem.eql(u8, self.expected_remaining[0..bytes.len], bytes)) {
1003 std.debug.print(
1004 \\====== expected this output: =========
1005 \\{s}
1006 \\======== instead found this: =========
1007 \\{s}
1008 \\======================================
1009 , .{
1010 self.expected_remaining[0..bytes.len],
1011 bytes,
1012 });
1013 return error.DifferentData;
1014 }
1015 self.expected_remaining = self.expected_remaining[bytes.len..];
1016 return bytes.len;
1017 }
1018 };
1019
1020 var vos = ValidationWriter.init(expected);
1021 try value(v, options, vos.writer());
1022 if (vos.expected_remaining.len > 0) return error.NotEnoughData;
1023}
1024
1025test "raw streaming" {
1026 var out_buf: [1024]u8 = undefined;
1027 var out: std.io.BufferedWriter = undefined;
1028 out.initFixed(&out_buf);
1029
1030 var w: Stringify = .{ .writer = &out, .options = .{ .whitespace = .indent_2 } };
1031 try w.beginObject();
1032 try w.beginObjectFieldRaw();
1033 try w.stream.writeAll("\"long");
1034 try w.stream.writeAll(" key\"");
1035 w.endObjectFieldRaw();
1036 try w.beginWriteRaw();
1037 try w.stream.writeAll("\"long");
1038 try w.stream.writeAll(" value\"");
1039 w.endWriteRaw();
1040 try w.endObject();
1041
1042 const expected =
1043 \\{
1044 \\ "long key": "long value"
1045 \\}
1046 ;
1047 try std.testing.expectEqualStrings(expected, w.writer.getWritten());
1048}
lib/std/json/dynamic.zig+4-7
...@@ -4,9 +4,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;...@@ -4,9 +4,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
4const ArrayList = std.ArrayList;4const ArrayList = std.ArrayList;
5const StringArrayHashMap = std.StringArrayHashMap;5const StringArrayHashMap = std.StringArrayHashMap;
6const Allocator = std.mem.Allocator;6const Allocator = std.mem.Allocator;
77const json = std.json;
8const StringifyOptions = @import("./stringify.zig").StringifyOptions;
9const stringify = @import("./stringify.zig").stringify;
108
11const ParseOptions = @import("./static.zig").ParseOptions;9const ParseOptions = @import("./static.zig").ParseOptions;
12const ParseError = @import("./static.zig").ParseError;10const ParseError = @import("./static.zig").ParseError;
...@@ -52,12 +50,11 @@ pub const Value = union(enum) {...@@ -52,12 +50,11 @@ pub const Value = union(enum) {
52 }50 }
53 }51 }
5452
55 pub fn dump(self: Value) void {53 pub fn dump(v: Value) void {
56 std.debug.lockStdErr();54 var bw = std.debug.lockStdErr2();
57 defer std.debug.unlockStdErr();55 defer std.debug.unlockStdErr();
5856
59 const stderr = std.io.getStdErr().writer();57 json.Stringify.value(v, .{}, &bw) catch return;
60 stringify(self, .{}, stderr) catch return;
61 }58 }
6259
63 pub fn jsonStringify(value: @This(), jws: anytype) !void {60 pub fn jsonStringify(value: @This(), jws: anytype) !void {
lib/std/json/dynamic_test.zig+10-10
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1const std = @import("std");1const std = @import("std");
2const json = std.json;
2const mem = std.mem;3const mem = std.mem;
3const testing = std.testing;4const testing = std.testing;
4const ArenaAllocator = std.heap.ArenaAllocator;5const ArenaAllocator = std.heap.ArenaAllocator;
...@@ -70,13 +71,11 @@ test "json.parser.dynamic" {...@@ -70,13 +71,11 @@ test "json.parser.dynamic" {
70 try testing.expect(mem.eql(u8, large_int.number_string, "18446744073709551615"));71 try testing.expect(mem.eql(u8, large_int.number_string, "18446744073709551615"));
71}72}
7273
73const writeStream = @import("./stringify.zig").writeStream;
74test "write json then parse it" {74test "write json then parse it" {
75 var out_buffer: [1000]u8 = undefined;75 var out_buffer: [1000]u8 = undefined;
7676 var fixed_writer: std.io.BufferedWriter = undefined;
77 var fixed_buffer_stream = std.io.fixedBufferStream(&out_buffer);77 fixed_writer.initFixed(&out_buffer);
78 const out_stream = fixed_buffer_stream.writer();78 var jw: json.Stringify = .{ .writer = &fixed_writer, .options = .{} };
79 var jw = writeStream(out_stream, .{});
80 defer jw.deinit();79 defer jw.deinit();
8180
82 try jw.beginObject();81 try jw.beginObject();
...@@ -101,8 +100,8 @@ test "write json then parse it" {...@@ -101,8 +100,8 @@ test "write json then parse it" {
101100
102 try jw.endObject();101 try jw.endObject();
103102
104 fixed_buffer_stream = std.io.fixedBufferStream(fixed_buffer_stream.getWritten());103 var fbs: std.io.FixedBufferStream = .{ .buffer = fixed_writer.getWritten() };
105 var json_reader = jsonReader(testing.allocator, fixed_buffer_stream.reader());104 var json_reader = jsonReader(testing.allocator, fbs.reader());
106 defer json_reader.deinit();105 defer json_reader.deinit();
107 var parsed = try parseFromTokenSource(Value, testing.allocator, &json_reader, .{});106 var parsed = try parseFromTokenSource(Value, testing.allocator, &json_reader, .{});
108 defer parsed.deinit();107 defer parsed.deinit();
...@@ -242,9 +241,10 @@ test "Value.jsonStringify" {...@@ -242,9 +241,10 @@ test "Value.jsonStringify" {
242 .{ .object = obj },241 .{ .object = obj },
243 };242 };
244 var buffer: [0x1000]u8 = undefined;243 var buffer: [0x1000]u8 = undefined;
245 var fbs = std.io.fixedBufferStream(&buffer);244 var fixed_writer: std.io.BufferedWriter = undefined;
245 fixed_writer.initFixed(&buffer);
246246
247 var jw = writeStream(fbs.writer(), .{ .whitespace = .indent_1 });247 var jw: json.Stringify = .{ .writer = &fixed_writer, .options = .{ .whitespace = .indent_1 } };
248 defer jw.deinit();248 defer jw.deinit();
249 try jw.write(array);249 try jw.write(array);
250250
...@@ -266,7 +266,7 @@ test "Value.jsonStringify" {...@@ -266,7 +266,7 @@ test "Value.jsonStringify" {
266 \\ }266 \\ }
267 \\]267 \\]
268 ;268 ;
269 try testing.expectEqualSlices(u8, expected, fbs.getWritten());269 try testing.expectEqualStrings(expected, fixed_writer.getWritten());
270}270}
271271
272test "parseFromValue(std.json.Value,...)" {272test "parseFromValue(std.json.Value,...)" {
lib/std/json/fmt.zig deleted-46
...@@ -1,46 +0,0 @@
1const std = @import("std");
2
3const stringify = @import("stringify.zig").stringify;
4const StringifyOptions = @import("stringify.zig").StringifyOptions;
5
6/// Returns a formatter that formats the given value using stringify.
7pub fn fmt(value: anytype, options: StringifyOptions) Formatter(@TypeOf(value)) {
8 return Formatter(@TypeOf(value)){ .value = value, .options = options };
9}
10
11/// Formats the given value using stringify.
12pub fn Formatter(comptime T: type) type {
13 return struct {
14 value: T,
15 options: StringifyOptions,
16
17 pub fn format(
18 self: @This(),
19 comptime fmt_spec: []const u8,
20 options: std.fmt.FormatOptions,
21 writer: anytype,
22 ) !void {
23 _ = fmt_spec;
24 _ = options;
25 try stringify(self.value, self.options, writer);
26 }
27 };
28}
29
30test fmt {
31 const expectFmt = std.testing.expectFmt;
32 try expectFmt("123", "{}", .{fmt(@as(u32, 123), .{})});
33 try expectFmt(
34 \\{"num":927,"msg":"hello","sub":{"mybool":true}}
35 , "{}", .{fmt(struct {
36 num: u32,
37 msg: []const u8,
38 sub: struct {
39 mybool: bool,
40 },
41 }{
42 .num = 927,
43 .msg = "hello",
44 .sub = .{ .mybool = true },
45 }, .{})});
46}
lib/std/json/hashmap_test.zig+6-6
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1const std = @import("std");1const std = @import("std");
2const json = std.json;
2const testing = std.testing;3const testing = std.testing;
34
4const ArrayHashMap = @import("hashmap.zig").ArrayHashMap;5const ArrayHashMap = @import("hashmap.zig").ArrayHashMap;
...@@ -7,7 +8,6 @@ const parseFromSlice = @import("static.zig").parseFromSlice;...@@ -7,7 +8,6 @@ const parseFromSlice = @import("static.zig").parseFromSlice;
7const parseFromSliceLeaky = @import("static.zig").parseFromSliceLeaky;8const parseFromSliceLeaky = @import("static.zig").parseFromSliceLeaky;
8const parseFromTokenSource = @import("static.zig").parseFromTokenSource;9const parseFromTokenSource = @import("static.zig").parseFromTokenSource;
9const parseFromValue = @import("static.zig").parseFromValue;10const parseFromValue = @import("static.zig").parseFromValue;
10const stringifyAlloc = @import("stringify.zig").stringifyAlloc;
11const Value = @import("dynamic.zig").Value;11const Value = @import("dynamic.zig").Value;
1212
13const jsonReader = @import("./scanner.zig").reader;13const jsonReader = @import("./scanner.zig").reader;
...@@ -89,7 +89,7 @@ test "stringify json hashmap" {...@@ -89,7 +89,7 @@ test "stringify json hashmap" {
89 var value = ArrayHashMap(T){};89 var value = ArrayHashMap(T){};
90 defer value.deinit(testing.allocator);90 defer value.deinit(testing.allocator);
91 {91 {
92 const doc = try stringifyAlloc(testing.allocator, value, .{});92 const doc = try json.Stringify.valueAlloc(testing.allocator, value, .{});
93 defer testing.allocator.free(doc);93 defer testing.allocator.free(doc);
94 try testing.expectEqualStrings("{}", doc);94 try testing.expectEqualStrings("{}", doc);
95 }95 }
...@@ -98,7 +98,7 @@ test "stringify json hashmap" {...@@ -98,7 +98,7 @@ test "stringify json hashmap" {
98 try value.map.put(testing.allocator, "xyz", .{ .i = 1, .s = "w" });98 try value.map.put(testing.allocator, "xyz", .{ .i = 1, .s = "w" });
9999
100 {100 {
101 const doc = try stringifyAlloc(testing.allocator, value, .{});101 const doc = try json.Stringify.valueAlloc(testing.allocator, value, .{});
102 defer testing.allocator.free(doc);102 defer testing.allocator.free(doc);
103 try testing.expectEqualStrings(103 try testing.expectEqualStrings(
104 \\{"abc":{"i":0,"s":"d"},"xyz":{"i":1,"s":"w"}}104 \\{"abc":{"i":0,"s":"d"},"xyz":{"i":1,"s":"w"}}
...@@ -107,7 +107,7 @@ test "stringify json hashmap" {...@@ -107,7 +107,7 @@ test "stringify json hashmap" {
107107
108 try testing.expect(value.map.swapRemove("abc"));108 try testing.expect(value.map.swapRemove("abc"));
109 {109 {
110 const doc = try stringifyAlloc(testing.allocator, value, .{});110 const doc = try json.Stringify.valueAlloc(testing.allocator, value, .{});
111 defer testing.allocator.free(doc);111 defer testing.allocator.free(doc);
112 try testing.expectEqualStrings(112 try testing.expectEqualStrings(
113 \\{"xyz":{"i":1,"s":"w"}}113 \\{"xyz":{"i":1,"s":"w"}}
...@@ -116,7 +116,7 @@ test "stringify json hashmap" {...@@ -116,7 +116,7 @@ test "stringify json hashmap" {
116116
117 try testing.expect(value.map.swapRemove("xyz"));117 try testing.expect(value.map.swapRemove("xyz"));
118 {118 {
119 const doc = try stringifyAlloc(testing.allocator, value, .{});119 const doc = try json.Stringify.valueAlloc(testing.allocator, value, .{});
120 defer testing.allocator.free(doc);120 defer testing.allocator.free(doc);
121 try testing.expectEqualStrings("{}", doc);121 try testing.expectEqualStrings("{}", doc);
122 }122 }
...@@ -129,7 +129,7 @@ test "stringify json hashmap whitespace" {...@@ -129,7 +129,7 @@ test "stringify json hashmap whitespace" {
129 try value.map.put(testing.allocator, "xyz", .{ .i = 1, .s = "w" });129 try value.map.put(testing.allocator, "xyz", .{ .i = 1, .s = "w" });
130130
131 {131 {
132 const doc = try stringifyAlloc(testing.allocator, value, .{ .whitespace = .indent_2 });132 const doc = try json.Stringify.valueAlloc(testing.allocator, value, .{ .whitespace = .indent_2 });
133 defer testing.allocator.free(doc);133 defer testing.allocator.free(doc);
134 try testing.expectEqualStrings(134 try testing.expectEqualStrings(
135 \\{135 \\{
lib/std/json/stringify.zig deleted-770
...@@ -1,770 +0,0 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const Allocator = std.mem.Allocator;
4const ArrayList = std.ArrayList;
5const BitStack = std.BitStack;
6
7const OBJECT_MODE = 0;
8const ARRAY_MODE = 1;
9
10pub const StringifyOptions = struct {
11 /// Controls the whitespace emitted.
12 /// The default `.minified` is a compact encoding with no whitespace between tokens.
13 /// Any setting other than `.minified` will use newlines, indentation, and a space after each ':'.
14 /// `.indent_1` means 1 space for each indentation level, `.indent_2` means 2 spaces, etc.
15 /// `.indent_tab` uses a tab for each indentation level.
16 whitespace: enum {
17 minified,
18 indent_1,
19 indent_2,
20 indent_3,
21 indent_4,
22 indent_8,
23 indent_tab,
24 } = .minified,
25
26 /// Should optional fields with null value be written?
27 emit_null_optional_fields: bool = true,
28
29 /// Arrays/slices of u8 are typically encoded as JSON strings.
30 /// This option emits them as arrays of numbers instead.
31 /// Does not affect calls to `objectField*()`.
32 emit_strings_as_arrays: bool = false,
33
34 /// Should unicode characters be escaped in strings?
35 escape_unicode: bool = false,
36
37 /// When true, renders numbers outside the range `+-1<<53` (the precise integer range of f64) as JSON strings in base 10.
38 emit_nonportable_numbers_as_strings: bool = false,
39};
40
41/// Writes the given value to the `std.io.Writer` stream.
42/// See `WriteStream` for how the given value is serialized into JSON.
43/// The maximum nesting depth of the output JSON document is 256.
44/// See also `stringifyMaxDepth` and `stringifyArbitraryDepth`.
45pub fn stringify(
46 value: anytype,
47 options: StringifyOptions,
48 out_stream: anytype,
49) @TypeOf(out_stream).Error!void {
50 var jw = writeStream(out_stream, options);
51 defer jw.deinit();
52 try jw.write(value);
53}
54
55/// Like `stringify` with configurable nesting depth.
56/// `max_depth` is rounded up to the nearest multiple of 8.
57/// Give `null` for `max_depth` to disable some safety checks and allow arbitrary nesting depth.
58/// See `writeStreamMaxDepth` for more info.
59pub fn stringifyMaxDepth(
60 value: anytype,
61 options: StringifyOptions,
62 out_stream: anytype,
63 comptime max_depth: ?usize,
64) @TypeOf(out_stream).Error!void {
65 var jw = writeStreamMaxDepth(out_stream, options, max_depth);
66 try jw.write(value);
67}
68
69/// Like `stringify` but takes an allocator to facilitate safety checks while allowing arbitrary nesting depth.
70/// These safety checks can be helpful when debugging custom `jsonStringify` implementations;
71/// See `WriteStream`.
72pub fn stringifyArbitraryDepth(
73 allocator: Allocator,
74 value: anytype,
75 options: StringifyOptions,
76 out_stream: anytype,
77) WriteStream(@TypeOf(out_stream), .checked_to_arbitrary_depth).Error!void {
78 var jw = writeStreamArbitraryDepth(allocator, out_stream, options);
79 defer jw.deinit();
80 try jw.write(value);
81}
82
83/// Calls `stringifyArbitraryDepth` and stores the result in dynamically allocated memory
84/// instead of taking a `std.io.Writer`.
85///
86/// Caller owns returned memory.
87pub fn stringifyAlloc(
88 allocator: Allocator,
89 value: anytype,
90 options: StringifyOptions,
91) error{OutOfMemory}![]u8 {
92 var list = std.ArrayList(u8).init(allocator);
93 errdefer list.deinit();
94 try stringifyArbitraryDepth(allocator, value, options, list.writer());
95 return list.toOwnedSlice();
96}
97
98/// See `WriteStream` for documentation.
99/// Equivalent to calling `writeStreamMaxDepth` with a depth of `256`.
100///
101/// The caller does *not* need to call `deinit()` on the returned object.
102pub fn writeStream(
103 out_stream: anytype,
104 options: StringifyOptions,
105) WriteStream(@TypeOf(out_stream), .{ .checked_to_fixed_depth = 256 }) {
106 return writeStreamMaxDepth(out_stream, options, 256);
107}
108
109/// See `WriteStream` for documentation.
110/// The returned object includes 1 bit of size per `max_depth` to enable safety checks on the order of method calls;
111/// see the grammar in the `WriteStream` documentation.
112/// `max_depth` is rounded up to the nearest multiple of 8.
113/// If the nesting depth exceeds `max_depth`, it is detectable illegal behavior.
114/// Give `null` for `max_depth` to disable safety checks for the grammar and allow arbitrary nesting depth.
115/// In `ReleaseFast` and `ReleaseSmall`, `max_depth` is ignored, effectively equivalent to passing `null`.
116/// Alternatively, see `writeStreamArbitraryDepth` to do safety checks to arbitrary depth.
117///
118/// The caller does *not* need to call `deinit()` on the returned object.
119pub fn writeStreamMaxDepth(
120 out_stream: anytype,
121 options: StringifyOptions,
122 comptime max_depth: ?usize,
123) WriteStream(
124 @TypeOf(out_stream),
125 if (max_depth) |d| .{ .checked_to_fixed_depth = d } else .assumed_correct,
126) {
127 return WriteStream(
128 @TypeOf(out_stream),
129 if (max_depth) |d| .{ .checked_to_fixed_depth = d } else .assumed_correct,
130 ).init(undefined, out_stream, options);
131}
132
133/// See `WriteStream` for documentation.
134/// This version of the write stream enables safety checks to arbitrarily deep nesting levels
135/// by using the given allocator.
136/// The caller should call `deinit()` on the returned object to free allocated memory.
137///
138/// In `ReleaseFast` and `ReleaseSmall` mode, this function is effectively equivalent to calling `writeStreamMaxDepth(..., null)`;
139/// in those build modes, the allocator is *not used*.
140pub fn writeStreamArbitraryDepth(
141 allocator: Allocator,
142 out_stream: anytype,
143 options: StringifyOptions,
144) WriteStream(@TypeOf(out_stream), .checked_to_arbitrary_depth) {
145 return WriteStream(@TypeOf(out_stream), .checked_to_arbitrary_depth).init(allocator, out_stream, options);
146}
147
148/// Writes JSON ([RFC8259](https://tools.ietf.org/html/rfc8259)) formatted data
149/// to a stream.
150///
151/// The sequence of method calls to write JSON content must follow this grammar:
152/// ```
153/// <once> = <value>
154/// <value> =
155/// | <object>
156/// | <array>
157/// | write
158/// | print
159/// | <writeRawStream>
160/// <object> = beginObject ( <field> <value> )* endObject
161/// <field> = objectField | objectFieldRaw | <objectFieldRawStream>
162/// <array> = beginArray ( <value> )* endArray
163/// <writeRawStream> = beginWriteRaw ( stream.writeAll )* endWriteRaw
164/// <objectFieldRawStream> = beginObjectFieldRaw ( stream.writeAll )* endObjectFieldRaw
165/// ```
166///
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.
176/// In `ReleaseFast` and `ReleaseSmall` mode, the given `safety_checks_hint` is ignored and is always treated as `.assumed_correct`.
177pub fn WriteStream(
178 comptime OutStream: type,
179 comptime safety_checks_hint: union(enum) {
180 checked_to_arbitrary_depth,
181 checked_to_fixed_depth: usize, // Rounded up to the nearest multiple of 8.
182 assumed_correct,
183 },
184) type {
185 return struct {
186 const Self = @This();
187 const build_mode_has_safety = switch (@import("builtin").mode) {
188 .Debug, .ReleaseSafe => true,
189 .ReleaseFast, .ReleaseSmall => false,
190 };
191 const safety_checks: @TypeOf(safety_checks_hint) = if (build_mode_has_safety)
192 safety_checks_hint
193 else
194 .assumed_correct;
195
196 pub const Stream = OutStream;
197 pub const Error = switch (safety_checks) {
198 .checked_to_arbitrary_depth => Stream.Error || error{OutOfMemory},
199 .checked_to_fixed_depth, .assumed_correct => Stream.Error,
200 };
201
202 options: StringifyOptions,
203
204 stream: OutStream,
205 indent_level: usize = 0,
206 next_punctuation: enum {
207 the_beginning,
208 none,
209 comma,
210 colon,
211 } = .the_beginning,
212
213 nesting_stack: switch (safety_checks) {
214 .checked_to_arbitrary_depth => BitStack,
215 .checked_to_fixed_depth => |fixed_buffer_size| [(fixed_buffer_size + 7) >> 3]u8,
216 .assumed_correct => void,
217 },
218
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
224 pub fn init(safety_allocator: Allocator, stream: OutStream, options: StringifyOptions) Self {
225 return .{
226 .options = options,
227 .stream = stream,
228 .nesting_stack = switch (safety_checks) {
229 .checked_to_arbitrary_depth => BitStack.init(safety_allocator),
230 .checked_to_fixed_depth => |fixed_buffer_size| [_]u8{0} ** ((fixed_buffer_size + 7) >> 3),
231 .assumed_correct => {},
232 },
233 };
234 }
235
236 /// Only necessary with .checked_to_arbitrary_depth.
237 pub fn deinit(self: *Self) void {
238 switch (safety_checks) {
239 .checked_to_arbitrary_depth => self.nesting_stack.deinit(),
240 .checked_to_fixed_depth, .assumed_correct => {},
241 }
242 self.* = undefined;
243 }
244
245 pub fn beginArray(self: *Self) Error!void {
246 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
247 try self.valueStart();
248 try self.stream.writeByte('[');
249 try self.pushIndentation(ARRAY_MODE);
250 self.next_punctuation = .none;
251 }
252
253 pub fn beginObject(self: *Self) Error!void {
254 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
255 try self.valueStart();
256 try self.stream.writeByte('{');
257 try self.pushIndentation(OBJECT_MODE);
258 self.next_punctuation = .none;
259 }
260
261 pub fn endArray(self: *Self) Error!void {
262 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
263 self.popIndentation(ARRAY_MODE);
264 switch (self.next_punctuation) {
265 .none => {},
266 .comma => {
267 try self.indent();
268 },
269 .the_beginning, .colon => unreachable,
270 }
271 try self.stream.writeByte(']');
272 self.valueDone();
273 }
274
275 pub fn endObject(self: *Self) Error!void {
276 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
277 self.popIndentation(OBJECT_MODE);
278 switch (self.next_punctuation) {
279 .none => {},
280 .comma => {
281 try self.indent();
282 },
283 .the_beginning, .colon => unreachable,
284 }
285 try self.stream.writeByte('}');
286 self.valueDone();
287 }
288
289 fn pushIndentation(self: *Self, mode: u1) !void {
290 switch (safety_checks) {
291 .checked_to_arbitrary_depth => {
292 try self.nesting_stack.push(mode);
293 self.indent_level += 1;
294 },
295 .checked_to_fixed_depth => {
296 BitStack.pushWithStateAssumeCapacity(&self.nesting_stack, &self.indent_level, mode);
297 },
298 .assumed_correct => {
299 self.indent_level += 1;
300 },
301 }
302 }
303 fn popIndentation(self: *Self, assert_its_this_one: u1) void {
304 switch (safety_checks) {
305 .checked_to_arbitrary_depth => {
306 assert(self.nesting_stack.pop() == assert_its_this_one);
307 self.indent_level -= 1;
308 },
309 .checked_to_fixed_depth => {
310 assert(BitStack.popWithState(&self.nesting_stack, &self.indent_level) == assert_its_this_one);
311 },
312 .assumed_correct => {
313 self.indent_level -= 1;
314 },
315 }
316 }
317
318 fn indent(self: *Self) !void {
319 var char: u8 = ' ';
320 const n_chars = switch (self.options.whitespace) {
321 .minified => return,
322 .indent_1 => 1 * self.indent_level,
323 .indent_2 => 2 * self.indent_level,
324 .indent_3 => 3 * self.indent_level,
325 .indent_4 => 4 * self.indent_level,
326 .indent_8 => 8 * self.indent_level,
327 .indent_tab => blk: {
328 char = '\t';
329 break :blk self.indent_level;
330 },
331 };
332 try self.stream.writeByte('\n');
333 try self.stream.writeByteNTimes(char, n_chars);
334 }
335
336 fn valueStart(self: *Self) !void {
337 if (self.isObjectKeyExpected()) |is_it| assert(!is_it); // Call objectField*(), not write(), for object keys.
338 return self.valueStartAssumeTypeOk();
339 }
340 fn objectFieldStart(self: *Self) !void {
341 if (self.isObjectKeyExpected()) |is_it| assert(is_it); // Expected write(), not objectField*().
342 return self.valueStartAssumeTypeOk();
343 }
344 fn valueStartAssumeTypeOk(self: *Self) !void {
345 assert(!self.isComplete()); // JSON document already complete.
346 switch (self.next_punctuation) {
347 .the_beginning => {
348 // No indentation for the very beginning.
349 },
350 .none => {
351 // First item in a container.
352 try self.indent();
353 },
354 .comma => {
355 // Subsequent item in a container.
356 try self.stream.writeByte(',');
357 try self.indent();
358 },
359 .colon => {
360 try self.stream.writeByte(':');
361 if (self.options.whitespace != .minified) {
362 try self.stream.writeByte(' ');
363 }
364 },
365 }
366 }
367 fn valueDone(self: *Self) void {
368 self.next_punctuation = .comma;
369 }
370
371 // Only when safety is enabled:
372 fn isObjectKeyExpected(self: *const Self) ?bool {
373 switch (safety_checks) {
374 .checked_to_arbitrary_depth => return self.indent_level > 0 and
375 self.nesting_stack.peek() == OBJECT_MODE and
376 self.next_punctuation != .colon,
377 .checked_to_fixed_depth => return self.indent_level > 0 and
378 BitStack.peekWithState(&self.nesting_stack, self.indent_level) == OBJECT_MODE and
379 self.next_punctuation != .colon,
380 .assumed_correct => return null,
381 }
382 }
383 fn isComplete(self: *const Self) bool {
384 return self.indent_level == 0 and self.next_punctuation == .comma;
385 }
386
387 /// An alternative to calling `write` that formats a value with `std.fmt`.
388 /// This function does the usual punctuation and indentation formatting
389 /// assuming the resulting formatted string represents a single complete value;
390 /// e.g. `"1"`, `"[]"`, `"[1,2]"`, not `"1,2"`.
391 /// This function may be useful for doing your own number formatting.
392 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);
394 try self.valueStart();
395 try self.stream.print(fmt, args);
396 self.valueDone();
397 }
398
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
420 /// See `WriteStream` for when to call this method.
421 /// `key` is the string content of the property name.
422 /// Surrounding quotes will be added and any special characters will be escaped.
423 /// See also `objectFieldRaw`.
424 pub fn objectField(self: *Self, key: []const u8) Error!void {
425 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
426 try self.objectFieldStart();
427 try encodeJsonString(key, self.options, self.stream);
428 self.next_punctuation = .colon;
429 }
430 /// See `WriteStream` for when to call this method.
431 /// `quoted_key` is the complete bytes of the key including quotes and any necessary escape sequences.
432 /// A few assertions are performed on the given value to ensure that the caller of this function understands the API contract.
433 /// See also `objectField`.
434 pub fn objectFieldRaw(self: *Self, quoted_key: []const u8) Error!void {
435 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
436 assert(quoted_key.len >= 2 and quoted_key[0] == '"' and quoted_key[quoted_key.len - 1] == '"'); // quoted_key should be "quoted".
437 try self.objectFieldStart();
438 try self.stream.writeAll(quoted_key);
439 self.next_punctuation = .colon;
440 }
441
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 /// * If the enum is non-exhaustive, unnamed values are rendered as integers.
487 /// * Zig untyped enum literal -> JSON string naming the active tag.
488 /// * Zig error -> JSON string naming the error.
489 /// * Zig `*T` -> the rendering of `T`. Note there is no guard against circular-reference infinite recursion.
490 ///
491 /// See also alternative functions `print` and `beginWriteRaw`.
492 /// For writing object field names, use `objectField` instead.
493 pub fn write(self: *Self, value: anytype) Error!void {
494 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
495 const T = @TypeOf(value);
496 switch (@typeInfo(T)) {
497 .int => {
498 try self.valueStart();
499 if (self.options.emit_nonportable_numbers_as_strings and
500 (value <= -(1 << 53) or value >= (1 << 53)))
501 {
502 try self.stream.print("\"{}\"", .{value});
503 } else {
504 try self.stream.print("{}", .{value});
505 }
506 self.valueDone();
507 return;
508 },
509 .comptime_int => {
510 return self.write(@as(std.math.IntFittingRange(value, value), value));
511 },
512 .float, .comptime_float => {
513 if (@as(f64, @floatCast(value)) == value) {
514 try self.valueStart();
515 try self.stream.print("{}", .{@as(f64, @floatCast(value))});
516 self.valueDone();
517 return;
518 }
519 try self.valueStart();
520 try self.stream.print("\"{}\"", .{value});
521 self.valueDone();
522 return;
523 },
524
525 .bool => {
526 try self.valueStart();
527 try self.stream.writeAll(if (value) "true" else "false");
528 self.valueDone();
529 return;
530 },
531 .null => {
532 try self.valueStart();
533 try self.stream.writeAll("null");
534 self.valueDone();
535 return;
536 },
537 .optional => {
538 if (value) |payload| {
539 return try self.write(payload);
540 } else {
541 return try self.write(null);
542 }
543 },
544 .@"enum" => |enum_info| {
545 if (std.meta.hasFn(T, "jsonStringify")) {
546 return value.jsonStringify(self);
547 }
548
549 if (!enum_info.is_exhaustive) {
550 inline for (enum_info.fields) |field| {
551 if (value == @field(T, field.name)) {
552 break;
553 }
554 } else {
555 return self.write(@intFromEnum(value));
556 }
557 }
558
559 return self.stringValue(@tagName(value));
560 },
561 .enum_literal => {
562 return self.stringValue(@tagName(value));
563 },
564 .@"union" => {
565 if (std.meta.hasFn(T, "jsonStringify")) {
566 return value.jsonStringify(self);
567 }
568
569 const info = @typeInfo(T).@"union";
570 if (info.tag_type) |UnionTagType| {
571 try self.beginObject();
572 inline for (info.fields) |u_field| {
573 if (value == @field(UnionTagType, u_field.name)) {
574 try self.objectField(u_field.name);
575 if (u_field.type == void) {
576 // void value is {}
577 try self.beginObject();
578 try self.endObject();
579 } else {
580 try self.write(@field(value, u_field.name));
581 }
582 break;
583 }
584 } else {
585 unreachable; // No active tag?
586 }
587 try self.endObject();
588 return;
589 } else {
590 @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'");
591 }
592 },
593 .@"struct" => |S| {
594 if (std.meta.hasFn(T, "jsonStringify")) {
595 return value.jsonStringify(self);
596 }
597
598 if (S.is_tuple) {
599 try self.beginArray();
600 } else {
601 try self.beginObject();
602 }
603 inline for (S.fields) |Field| {
604 // don't include void fields
605 if (Field.type == void) continue;
606
607 var emit_field = true;
608
609 // don't include optional fields that are null when emit_null_optional_fields is set to false
610 if (@typeInfo(Field.type) == .optional) {
611 if (self.options.emit_null_optional_fields == false) {
612 if (@field(value, Field.name) == null) {
613 emit_field = false;
614 }
615 }
616 }
617
618 if (emit_field) {
619 if (!S.is_tuple) {
620 try self.objectField(Field.name);
621 }
622 try self.write(@field(value, Field.name));
623 }
624 }
625 if (S.is_tuple) {
626 try self.endArray();
627 } else {
628 try self.endObject();
629 }
630 return;
631 },
632 .error_set => return self.stringValue(@errorName(value)),
633 .pointer => |ptr_info| switch (ptr_info.size) {
634 .one => switch (@typeInfo(ptr_info.child)) {
635 .array => {
636 // Coerce `*[N]T` to `[]const T`.
637 const Slice = []const std.meta.Elem(ptr_info.child);
638 return self.write(@as(Slice, value));
639 },
640 else => {
641 return self.write(value.*);
642 },
643 },
644 .many, .slice => {
645 if (ptr_info.size == .many and ptr_info.sentinel() == null)
646 @compileError("unable to stringify type '" ++ @typeName(T) ++ "' without sentinel");
647 const slice = if (ptr_info.size == .many) std.mem.span(value) else value;
648
649 if (ptr_info.child == u8) {
650 // This is a []const u8, or some similar Zig string.
651 if (!self.options.emit_strings_as_arrays and std.unicode.utf8ValidateSlice(slice)) {
652 return self.stringValue(slice);
653 }
654 }
655
656 try self.beginArray();
657 for (slice) |x| {
658 try self.write(x);
659 }
660 try self.endArray();
661 return;
662 },
663 else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
664 },
665 .array => {
666 // Coerce `[N]T` to `*const [N]T` (and then to `[]const T`).
667 return self.write(&value);
668 },
669 .vector => |info| {
670 const array: [info.len]info.child = value;
671 return self.write(&array);
672 },
673 else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
674 }
675 unreachable;
676 }
677
678 fn stringValue(self: *Self, s: []const u8) !void {
679 try self.valueStart();
680 try encodeJsonString(s, self.options, self.stream);
681 self.valueDone();
682 }
683 };
684}
685
686fn outputUnicodeEscape(codepoint: u21, out_stream: anytype) !void {
687 if (codepoint <= 0xFFFF) {
688 // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF),
689 // then it may be represented as a six-character sequence: a reverse solidus, followed
690 // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point.
691 try out_stream.writeAll("\\u");
692 try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream);
693 } else {
694 assert(codepoint <= 0x10FFFF);
695 // To escape an extended character that is not in the Basic Multilingual Plane,
696 // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair.
697 const high = @as(u16, @intCast((codepoint - 0x10000) >> 10)) + 0xD800;
698 const low = @as(u16, @intCast(codepoint & 0x3FF)) + 0xDC00;
699 try out_stream.writeAll("\\u");
700 try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream);
701 try out_stream.writeAll("\\u");
702 try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream);
703 }
704}
705
706fn outputSpecialEscape(c: u8, writer: anytype) !void {
707 switch (c) {
708 '\\' => try writer.writeAll("\\\\"),
709 '\"' => try writer.writeAll("\\\""),
710 0x08 => try writer.writeAll("\\b"),
711 0x0C => try writer.writeAll("\\f"),
712 '\n' => try writer.writeAll("\\n"),
713 '\r' => try writer.writeAll("\\r"),
714 '\t' => try writer.writeAll("\\t"),
715 else => try outputUnicodeEscape(c, writer),
716 }
717}
718
719/// Write `string` to `writer` as a JSON encoded string.
720pub fn encodeJsonString(string: []const u8, options: StringifyOptions, writer: anytype) !void {
721 try writer.writeByte('\"');
722 try encodeJsonStringChars(string, options, writer);
723 try writer.writeByte('\"');
724}
725
726/// Write `chars` to `writer` as JSON encoded string characters.
727pub fn encodeJsonStringChars(chars: []const u8, options: StringifyOptions, writer: anytype) !void {
728 var write_cursor: usize = 0;
729 var i: usize = 0;
730 if (options.escape_unicode) {
731 while (i < chars.len) : (i += 1) {
732 switch (chars[i]) {
733 // normal ascii character
734 0x20...0x21, 0x23...0x5B, 0x5D...0x7E => {},
735 0x00...0x1F, '\\', '\"' => {
736 // Always must escape these.
737 try writer.writeAll(chars[write_cursor..i]);
738 try outputSpecialEscape(chars[i], writer);
739 write_cursor = i + 1;
740 },
741 0x7F...0xFF => {
742 try writer.writeAll(chars[write_cursor..i]);
743 const ulen = std.unicode.utf8ByteSequenceLength(chars[i]) catch unreachable;
744 const codepoint = std.unicode.utf8Decode(chars[i..][0..ulen]) catch unreachable;
745 try outputUnicodeEscape(codepoint, writer);
746 i += ulen - 1;
747 write_cursor = i + 1;
748 },
749 }
750 }
751 } else {
752 while (i < chars.len) : (i += 1) {
753 switch (chars[i]) {
754 // normal bytes
755 0x20...0x21, 0x23...0x5B, 0x5D...0xFF => {},
756 0x00...0x1F, '\\', '\"' => {
757 // Always must escape these.
758 try writer.writeAll(chars[write_cursor..i]);
759 try outputSpecialEscape(chars[i], writer);
760 write_cursor = i + 1;
761 },
762 }
763 }
764 }
765 try writer.writeAll(chars[write_cursor..chars.len]);
766}
767
768test {
769 _ = @import("./stringify_test.zig");
770}
lib/std/json/stringify_test.zig deleted-504
...@@ -1,504 +0,0 @@
1const std = @import("std");
2const mem = std.mem;
3const testing = std.testing;
4
5const ObjectMap = @import("dynamic.zig").ObjectMap;
6const Value = @import("dynamic.zig").Value;
7
8const StringifyOptions = @import("stringify.zig").StringifyOptions;
9const stringify = @import("stringify.zig").stringify;
10const stringifyMaxDepth = @import("stringify.zig").stringifyMaxDepth;
11const stringifyArbitraryDepth = @import("stringify.zig").stringifyArbitraryDepth;
12const stringifyAlloc = @import("stringify.zig").stringifyAlloc;
13const writeStream = @import("stringify.zig").writeStream;
14const writeStreamMaxDepth = @import("stringify.zig").writeStreamMaxDepth;
15const writeStreamArbitraryDepth = @import("stringify.zig").writeStreamArbitraryDepth;
16
17test "json write stream" {
18 var out_buf: [1024]u8 = undefined;
19 var slice_stream = std.io.fixedBufferStream(&out_buf);
20 const out = slice_stream.writer();
21
22 {
23 var w = writeStream(out, .{ .whitespace = .indent_2 });
24 try testBasicWriteStream(&w, &slice_stream);
25 }
26
27 {
28 var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, 8);
29 try testBasicWriteStream(&w, &slice_stream);
30 }
31
32 {
33 var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, null);
34 try testBasicWriteStream(&w, &slice_stream);
35 }
36
37 {
38 var w = writeStreamArbitraryDepth(testing.allocator, out, .{ .whitespace = .indent_2 });
39 defer w.deinit();
40 try testBasicWriteStream(&w, &slice_stream);
41 }
42}
43
44fn testBasicWriteStream(w: anytype, slice_stream: anytype) !void {
45 slice_stream.reset();
46
47 try w.beginObject();
48
49 try w.objectField("object");
50 var arena_allocator = std.heap.ArenaAllocator.init(testing.allocator);
51 defer arena_allocator.deinit();
52 try w.write(try getJsonObject(arena_allocator.allocator()));
53
54 try w.objectFieldRaw("\"string\"");
55 try w.write("This is a string");
56
57 try w.objectField("array");
58 try w.beginArray();
59 try w.write("Another string");
60 try w.write(@as(i32, 1));
61 try w.write(@as(f32, 3.5));
62 try w.endArray();
63
64 try w.objectField("int");
65 try w.write(@as(i32, 10));
66
67 try w.objectField("float");
68 try w.write(@as(f32, 3.5));
69
70 try w.endObject();
71
72 const result = slice_stream.getWritten();
73 const expected =
74 \\{
75 \\ "object": {
76 \\ "one": 1,
77 \\ "two": 2e0
78 \\ },
79 \\ "string": "This is a string",
80 \\ "array": [
81 \\ "Another string",
82 \\ 1,
83 \\ 3.5e0
84 \\ ],
85 \\ "int": 10,
86 \\ "float": 3.5e0
87 \\}
88 ;
89 try std.testing.expectEqualStrings(expected, result);
90}
91
92fn getJsonObject(allocator: std.mem.Allocator) !Value {
93 var value = Value{ .object = ObjectMap.init(allocator) };
94 try value.object.put("one", Value{ .integer = @as(i64, @intCast(1)) });
95 try value.object.put("two", Value{ .float = 2.0 });
96 return value;
97}
98
99test "stringify null optional fields" {
100 const MyStruct = struct {
101 optional: ?[]const u8 = null,
102 required: []const u8 = "something",
103 another_optional: ?[]const u8 = null,
104 another_required: []const u8 = "something else",
105 };
106 try testStringify(
107 \\{"optional":null,"required":"something","another_optional":null,"another_required":"something else"}
108 ,
109 MyStruct{},
110 .{},
111 );
112 try testStringify(
113 \\{"required":"something","another_required":"something else"}
114 ,
115 MyStruct{},
116 .{ .emit_null_optional_fields = false },
117 );
118}
119
120test "stringify basic types" {
121 try testStringify("false", false, .{});
122 try testStringify("true", true, .{});
123 try testStringify("null", @as(?u8, null), .{});
124 try testStringify("null", @as(?*u32, null), .{});
125 try testStringify("42", 42, .{});
126 try testStringify("4.2e1", 42.0, .{});
127 try testStringify("42", @as(u8, 42), .{});
128 try testStringify("42", @as(u128, 42), .{});
129 try testStringify("9999999999999999", 9999999999999999, .{});
130 try testStringify("4.2e1", @as(f32, 42), .{});
131 try testStringify("4.2e1", @as(f64, 42), .{});
132 try testStringify("\"ItBroke\"", @as(anyerror, error.ItBroke), .{});
133 try testStringify("\"ItBroke\"", error.ItBroke, .{});
134}
135
136test "stringify string" {
137 try testStringify("\"hello\"", "hello", .{});
138 try testStringify("\"with\\nescapes\\r\"", "with\nescapes\r", .{});
139 try testStringify("\"with\\nescapes\\r\"", "with\nescapes\r", .{ .escape_unicode = true });
140 try testStringify("\"with unicode\\u0001\"", "with unicode\u{1}", .{});
141 try testStringify("\"with unicode\\u0001\"", "with unicode\u{1}", .{ .escape_unicode = true });
142 try testStringify("\"with unicode\u{80}\"", "with unicode\u{80}", .{});
143 try testStringify("\"with unicode\\u0080\"", "with unicode\u{80}", .{ .escape_unicode = true });
144 try testStringify("\"with unicode\u{FF}\"", "with unicode\u{FF}", .{});
145 try testStringify("\"with unicode\\u00ff\"", "with unicode\u{FF}", .{ .escape_unicode = true });
146 try testStringify("\"with unicode\u{100}\"", "with unicode\u{100}", .{});
147 try testStringify("\"with unicode\\u0100\"", "with unicode\u{100}", .{ .escape_unicode = true });
148 try testStringify("\"with unicode\u{800}\"", "with unicode\u{800}", .{});
149 try testStringify("\"with unicode\\u0800\"", "with unicode\u{800}", .{ .escape_unicode = true });
150 try testStringify("\"with unicode\u{8000}\"", "with unicode\u{8000}", .{});
151 try testStringify("\"with unicode\\u8000\"", "with unicode\u{8000}", .{ .escape_unicode = true });
152 try testStringify("\"with unicode\u{D799}\"", "with unicode\u{D799}", .{});
153 try testStringify("\"with unicode\\ud799\"", "with unicode\u{D799}", .{ .escape_unicode = true });
154 try testStringify("\"with unicode\u{10000}\"", "with unicode\u{10000}", .{});
155 try testStringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}", .{ .escape_unicode = true });
156 try testStringify("\"with unicode\u{10FFFF}\"", "with unicode\u{10FFFF}", .{});
157 try testStringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}", .{ .escape_unicode = true });
158}
159
160test "stringify many-item sentinel-terminated string" {
161 try testStringify("\"hello\"", @as([*:0]const u8, "hello"), .{});
162 try testStringify("\"with\\nescapes\\r\"", @as([*:0]const u8, "with\nescapes\r"), .{ .escape_unicode = true });
163 try testStringify("\"with unicode\\u0001\"", @as([*:0]const u8, "with unicode\u{1}"), .{ .escape_unicode = true });
164}
165
166test "stringify enums" {
167 const E = enum {
168 foo,
169 bar,
170 };
171 try testStringify("\"foo\"", E.foo, .{});
172 try testStringify("\"bar\"", E.bar, .{});
173}
174
175test "stringify non-exhaustive enum" {
176 const E = enum(u8) {
177 foo = 0,
178 _,
179 };
180 try testStringify("\"foo\"", E.foo, .{});
181 try testStringify("1", @as(E, @enumFromInt(1)), .{});
182}
183
184test "stringify enum literals" {
185 try testStringify("\"foo\"", .foo, .{});
186 try testStringify("\"bar\"", .bar, .{});
187}
188
189test "stringify tagged unions" {
190 const T = union(enum) {
191 nothing,
192 foo: u32,
193 bar: bool,
194 };
195 try testStringify("{\"nothing\":{}}", T{ .nothing = {} }, .{});
196 try testStringify("{\"foo\":42}", T{ .foo = 42 }, .{});
197 try testStringify("{\"bar\":true}", T{ .bar = true }, .{});
198}
199
200test "stringify struct" {
201 try testStringify("{\"foo\":42}", struct {
202 foo: u32,
203 }{ .foo = 42 }, .{});
204}
205
206test "emit_strings_as_arrays" {
207 // Should only affect string values, not object keys.
208 try testStringify("{\"foo\":\"bar\"}", .{ .foo = "bar" }, .{});
209 try testStringify("{\"foo\":[98,97,114]}", .{ .foo = "bar" }, .{ .emit_strings_as_arrays = true });
210 // Should *not* affect these types:
211 try testStringify("\"foo\"", @as(enum { foo, bar }, .foo), .{ .emit_strings_as_arrays = true });
212 try testStringify("\"ItBroke\"", error.ItBroke, .{ .emit_strings_as_arrays = true });
213 // Should work on these:
214 try testStringify("\"bar\"", @Vector(3, u8){ 'b', 'a', 'r' }, .{});
215 try testStringify("[98,97,114]", @Vector(3, u8){ 'b', 'a', 'r' }, .{ .emit_strings_as_arrays = true });
216 try testStringify("\"bar\"", [3]u8{ 'b', 'a', 'r' }, .{});
217 try testStringify("[98,97,114]", [3]u8{ 'b', 'a', 'r' }, .{ .emit_strings_as_arrays = true });
218}
219
220test "stringify struct with indentation" {
221 try testStringify(
222 \\{
223 \\ "foo": 42,
224 \\ "bar": [
225 \\ 1,
226 \\ 2,
227 \\ 3
228 \\ ]
229 \\}
230 ,
231 struct {
232 foo: u32,
233 bar: [3]u32,
234 }{
235 .foo = 42,
236 .bar = .{ 1, 2, 3 },
237 },
238 .{ .whitespace = .indent_4 },
239 );
240 try testStringify(
241 "{\n\t\"foo\": 42,\n\t\"bar\": [\n\t\t1,\n\t\t2,\n\t\t3\n\t]\n}",
242 struct {
243 foo: u32,
244 bar: [3]u32,
245 }{
246 .foo = 42,
247 .bar = .{ 1, 2, 3 },
248 },
249 .{ .whitespace = .indent_tab },
250 );
251 try testStringify(
252 \\{"foo":42,"bar":[1,2,3]}
253 ,
254 struct {
255 foo: u32,
256 bar: [3]u32,
257 }{
258 .foo = 42,
259 .bar = .{ 1, 2, 3 },
260 },
261 .{ .whitespace = .minified },
262 );
263}
264
265test "stringify struct with void field" {
266 try testStringify("{\"foo\":42}", struct {
267 foo: u32,
268 bar: void = {},
269 }{ .foo = 42 }, .{});
270}
271
272test "stringify array of structs" {
273 const MyStruct = struct {
274 foo: u32,
275 };
276 try testStringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
277 MyStruct{ .foo = 42 },
278 MyStruct{ .foo = 100 },
279 MyStruct{ .foo = 1000 },
280 }, .{});
281}
282
283test "stringify struct with custom stringifier" {
284 try testStringify("[\"something special\",42]", struct {
285 foo: u32,
286 const Self = @This();
287 pub fn jsonStringify(value: @This(), jws: anytype) !void {
288 _ = value;
289 try jws.beginArray();
290 try jws.write("something special");
291 try jws.write(42);
292 try jws.endArray();
293 }
294 }{ .foo = 42 }, .{});
295}
296
297test "stringify vector" {
298 try testStringify("[1,1]", @as(@Vector(2, u32), @splat(1)), .{});
299 try testStringify("\"AA\"", @as(@Vector(2, u8), @splat('A')), .{});
300 try testStringify("[65,65]", @as(@Vector(2, u8), @splat('A')), .{ .emit_strings_as_arrays = true });
301}
302
303test "stringify tuple" {
304 try testStringify("[\"foo\",42]", std.meta.Tuple(&.{ []const u8, usize }){ "foo", 42 }, .{});
305}
306
307fn testStringify(expected: []const u8, value: anytype, options: StringifyOptions) !void {
308 const ValidationWriter = struct {
309 const Self = @This();
310 pub const Writer = std.io.Writer(*Self, Error, write);
311 pub const Error = error{
312 TooMuchData,
313 DifferentData,
314 };
315
316 expected_remaining: []const u8,
317
318 fn init(exp: []const u8) Self {
319 return .{ .expected_remaining = exp };
320 }
321
322 pub fn writer(self: *Self) Writer {
323 return .{ .context = self };
324 }
325
326 fn write(self: *Self, bytes: []const u8) Error!usize {
327 if (self.expected_remaining.len < bytes.len) {
328 std.debug.print(
329 \\====== expected this output: =========
330 \\{s}
331 \\======== instead found this: =========
332 \\{s}
333 \\======================================
334 , .{
335 self.expected_remaining,
336 bytes,
337 });
338 return error.TooMuchData;
339 }
340 if (!mem.eql(u8, self.expected_remaining[0..bytes.len], bytes)) {
341 std.debug.print(
342 \\====== expected this output: =========
343 \\{s}
344 \\======== instead found this: =========
345 \\{s}
346 \\======================================
347 , .{
348 self.expected_remaining[0..bytes.len],
349 bytes,
350 });
351 return error.DifferentData;
352 }
353 self.expected_remaining = self.expected_remaining[bytes.len..];
354 return bytes.len;
355 }
356 };
357
358 var vos = ValidationWriter.init(expected);
359 try stringifyArbitraryDepth(testing.allocator, value, options, vos.writer());
360 if (vos.expected_remaining.len > 0) return error.NotEnoughData;
361
362 // Also test with safety disabled.
363 try testStringifyMaxDepth(expected, value, options, null);
364 try testStringifyArbitraryDepth(expected, value, options);
365}
366
367fn testStringifyMaxDepth(expected: []const u8, value: anytype, options: StringifyOptions, comptime max_depth: ?usize) !void {
368 var out_buf: [1024]u8 = undefined;
369 var slice_stream = std.io.fixedBufferStream(&out_buf);
370 const out = slice_stream.writer();
371
372 try stringifyMaxDepth(value, options, out, max_depth);
373 const got = slice_stream.getWritten();
374
375 try testing.expectEqualStrings(expected, got);
376}
377
378fn testStringifyArbitraryDepth(expected: []const u8, value: anytype, options: StringifyOptions) !void {
379 var out_buf: [1024]u8 = undefined;
380 var slice_stream = std.io.fixedBufferStream(&out_buf);
381 const out = slice_stream.writer();
382
383 try stringifyArbitraryDepth(testing.allocator, value, options, out);
384 const got = slice_stream.getWritten();
385
386 try testing.expectEqualStrings(expected, got);
387}
388
389test "stringify alloc" {
390 const allocator = std.testing.allocator;
391 const expected =
392 \\{"foo":"bar","answer":42,"my_friend":"sammy"}
393 ;
394 const actual = try stringifyAlloc(allocator, .{ .foo = "bar", .answer = 42, .my_friend = "sammy" }, .{});
395 defer allocator.free(actual);
396
397 try std.testing.expectEqualStrings(expected, actual);
398}
399
400test "comptime stringify" {
401 comptime testStringifyMaxDepth("false", false, .{}, null) catch unreachable;
402 comptime testStringifyMaxDepth("false", false, .{}, 0) catch unreachable;
403 comptime testStringifyArbitraryDepth("false", false, .{}) catch unreachable;
404
405 const MyStruct = struct {
406 foo: u32,
407 };
408 comptime testStringifyMaxDepth("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
409 MyStruct{ .foo = 42 },
410 MyStruct{ .foo = 100 },
411 MyStruct{ .foo = 1000 },
412 }, .{}, null) catch unreachable;
413 comptime testStringifyMaxDepth("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
414 MyStruct{ .foo = 42 },
415 MyStruct{ .foo = 100 },
416 MyStruct{ .foo = 1000 },
417 }, .{}, 8) catch unreachable;
418}
419
420test "print" {
421 var out_buf: [1024]u8 = undefined;
422 var slice_stream = std.io.fixedBufferStream(&out_buf);
423 const out = slice_stream.writer();
424
425 var w = writeStream(out, .{ .whitespace = .indent_2 });
426 defer w.deinit();
427
428 try w.beginObject();
429 try w.objectField("a");
430 try w.print("[ ]", .{});
431 try w.objectField("b");
432 try w.beginArray();
433 try w.print("[{s}] ", .{"[]"});
434 try w.print(" {}", .{12345});
435 try w.endArray();
436 try w.endObject();
437
438 const result = slice_stream.getWritten();
439 const expected =
440 \\{
441 \\ "a": [ ],
442 \\ "b": [
443 \\ [[]] ,
444 \\ 12345
445 \\ ]
446 \\}
447 ;
448 try std.testing.expectEqualStrings(expected, result);
449}
450
451test "nonportable numbers" {
452 try testStringify("9999999999999999", 9999999999999999, .{});
453 try testStringify("\"9999999999999999\"", 9999999999999999, .{ .emit_nonportable_numbers_as_strings = true });
454}
455
456test "stringify raw streaming" {
457 var out_buf: [1024]u8 = undefined;
458 var slice_stream = std.io.fixedBufferStream(&out_buf);
459 const out = slice_stream.writer();
460
461 {
462 var w = writeStream(out, .{ .whitespace = .indent_2 });
463 try testRawStreaming(&w, &slice_stream);
464 }
465
466 {
467 var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, 8);
468 try testRawStreaming(&w, &slice_stream);
469 }
470
471 {
472 var w = writeStreamMaxDepth(out, .{ .whitespace = .indent_2 }, null);
473 try testRawStreaming(&w, &slice_stream);
474 }
475
476 {
477 var w = writeStreamArbitraryDepth(testing.allocator, out, .{ .whitespace = .indent_2 });
478 defer w.deinit();
479 try testRawStreaming(&w, &slice_stream);
480 }
481}
482
483fn testRawStreaming(w: anytype, slice_stream: anytype) !void {
484 slice_stream.reset();
485
486 try w.beginObject();
487 try w.beginObjectFieldRaw();
488 try w.stream.writeAll("\"long");
489 try w.stream.writeAll(" key\"");
490 w.endObjectFieldRaw();
491 try w.beginWriteRaw();
492 try w.stream.writeAll("\"long");
493 try w.stream.writeAll(" value\"");
494 w.endWriteRaw();
495 try w.endObject();
496
497 const result = slice_stream.getWritten();
498 const expected =
499 \\{
500 \\ "long key": "long value"
501 \\}
502 ;
503 try std.testing.expectEqualStrings(expected, result);
504}
lib/std/json/test.zig+2-2
...@@ -1,10 +1,10 @@...@@ -1,10 +1,10 @@
1const std = @import("std");1const std = @import("std");
2const json = std.json;
2const testing = std.testing;3const testing = std.testing;
3const parseFromSlice = @import("./static.zig").parseFromSlice;4const parseFromSlice = @import("./static.zig").parseFromSlice;
4const validate = @import("./scanner.zig").validate;5const validate = @import("./scanner.zig").validate;
5const JsonScanner = @import("./scanner.zig").Scanner;6const JsonScanner = @import("./scanner.zig").Scanner;
6const Value = @import("./dynamic.zig").Value;7const Value = @import("./dynamic.zig").Value;
7const stringifyAlloc = @import("./stringify.zig").stringifyAlloc;
88
9// Support for JSONTestSuite.zig9// Support for JSONTestSuite.zig
10pub fn ok(s: []const u8) !void {10pub fn ok(s: []const u8) !void {
...@@ -52,7 +52,7 @@ fn roundTrip(s: []const u8) !void {...@@ -52,7 +52,7 @@ fn roundTrip(s: []const u8) !void {
52 var parsed = try parseFromSlice(Value, testing.allocator, s, .{});52 var parsed = try parseFromSlice(Value, testing.allocator, s, .{});
53 defer parsed.deinit();53 defer parsed.deinit();
5454
55 const rendered = try stringifyAlloc(testing.allocator, parsed.value, .{});55 const rendered = try json.Stringify.valueAlloc(testing.allocator, parsed.value, .{});
56 defer testing.allocator.free(rendered);56 defer testing.allocator.free(rendered);
5757
58 try testing.expectEqualStrings(s, rendered);58 try testing.expectEqualStrings(s, rendered);