| author | |
| committer | |
| log | e154b62d63539cdd0f281dd4eaadc5ab229d2e24 |
| tree | d1f8267a580892f1f0447c8fd6009bb2a7108a4e |
| parent | 7569494b454aebeff67825fbbbe0cfe45e6e60d7 |
Avoid Redundant Names in Fully-Qualified Namespaces10 files changed, 1123 insertions(+), 1377 deletions(-)
CMakeLists.txt-1| ... | ... | @@ -459,7 +459,6 @@ set(ZIG_STAGE2_SOURCES |
| 459 | 459 | lib/std/io/limited_reader.zig |
| 460 | 460 | lib/std/io/seekable_stream.zig |
| 461 | 461 | lib/std/json.zig |
| 462 | lib/std/json/stringify.zig | |
| 463 | 462 | lib/std/leb128.zig |
| 464 | 463 | lib/std/log.zig |
| 465 | 464 | lib/std/macho.zig |
lib/std/json.zig+53-31| ... | ... | @@ -10,8 +10,8 @@ |
| 10 | 10 | //! The high-level `stringify` serializes a Zig or `Value` type into JSON. |
| 11 | 11 | |
| 12 | 12 | const builtin = @import("builtin"); |
| 13 | const testing = @import("std").testing; | |
| 14 | const ArrayList = @import("std").ArrayList; | |
| 13 | const std = @import("std"); | |
| 14 | const testing = std.testing; | |
| 15 | 15 | |
| 16 | 16 | test Scanner { |
| 17 | 17 | var scanner = Scanner.initCompleteInput(testing.allocator, "{\"foo\": 123}\n"); |
| ... | ... | @@ -41,11 +41,13 @@ test Value { |
| 41 | 41 | try testing.expectEqualSlices(u8, "goes", parsed.value.object.get("anything").?.string); |
| 42 | 42 | } |
| 43 | 43 | |
| 44 | test writeStream { | |
| 45 | var out = ArrayList(u8).init(testing.allocator); | |
| 44 | test Stringify { | |
| 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 | 50 | defer out.deinit(); |
| 47 | var write_stream = writeStream(out.writer(), .{ .whitespace = .indent_2 }); | |
| 48 | defer write_stream.deinit(); | |
| 49 | 51 | try write_stream.beginObject(); |
| 50 | 52 | try write_stream.objectField("foo"); |
| 51 | 53 | try write_stream.write(123); |
| ... | ... | @@ -55,16 +57,7 @@ test writeStream { |
| 55 | 57 | \\ "foo": 123 |
| 56 | 58 | \\} |
| 57 | 59 | ; |
| 58 | try testing.expectEqualSlices(u8, expected, out.items); | |
| 59 | } | |
| 60 | ||
| 61 | test 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); | |
| 60 | try testing.expectEqualSlices(u8, expected, out.getWritten()); | |
| 68 | 61 | } |
| 69 | 62 | |
| 70 | 63 | pub const ObjectMap = @import("json/dynamic.zig").ObjectMap; |
| ... | ... | @@ -99,20 +92,49 @@ pub const innerParseFromValue = @import("json/static.zig").innerParseFromValue; |
| 99 | 92 | pub const ParseError = @import("json/static.zig").ParseError; |
| 100 | 93 | pub const ParseFromValueError = @import("json/static.zig").ParseFromValueError; |
| 101 | 94 | |
| 102 | pub const StringifyOptions = @import("json/stringify.zig").StringifyOptions; | |
| 103 | pub const stringify = @import("json/stringify.zig").stringify; | |
| 104 | pub const stringifyMaxDepth = @import("json/stringify.zig").stringifyMaxDepth; | |
| 105 | pub const stringifyArbitraryDepth = @import("json/stringify.zig").stringifyArbitraryDepth; | |
| 106 | pub const stringifyAlloc = @import("json/stringify.zig").stringifyAlloc; | |
| 107 | pub const writeStream = @import("json/stringify.zig").writeStream; | |
| 108 | pub const writeStreamMaxDepth = @import("json/stringify.zig").writeStreamMaxDepth; | |
| 109 | pub const writeStreamArbitraryDepth = @import("json/stringify.zig").writeStreamArbitraryDepth; | |
| 110 | pub const WriteStream = @import("json/stringify.zig").WriteStream; | |
| 111 | pub const encodeJsonString = @import("json/stringify.zig").encodeJsonString; | |
| 112 | pub const encodeJsonStringChars = @import("json/stringify.zig").encodeJsonStringChars; | |
| 113 | ||
| 114 | pub const Formatter = @import("json/fmt.zig").Formatter; | |
| 115 | pub const fmt = @import("json/fmt.zig").fmt; | |
| 95 | pub const Stringify = @import("json/Stringify.zig"); | |
| 96 | ||
| 97 | /// Returns a formatter that formats the given value using stringify. | |
| 98 | pub fn fmt(value: anytype, options: Stringify.Options) Formatter(@TypeOf(value)) { | |
| 99 | return Formatter(@TypeOf(value)){ .value = value, .options = options }; | |
| 100 | } | |
| 101 | ||
| 102 | test fmt { | |
| 103 | const expectFmt = std.testing.expectFmt; | |
| 104 | try expectFmt("123", "{}", .{fmt(@as(u32, 123), .{})}); | |
| 105 | try expectFmt( | |
| 106 | \\{"num":927,"msg":"hello","sub":{"mybool":true}} | |
| 107 | , "{}", .{fmt(struct { | |
| 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. | |
| 121 | pub 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 | } | |
| 116 | 138 | |
| 117 | 139 | test { |
| 118 | 140 | _ = @import("json/test.zig"); |
| ... | ... | @@ -120,6 +142,6 @@ test { |
| 120 | 142 | _ = @import("json/dynamic.zig"); |
| 121 | 143 | _ = @import("json/hashmap.zig"); |
| 122 | 144 | _ = @import("json/static.zig"); |
| 123 | _ = @import("json/stringify.zig"); | |
| 145 | _ = Stringify; | |
| 124 | 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 | ||
| 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 | ||
| 20 | const std = @import("../std.zig"); | |
| 21 | const assert = std.debug.assert; | |
| 22 | const Allocator = std.mem.Allocator; | |
| 23 | const ArrayList = std.ArrayList; | |
| 24 | const BitStack = std.BitStack; | |
| 25 | const Stringify = @This(); | |
| 26 | ||
| 27 | const OBJECT_MODE = 0; | |
| 28 | const ARRAY_MODE = 1; | |
| 29 | ||
| 30 | writer: *std.io.BufferedWriter, | |
| 31 | options: Options = .{}, | |
| 32 | indent_level: usize = 0, | |
| 33 | next_punctuation: enum { | |
| 34 | the_beginning, | |
| 35 | none, | |
| 36 | comma, | |
| 37 | colon, | |
| 38 | } = .the_beginning, | |
| 39 | ||
| 40 | nesting_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 | ||
| 48 | raw_streaming_mode: if (build_mode_has_safety) | |
| 49 | enum { none, value, objectField } | |
| 50 | else | |
| 51 | void = if (build_mode_has_safety) .none else {}, | |
| 52 | ||
| 53 | const 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`. | |
| 67 | const 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 | ||
| 73 | const safety_checks: @TypeOf(safety_checks_hint) = if (build_mode_has_safety) | |
| 74 | safety_checks_hint | |
| 75 | else | |
| 76 | .assumed_correct; | |
| 77 | ||
| 78 | pub 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 | ||
| 86 | pub 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 | ||
| 94 | pub 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 | ||
| 108 | pub 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 | ||
| 122 | fn 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 | } | |
| 132 | fn 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 | ||
| 143 | fn 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 | ||
| 161 | fn 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 | } | |
| 165 | fn objectFieldStart(self: *Stringify) !void { | |
| 166 | if (self.isObjectKeyExpected()) |is_it| assert(is_it); // Expected write(), not objectField*(). | |
| 167 | return self.valueStartAssumeTypeOk(); | |
| 168 | } | |
| 169 | fn 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 | } | |
| 192 | fn valueDone(self: *Stringify) void { | |
| 193 | self.next_punctuation = .comma; | |
| 194 | } | |
| 195 | ||
| 196 | // Only when safety is enabled: | |
| 197 | fn 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 | } | |
| 205 | fn 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. | |
| 214 | pub 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 | ||
| 221 | test 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. | |
| 255 | pub 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`. | |
| 264 | pub 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`. | |
| 276 | pub 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`. | |
| 286 | pub 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. | |
| 298 | pub 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`. | |
| 307 | pub 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. | |
| 345 | pub 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 | ||
| 530 | fn stringValue(self: *Stringify, s: []const u8) !void { | |
| 531 | try self.valueStart(); | |
| 532 | try encodeJsonString(s, self.options, self.stream); | |
| 533 | self.valueDone(); | |
| 534 | } | |
| 535 | ||
| 536 | pub 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. | |
| 570 | pub 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 | ||
| 575 | test 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. | |
| 615 | pub 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 | ||
| 623 | test 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 | ||
| 634 | fn 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 | ||
| 654 | fn 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. | |
| 668 | pub 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. | |
| 675 | pub 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 | ||
| 716 | test "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 | ||
| 724 | fn 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 | ||
| 771 | fn 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 | ||
| 778 | test "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 | ||
| 799 | test "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 | ||
| 815 | test "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 | ||
| 839 | test "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 | ||
| 845 | test "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 | ||
| 854 | test "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 | ||
| 863 | test "stringify enum literals" { | |
| 864 | try testStringify("\"foo\"", .foo, .{}); | |
| 865 | try testStringify("\"bar\"", .bar, .{}); | |
| 866 | } | |
| 867 | ||
| 868 | test "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 | ||
| 879 | test "stringify struct" { | |
| 880 | try testStringify("{\"foo\":42}", struct { | |
| 881 | foo: u32, | |
| 882 | }{ .foo = 42 }, .{}); | |
| 883 | } | |
| 884 | ||
| 885 | test "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 | ||
| 899 | test "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 | ||
| 944 | test "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 | ||
| 955 | test "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 | ||
| 969 | fn 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 | ||
| 1025 | test "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 | 4 | const ArrayList = std.ArrayList; |
| 5 | 5 | const StringArrayHashMap = std.StringArrayHashMap; |
| 6 | 6 | const Allocator = std.mem.Allocator; |
| 7 | ||
| 8 | const StringifyOptions = @import("./stringify.zig").StringifyOptions; | |
| 9 | const stringify = @import("./stringify.zig").stringify; | |
| 7 | const json = std.json; | |
| 10 | 8 | |
| 11 | 9 | const ParseOptions = @import("./static.zig").ParseOptions; |
| 12 | 10 | const ParseError = @import("./static.zig").ParseError; |
| ... | ... | @@ -52,12 +50,11 @@ pub const Value = union(enum) { |
| 52 | 50 | } |
| 53 | 51 | } |
| 54 | 52 | |
| 55 | pub fn dump(self: Value) void { | |
| 56 | std.debug.lockStdErr(); | |
| 53 | pub fn dump(v: Value) void { | |
| 54 | var bw = std.debug.lockStdErr2(); | |
| 57 | 55 | defer std.debug.unlockStdErr(); |
| 58 | 56 | |
| 59 | const stderr = std.io.getStdErr().writer(); | |
| 60 | stringify(self, .{}, stderr) catch return; | |
| 57 | json.Stringify.value(v, .{}, &bw) catch return; | |
| 61 | 58 | } |
| 62 | 59 | |
| 63 | 60 | pub fn jsonStringify(value: @This(), jws: anytype) !void { |
lib/std/json/dynamic_test.zig+10-10| ... | ... | @@ -1,4 +1,5 @@ |
| 1 | 1 | const std = @import("std"); |
| 2 | const json = std.json; | |
| 2 | 3 | const mem = std.mem; |
| 3 | 4 | const testing = std.testing; |
| 4 | 5 | const ArenaAllocator = std.heap.ArenaAllocator; |
| ... | ... | @@ -70,13 +71,11 @@ test "json.parser.dynamic" { |
| 70 | 71 | try testing.expect(mem.eql(u8, large_int.number_string, "18446744073709551615")); |
| 71 | 72 | } |
| 72 | 73 | |
| 73 | const writeStream = @import("./stringify.zig").writeStream; | |
| 74 | 74 | test "write json then parse it" { |
| 75 | 75 | var out_buffer: [1000]u8 = undefined; |
| 76 | ||
| 77 | var fixed_buffer_stream = std.io.fixedBufferStream(&out_buffer); | |
| 78 | const out_stream = fixed_buffer_stream.writer(); | |
| 79 | var jw = writeStream(out_stream, .{}); | |
| 76 | var fixed_writer: std.io.BufferedWriter = undefined; | |
| 77 | fixed_writer.initFixed(&out_buffer); | |
| 78 | var jw: json.Stringify = .{ .writer = &fixed_writer, .options = .{} }; | |
| 80 | 79 | defer jw.deinit(); |
| 81 | 80 | |
| 82 | 81 | try jw.beginObject(); |
| ... | ... | @@ -101,8 +100,8 @@ test "write json then parse it" { |
| 101 | 100 | |
| 102 | 101 | try jw.endObject(); |
| 103 | 102 | |
| 104 | fixed_buffer_stream = std.io.fixedBufferStream(fixed_buffer_stream.getWritten()); | |
| 105 | var json_reader = jsonReader(testing.allocator, fixed_buffer_stream.reader()); | |
| 103 | var fbs: std.io.FixedBufferStream = .{ .buffer = fixed_writer.getWritten() }; | |
| 104 | var json_reader = jsonReader(testing.allocator, fbs.reader()); | |
| 106 | 105 | defer json_reader.deinit(); |
| 107 | 106 | var parsed = try parseFromTokenSource(Value, testing.allocator, &json_reader, .{}); |
| 108 | 107 | defer parsed.deinit(); |
| ... | ... | @@ -242,9 +241,10 @@ test "Value.jsonStringify" { |
| 242 | 241 | .{ .object = obj }, |
| 243 | 242 | }; |
| 244 | 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); | |
| 246 | 246 | |
| 247 | var jw = writeStream(fbs.writer(), .{ .whitespace = .indent_1 }); | |
| 247 | var jw: json.Stringify = .{ .writer = &fixed_writer, .options = .{ .whitespace = .indent_1 } }; | |
| 248 | 248 | defer jw.deinit(); |
| 249 | 249 | try jw.write(array); |
| 250 | 250 | |
| ... | ... | @@ -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 | } |
| 271 | 271 | |
| 272 | 272 | test "parseFromValue(std.json.Value,...)" { |
lib/std/json/fmt.zig deleted-46| ... | ... | @@ -1,46 +0,0 @@ |
| 1 | const std = @import("std"); | |
| 2 | ||
| 3 | const stringify = @import("stringify.zig").stringify; | |
| 4 | const StringifyOptions = @import("stringify.zig").StringifyOptions; | |
| 5 | ||
| 6 | /// Returns a formatter that formats the given value using stringify. | |
| 7 | pub 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. | |
| 12 | pub 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 | ||
| 30 | test 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 | 1 | const std = @import("std"); |
| 2 | const json = std.json; | |
| 2 | 3 | const testing = std.testing; |
| 3 | 4 | |
| 4 | 5 | const ArrayHashMap = @import("hashmap.zig").ArrayHashMap; |
| ... | ... | @@ -7,7 +8,6 @@ const parseFromSlice = @import("static.zig").parseFromSlice; |
| 7 | 8 | const parseFromSliceLeaky = @import("static.zig").parseFromSliceLeaky; |
| 8 | 9 | const parseFromTokenSource = @import("static.zig").parseFromTokenSource; |
| 9 | 10 | const parseFromValue = @import("static.zig").parseFromValue; |
| 10 | const stringifyAlloc = @import("stringify.zig").stringifyAlloc; | |
| 11 | 11 | const Value = @import("dynamic.zig").Value; |
| 12 | 12 | |
| 13 | 13 | const jsonReader = @import("./scanner.zig").reader; |
| ... | ... | @@ -89,7 +89,7 @@ test "stringify json hashmap" { |
| 89 | 89 | var value = ArrayHashMap(T){}; |
| 90 | 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 | 93 | defer testing.allocator.free(doc); |
| 94 | 94 | try testing.expectEqualStrings("{}", doc); |
| 95 | 95 | } |
| ... | ... | @@ -98,7 +98,7 @@ test "stringify json hashmap" { |
| 98 | 98 | try value.map.put(testing.allocator, "xyz", .{ .i = 1, .s = "w" }); |
| 99 | 99 | |
| 100 | 100 | { |
| 101 | const doc = try stringifyAlloc(testing.allocator, value, .{}); | |
| 101 | const doc = try json.Stringify.valueAlloc(testing.allocator, value, .{}); | |
| 102 | 102 | defer testing.allocator.free(doc); |
| 103 | 103 | try testing.expectEqualStrings( |
| 104 | 104 | \\{"abc":{"i":0,"s":"d"},"xyz":{"i":1,"s":"w"}} |
| ... | ... | @@ -107,7 +107,7 @@ test "stringify json hashmap" { |
| 107 | 107 | |
| 108 | 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 | 111 | defer testing.allocator.free(doc); |
| 112 | 112 | try testing.expectEqualStrings( |
| 113 | 113 | \\{"xyz":{"i":1,"s":"w"}} |
| ... | ... | @@ -116,7 +116,7 @@ test "stringify json hashmap" { |
| 116 | 116 | |
| 117 | 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 | 120 | defer testing.allocator.free(doc); |
| 121 | 121 | try testing.expectEqualStrings("{}", doc); |
| 122 | 122 | } |
| ... | ... | @@ -129,7 +129,7 @@ test "stringify json hashmap whitespace" { |
| 129 | 129 | try value.map.put(testing.allocator, "xyz", .{ .i = 1, .s = "w" }); |
| 130 | 130 | |
| 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 | 133 | defer testing.allocator.free(doc); |
| 134 | 134 | try testing.expectEqualStrings( |
| 135 | 135 | \\{ |
lib/std/json/stringify.zig deleted-770| ... | ... | @@ -1,770 +0,0 @@ |
| 1 | const std = @import("std"); | |
| 2 | const assert = std.debug.assert; | |
| 3 | const Allocator = std.mem.Allocator; | |
| 4 | const ArrayList = std.ArrayList; | |
| 5 | const BitStack = std.BitStack; | |
| 6 | ||
| 7 | const OBJECT_MODE = 0; | |
| 8 | const ARRAY_MODE = 1; | |
| 9 | ||
| 10 | pub 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`. | |
| 45 | pub 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. | |
| 59 | pub 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`. | |
| 72 | pub 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. | |
| 87 | pub 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. | |
| 102 | pub 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. | |
| 119 | pub 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*. | |
| 140 | pub 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 | ||
| 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`. | |
| 177 | pub 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 | ||
| 686 | fn 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 | ||
| 706 | fn 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. | |
| 720 | pub 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. | |
| 727 | pub 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 | ||
| 768 | test { | |
| 769 | _ = @import("./stringify_test.zig"); | |
| 770 | } |
lib/std/json/stringify_test.zig deleted-504| ... | ... | @@ -1,504 +0,0 @@ |
| 1 | const std = @import("std"); | |
| 2 | const mem = std.mem; | |
| 3 | const testing = std.testing; | |
| 4 | ||
| 5 | const ObjectMap = @import("dynamic.zig").ObjectMap; | |
| 6 | const Value = @import("dynamic.zig").Value; | |
| 7 | ||
| 8 | const StringifyOptions = @import("stringify.zig").StringifyOptions; | |
| 9 | const stringify = @import("stringify.zig").stringify; | |
| 10 | const stringifyMaxDepth = @import("stringify.zig").stringifyMaxDepth; | |
| 11 | const stringifyArbitraryDepth = @import("stringify.zig").stringifyArbitraryDepth; | |
| 12 | const stringifyAlloc = @import("stringify.zig").stringifyAlloc; | |
| 13 | const writeStream = @import("stringify.zig").writeStream; | |
| 14 | const writeStreamMaxDepth = @import("stringify.zig").writeStreamMaxDepth; | |
| 15 | const writeStreamArbitraryDepth = @import("stringify.zig").writeStreamArbitraryDepth; | |
| 16 | ||
| 17 | test "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 | ||
| 44 | fn 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 | ||
| 92 | fn 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 | ||
| 99 | test "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 | ||
| 120 | test "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 | ||
| 136 | test "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 | ||
| 160 | test "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 | ||
| 166 | test "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 | ||
| 175 | test "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 | ||
| 184 | test "stringify enum literals" { | |
| 185 | try testStringify("\"foo\"", .foo, .{}); | |
| 186 | try testStringify("\"bar\"", .bar, .{}); | |
| 187 | } | |
| 188 | ||
| 189 | test "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 | ||
| 200 | test "stringify struct" { | |
| 201 | try testStringify("{\"foo\":42}", struct { | |
| 202 | foo: u32, | |
| 203 | }{ .foo = 42 }, .{}); | |
| 204 | } | |
| 205 | ||
| 206 | test "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 | ||
| 220 | test "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 | ||
| 265 | test "stringify struct with void field" { | |
| 266 | try testStringify("{\"foo\":42}", struct { | |
| 267 | foo: u32, | |
| 268 | bar: void = {}, | |
| 269 | }{ .foo = 42 }, .{}); | |
| 270 | } | |
| 271 | ||
| 272 | test "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 | ||
| 283 | test "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 | ||
| 297 | test "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 | ||
| 303 | test "stringify tuple" { | |
| 304 | try testStringify("[\"foo\",42]", std.meta.Tuple(&.{ []const u8, usize }){ "foo", 42 }, .{}); | |
| 305 | } | |
| 306 | ||
| 307 | fn 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 | ||
| 367 | fn 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 | ||
| 378 | fn 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 | ||
| 389 | test "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 | ||
| 400 | test "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 | ||
| 420 | test "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 | ||
| 451 | test "nonportable numbers" { | |
| 452 | try testStringify("9999999999999999", 9999999999999999, .{}); | |
| 453 | try testStringify("\"9999999999999999\"", 9999999999999999, .{ .emit_nonportable_numbers_as_strings = true }); | |
| 454 | } | |
| 455 | ||
| 456 | test "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 | ||
| 483 | fn 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 | 1 | const std = @import("std"); |
| 2 | const json = std.json; | |
| 2 | 3 | const testing = std.testing; |
| 3 | 4 | const parseFromSlice = @import("./static.zig").parseFromSlice; |
| 4 | 5 | const validate = @import("./scanner.zig").validate; |
| 5 | 6 | const JsonScanner = @import("./scanner.zig").Scanner; |
| 6 | 7 | const Value = @import("./dynamic.zig").Value; |
| 7 | const stringifyAlloc = @import("./stringify.zig").stringifyAlloc; | |
| 8 | 8 | |
| 9 | 9 | // Support for JSONTestSuite.zig |
| 10 | 10 | pub fn ok(s: []const u8) !void { |
| ... | ... | @@ -52,7 +52,7 @@ fn roundTrip(s: []const u8) !void { |
| 52 | 52 | var parsed = try parseFromSlice(Value, testing.allocator, s, .{}); |
| 53 | 53 | defer parsed.deinit(); |
| 54 | 54 | |
| 55 | const rendered = try stringifyAlloc(testing.allocator, parsed.value, .{}); | |
| 55 | const rendered = try json.Stringify.valueAlloc(testing.allocator, parsed.value, .{}); | |
| 56 | 56 | defer testing.allocator.free(rendered); |
| 57 | 57 | |
| 58 | 58 | try testing.expectEqualStrings(s, rendered); |