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