├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig └── tres.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.zig text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**.zig" 7 | pull_request: 8 | paths: 9 | - "**.zig" 10 | schedule: 11 | - cron: "0 0 * * *" 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | submodules: true 25 | - uses: goto-bus-stop/setup-zig@v2 26 | with: 27 | version: master 28 | 29 | - run: zig version 30 | - run: zig env 31 | 32 | - name: Run Tests 33 | run: zig test tres.zig 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /zig-cache 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 tres contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tres 2 | 3 | `std.json.parse` but on `ValueTree`s. Stringify that supports Undefinedables, ArrayLists, and HashMaps. 4 | 5 | ## Features 6 | 7 | - `parse` (std.json.Value -> T) 8 | - `stringify` (T -> []const u8) 9 | - `toValue` (T -> std.json.Value) 10 | 11 | All the above modes support `std.json` standard features as well as: 12 | - Enhanced optionals (`tres_null_meaning`, see test "parse and stringify null meaning") 13 | - String enums (`tres_string_enum`, see test "json.stringify enums") 14 | - Field name remapping (`tres_remap`, see test "remapping") 15 | - Map and ArrayList support (unmanaged too!) 16 | 17 | ## License 18 | 19 | MIT 20 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | _ = b.addModule("tres", .{ 5 | .source_file = .{ .path = "tres.zig" }, 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /tres.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Validate with more granularity (for example, tres_string_enum makes no sense in a struct) 4 | fn validateCustomDecls(comptime T: type) void { 5 | const map = std.ComptimeStringMap(void, .{ 6 | .{ "tres_null_meaning", {} }, 7 | .{ "tres_string_enum", {} }, 8 | .{ "tres_remap", {} }, 9 | }); 10 | 11 | return switch (@typeInfo(T)) { 12 | .Struct, .Enum, .Union => { 13 | const decls = std.meta.declarations(T); 14 | inline for (decls) |decl| { 15 | if (map.has(decl.name) and !decl.is_pub) { 16 | @compileError("Found '" ++ decl.name ++ "' in '" ++ @typeName(T) ++ "' but it isn't public!"); 17 | } 18 | } 19 | }, 20 | else => {}, 21 | }; 22 | } 23 | 24 | /// Use after `isArrayList` and/or `isHashMap` 25 | pub fn isManaged(comptime T: type) bool { 26 | return @hasField(T, "allocator"); 27 | } 28 | 29 | pub fn isArrayList(comptime T: type) bool { 30 | // TODO: Improve this ArrayList check, specifically by actually checking the functions we use 31 | // TODO: Consider unmanaged ArrayLists 32 | if (!@hasField(T, "items")) return false; 33 | if (!@hasField(T, "capacity")) return false; 34 | 35 | return true; 36 | } 37 | 38 | pub fn isHashMap(comptime T: type) bool { 39 | // TODO: Consider unmanaged HashMaps 40 | 41 | if (!@hasDecl(T, "KV")) return false; 42 | 43 | if (!@hasField(T.KV, "key")) return false; 44 | if (!@hasField(T.KV, "value")) return false; 45 | 46 | const Key = std.meta.fields(T.KV)[std.meta.fieldIndex(T.KV, "key") orelse unreachable].type; 47 | const Value = std.meta.fields(T.KV)[std.meta.fieldIndex(T.KV, "value") orelse unreachable].type; 48 | 49 | if (!@hasDecl(T, "put")) return false; 50 | 51 | const put = @typeInfo(@TypeOf(T.put)); 52 | 53 | if (put != .Fn) return false; 54 | 55 | switch (put.Fn.params.len) { 56 | 3 => { 57 | if (put.Fn.params[0].type.? != *T) return false; 58 | if (put.Fn.params[1].type.? != Key) return false; 59 | if (put.Fn.params[2].type.? != Value) return false; 60 | }, 61 | 4 => { 62 | if (put.Fn.params[0].type.? != *T) return false; 63 | if (put.Fn.params[1].type.? != std.mem.Allocator) return false; 64 | if (put.Fn.params[2].type.? != Key) return false; 65 | if (put.Fn.params[3].type.? != Value) return false; 66 | }, 67 | else => return false, 68 | } 69 | 70 | if (put.Fn.return_type == null) return false; 71 | 72 | const put_return = @typeInfo(put.Fn.return_type.?); 73 | if (put_return != .ErrorUnion) return false; 74 | if (put_return.ErrorUnion.payload != void) return false; 75 | 76 | return true; 77 | } 78 | 79 | test "isManaged, isArrayList, isHashMap" { 80 | const T1 = std.ArrayList(u8); 81 | try std.testing.expect(isArrayList(T1) and isManaged(T1)); 82 | const T2 = std.ArrayListUnmanaged(u8); 83 | try std.testing.expect(isArrayList(T2) and !isManaged(T2)); 84 | 85 | const T3 = std.AutoHashMap(u8, u16); 86 | try std.testing.expect(isHashMap(T3) and isManaged(T3)); 87 | const T4 = std.AutoHashMapUnmanaged(u8, u16); 88 | try std.testing.expect(isHashMap(T4) and !isManaged(T4)); 89 | } 90 | 91 | /// Arena recommended. 92 | pub fn parse(comptime T: type, tree: std.json.Value, allocator: ?std.mem.Allocator) ParseInternalError(T)!T { 93 | return try parseInternal(T, tree, allocator, false); 94 | } 95 | 96 | pub fn Undefinedable(comptime T: type) type { 97 | return struct { 98 | const __json_T = T; 99 | const __json_is_undefinedable = true; 100 | 101 | value: T, 102 | missing: bool, 103 | 104 | pub fn asOptional(self: @This()) ?T { 105 | return if (self.missing) 106 | null 107 | else 108 | self.value; 109 | } 110 | 111 | pub fn format(self: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 112 | _ = fmt; 113 | _ = options; 114 | 115 | if (self.missing) 116 | try writer.print("Undefinedable({s}){{ missing }}", .{@typeName(T)}) 117 | else { 118 | try writer.print("Undefinedable({s}){{ .value = {any} }}", .{ @typeName(T), self.value }); 119 | } 120 | } 121 | }; 122 | } 123 | 124 | const NullMeaning = enum { 125 | /// ?T; a null leads to the field not being written 126 | field, 127 | /// ?T; a null leads to the field being written with the value null 128 | value, 129 | /// ??T; first null is field, second null is value 130 | dual, 131 | }; 132 | 133 | fn dualable(comptime T: type) bool { 134 | return @typeInfo(T) == .Optional and @typeInfo(@typeInfo(T).Optional.child) == .Optional; 135 | } 136 | 137 | // TODO: Respect stringify options 138 | fn nullMeaning(comptime T: type, comptime field: std.builtin.Type.StructField) ?NullMeaning { 139 | const true_default = td: { 140 | if (dualable(T)) break :td NullMeaning.dual; 141 | break :td null; 142 | }; 143 | if (!@hasDecl(T, "tres_null_meaning")) return true_default; 144 | const tnm = @field(T, "tres_null_meaning"); 145 | if (!@hasField(@TypeOf(tnm), field.name)) return true_default; 146 | return @field(tnm, field.name); 147 | } 148 | 149 | fn mightRemap(comptime T: type, comptime field: []const u8) []const u8 { 150 | if (!@hasDecl(T, "tres_remap")) return field; 151 | const remap = @field(T, "tres_remap"); 152 | if (!@hasField(@TypeOf(remap), field)) return field; 153 | return @field(remap, field); 154 | } 155 | 156 | pub fn ParseInternalError(comptime T: type) type { 157 | // `inferred_types` is used to avoid infinite recursion for recursive type definitions. 158 | const inferred_types = [_]type{}; 159 | return ParseInternalErrorImpl(T, &inferred_types); 160 | } 161 | 162 | fn ParseInternalErrorImpl(comptime T: type, comptime inferred_types: []const type) type { 163 | if (comptime std.meta.trait.isContainer(T) and @hasDecl(T, "tresParse")) { 164 | const tresParse_return = @typeInfo(@typeInfo(@TypeOf(T.tresParse)).Fn.return_type.?); 165 | if (tresParse_return == .ErrorUnion) { 166 | return tresParse_return.ErrorUnion.error_set; 167 | } else { 168 | return error{}; 169 | } 170 | } 171 | 172 | for (inferred_types) |ty| { 173 | if (T == ty) return error{}; 174 | } 175 | 176 | const inferred_set = inferred_types ++ [_]type{T}; 177 | 178 | switch (@typeInfo(T)) { 179 | .Bool, .Float => return error{UnexpectedFieldType}, 180 | .Int => return error{ UnexpectedFieldType, Overflow }, 181 | .Optional => |info| return ParseInternalErrorImpl(info.child, inferred_set), 182 | .Enum => return error{ InvalidEnumTag, UnexpectedFieldType }, 183 | .Union => |info| { 184 | var errors = error{UnexpectedFieldType}; 185 | 186 | for (info.fields) |field| { 187 | errors = errors || ParseInternalErrorImpl(field.type, inferred_set); 188 | } 189 | 190 | return errors; 191 | }, 192 | .Struct => |info| { 193 | var errors = error{ 194 | UnexpectedFieldType, 195 | InvalidFieldValue, 196 | MissingRequiredField, 197 | }; 198 | 199 | if (isArrayList(T)) { 200 | const Child = std.meta.Child(@field(T, "Slice")); 201 | 202 | errors = errors || ParseInternalErrorImpl(Child, inferred_set); 203 | } 204 | 205 | if (isHashMap(T)) { 206 | const Value = std.meta.fields(T.KV)[std.meta.fieldIndex(T.KV, "value") orelse unreachable].type; 207 | 208 | errors = errors || ParseInternalErrorImpl(Value, inferred_set); 209 | } 210 | 211 | if (isAllocatorRequired(T)) { 212 | errors = errors || error{AllocatorRequired} || std.mem.Allocator.Error; 213 | } 214 | 215 | for (info.fields) |field| { 216 | errors = errors || ParseInternalErrorImpl(field.type, inferred_set); 217 | } 218 | 219 | return errors; 220 | }, 221 | .Pointer => |info| { 222 | var errors = error{UnexpectedFieldType}; 223 | 224 | if (isAllocatorRequired(T)) { 225 | errors = errors || error{AllocatorRequired} || std.mem.Allocator.Error; 226 | } 227 | 228 | if (info.size == .Slice and info.child == u8 or info.child == std.json.Value) 229 | return errors; 230 | 231 | errors = errors || ParseInternalErrorImpl(info.child, inferred_set); 232 | 233 | return errors; 234 | }, 235 | .Array => |info| { 236 | var errors = error{UnexpectedFieldType}; 237 | 238 | errors = errors || ParseInternalErrorImpl(info.child, inferred_set); 239 | 240 | return errors; 241 | }, 242 | .Vector => |info| { 243 | var errors = error{UnexpectedFieldType}; 244 | 245 | errors = errors || ParseInternalErrorImpl(info.child, inferred_set); 246 | 247 | return errors; 248 | }, 249 | 250 | else => return error{}, 251 | } 252 | } 253 | 254 | pub fn isAllocatorRequired(comptime T: type) bool { 255 | // `inferred_types` is used to avoid infinite recursion for recursive type definitions. 256 | const inferred_types = [_]type{}; 257 | return isAllocatorRequiredImpl(T, &inferred_types); 258 | } 259 | 260 | fn isAllocatorRequiredImpl(comptime T: type, comptime inferred_types: []const type) bool { 261 | for (inferred_types) |ty| { 262 | if (T == ty) return false; 263 | } 264 | 265 | const inferred_set = inferred_types ++ [_]type{T}; 266 | 267 | switch (@typeInfo(T)) { 268 | .Optional => |info| return isAllocatorRequiredImpl(info.child, inferred_set), 269 | .Union => |info| { 270 | for (info.fields) |field| { 271 | if (isAllocatorRequiredImpl(field.type, inferred_set)) 272 | return true; 273 | } 274 | }, 275 | .Struct => |info| { 276 | if (isArrayList(T)) { 277 | if (T == std.json.Array) 278 | return false; 279 | 280 | return true; 281 | } 282 | 283 | if (isHashMap(T)) { 284 | if (T == std.json.ObjectMap) 285 | return false; 286 | 287 | return true; 288 | } 289 | 290 | for (info.fields) |field| { 291 | if (@typeInfo(field.type) == .Struct and @hasDecl(field.type, "__json_is_undefinedable")) { 292 | if (isAllocatorRequiredImpl(field.type.__json_T, inferred_set)) 293 | return true; 294 | } else if (isAllocatorRequiredImpl(field.type, inferred_set)) 295 | return true; 296 | } 297 | }, 298 | .Pointer => |info| { 299 | if (info.size == .Slice and info.child == u8 or info.child == std.json.Value) 300 | return false; 301 | 302 | return true; 303 | }, 304 | .Array => |info| { 305 | return isAllocatorRequiredImpl(info.child, inferred_set); 306 | }, 307 | .Vector => |info| { 308 | return isAllocatorRequiredImpl(info.child, inferred_set); // is it even possible for this to be true? 309 | }, 310 | else => {}, 311 | } 312 | 313 | return false; 314 | } 315 | 316 | const logger = std.log.scoped(.json); 317 | fn parseInternal( 318 | comptime T: type, 319 | json_value: std.json.Value, 320 | maybe_allocator: ?std.mem.Allocator, 321 | comptime suppress_error_logs: bool, 322 | ) ParseInternalError(T)!T { 323 | comptime validateCustomDecls(T); 324 | if (T == std.json.Value) return json_value; 325 | if (comptime std.meta.trait.isContainer(T) and @hasDecl(T, "tresParse")) { 326 | return T.tresParse(json_value, maybe_allocator); 327 | } 328 | 329 | switch (@typeInfo(T)) { 330 | .Bool => { 331 | if (json_value == .bool) { 332 | return json_value.bool; 333 | } else { 334 | if (comptime !suppress_error_logs) logger.debug("expected Bool, found {s}", .{@tagName(json_value)}); 335 | 336 | return error.UnexpectedFieldType; 337 | } 338 | }, 339 | .Float => { 340 | if (json_value == .float) { 341 | return @as(T, @floatCast(json_value.float)); 342 | } else if (json_value == .integer) { 343 | return @as(T, @floatFromInt(json_value.integer)); 344 | } else { 345 | if (comptime !suppress_error_logs) logger.debug("expected Float, found {s}", .{@tagName(json_value)}); 346 | 347 | return error.UnexpectedFieldType; 348 | } 349 | }, 350 | .Int => { 351 | if (json_value == .integer) { 352 | return std.math.cast(T, json_value.integer) orelse return error.Overflow; 353 | } else { 354 | if (comptime !suppress_error_logs) logger.debug("expected Integer, found {s}", .{@tagName(json_value)}); 355 | 356 | return error.UnexpectedFieldType; 357 | } 358 | }, 359 | .Optional => |info| { 360 | if (json_value == .null) { 361 | return null; 362 | } else { 363 | return try parseInternal( 364 | info.child, 365 | json_value, 366 | maybe_allocator, 367 | suppress_error_logs, 368 | ); 369 | } 370 | }, 371 | .Enum => { 372 | if (json_value == .integer) { 373 | // we use this to convert signed to unsigned and check if it actually fits. 374 | const tag = std.math.cast(std.meta.Tag(T), json_value.integer) orelse { 375 | if (comptime !suppress_error_logs) logger.debug("invalid enum tag for {s}, found {d}", .{ @typeName(T), json_value.integer }); 376 | 377 | return error.InvalidEnumTag; 378 | }; 379 | 380 | return try std.meta.intToEnum(T, tag); 381 | } else if (json_value == .string) { 382 | return std.meta.stringToEnum(T, json_value.string) orelse { 383 | if (comptime !suppress_error_logs) logger.debug("invalid enum tag for {s}, found '{s}'", .{ @typeName(T), json_value.string }); 384 | 385 | return error.InvalidEnumTag; 386 | }; 387 | } else { 388 | if (comptime !suppress_error_logs) logger.debug("expected Integer or String, found {s}", .{@tagName(json_value)}); 389 | 390 | return error.UnexpectedFieldType; 391 | } 392 | }, 393 | .Union => |info| { 394 | if (info.tag_type != null) { 395 | inline for (info.fields) |field| { 396 | if (parseInternal( 397 | field.type, 398 | json_value, 399 | maybe_allocator, 400 | true, 401 | )) |parsed_value| { 402 | return @unionInit(T, field.name, parsed_value); 403 | } else |_| {} 404 | } 405 | 406 | if (comptime !suppress_error_logs) logger.debug("union fell through for {s}, found {s}", .{ @typeName(T), @tagName(json_value) }); 407 | 408 | return error.UnexpectedFieldType; 409 | } else { 410 | @compileError("cannot parse an untagged union: " ++ @typeName(T)); 411 | } 412 | }, 413 | .Struct => |info| { 414 | if (comptime isArrayList(T)) { 415 | const Child = std.meta.Child(@field(T, "Slice")); 416 | 417 | if (json_value == .array) { 418 | if (T == std.json.Array) return json_value.array; 419 | 420 | const allocator = maybe_allocator orelse return error.AllocatorRequired; 421 | 422 | var array_list = try T.initCapacity(allocator, json_value.array.capacity); 423 | 424 | for (json_value.array.items) |item| { 425 | if (comptime isManaged(T)) 426 | try array_list.append(try parseInternal( 427 | Child, 428 | item, 429 | maybe_allocator, 430 | suppress_error_logs, 431 | )) 432 | else 433 | try array_list.append(allocator, try parseInternal( 434 | Child, 435 | item, 436 | maybe_allocator, 437 | suppress_error_logs, 438 | )); 439 | } 440 | 441 | return array_list; 442 | } else { 443 | if (comptime !suppress_error_logs) logger.debug("expected array of {s}, found {s}", .{ @typeName(Child), @tagName(json_value) }); 444 | return error.UnexpectedFieldType; 445 | } 446 | } 447 | 448 | if (comptime isHashMap(T)) { 449 | const managed = comptime isManaged(T); 450 | 451 | const Key = std.meta.fields(T.KV)[std.meta.fieldIndex(T.KV, "key") orelse unreachable].type; 452 | const Value = std.meta.fields(T.KV)[std.meta.fieldIndex(T.KV, "value") orelse unreachable].type; 453 | 454 | if (Key != []const u8) @compileError("HashMap key must be of type []const u8!"); 455 | 456 | if (json_value == .object) { 457 | if (T == std.json.ObjectMap) return json_value.object; 458 | 459 | const allocator = maybe_allocator orelse return error.AllocatorRequired; 460 | 461 | var map: T = if (managed) T.init(allocator) else .{}; 462 | var map_iterator = json_value.object.iterator(); 463 | 464 | while (map_iterator.next()) |entry| { 465 | if (managed) 466 | try map.put(entry.key_ptr.*, try parseInternal( 467 | Value, 468 | entry.value_ptr.*, 469 | maybe_allocator, 470 | suppress_error_logs, 471 | )) 472 | else 473 | try map.put(allocator, entry.key_ptr.*, try parseInternal( 474 | Value, 475 | entry.value_ptr.*, 476 | maybe_allocator, 477 | suppress_error_logs, 478 | )); 479 | } 480 | 481 | return map; 482 | } else { 483 | if (comptime !suppress_error_logs) logger.debug("expected map of {s} found {s}", .{ @typeName(Value), @tagName(json_value) }); 484 | return error.UnexpectedFieldType; 485 | } 486 | } 487 | 488 | if (info.is_tuple) { 489 | if (json_value != .array) { 490 | if (comptime !suppress_error_logs) logger.debug("expected Array, found {s}", .{@tagName(json_value)}); 491 | return error.UnexpectedFieldType; 492 | } 493 | 494 | if (json_value.array.items.len != std.meta.fields(T).len) { 495 | if (comptime !suppress_error_logs) logger.debug("expected Array to match length of Tuple {s} but it doesn't", .{@typeName(T)}); 496 | return error.UnexpectedFieldType; 497 | } 498 | 499 | var tuple: T = undefined; 500 | comptime var index: usize = 0; 501 | 502 | inline while (index < std.meta.fields(T).len) : (index += 1) { 503 | tuple[index] = try parseInternal( 504 | std.meta.fields(T)[index].type, 505 | json_value.array.items[index], 506 | maybe_allocator, 507 | suppress_error_logs, 508 | ); 509 | } 510 | 511 | return tuple; 512 | } 513 | 514 | if (json_value == .object) { 515 | var result: T = undefined; 516 | 517 | // Must use in order to bypass [#2727](https://github.com/ziglang/zig/issues/2727) :( 518 | var missing_field = false; 519 | 520 | inline for (info.fields) |field| { 521 | const nm = comptime nullMeaning(T, field) orelse .value; 522 | 523 | const field_value = json_value.object.get(mightRemap(T, field.name)); 524 | 525 | if (field.is_comptime) { 526 | if (field_value == null) { 527 | if (comptime !suppress_error_logs) logger.debug("comptime field {s}.{s} missing", .{ @typeName(T), field.name }); 528 | 529 | return error.InvalidFieldValue; 530 | } 531 | 532 | if (field.default_value) |default| { 533 | const parsed_value = try parseInternal( 534 | field.type, 535 | field_value.?, 536 | maybe_allocator, 537 | suppress_error_logs, 538 | ); 539 | const default_value = @as(*const field.type, @ptrCast(@alignCast(default))).*; 540 | 541 | // NOTE: This only works for strings! 542 | // TODODODODODODO ASAP 543 | if (!std.mem.eql(u8, parsed_value, default_value)) { 544 | if (comptime !suppress_error_logs) logger.debug("comptime field {s}.{s} does not match", .{ @typeName(T), field.name }); 545 | 546 | return error.InvalidFieldValue; 547 | } 548 | } else unreachable; // zig requires comptime fields to have a default initialization value 549 | } else if (comptime dualable(field.type) and nm == .dual) { 550 | if (field_value == null) { 551 | @field(result, field.name) = null; 552 | } else { 553 | @field(result, field.name) = try parseInternal(@typeInfo(@TypeOf(@field(result, field.name))).Optional.child, field_value.?, maybe_allocator, suppress_error_logs); 554 | } 555 | } else { 556 | if (field_value) |fv| { 557 | if (@typeInfo(field.type) == .Struct and @hasDecl(field.type, "__json_is_undefinedable")) 558 | @field(result, field.name) = .{ 559 | .value = try parseInternal( 560 | field.type.__json_T, 561 | fv, 562 | maybe_allocator, 563 | suppress_error_logs, 564 | ), 565 | .missing = false, 566 | } 567 | else 568 | @field(result, field.name) = try parseInternal( 569 | field.type, 570 | fv, 571 | maybe_allocator, 572 | suppress_error_logs, 573 | ); 574 | } else { 575 | if (@typeInfo(field.type) == .Struct and @hasDecl(field.type, "__json_is_undefinedable")) { 576 | @field(result, field.name) = .{ 577 | .value = undefined, 578 | .missing = true, 579 | }; 580 | } else if (field.default_value) |default| { 581 | const default_value = @as(*const field.type, @ptrCast(@alignCast(default))).*; 582 | @field(result, field.name) = default_value; 583 | } else if (@typeInfo(field.type) == .Optional and nm == .field) { 584 | @field(result, field.name) = null; 585 | } else { 586 | if (comptime !suppress_error_logs) logger.debug("required field {s}.{s} missing", .{ @typeName(T), field.name }); 587 | 588 | missing_field = true; 589 | } 590 | } 591 | } 592 | } 593 | 594 | if (missing_field) return error.MissingRequiredField; 595 | 596 | return result; 597 | } else { 598 | if (comptime !suppress_error_logs) logger.debug("expected Object, found {s}", .{@tagName(json_value)}); 599 | 600 | return error.UnexpectedFieldType; 601 | } 602 | }, 603 | .Pointer => |info| { 604 | if (info.size == .Slice) { 605 | if (info.child == u8) { 606 | if (json_value == .string) { 607 | return json_value.string; 608 | } else { 609 | if (comptime !suppress_error_logs) logger.debug("expected String, found {s}", .{@tagName(json_value)}); 610 | 611 | return error.UnexpectedFieldType; 612 | } 613 | } else if (info.child == std.json.Value) { 614 | return json_value.array.items; 615 | } 616 | } 617 | 618 | const allocator = maybe_allocator orelse return error.AllocatorRequired; 619 | switch (info.size) { 620 | .Slice, .Many => { 621 | const sentinel = if (info.sentinel) |ptr| @as(*const info.child, @ptrCast(ptr)).* else null; 622 | 623 | if (info.child == u8 and json_value == .string) { 624 | const array = try allocator.allocWithOptions( 625 | info.child, 626 | json_value.string.len, 627 | info.alignment, 628 | sentinel, 629 | ); 630 | 631 | std.mem.copy(u8, array, json_value.string); 632 | 633 | return @as(T, @ptrCast(array)); 634 | } 635 | 636 | if (json_value == .array) { 637 | if (info.child == std.json.Value) return json_value.array.items; 638 | 639 | const array = try allocator.allocWithOptions( 640 | info.child, 641 | json_value.array.items.len, 642 | info.alignment, 643 | sentinel, 644 | ); 645 | 646 | for (json_value.array.items, 0..) |item, index| 647 | array[index] = try parseInternal( 648 | info.child, 649 | item, 650 | maybe_allocator, 651 | suppress_error_logs, 652 | ); 653 | 654 | return @as(T, @ptrCast(array)); 655 | } else { 656 | if (comptime !suppress_error_logs) logger.debug("expected Array, found {s}", .{@tagName(json_value)}); 657 | 658 | return error.UnexpectedFieldType; 659 | } 660 | }, 661 | .One, .C => { 662 | const data = try allocator.allocWithOptions(info.child, 1, info.alignment, null); 663 | 664 | data[0] = try parseInternal( 665 | info.child, 666 | json_value, 667 | maybe_allocator, 668 | suppress_error_logs, 669 | ); 670 | 671 | return &data[0]; 672 | }, 673 | } 674 | }, 675 | .Array => |info| { 676 | if (json_value == .array) { 677 | var array: T = undefined; 678 | 679 | if (info.sentinel) |ptr| { 680 | const sentinel = @as(*const info.child, @ptrCast(ptr)).*; 681 | 682 | array[array.len] = sentinel; 683 | } 684 | 685 | if (json_value.array.items.len != info.len) { 686 | if (comptime !suppress_error_logs) logger.debug("expected Array to match length of {s} but it doesn't", .{@typeName(T)}); 687 | return error.UnexpectedFieldType; 688 | } 689 | 690 | for (array, 0..) |_, index| 691 | array[index] = try parseInternal( 692 | info.child, 693 | json_value.array.items[index], 694 | maybe_allocator, 695 | suppress_error_logs, 696 | ); 697 | 698 | return array; 699 | } else { 700 | if (comptime !suppress_error_logs) logger.debug("expected Array, found {s}", .{@tagName(json_value)}); 701 | 702 | return error.UnexpectedFieldType; 703 | } 704 | }, 705 | .Vector => |info| { 706 | if (json_value == .array) { 707 | var vector: T = undefined; 708 | 709 | if (json_value.array.items.len != info.len) { 710 | if (comptime !suppress_error_logs) logger.debug("expected Array to match length of {s} ({d}) but it doesn't", .{ @typeName(T), info.len }); 711 | return error.UnexpectedFieldType; 712 | } 713 | 714 | for (vector) |*item| 715 | item.* = try parseInternal( 716 | info.child, 717 | item, 718 | maybe_allocator, 719 | suppress_error_logs, 720 | ); 721 | 722 | return vector; 723 | } else { 724 | if (comptime !suppress_error_logs) logger.debug("expected Array, found {s}", .{@tagName(json_value)}); 725 | 726 | return error.UnexpectedFieldType; 727 | } 728 | }, 729 | .Void => return, 730 | else => { 731 | @compileError("unhandled json type: " ++ @typeName(T)); 732 | }, 733 | } 734 | } 735 | 736 | fn outputUnicodeEscape( 737 | codepoint: u21, 738 | out_stream: anytype, 739 | ) !void { 740 | if (codepoint <= 0xFFFF) { 741 | // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF), 742 | // then it may be represented as a six-character sequence: a reverse solidus, followed 743 | // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point. 744 | try out_stream.writeAll("\\u"); 745 | try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); 746 | } else { 747 | std.debug.assert(codepoint <= 0x10FFFF); 748 | // To escape an extended character that is not in the Basic Multilingual Plane, 749 | // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair. 750 | const high = @as(u16, @intCast((codepoint - 0x10000) >> 10)) + 0xD800; 751 | const low = @as(u16, @intCast(codepoint & 0x3FF)) + 0xDC00; 752 | try out_stream.writeAll("\\u"); 753 | try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); 754 | try out_stream.writeAll("\\u"); 755 | try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); 756 | } 757 | } 758 | 759 | fn outputJsonString(value: []const u8, options: std.json.StringifyOptions, out_stream: anytype) !void { 760 | try out_stream.writeByte('\"'); 761 | var i: usize = 0; 762 | while (i < value.len) : (i += 1) { 763 | switch (value[i]) { 764 | // normal ascii character 765 | 0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => |c| try out_stream.writeByte(c), 766 | // only 2 characters that *must* be escaped 767 | '\\' => try out_stream.writeAll("\\\\"), 768 | '\"' => try out_stream.writeAll("\\\""), 769 | // solidus is optional to escape 770 | '/' => { 771 | if (options.string.String.escape_solidus) { 772 | try out_stream.writeAll("\\/"); 773 | } else { 774 | try out_stream.writeByte('/'); 775 | } 776 | }, 777 | // control characters with short escapes 778 | // TODO: option to switch between unicode and 'short' forms? 779 | 0x8 => try out_stream.writeAll("\\b"), 780 | 0xC => try out_stream.writeAll("\\f"), 781 | '\n' => try out_stream.writeAll("\\n"), 782 | '\r' => try out_stream.writeAll("\\r"), 783 | '\t' => try out_stream.writeAll("\\t"), 784 | else => { 785 | const ulen = std.unicode.utf8ByteSequenceLength(value[i]) catch unreachable; 786 | // control characters (only things left with 1 byte length) should always be printed as unicode escapes 787 | if (ulen == 1 or options.string.String.escape_unicode) { 788 | const codepoint = std.unicode.utf8Decode(value[i .. i + ulen]) catch unreachable; 789 | try outputUnicodeEscape(codepoint, out_stream); 790 | } else { 791 | try out_stream.writeAll(value[i .. i + ulen]); 792 | } 793 | i += ulen - 1; 794 | }, 795 | } 796 | } 797 | try out_stream.writeByte('\"'); 798 | } 799 | 800 | pub fn stringify( 801 | value: anytype, 802 | options: std.json.StringifyOptions, 803 | out_stream: anytype, 804 | ) @TypeOf(out_stream).Error!void { 805 | const T = @TypeOf(value); 806 | comptime validateCustomDecls(T); 807 | switch (@typeInfo(T)) { 808 | .Float, .ComptimeFloat => { 809 | return std.fmt.formatFloatScientific(value, std.fmt.FormatOptions{}, out_stream); 810 | }, 811 | .Int, .ComptimeInt => { 812 | return std.fmt.formatIntValue(value, "", std.fmt.FormatOptions{}, out_stream); 813 | }, 814 | .Bool => { 815 | return out_stream.writeAll(if (value) "true" else "false"); 816 | }, 817 | .Null => { 818 | return out_stream.writeAll("null"); 819 | }, 820 | .Optional => { 821 | if (value) |payload| { 822 | return try stringify(payload, options, out_stream); 823 | } else { 824 | return try stringify(null, options, out_stream); 825 | } 826 | }, 827 | .Enum => { 828 | if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { 829 | return value.jsonStringify(options, out_stream); 830 | } 831 | 832 | if (@hasDecl(T, "tres_string_enum")) { 833 | return try stringify(@tagName(value), options, out_stream); 834 | } else { 835 | return try stringify(@intFromEnum(value), options, out_stream); 836 | } 837 | }, 838 | .Union => { 839 | if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { 840 | return value.jsonStringify(options, out_stream); 841 | } 842 | 843 | const info = @typeInfo(T).Union; 844 | if (info.tag_type) |UnionTagType| { 845 | inline for (info.fields) |u_field| { 846 | if (value == @field(UnionTagType, u_field.name)) { 847 | return try stringify(@field(value, u_field.name), options, out_stream); 848 | } 849 | } 850 | return; 851 | } else { 852 | @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'"); 853 | } 854 | }, 855 | .Struct => |S| { 856 | if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { 857 | return value.jsonStringify(options, out_stream); 858 | } 859 | 860 | if (comptime isArrayList(T)) { 861 | return stringify(value.items, options, out_stream); 862 | } 863 | 864 | try out_stream.writeByte('{'); 865 | var field_output = false; 866 | var child_options = options; 867 | child_options.whitespace.indent_level += 1; 868 | 869 | if (comptime isHashMap(T)) { 870 | var iterator = value.iterator(); 871 | 872 | while (iterator.next()) |entry| { 873 | if (!field_output) { 874 | field_output = true; 875 | } else { 876 | try out_stream.writeByte(','); 877 | } 878 | try child_options.whitespace.outputIndent(out_stream); 879 | try outputJsonString(entry.key_ptr.*, options, out_stream); 880 | try out_stream.writeByte(':'); 881 | if (child_options.whitespace.separator) { 882 | try out_stream.writeByte(' '); 883 | } 884 | 885 | try stringify(entry.value_ptr.*, child_options, out_stream); 886 | } 887 | } else { 888 | inline for (S.fields) |Field| { 889 | const nm = nullMeaning(T, Field) orelse (if (options.emit_null_optional_fields) NullMeaning.value else NullMeaning.field); 890 | 891 | // don't include void fields 892 | if (Field.type == void) continue; 893 | 894 | var emit_field = true; 895 | 896 | // don't include optional fields that are null when emit_null_optional_fields is set to false 897 | if (@typeInfo(Field.type) == .Optional) { 898 | if (nm == .field or nm == .dual) { 899 | if (@field(value, Field.name) == null) { 900 | emit_field = false; 901 | } 902 | } 903 | } 904 | 905 | const is_undefinedable = comptime @typeInfo(@TypeOf(@field(value, Field.name))) == .Struct and @hasDecl(@TypeOf(@field(value, Field.name)), "__json_is_undefinedable"); 906 | if (is_undefinedable) { 907 | if (@field(value, Field.name).missing) 908 | emit_field = false; 909 | } 910 | 911 | if (emit_field) { 912 | if (!field_output) { 913 | field_output = true; 914 | } else { 915 | try out_stream.writeByte(','); 916 | } 917 | try child_options.whitespace.outputIndent(out_stream); 918 | try outputJsonString(mightRemap(T, Field.name), options, out_stream); 919 | try out_stream.writeByte(':'); 920 | if (child_options.whitespace.separator) { 921 | try out_stream.writeByte(' '); 922 | } 923 | 924 | if (is_undefinedable) { 925 | try stringify(@field(value, Field.name).value, child_options, out_stream); 926 | } else if ((comptime dualable(Field.type)) and nm == .dual) 927 | try stringify(@field(value, Field.name).?, child_options, out_stream) 928 | else { 929 | try stringify(@field(value, Field.name), child_options, out_stream); 930 | } 931 | } 932 | } 933 | } 934 | 935 | if (field_output) { 936 | try options.whitespace.outputIndent(out_stream); 937 | } 938 | try out_stream.writeByte('}'); 939 | return; 940 | }, 941 | .ErrorSet => return stringify(@as([]const u8, @errorName(value)), options, out_stream), 942 | .Pointer => |ptr_info| switch (ptr_info.size) { 943 | .One => switch (@typeInfo(ptr_info.child)) { 944 | .Array => { 945 | const Slice = []const std.meta.Elem(ptr_info.child); 946 | return stringify(@as(Slice, value), options, out_stream); 947 | }, 948 | else => { 949 | // TODO: avoid loops? 950 | return stringify(value.*, options, out_stream); 951 | }, 952 | }, 953 | // TODO: .Many when there is a sentinel (waiting for https://github.com/ziglang/zig/pull/3972) 954 | .Slice => { 955 | if (ptr_info.child == u8 and options.string == .String and std.unicode.utf8ValidateSlice(value)) { 956 | try outputJsonString(value, options, out_stream); 957 | return; 958 | } 959 | 960 | try out_stream.writeByte('['); 961 | var child_options = options; 962 | child_options.whitespace.indent_level += 1; 963 | 964 | for (value, 0..) |x, i| { 965 | if (i != 0) { 966 | try out_stream.writeByte(','); 967 | } 968 | try child_options.whitespace.outputIndent(out_stream); 969 | try stringify(x, child_options, out_stream); 970 | } 971 | if (value.len != 0) { 972 | try options.whitespace.outputIndent(out_stream); 973 | } 974 | try out_stream.writeByte(']'); 975 | return; 976 | }, 977 | else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"), 978 | }, 979 | .Array => return stringify(&value, options, out_stream), 980 | .Vector => |info| { 981 | const array: [info.len]info.child = value; 982 | return stringify(&array, options, out_stream); 983 | }, 984 | .Void => return try out_stream.writeAll("{}"), 985 | else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"), 986 | } 987 | unreachable; 988 | } 989 | 990 | pub const ToValueOptions = struct { 991 | copy_strings: bool = false, 992 | // TODO: Add string options 993 | }; 994 | 995 | /// Arena recommended. 996 | pub fn toValue( 997 | allocator: std.mem.Allocator, 998 | value: anytype, 999 | options: ToValueOptions, 1000 | ) std.mem.Allocator.Error!std.json.Value { 1001 | const T = @TypeOf(value); 1002 | comptime validateCustomDecls(T); 1003 | 1004 | if (T == std.json.Value) return value; 1005 | if (comptime std.meta.trait.isContainer(T) and @hasDecl(T, "tresParse")) { 1006 | return T.tresToValue(allocator, value, options); 1007 | } 1008 | 1009 | switch (@typeInfo(T)) { 1010 | .Bool => { 1011 | return .{ 1012 | .bool = value, 1013 | }; 1014 | }, 1015 | .Float => { 1016 | return .{ 1017 | .float = value, 1018 | }; 1019 | }, 1020 | .Int => |i| { 1021 | return if (i.bits > 64) .{ 1022 | .number_string = std.fmt.allocPrint(allocator, "{d}", .{value}), 1023 | } else .{ 1024 | .integer = value, 1025 | }; 1026 | }, 1027 | .Optional => { 1028 | return if (value) |val| 1029 | toValue(allocator, val, options) 1030 | else 1031 | .null; 1032 | }, 1033 | .Enum => { 1034 | return if (@hasDecl(T, "tres_string_enum")) .{ .string = @tagName(value) } else toValue(allocator, @intFromEnum(value), options); 1035 | }, 1036 | .Union => |info| { 1037 | if (info.tag_type != null) { 1038 | inline for (info.fields) |field| { 1039 | if (@field(T, field.name) == value) { 1040 | return toValue(allocator, @field(value, field.name), options); 1041 | } 1042 | } 1043 | 1044 | unreachable; 1045 | } else { 1046 | @compileError("cannot toValue an untagged union: " ++ @typeName(T)); 1047 | } 1048 | }, 1049 | .Struct => |info| { 1050 | if (comptime isArrayList(T)) { 1051 | const Child = std.meta.Child(@field(T, "Slice")); 1052 | 1053 | if (Child == u8) { 1054 | return .{ .string = if (options.copy_strings) 1055 | try allocator.dupe(u8, value.items) 1056 | else 1057 | value.items }; 1058 | } else { 1059 | var arr = std.json.Array.initCapacity(allocator, value.items); 1060 | for (value.items) |item| try arr.append(try toValue(allocator, item, options)); 1061 | return .{ .array = arr }; 1062 | } 1063 | } 1064 | 1065 | if (comptime isHashMap(T)) { 1066 | const Key = std.meta.fields(T.KV)[std.meta.fieldIndex(T.KV, "key") orelse unreachable].type; 1067 | // const Value = std.meta.fields(T.KV)[std.meta.fieldIndex(T.KV, "value") orelse unreachable].type; 1068 | 1069 | if (Key != []const u8) @compileError("HashMap key must be of type []const u8!"); 1070 | 1071 | var obj = std.json.ObjectMap.init(allocator); 1072 | var it = value.iterator(); 1073 | while (it.next()) |entry| { 1074 | try obj.put(if (options.copy_strings) 1075 | try allocator.dupe(u8, entry.key_ptr.*) 1076 | else 1077 | entry.key_ptr.*, try toValue(allocator, entry.value_ptr.*, options)); 1078 | } 1079 | return .{ .object = obj }; 1080 | } 1081 | 1082 | if (info.is_tuple) { 1083 | var arr = std.json.Array.initCapacity(allocator, info.fields.len); 1084 | inline for (value) |item| try arr.append(try toValue(allocator, item, options)); 1085 | return .{ .array = arr }; 1086 | } 1087 | 1088 | var obj = std.json.ObjectMap.init(allocator); 1089 | 1090 | inline for (info.fields) |field| { 1091 | const field_val = @field(value, field.name); 1092 | const nm = nullMeaning(T, field) orelse .value; 1093 | 1094 | const field_name = mightRemap(T, field.name); 1095 | 1096 | if (field.is_comptime) { 1097 | if (field.default_value) |default| { 1098 | const default_value = @as(*const field.type, @ptrCast(@alignCast(default))).*; 1099 | try obj.put(field_name, try toValue(allocator, default_value, options)); 1100 | } else unreachable; // zig requires comptime fields to have a default initialization value 1101 | } else if (comptime dualable(field.type) and nm == .dual) { 1102 | if (field_val) |val| { 1103 | if (val) |val2| { 1104 | try obj.put(field_name, try toValue(allocator, val2, options)); 1105 | } else try obj.put(field_name, .null); 1106 | } 1107 | } else if (@typeInfo(field.type) == .Optional and nm == .field) { 1108 | if (field_val) |val| { 1109 | try obj.put(field_name, try toValue(allocator, val, options)); 1110 | } 1111 | } else if (@typeInfo(field.type) == .Struct and @hasDecl(field.type, "__json_is_undefinedable")) { 1112 | if (!field_val.missing) { 1113 | try obj.put(field_name, try toValue(allocator, field_val.value, options)); 1114 | } 1115 | } else { 1116 | try obj.put(field_name, try toValue(allocator, field_val, options)); 1117 | } 1118 | } 1119 | 1120 | return .{ .object = obj }; 1121 | }, 1122 | // .Pointer => |info| { 1123 | // if (info.size == .Slice) { 1124 | // if (info.child == u8) { 1125 | // if (json_value == .string) { 1126 | // return json_value.string; 1127 | // } else { 1128 | // if (comptime !suppress_error_logs) logger.debug("expected String, found {s}", .{@tagName(json_value)}); 1129 | 1130 | // return error.UnexpectedFieldType; 1131 | // } 1132 | // } else if (info.child == std.json.Value) { 1133 | // return json_value.array.items; 1134 | // } 1135 | // } 1136 | 1137 | // const allocator = maybe_allocator orelse return error.AllocatorRequired; 1138 | // switch (info.size) { 1139 | // .Slice, .Many => { 1140 | // const sentinel = if (info.sentinel) |ptr| @ptrCast(*const info.child, ptr).* else null; 1141 | 1142 | // if (info.child == u8 and json_value == .string) { 1143 | // const array = try allocator.allocWithOptions( 1144 | // info.child, 1145 | // json_value.String.len, 1146 | // info.alignment, 1147 | // sentinel, 1148 | // ); 1149 | 1150 | // std.mem.copy(u8, array, json_value.string); 1151 | 1152 | // return @ptrCast(T, array); 1153 | // } 1154 | 1155 | // if (json_value == .array) { 1156 | // if (info.child == std.json.Value) return json_value.array.items; 1157 | 1158 | // const array = try allocator.allocWithOptions( 1159 | // info.child, 1160 | // json_value.array.items.len, 1161 | // info.alignment, 1162 | // sentinel, 1163 | // ); 1164 | 1165 | // for (json_value.array.items) |item, index| 1166 | // array[index] = try parseInternal( 1167 | // info.child, 1168 | // item, 1169 | // maybe_allocator, 1170 | // suppress_error_logs, 1171 | // ); 1172 | 1173 | // return @ptrCast(T, array); 1174 | // } else { 1175 | // if (comptime !suppress_error_logs) logger.debug("expected Array, found {s}", .{@tagName(json_value)}); 1176 | 1177 | // return error.UnexpectedFieldType; 1178 | // } 1179 | // }, 1180 | // .One, .C => { 1181 | // const data = try allocator.allocWithOptions(info.child, 1, info.alignment, null); 1182 | 1183 | // data[0] = try parseInternal( 1184 | // info.child, 1185 | // json_value, 1186 | // maybe_allocator, 1187 | // suppress_error_logs, 1188 | // ); 1189 | 1190 | // return &data[0]; 1191 | // }, 1192 | // } 1193 | // }, 1194 | .Array => |info| { 1195 | const l = info.len + if (info.sentinel) 1 else 0; 1196 | var arr = try std.json.Array.initCapacity(allocator, l); 1197 | arr.items.len = l; 1198 | 1199 | if (info.sentinel) |ptr| { 1200 | const sentinel = @as(*const info.child, @ptrCast(ptr)).*; 1201 | 1202 | arr.items[l - 1] = sentinel; 1203 | } 1204 | 1205 | for (arr, 0..) |*item, index| 1206 | item.* = try toValue(allocator, value[index], options); 1207 | 1208 | return arr; 1209 | }, 1210 | .Vector => |info| { 1211 | var arr = try std.json.Array.initCapacity(allocator, info.len); 1212 | arr.items.len = info.len; 1213 | 1214 | for (arr.items, 0..) |*item, i| 1215 | item.* = try toValue(allocator, value[i], options); 1216 | 1217 | return arr; 1218 | }, 1219 | .Void => return .{ .object = std.json.ObjectMap.init(allocator) }, 1220 | else => { 1221 | @compileError("unhandled json type: " ++ @typeName(T)); 1222 | }, 1223 | } 1224 | } 1225 | 1226 | const FullStruct = struct { 1227 | const Role = enum(i64) { crewmate, impostor, ghost }; 1228 | 1229 | const Union = union(enum) { 1230 | a: i64, 1231 | b: []const u8, 1232 | }; 1233 | 1234 | const Substruct = struct { 1235 | value: std.json.Value, 1236 | slice_of_values: []std.json.Value, 1237 | 1238 | union_a: Union, 1239 | union_b: Union, 1240 | }; 1241 | 1242 | const Player = struct { 1243 | name: []const u8, 1244 | based: bool, 1245 | }; 1246 | 1247 | const MyTuple = std.meta.Tuple(&[_]type{ i64, bool }); 1248 | 1249 | bool_true: bool, 1250 | bool_false: bool, 1251 | integer: u8, 1252 | float: f64, 1253 | optional: ?f32, 1254 | an_enum: Role, 1255 | an_enum_string: Role, 1256 | slice: []i64, 1257 | substruct: Substruct, 1258 | 1259 | random_map: std.json.ObjectMap, 1260 | number_map: std.StringArrayHashMap(i64), 1261 | players: std.StringHashMap(Player), 1262 | 1263 | bingus: std.StringHashMapUnmanaged(u8), 1264 | dumbo_shrimp: std.ArrayListUnmanaged([]const u8), 1265 | 1266 | my_tuple: MyTuple, 1267 | my_array: [2]u8, 1268 | my_array_of_any: [2]std.json.Value, 1269 | my_array_list: std.ArrayList(i64), 1270 | my_array_list_of_any: std.json.Array, 1271 | 1272 | a_pointer: *u8, 1273 | a_weird_string: [*:0]u8, 1274 | }; 1275 | 1276 | test "json.parse simple struct" { 1277 | @setEvalBranchQuota(10_000); 1278 | 1279 | const json = 1280 | \\{ 1281 | \\ "bool_true": true, 1282 | \\ "bool_false": false, 1283 | \\ "integer": 100, 1284 | \\ "float": 4.2069, 1285 | \\ "optional": null, 1286 | \\ "an_enum": 1, 1287 | \\ "an_enum_string": "crewmate", 1288 | \\ "slice": [1, 2, 3, 4, 5, 6], 1289 | \\ "substruct": { 1290 | \\ "value": "hello", 1291 | \\ "slice_of_values": ["hello", "world"], 1292 | \\ "union_a": -42, 1293 | \\ "union_b": "hello" 1294 | \\ }, 1295 | \\ "random_map": { 1296 | \\ "a": 123, 1297 | \\ "b": "Amogus!!" 1298 | \\ }, 1299 | \\ "number_map": { 1300 | \\ "a": 123, 1301 | \\ "b": 456 1302 | \\ }, 1303 | \\ "players": { 1304 | \\ "aurame": {"name": "Auguste", "based": true}, 1305 | \\ "mattnite": {"name": "Matt", "based": true} 1306 | \\ }, 1307 | \\ "bingus": {"bingus1": 10, "bingus2": 25}, 1308 | \\ "dumbo_shrimp": ["Me", "You", "Everybody"], 1309 | \\ "my_tuple": [10, false], 1310 | \\ "my_array": [1, 255], 1311 | \\ "my_array_of_any": ["a", 2], 1312 | \\ "my_array_list": [2, 254], 1313 | \\ "my_array_list_of_any": ["b", 3], 1314 | \\ "a_pointer": 5, 1315 | \\ "a_weird_string": "hello" 1316 | \\} 1317 | ; 1318 | 1319 | // NOTE: In practice, we're going to use an arena, thus no parseFree exists because it is not required :) 1320 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1321 | defer arena.deinit(); 1322 | 1323 | const root = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json, .{}); 1324 | 1325 | const parsed = try parse(FullStruct, root.value, arena.allocator()); 1326 | 1327 | try std.testing.expectEqual(true, parsed.bool_true); 1328 | try std.testing.expectEqual(false, parsed.bool_false); 1329 | try std.testing.expectEqual(@as(u8, 100), parsed.integer); 1330 | try std.testing.expectApproxEqRel(@as(f64, 4.2069), parsed.float, std.math.floatEps(f64)); 1331 | try std.testing.expectEqual(@as(?f32, null), parsed.optional); 1332 | try std.testing.expectEqual(FullStruct.Role.impostor, parsed.an_enum); 1333 | try std.testing.expectEqual(FullStruct.Role.crewmate, parsed.an_enum_string); 1334 | try std.testing.expectEqualSlices(i64, &[_]i64{ 1, 2, 3, 4, 5, 6 }, parsed.slice); 1335 | 1336 | try std.testing.expect(parsed.substruct.value == .string); 1337 | try std.testing.expectEqualStrings("hello", parsed.substruct.value.string); 1338 | try std.testing.expect(parsed.substruct.slice_of_values.len == 2); 1339 | try std.testing.expect(parsed.substruct.slice_of_values[0] == .string); 1340 | try std.testing.expectEqualStrings("hello", parsed.substruct.slice_of_values[0].string); 1341 | try std.testing.expect(parsed.substruct.slice_of_values[1] == .string); 1342 | try std.testing.expectEqualStrings("world", parsed.substruct.slice_of_values[1].string); 1343 | try std.testing.expect(parsed.substruct.union_a == .a); 1344 | try std.testing.expectEqual(@as(i64, -42), parsed.substruct.union_a.a); 1345 | try std.testing.expect(parsed.substruct.union_b == .b); 1346 | try std.testing.expectEqualStrings("hello", parsed.substruct.union_b.b); 1347 | 1348 | try std.testing.expectEqual(@as(i64, 123), parsed.random_map.get("a").?.integer); 1349 | try std.testing.expectEqualStrings("Amogus!!", parsed.random_map.get("b").?.string); 1350 | 1351 | try std.testing.expectEqual(@as(i64, 123), parsed.number_map.get("a").?); 1352 | try std.testing.expectEqual(@as(i64, 456), parsed.number_map.get("b").?); 1353 | 1354 | try std.testing.expectEqualStrings("Auguste", parsed.players.get("aurame").?.name); 1355 | try std.testing.expectEqualStrings("Matt", parsed.players.get("mattnite").?.name); 1356 | try std.testing.expectEqual(true, parsed.players.get("aurame").?.based); 1357 | try std.testing.expectEqual(true, parsed.players.get("mattnite").?.based); 1358 | 1359 | try std.testing.expectEqual(@as(u8, 10), parsed.bingus.get("bingus1").?); 1360 | try std.testing.expectEqual(@as(u8, 25), parsed.bingus.get("bingus2").?); 1361 | 1362 | try std.testing.expectEqualStrings("Me", parsed.dumbo_shrimp.items[0]); 1363 | try std.testing.expectEqualStrings("You", parsed.dumbo_shrimp.items[1]); 1364 | try std.testing.expectEqualStrings("Everybody", parsed.dumbo_shrimp.items[2]); 1365 | 1366 | try std.testing.expectEqual(FullStruct.MyTuple{ 10, false }, parsed.my_tuple); 1367 | 1368 | try std.testing.expectEqual([2]u8{ 1, 255 }, parsed.my_array); 1369 | 1370 | try std.testing.expect(parsed.my_array_of_any[0] == .string); 1371 | try std.testing.expectEqualStrings("a", parsed.my_array_of_any[0].string); 1372 | try std.testing.expect(parsed.my_array_of_any[1] == .integer); 1373 | try std.testing.expectEqual(@as(i64, 2), parsed.my_array_of_any[1].integer); 1374 | 1375 | try std.testing.expectEqual(@as(usize, 2), parsed.my_array_list.items.len); 1376 | try std.testing.expectEqualSlices(i64, &[_]i64{ 2, 254 }, parsed.my_array_list.items); 1377 | 1378 | try std.testing.expectEqual(@as(usize, 2), parsed.my_array_list_of_any.items.len); 1379 | try std.testing.expect(parsed.my_array_list_of_any.items[0] == .string); 1380 | try std.testing.expectEqualStrings("b", parsed.my_array_list_of_any.items[0].string); 1381 | try std.testing.expect(parsed.my_array_list_of_any.items[1] == .integer); 1382 | try std.testing.expectEqual(@as(i64, 3), parsed.my_array_list_of_any.items[1].integer); 1383 | 1384 | try std.testing.expectEqual(@as(u8, 5), parsed.a_pointer.*); 1385 | 1386 | try std.testing.expectEqualStrings("hello", std.mem.sliceTo(parsed.a_weird_string, 0)); 1387 | } 1388 | 1389 | test "json.parse missing field" { 1390 | const Struct = struct { 1391 | my_super_duper_important_field: bool, 1392 | }; 1393 | 1394 | const json = 1395 | \\{} 1396 | ; 1397 | 1398 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1399 | defer arena.deinit(); 1400 | 1401 | const tree = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json, .{}); 1402 | 1403 | const parsed = parse(Struct, tree.value, arena.allocator()); 1404 | 1405 | try std.testing.expectError(error.MissingRequiredField, parsed); 1406 | } 1407 | 1408 | test "json.parse undefinedable fields and default values" { 1409 | const Struct = struct { 1410 | meh: Undefinedable(i64), 1411 | meh2: Undefinedable(i64), 1412 | default: u8 = 123, 1413 | }; 1414 | 1415 | const json = 1416 | \\{ 1417 | \\ "meh": 42069 1418 | \\} 1419 | ; 1420 | 1421 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1422 | defer arena.deinit(); 1423 | 1424 | const tree = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json, .{}); 1425 | 1426 | const parsed = try parse(Struct, tree.value, arena.allocator()); 1427 | 1428 | try std.testing.expectEqual(@as(i64, 42069), parsed.meh.value); 1429 | try std.testing.expectEqual(true, parsed.meh2.missing); 1430 | try std.testing.expectEqual(@as(u8, 123), parsed.default); 1431 | } 1432 | 1433 | test "json.parse comptime fields" { 1434 | const YoureTheImpostorMessage = struct { 1435 | comptime method: []const u8 = "ship/impostor", 1436 | sussiness: f64, 1437 | }; 1438 | 1439 | const YoureCuteUwUMessage = struct { 1440 | comptime method: []const u8 = "a/cutiepie", 1441 | cuteness: i64, 1442 | }; 1443 | 1444 | const Message = union(enum) { 1445 | youre_the_impostor: YoureTheImpostorMessage, 1446 | youre_cute_uwu: YoureCuteUwUMessage, 1447 | }; 1448 | 1449 | const first_message = 1450 | \\{ 1451 | \\ "method": "ship/impostor", 1452 | \\ "sussiness": 69.420 1453 | \\} 1454 | ; 1455 | 1456 | const second_message = 1457 | \\{ 1458 | \\ "method": "a/cutiepie", 1459 | \\ "cuteness": 100 1460 | \\} 1461 | ; 1462 | 1463 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1464 | defer arena.deinit(); 1465 | 1466 | const first_tree = try std.json.parseFromSlice(std.json.Value, arena.allocator(), first_message, .{}); 1467 | const first_parsed = try parse(Message, first_tree.value, arena.allocator()); 1468 | 1469 | try std.testing.expect(first_parsed == .youre_the_impostor); 1470 | 1471 | const second_tree = try std.json.parseFromSlice(std.json.Value, arena.allocator(), second_message, .{}); 1472 | const second_parsed = try parse(Message, second_tree.value, arena.allocator()); 1473 | 1474 | try std.testing.expect(second_parsed == .youre_cute_uwu); 1475 | } 1476 | 1477 | test "json.parse custom check functions for unions" { 1478 | // jsonrpc request 1479 | const RequestId = union(enum) { string: []const u8, integer: i64 }; 1480 | 1481 | const AmogusRequest = struct { 1482 | const method = "spaceship/amogus"; 1483 | 1484 | sussy: bool, 1485 | }; 1486 | 1487 | const MinecraftNotification = struct { 1488 | const method = "game/minecraft"; 1489 | 1490 | crafted: i64, 1491 | mined: i64, 1492 | }; 1493 | 1494 | const RequestParams = union(enum) { 1495 | amogus: AmogusRequest, 1496 | minecraft: MinecraftNotification, 1497 | }; 1498 | 1499 | const RequestOrNotification = struct { 1500 | const Self = @This(); 1501 | 1502 | jsonrpc: []const u8, 1503 | id: ?RequestId = null, 1504 | method: []const u8, 1505 | params: RequestParams, 1506 | 1507 | fn RequestOrNotificationParseError() type { 1508 | var err = ParseInternalError(RequestId); 1509 | inline for (std.meta.fields(RequestParams)) |field| { 1510 | err = err || ParseInternalError(field.type); 1511 | } 1512 | return err; 1513 | } 1514 | 1515 | pub fn tresParse(value: std.json.Value, allocator: ?std.mem.Allocator) RequestOrNotificationParseError()!Self { 1516 | // var allocator = options.allocator orelse return error.AllocatorRequired; 1517 | var object = value.object; 1518 | var request_or_notif: Self = undefined; 1519 | 1520 | request_or_notif.jsonrpc = object.get("jsonrpc").?.string; 1521 | request_or_notif.id = if (object.get("id")) |id| try parse(RequestId, id, allocator) else null; 1522 | request_or_notif.method = object.get("method").?.string; 1523 | 1524 | inline for (std.meta.fields(RequestParams)) |field| { 1525 | if (std.mem.eql(u8, request_or_notif.method, field.type.method)) { 1526 | request_or_notif.params = @unionInit(RequestParams, field.name, try parse(field.type, object.get("params").?, allocator)); 1527 | } 1528 | } 1529 | 1530 | return request_or_notif; 1531 | } 1532 | }; 1533 | 1534 | const first_message = 1535 | \\{ 1536 | \\ "jsonrpc": "2.0", 1537 | \\ "id": 10, 1538 | \\ "method": "spaceship/amogus", 1539 | \\ "params": {"sussy": true} 1540 | \\} 1541 | ; 1542 | 1543 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1544 | defer arena.deinit(); 1545 | 1546 | const first_tree = try std.json.parseFromSlice(std.json.Value, arena.allocator(), first_message, .{}); 1547 | const first_parsed = try parse(RequestOrNotification, first_tree.value, arena.allocator()); 1548 | 1549 | try std.testing.expectEqualStrings("2.0", first_parsed.jsonrpc); 1550 | try std.testing.expect(first_parsed.id != null); 1551 | try std.testing.expect(first_parsed.id.? == .integer); 1552 | try std.testing.expectEqual(@as(i64, 10), first_parsed.id.?.integer); 1553 | try std.testing.expectEqualStrings("spaceship/amogus", first_parsed.method); 1554 | try std.testing.expect(first_parsed.params == .amogus); 1555 | try std.testing.expectEqual(true, first_parsed.params.amogus.sussy); 1556 | 1557 | // TODO: Add second test 1558 | } 1559 | 1560 | test "json.parse allocator required errors" { 1561 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1562 | defer arena.deinit(); 1563 | 1564 | try std.testing.expectError(error.AllocatorRequired, parse([]i64, (try std.json.parseFromSlice(std.json.Value, arena.allocator(), "[1, 2, 3, 4]", .{})).value, null)); 1565 | try std.testing.expectError(error.AllocatorRequired, parse(std.StringArrayHashMap(i64), (try std.json.parseFromSlice(std.json.Value, arena.allocator(), 1566 | \\{"a": 123, "b": -69} 1567 | , .{})).value, null)); 1568 | } 1569 | 1570 | test "json.stringify basics" { 1571 | var stringify_buf: [28]u8 = undefined; 1572 | var fbs = std.io.fixedBufferStream(&stringify_buf); 1573 | 1574 | const Basic = struct { 1575 | unsigned: u16, 1576 | signed: i16, 1577 | }; 1578 | 1579 | var basic = Basic{ 1580 | .unsigned = 69, 1581 | .signed = -69, 1582 | }; 1583 | 1584 | try stringify(basic, .{}, fbs.writer()); 1585 | 1586 | try std.testing.expectEqualStrings( 1587 | \\{"unsigned":69,"signed":-69} 1588 | , &stringify_buf); 1589 | } 1590 | 1591 | test "json.stringify undefinedables" { 1592 | var furry_buf: [49]u8 = undefined; 1593 | var fbs = std.io.fixedBufferStream(&furry_buf); 1594 | 1595 | const Furry = struct { 1596 | name: Undefinedable([]const u8), 1597 | age: Undefinedable(i64), 1598 | plays_amogus: bool, 1599 | joe: Undefinedable([]const u8), 1600 | }; 1601 | 1602 | var rimu = Furry{ 1603 | .name = .{ .value = "Rimu", .missing = false }, 1604 | .age = .{ .value = undefined, .missing = true }, 1605 | .plays_amogus = false, 1606 | .joe = .{ .value = "Mama", .missing = false }, 1607 | }; 1608 | 1609 | try stringify(rimu, .{}, fbs.writer()); 1610 | 1611 | try std.testing.expectEqualStrings( 1612 | \\{"name":"Rimu","plays_amogus":false,"joe":"Mama"} 1613 | , &furry_buf); 1614 | } 1615 | 1616 | test "json.stringify arraylist" { 1617 | const allocator = std.testing.allocator; 1618 | 1619 | var stringify_buf: [512]u8 = undefined; 1620 | var fbs = std.io.fixedBufferStream(&stringify_buf); 1621 | 1622 | const Database = struct { 1623 | names_of_my_pals: std.ArrayList([]const u8), 1624 | more_peeps: std.ArrayListUnmanaged([]const u8), 1625 | }; 1626 | 1627 | var db = Database{ 1628 | .names_of_my_pals = std.ArrayList([]const u8).init(allocator), 1629 | .more_peeps = .{}, 1630 | }; 1631 | defer db.names_of_my_pals.deinit(); 1632 | defer db.more_peeps.deinit(allocator); 1633 | 1634 | try db.names_of_my_pals.append("Travis"); 1635 | try db.names_of_my_pals.append("Rimu"); 1636 | try db.names_of_my_pals.append("Flandere"); 1637 | 1638 | try db.more_peeps.append(allocator, "Matt"); 1639 | try db.more_peeps.append(allocator, "Felix"); 1640 | try db.more_peeps.append(allocator, "Ben"); 1641 | 1642 | try stringify(db, .{}, fbs.writer()); 1643 | 1644 | try std.testing.expectEqualStrings( 1645 | \\{"names_of_my_pals":["Travis","Rimu","Flandere"],"more_peeps":["Matt","Felix","Ben"]} 1646 | , fbs.getWritten()); 1647 | } 1648 | 1649 | test "json.stringify hashmaps" { 1650 | const allocator = std.testing.allocator; 1651 | 1652 | var stringify_buf: [512]u8 = undefined; 1653 | var fbs = std.io.fixedBufferStream(&stringify_buf); 1654 | 1655 | const Database = struct { 1656 | coolness: std.StringHashMap(f64), 1657 | height: std.StringHashMapUnmanaged(usize), 1658 | }; 1659 | 1660 | var db = Database{ 1661 | .coolness = std.StringHashMap(f64).init(allocator), 1662 | .height = .{}, 1663 | }; 1664 | defer db.coolness.deinit(); 1665 | defer db.height.deinit(allocator); 1666 | 1667 | try db.coolness.put("Montreal", -20); 1668 | try db.coolness.put("Beirut", 20); 1669 | 1670 | try db.height.put(allocator, "Hudson", 0); 1671 | try db.height.put(allocator, "Me", 100_000); 1672 | 1673 | try stringify(db, .{}, fbs.writer()); 1674 | 1675 | //HashMap entry iteration order is unpredictable, so just handle every case. 1676 | //From the docs: "No order is guaranteed" 1677 | //So we gotta do this to be completely reliable 1678 | //Note: this test *was* broken at the time or writing 1679 | 1680 | const str = fbs.getWritten(); 1681 | 1682 | if (std.mem.indexOfDiff(u8, 1683 | \\{"coolness":{"Montreal":-2.0e+01,"Beirut":2.0e+01},"height":{"Me":100000,"Hudson":0}} 1684 | , str) != null and 1685 | std.mem.indexOfDiff(u8, 1686 | \\{"coolness":{"Beirut":2.0e+01,"Montreal":-2.0e+01},"height":{"Me":100000,"Hudson":0}} 1687 | , str) != null and 1688 | std.mem.indexOfDiff(u8, 1689 | \\{"coolness":{"Montreal":-2.0e+01,"Beirut":2.0e+01},"height":{"Hudson":0,"Me":100000}} 1690 | , str) != null and 1691 | std.mem.indexOfDiff(u8, 1692 | \\{"coolness":{"Beirut":2.0e+01,"Montreal":-2.0e+01},"height":{"Hudson":0,"Me":100000}} 1693 | , str) != null) { 1694 | try std.testing.expectEqualStrings( 1695 | \\{"coolness":{"Montreal":-2.0e+01,"Beirut":2.0e+01},"height":{"Me":100000,"Hudson":0}} 1696 | , str); 1697 | } 1698 | } 1699 | 1700 | test "json.stringify enums" { 1701 | const NumericEnum = enum(u8) { 1702 | a = 0, 1703 | b = 1, 1704 | }; 1705 | 1706 | const StringEnum = enum(u64) { 1707 | pub const tres_string_enum = {}; 1708 | 1709 | a = 0, 1710 | b = 1, 1711 | }; 1712 | 1713 | var stringify_buf: [51]u8 = undefined; 1714 | var fbs = std.io.fixedBufferStream(&stringify_buf); 1715 | 1716 | try stringify(NumericEnum.a, .{}, fbs.writer()); 1717 | 1718 | try std.testing.expectEqualStrings( 1719 | \\0 1720 | , fbs.getWritten()); 1721 | 1722 | try fbs.seekTo(0); 1723 | 1724 | try stringify(StringEnum.a, .{}, fbs.writer()); 1725 | 1726 | try std.testing.expectEqualStrings( 1727 | \\"a" 1728 | , fbs.getWritten()); 1729 | } 1730 | 1731 | test "parse and stringify null meaning" { 1732 | const A = struct { 1733 | pub const tres_null_meaning = .{ 1734 | .a = .field, 1735 | .b = .value, 1736 | .c = .dual, 1737 | }; 1738 | 1739 | a: ?u8, 1740 | b: ?u8, 1741 | c: ??u8, 1742 | }; 1743 | 1744 | const allocator = std.testing.allocator; 1745 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1746 | defer arena.deinit(); 1747 | 1748 | var stringify_buf: [128]u8 = undefined; 1749 | var fbs = std.io.fixedBufferStream(&stringify_buf); 1750 | 1751 | try stringify(A{ .a = null, .b = null, .c = null }, .{}, fbs.writer()); 1752 | try std.testing.expectEqualStrings( 1753 | \\{"b":null} 1754 | , fbs.getWritten()); 1755 | 1756 | const a1 = try parse(A, (try std.json.parseFromSlice(std.json.Value, arena.allocator(), fbs.getWritten(), .{})).value, allocator); 1757 | try std.testing.expectEqual(@as(?u8, null), a1.a); 1758 | try std.testing.expectEqual(@as(?u8, null), a1.b); 1759 | try std.testing.expectEqual(@as(??u8, null), a1.c); 1760 | 1761 | fbs.reset(); 1762 | 1763 | try stringify(A{ .a = 5, .b = 7, .c = @as(?u8, null) }, .{}, fbs.writer()); 1764 | try std.testing.expectEqualStrings( 1765 | \\{"a":5,"b":7,"c":null} 1766 | , fbs.getWritten()); 1767 | 1768 | const a2 = try parse(A, (try std.json.parseFromSlice(std.json.Value, arena.allocator(), fbs.getWritten(), .{})).value, allocator); 1769 | try std.testing.expectEqual(@as(u8, 5), a2.a.?); 1770 | try std.testing.expectEqual(@as(u8, 7), a2.b.?); 1771 | try std.testing.expectEqual(@as(?u8, null), a2.c.?); 1772 | 1773 | fbs.reset(); 1774 | 1775 | try stringify(A{ .a = 5, .b = 7, .c = 10 }, .{}, fbs.writer()); 1776 | try std.testing.expectEqualStrings( 1777 | \\{"a":5,"b":7,"c":10} 1778 | , fbs.getWritten()); 1779 | 1780 | const a3 = try parse(A, (try std.json.parseFromSlice(std.json.Value, arena.allocator(), fbs.getWritten(), .{})).value, allocator); 1781 | try std.testing.expectEqual(@as(u8, 5), a3.a.?); 1782 | try std.testing.expectEqual(@as(u8, 7), a3.b.?); 1783 | try std.testing.expectEqual(@as(u8, 10), a3.c.?.?); 1784 | 1785 | fbs.reset(); 1786 | } 1787 | 1788 | test "custom standard stringify" { 1789 | const Bruh = struct { 1790 | pub fn jsonStringify( 1791 | _: @This(), 1792 | _: std.json.StringifyOptions, 1793 | writer: anytype, 1794 | ) @TypeOf(writer).Error!void { 1795 | try writer.writeAll("slay"); 1796 | } 1797 | }; 1798 | 1799 | var stringify_buf: [128]u8 = undefined; 1800 | var fbs = std.io.fixedBufferStream(&stringify_buf); 1801 | 1802 | try stringify(Bruh{}, .{}, fbs.writer()); 1803 | 1804 | try std.testing.expectEqualStrings("slay", fbs.getWritten()); 1805 | } 1806 | 1807 | test "json.toValue: basics" { 1808 | const allocator = std.testing.allocator; 1809 | 1810 | var arena = std.heap.ArenaAllocator.init(allocator); 1811 | defer arena.deinit(); 1812 | 1813 | const bool_simple = try toValue(arena.allocator(), @as(bool, false), .{}); 1814 | try std.testing.expect(bool_simple == .bool); 1815 | try std.testing.expectEqual(false, bool_simple.bool); 1816 | 1817 | const float_simple = try toValue(arena.allocator(), @as(f64, 7.89), .{}); 1818 | try std.testing.expect(float_simple == .float); 1819 | try std.testing.expectEqual(@as(f64, 7.89), float_simple.float); 1820 | 1821 | const int_simple = try toValue(arena.allocator(), @as(i64, 10), .{}); 1822 | try std.testing.expect(int_simple == .integer); 1823 | try std.testing.expectEqual(@as(i64, 10), int_simple.integer); 1824 | 1825 | const optional_simple_1 = try toValue(arena.allocator(), @as(?f64, 7.89), .{}); 1826 | try std.testing.expect(optional_simple_1 == .float); 1827 | try std.testing.expectEqual(@as(f64, 7.89), optional_simple_1.float); 1828 | 1829 | const optional_simple_2 = try toValue(arena.allocator(), @as(?f64, null), .{}); 1830 | try std.testing.expect(optional_simple_2 == .null); 1831 | 1832 | const SimpleEnum1 = enum(u32) { a = 0, b = 69, c = 420, d = 42069 }; 1833 | const simple_enum_1 = try toValue(arena.allocator(), SimpleEnum1.b, .{}); 1834 | try std.testing.expect(simple_enum_1 == .integer); 1835 | try std.testing.expectEqual(@as(i64, 69), simple_enum_1.integer); 1836 | 1837 | const SimpleEnum2 = enum(u32) { 1838 | pub const tres_string_enum = .{}; 1839 | 1840 | a = 0, 1841 | b = 69, 1842 | c = 420, 1843 | d = 42069, 1844 | }; 1845 | 1846 | const simple_enum_2 = try toValue(arena.allocator(), SimpleEnum2.b, .{}); 1847 | try std.testing.expect(simple_enum_2 == .string); 1848 | try std.testing.expectEqualStrings("b", simple_enum_2.string); 1849 | 1850 | const SimpleUnion = union(enum) { 1851 | a: i64, 1852 | b: bool, 1853 | }; 1854 | 1855 | const simple_union_1 = try toValue(arena.allocator(), SimpleUnion{ .a = 25 }, .{}); 1856 | try std.testing.expect(simple_union_1 == .integer); 1857 | try std.testing.expectEqual(@as(i64, 25), simple_union_1.integer); 1858 | 1859 | const simple_union_2 = try toValue(arena.allocator(), SimpleUnion{ .b = true }, .{}); 1860 | try std.testing.expect(simple_union_2 == .bool); 1861 | try std.testing.expectEqual(true, simple_union_2.bool); 1862 | 1863 | const SimpleStruct = struct { 1864 | abc: u8, 1865 | def: SimpleEnum1, 1866 | ghi: SimpleEnum2, 1867 | }; 1868 | 1869 | const simple_struct = try toValue(arena.allocator(), SimpleStruct{ .abc = 25, .def = .c, .ghi = .d }, .{}); 1870 | try std.testing.expect(simple_struct == .object); 1871 | try std.testing.expectEqual(@as(i64, 25), simple_struct.object.get("abc").?.integer); 1872 | try std.testing.expectEqual(@as(i64, 420), simple_struct.object.get("def").?.integer); 1873 | try std.testing.expectEqualStrings("d", simple_struct.object.get("ghi").?.string); 1874 | } 1875 | 1876 | test "remapping" { 1877 | const allocator = std.testing.allocator; 1878 | 1879 | var arena = std.heap.ArenaAllocator.init(allocator); 1880 | defer arena.deinit(); 1881 | 1882 | const Bruh = struct { 1883 | pub const tres_remap = .{ 1884 | .zig_snake_case = "jsonCamelCase", 1885 | .zigBadPractice = "json_doesn't_care", 1886 | }; 1887 | 1888 | zig_snake_case: u8, 1889 | zigBadPractice: u16, 1890 | }; 1891 | 1892 | const json = 1893 | \\{"jsonCamelCase":69,"json_doesn't_care":420} 1894 | ; 1895 | 1896 | const tree = try std.json.parseFromSlice(std.json.Value, arena.allocator(), json, .{}); 1897 | 1898 | const bruh_1 = try parse(Bruh, tree.value, null); 1899 | try std.testing.expectEqual(@as(u8, 69), bruh_1.zig_snake_case); 1900 | try std.testing.expectEqual(@as(u16, 420), bruh_1.zigBadPractice); 1901 | 1902 | var stringify_buf: [128]u8 = undefined; 1903 | var fbs = std.io.fixedBufferStream(&stringify_buf); 1904 | 1905 | try stringify(Bruh{ .zig_snake_case = 69, .zigBadPractice = 420 }, .{}, fbs.writer()); 1906 | 1907 | try std.testing.expectEqualStrings(json, fbs.getWritten()); 1908 | 1909 | const value = try toValue(arena.allocator(), bruh_1, .{}); 1910 | try std.testing.expectEqual(@as(i64, 69), value.object.get("jsonCamelCase").?.integer); 1911 | try std.testing.expectEqual(@as(i64, 420), value.object.get("json_doesn't_care").?.integer); 1912 | } 1913 | --------------------------------------------------------------------------------