authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2020-02-23 11:25:15-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2020-02-23 11:25:15-05:00
logcfe1fbad0f6a47fa0f5798df1423702b84bf34aa
tree0b1adf43ae5d52a3444788f14d0996c3d155b877
parent597648231b49500bbb8d15daa7f59cd52c1c3279
parente270db956c6dc45f415e0b02b69db408d4a990b1
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #4304 from daurnimator/auto-json

Add "automatic" JSON facilities

1 files changed, 821 insertions(+), 0 deletions(-)

lib/std/json.zig+821
......@@ -19,6 +19,74 @@ const StringEscapes = union(enum) {
1919 },
2020};
2121
22/// Checks to see if a string matches what it would be as a json-encoded string
23/// Assumes that `encoded` is a well-formed json string
24fn encodesTo(decoded: []const u8, encoded: []const u8) bool {
25 var i: usize = 0;
26 var j: usize = 0;
27 while (i < decoded.len) {
28 if (j >= encoded.len) return false;
29 if (encoded[j] != '\\') {
30 if (decoded[i] != encoded[j]) return false;
31 j += 1;
32 i += 1;
33 } else {
34 const escape_type = encoded[j + 1];
35 if (escape_type != 'u') {
36 const t: u8 = switch (escape_type) {
37 '\\' => '\\',
38 '/' => '/',
39 'n' => '\n',
40 'r' => '\r',
41 't' => '\t',
42 'f' => 12,
43 'b' => 8,
44 '"' => '"',
45 else => unreachable,
46 };
47 if (decoded[i] != t) return false;
48 j += 2;
49 i += 1;
50 } else {
51 var codepoint = std.fmt.parseInt(u21, encoded[j + 2 .. j + 6], 16) catch unreachable;
52 j += 6;
53 if (codepoint >= 0xD800 and codepoint < 0xDC00) {
54 // surrogate pair
55 assert(encoded[j] == '\\');
56 assert(encoded[j + 1] == 'u');
57 const low_surrogate = std.fmt.parseInt(u21, encoded[j + 2 .. j + 6], 16) catch unreachable;
58 codepoint = 0x10000 + (((codepoint & 0x03ff) << 10) | (low_surrogate & 0x03ff));
59 j += 6;
60 }
61 var buf: [4]u8 = undefined;
62 const len = std.unicode.utf8Encode(codepoint, &buf) catch unreachable;
63 if (i + len > decoded.len) return false;
64 if (!mem.eql(u8, decoded[i .. i + len], buf[0..len])) return false;
65 i += len;
66 }
67 }
68 }
69 assert(i == decoded.len);
70 assert(j == encoded.len);
71 return true;
72}
73
74test "encodesTo" {
75 // same
76 testing.expectEqual(true, encodesTo("false", "false"));
77 // totally different
78 testing.expectEqual(false, encodesTo("false", "true"));
79 // differnt lengths
80 testing.expectEqual(false, encodesTo("false", "other"));
81 // with escape
82 testing.expectEqual(true, encodesTo("\\", "\\\\"));
83 testing.expectEqual(true, encodesTo("with\nescape", "with\\nescape"));
84 // with unicode
85 testing.expectEqual(true, encodesTo("ą", "\\u0105"));
86 testing.expectEqual(true, encodesTo("😂", "\\ud83d\\ude02"));
87 testing.expectEqual(true, encodesTo("withąunicode😂", "with\\u0105unicode\\ud83d\\ude02"));
88}
89
2290/// A single token slice into the parent string.
2391///
2492/// Use `token.slice()` on the input at the current position to get the current slice.
......@@ -1201,6 +1269,493 @@ pub const Value = union(enum) {
12011269 }
12021270};
12031271
1272pub const ParseOptions = struct {
1273 allocator: ?*Allocator = null,
1274
1275 /// Behaviour when a duplicate field is encountered.
1276 duplicate_field_behavior: enum {
1277 UseFirst,
1278 Error,
1279 UseLast,
1280 } = .Error,
1281};
1282
1283fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options: ParseOptions) !T {
1284 switch (@typeInfo(T)) {
1285 .Bool => {
1286 return switch (token) {
1287 .True => true,
1288 .False => false,
1289 else => error.UnexpectedToken,
1290 };
1291 },
1292 .Float, .ComptimeFloat => {
1293 const numberToken = switch (token) {
1294 .Number => |n| n,
1295 else => return error.UnexpectedToken,
1296 };
1297 return try std.fmt.parseFloat(T, numberToken.slice(tokens.slice, tokens.i - 1));
1298 },
1299 .Int, .ComptimeInt => {
1300 const numberToken = switch (token) {
1301 .Number => |n| n,
1302 else => return error.UnexpectedToken,
1303 };
1304 if (!numberToken.is_integer) return error.UnexpectedToken;
1305 return try std.fmt.parseInt(T, numberToken.slice(tokens.slice, tokens.i - 1), 10);
1306 },
1307 .Optional => |optionalInfo| {
1308 if (token == .Null) {
1309 return null;
1310 } else {
1311 return try parseInternal(optionalInfo.child, token, tokens, options);
1312 }
1313 },
1314 .Enum => |enumInfo| {
1315 switch (token) {
1316 .Number => |numberToken| {
1317 if (!numberToken.is_integer) return error.UnexpectedToken;
1318 const n = try std.fmt.parseInt(enumInfo.tag_type, numberToken.slice(tokens.slice, tokens.i - 1), 10);
1319 return try std.meta.intToEnum(T, n);
1320 },
1321 .String => |stringToken| {
1322 const source_slice = stringToken.slice(tokens.slice, tokens.i - 1);
1323 switch (stringToken.escapes) {
1324 .None => return std.meta.stringToEnum(T, source_slice) orelse return error.InvalidEnumTag,
1325 .Some => {
1326 inline for (enumInfo.fields) |field| {
1327 if (field.name.len == stringToken.decodedLength() and encodesTo(field.name, source_slice)) {
1328 return @field(T, field.name);
1329 }
1330 }
1331 return error.InvalidEnumTag;
1332 },
1333 }
1334 },
1335 else => return error.UnexpectedToken,
1336 }
1337 },
1338 .Union => |unionInfo| {
1339 if (unionInfo.tag_type) |_| {
1340 // try each of the union fields until we find one that matches
1341 inline for (unionInfo.fields) |u_field| {
1342 if (parseInternal(u_field.field_type, token, tokens, options)) |value| {
1343 return @unionInit(T, u_field.name, value);
1344 } else |err| {
1345 // Bubble up error.OutOfMemory
1346 // Parsing some types won't have OutOfMemory in their
1347 // error-sets, for the condition to be valid, merge it in.
1348 if (@as(@TypeOf(err) || error{OutOfMemory}, err) == error.OutOfMemory) return err;
1349 // otherwise continue through the `inline for`
1350 }
1351 }
1352 return error.NoUnionMembersMatched;
1353 } else {
1354 @compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'");
1355 }
1356 },
1357 .Struct => |structInfo| {
1358 switch (token) {
1359 .ObjectBegin => {},
1360 else => return error.UnexpectedToken,
1361 }
1362 var r: T = undefined;
1363 var fields_seen = [_]bool{false} ** structInfo.fields.len;
1364 errdefer {
1365 inline for (structInfo.fields) |field, i| {
1366 if (fields_seen[i]) {
1367 parseFree(field.field_type, @field(r, field.name), options);
1368 }
1369 }
1370 }
1371
1372 while (true) {
1373 switch ((try tokens.next()) orelse return error.UnexpectedEndOfJson) {
1374 .ObjectEnd => break,
1375 .String => |stringToken| {
1376 const key_source_slice = stringToken.slice(tokens.slice, tokens.i - 1);
1377 var found = false;
1378 inline for (structInfo.fields) |field, i| {
1379 // TODO: using switches here segfault the compiler (#2727?)
1380 if ((stringToken.escapes == .None and mem.eql(u8, field.name, key_source_slice)) or (stringToken.escapes == .Some and (field.name.len == stringToken.decodedLength() and encodesTo(field.name, key_source_slice)))) {
1381 // if (switch (stringToken.escapes) {
1382 // .None => mem.eql(u8, field.name, key_source_slice),
1383 // .Some => (field.name.len == stringToken.decodedLength() and encodesTo(field.name, key_source_slice)),
1384 // }) {
1385 if (fields_seen[i]) {
1386 // switch (options.duplicate_field_behavior) {
1387 // .UseFirst => {},
1388 // .Error => {},
1389 // .UseLast => {},
1390 // }
1391 if (options.duplicate_field_behavior == .UseFirst) {
1392 break;
1393 } else if (options.duplicate_field_behavior == .Error) {
1394 return error.DuplicateJSONField;
1395 } else if (options.duplicate_field_behavior == .UseLast) {
1396 parseFree(field.field_type, @field(r, field.name), options);
1397 }
1398 }
1399 @field(r, field.name) = try parse(field.field_type, tokens, options);
1400 fields_seen[i] = true;
1401 found = true;
1402 break;
1403 }
1404 }
1405 if (!found) return error.UnknownField;
1406 },
1407 else => return error.UnexpectedToken,
1408 }
1409 }
1410 inline for (structInfo.fields) |field, i| {
1411 if (!fields_seen[i]) {
1412 if (field.default_value) |default| {
1413 @field(r, field.name) = default;
1414 } else {
1415 return error.MissingField;
1416 }
1417 }
1418 }
1419 return r;
1420 },
1421 .Array => |arrayInfo| {
1422 switch (token) {
1423 .ArrayBegin => {
1424 var r: T = undefined;
1425 var i: usize = 0;
1426 errdefer {
1427 while (true) : (i -= 1) {
1428 parseFree(arrayInfo.child, r[i], options);
1429 if (i == 0) break;
1430 }
1431 }
1432 while (i < r.len) : (i += 1) {
1433 r[i] = try parse(arrayInfo.child, tokens, options);
1434 }
1435 const tok = (try tokens.next()) orelse return error.UnexpectedEndOfJson;
1436 switch (tok) {
1437 .ArrayEnd => {},
1438 else => return error.UnexpectedToken,
1439 }
1440 return r;
1441 },
1442 .String => |stringToken| {
1443 if (arrayInfo.child != u8) return error.UnexpectedToken;
1444 var r: T = undefined;
1445 const source_slice = stringToken.slice(tokens.slice, tokens.i - 1);
1446 switch (stringToken.escapes) {
1447 .None => mem.copy(u8, &r, source_slice),
1448 .Some => try unescapeString(&r, source_slice),
1449 }
1450 return r;
1451 },
1452 else => return error.UnexpectedToken,
1453 }
1454 },
1455 .Pointer => |ptrInfo| {
1456 const allocator = options.allocator orelse return error.AllocatorRequired;
1457 switch (ptrInfo.size) {
1458 .One => {
1459 const r: T = allocator.create(ptrInfo.child);
1460 r.* = try parseInternal(ptrInfo.child, token, tokens, options);
1461 return r;
1462 },
1463 .Slice => {
1464 switch (token) {
1465 .ArrayBegin => {
1466 var arraylist = std.ArrayList(ptrInfo.child).init(allocator);
1467 errdefer {
1468 while (arraylist.popOrNull()) |v| {
1469 parseFree(ptrInfo.child, v, options);
1470 }
1471 arraylist.deinit();
1472 }
1473
1474 while (true) {
1475 const tok = (try tokens.next()) orelse return error.UnexpectedEndOfJson;
1476 switch (tok) {
1477 .ArrayEnd => break,
1478 else => {},
1479 }
1480
1481 try arraylist.ensureCapacity(arraylist.len + 1);
1482 const v = try parseInternal(ptrInfo.child, tok, tokens, options);
1483 arraylist.appendAssumeCapacity(v);
1484 }
1485 return arraylist.toOwnedSlice();
1486 },
1487 .String => |stringToken| {
1488 if (ptrInfo.child != u8) return error.UnexpectedToken;
1489 const source_slice = stringToken.slice(tokens.slice, tokens.i - 1);
1490 switch (stringToken.escapes) {
1491 .None => return mem.dupe(allocator, u8, source_slice),
1492 .Some => |some_escapes| {
1493 const output = try allocator.alloc(u8, stringToken.decodedLength());
1494 errdefer allocator.free(output);
1495 try unescapeString(output, source_slice);
1496 return output;
1497 },
1498 }
1499 },
1500 else => return error.UnexpectedToken,
1501 }
1502 },
1503 else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"),
1504 }
1505 },
1506 else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"),
1507 }
1508 unreachable;
1509}
1510
1511pub fn parse(comptime T: type, tokens: *TokenStream, options: ParseOptions) !T {
1512 const token = (try tokens.next()) orelse return error.UnexpectedEndOfJson;
1513 return parseInternal(T, token, tokens, options);
1514}
1515
1516/// Releases resources created by `parse`.
1517/// Should be called with the same type and `ParseOptions` that were passed to `parse`
1518pub fn parseFree(comptime T: type, value: T, options: ParseOptions) void {
1519 switch (@typeInfo(T)) {
1520 .Bool, .Float, .ComptimeFloat, .Int, .ComptimeInt, .Enum => {},
1521 .Optional => {
1522 if (value) |v| {
1523 return parseFree(@TypeOf(v), v, options);
1524 }
1525 },
1526 .Union => |unionInfo| {
1527 if (unionInfo.tag_type) |UnionTagType| {
1528 inline for (unionInfo.fields) |u_field| {
1529 if (@enumToInt(@as(UnionTagType, value)) == u_field.enum_field.?.value) {
1530 parseFree(u_field.field_type, @field(value, u_field.name), options);
1531 break;
1532 }
1533 }
1534 } else {
1535 unreachable;
1536 }
1537 },
1538 .Struct => |structInfo| {
1539 inline for (structInfo.fields) |field| {
1540 parseFree(field.field_type, @field(value, field.name), options);
1541 }
1542 },
1543 .Array => |arrayInfo| {
1544 for (value) |v| {
1545 parseFree(arrayInfo.child, v, options);
1546 }
1547 },
1548 .Pointer => |ptrInfo| {
1549 const allocator = options.allocator orelse unreachable;
1550 switch (ptrInfo.size) {
1551 .One => {
1552 parseFree(ptrInfo.child, value.*, options);
1553 allocator.destroy(v);
1554 },
1555 .Slice => {
1556 for (value) |v| {
1557 parseFree(ptrInfo.child, v, options);
1558 }
1559 allocator.free(value);
1560 },
1561 else => unreachable,
1562 }
1563 },
1564 else => unreachable,
1565 }
1566}
1567
1568test "parse" {
1569 testing.expectEqual(false, try parse(bool, &TokenStream.init("false"), ParseOptions{}));
1570 testing.expectEqual(true, try parse(bool, &TokenStream.init("true"), ParseOptions{}));
1571 testing.expectEqual(@as(u1, 1), try parse(u1, &TokenStream.init("1"), ParseOptions{}));
1572 testing.expectError(error.Overflow, parse(u1, &TokenStream.init("50"), ParseOptions{}));
1573 testing.expectEqual(@as(u64, 42), try parse(u64, &TokenStream.init("42"), ParseOptions{}));
1574 testing.expectEqual(@as(f64, 42), try parse(f64, &TokenStream.init("42.0"), ParseOptions{}));
1575 testing.expectEqual(@as(?bool, null), try parse(?bool, &TokenStream.init("null"), ParseOptions{}));
1576 testing.expectEqual(@as(?bool, true), try parse(?bool, &TokenStream.init("true"), ParseOptions{}));
1577
1578 testing.expectEqual(@as([3]u8, "foo".*), try parse([3]u8, &TokenStream.init("\"foo\""), ParseOptions{}));
1579 testing.expectEqual(@as([3]u8, "foo".*), try parse([3]u8, &TokenStream.init("[102, 111, 111]"), ParseOptions{}));
1580}
1581
1582test "parse into enum" {
1583 const T = extern enum {
1584 Foo = 42,
1585 Bar,
1586 @"with\\escape",
1587 };
1588 testing.expectEqual(@as(T, .Foo), try parse(T, &TokenStream.init("\"Foo\""), ParseOptions{}));
1589 testing.expectEqual(@as(T, .Foo), try parse(T, &TokenStream.init("42"), ParseOptions{}));
1590 testing.expectEqual(@as(T, .@"with\\escape"), try parse(T, &TokenStream.init("\"with\\\\escape\""), ParseOptions{}));
1591 testing.expectError(error.InvalidEnumTag, parse(T, &TokenStream.init("5"), ParseOptions{}));
1592 testing.expectError(error.InvalidEnumTag, parse(T, &TokenStream.init("\"Qux\""), ParseOptions{}));
1593}
1594
1595test "parse into that allocates a slice" {
1596 testing.expectError(error.AllocatorRequired, parse([]u8, &TokenStream.init("\"foo\""), ParseOptions{}));
1597
1598 const options = ParseOptions{ .allocator = testing.allocator };
1599 {
1600 const r = try parse([]u8, &TokenStream.init("\"foo\""), options);
1601 defer parseFree([]u8, r, options);
1602 testing.expectEqualSlices(u8, "foo", r);
1603 }
1604 {
1605 const r = try parse([]u8, &TokenStream.init("[102, 111, 111]"), options);
1606 defer parseFree([]u8, r, options);
1607 testing.expectEqualSlices(u8, "foo", r);
1608 }
1609 {
1610 const r = try parse([]u8, &TokenStream.init("\"with\\\\escape\""), options);
1611 defer parseFree([]u8, r, options);
1612 testing.expectEqualSlices(u8, "with\\escape", r);
1613 }
1614}
1615
1616test "parse into tagged union" {
1617 {
1618 const T = union(enum) {
1619 int: i32,
1620 float: f64,
1621 string: []const u8,
1622 };
1623 testing.expectEqual(T{ .float = 1.5 }, try parse(T, &TokenStream.init("1.5"), ParseOptions{}));
1624 }
1625
1626 { // if union matches string member, fails with NoUnionMembersMatched rather than AllocatorRequired
1627 // Note that this behaviour wasn't necessarily by design, but was
1628 // what fell out of the implementation and may result in interesting
1629 // API breakage if changed
1630 const T = union(enum) {
1631 int: i32,
1632 float: f64,
1633 string: []const u8,
1634 };
1635 testing.expectError(error.NoUnionMembersMatched, parse(T, &TokenStream.init("\"foo\""), ParseOptions{}));
1636 }
1637
1638 { // failing allocations should be bubbled up instantly without trying next member
1639 var fail_alloc = testing.FailingAllocator.init(testing.allocator, 0);
1640 const options = ParseOptions{ .allocator = &fail_alloc.allocator };
1641 const T = union(enum) {
1642 // both fields here match the input
1643 string: []const u8,
1644 array: [3]u8,
1645 };
1646 testing.expectError(error.OutOfMemory, parse(T, &TokenStream.init("[1,2,3]"), options));
1647 }
1648
1649 {
1650 // if multiple matches possible, takes first option
1651 const T = union(enum) {
1652 x: u8,
1653 y: u8,
1654 };
1655 testing.expectEqual(T{ .x = 42 }, try parse(T, &TokenStream.init("42"), ParseOptions{}));
1656 }
1657}
1658
1659test "parseFree descends into tagged union" {
1660 // tagged unions are broken on arm64: https://github.com/ziglang/zig/issues/4492
1661 if (std.builtin.arch == .aarch64) return error.SkipZigTest;
1662
1663 var fail_alloc = testing.FailingAllocator.init(testing.allocator, 1);
1664 const options = ParseOptions{ .allocator = &fail_alloc.allocator };
1665 const T = union(enum) {
1666 int: i32,
1667 float: f64,
1668 string: []const u8,
1669 };
1670 // use a string with unicode escape so we know result can't be a reference to global constant
1671 const r = try parse(T, &TokenStream.init("\"with\\u0105unicode\""), options);
1672 testing.expectEqual(@TagType(T).string, @as(@TagType(T), r));
1673 testing.expectEqualSlices(u8, "withąunicode", r.string);
1674 testing.expectEqual(@as(usize, 0), fail_alloc.deallocations);
1675 parseFree(T, r, options);
1676 testing.expectEqual(@as(usize, 1), fail_alloc.deallocations);
1677}
1678
1679test "parse into struct with no fields" {
1680 const T = struct {};
1681 testing.expectEqual(T{}, try parse(T, &TokenStream.init("{}"), ParseOptions{}));
1682}
1683
1684test "parse into struct with misc fields" {
1685 @setEvalBranchQuota(10000);
1686 const options = ParseOptions{ .allocator = testing.allocator };
1687 const T = struct {
1688 int: i64,
1689 float: f64,
1690 @"with\\escape": bool,
1691 @"withąunicode😂": bool,
1692 language: []const u8,
1693 optional: ?bool,
1694 default_field: i32 = 42,
1695 static_array: [3]f64,
1696 dynamic_array: []f64,
1697
1698 const Bar = struct {
1699 nested: []const u8,
1700 };
1701 complex: Bar,
1702
1703 const Baz = struct {
1704 foo: []const u8,
1705 };
1706 veryComplex: []Baz,
1707
1708 const Union = union(enum) {
1709 x: u8,
1710 float: f64,
1711 string: []const u8,
1712 };
1713 a_union: Union,
1714 };
1715 const r = try parse(T, &TokenStream.init(
1716 \\{
1717 \\ "int": 420,
1718 \\ "float": 3.14,
1719 \\ "with\\escape": true,
1720 \\ "with\u0105unicode\ud83d\ude02": false,
1721 \\ "language": "zig",
1722 \\ "optional": null,
1723 \\ "static_array": [66.6, 420.420, 69.69],
1724 \\ "dynamic_array": [66.6, 420.420, 69.69],
1725 \\ "complex": {
1726 \\ "nested": "zig"
1727 \\ },
1728 \\ "veryComplex": [
1729 \\ {
1730 \\ "foo": "zig"
1731 \\ }, {
1732 \\ "foo": "rocks"
1733 \\ }
1734 \\ ],
1735 \\ "a_union": 100000
1736 \\}
1737 ), options);
1738 defer parseFree(T, r, options);
1739 testing.expectEqual(@as(i64, 420), r.int);
1740 testing.expectEqual(@as(f64, 3.14), r.float);
1741 testing.expectEqual(true, r.@"with\\escape");
1742 testing.expectEqual(false, r.@"withąunicode😂");
1743 testing.expectEqualSlices(u8, "zig", r.language);
1744 testing.expectEqual(@as(?bool, null), r.optional);
1745 testing.expectEqual(@as(i32, 42), r.default_field);
1746 testing.expectEqual(@as(f64, 66.6), r.static_array[0]);
1747 testing.expectEqual(@as(f64, 420.420), r.static_array[1]);
1748 testing.expectEqual(@as(f64, 69.69), r.static_array[2]);
1749 testing.expectEqual(@as(usize, 3), r.dynamic_array.len);
1750 testing.expectEqual(@as(f64, 66.6), r.dynamic_array[0]);
1751 testing.expectEqual(@as(f64, 420.420), r.dynamic_array[1]);
1752 testing.expectEqual(@as(f64, 69.69), r.dynamic_array[2]);
1753 testing.expectEqualSlices(u8, r.complex.nested, "zig");
1754 testing.expectEqualSlices(u8, "zig", r.veryComplex[0].foo);
1755 testing.expectEqualSlices(u8, "rocks", r.veryComplex[1].foo);
1756 testing.expectEqual(T.Union{ .float = 100000 }, r.a_union);
1757}
1758
12041759/// A non-stream JSON parser which constructs a tree of Value's.
12051760pub const Parser = struct {
12061761 allocator: *Allocator,
......@@ -1686,3 +2241,269 @@ test "string copy option" {
16862241 }
16872242 testing.expect(found_nocopy);
16882243}
2244
2245pub const StringifyOptions = struct {
2246 // TODO: indentation options?
2247 // TODO: make escaping '/' in strings optional?
2248 // TODO: allow picking if []u8 is string or array?
2249};
2250
2251pub fn stringify(
2252 value: var,
2253 options: StringifyOptions,
2254 context: var,
2255 comptime Errors: type,
2256 comptime output: fn (@TypeOf(context), []const u8) Errors!void,
2257) Errors!void {
2258 const T = @TypeOf(value);
2259 switch (@typeInfo(T)) {
2260 .Float, .ComptimeFloat => {
2261 return std.fmt.formatFloatScientific(value, std.fmt.FormatOptions{}, context, Errors, output);
2262 },
2263 .Int, .ComptimeInt => {
2264 return std.fmt.formatIntValue(value, "", std.fmt.FormatOptions{}, context, Errors, output);
2265 },
2266 .Bool => {
2267 return output(context, if (value) "true" else "false");
2268 },
2269 .Optional => {
2270 if (value) |payload| {
2271 return try stringify(payload, options, context, Errors, output);
2272 } else {
2273 return output(context, "null");
2274 }
2275 },
2276 .Enum => {
2277 if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
2278 return value.jsonStringify(options, context, Errors, output);
2279 }
2280
2281 @compileError("Unable to stringify enum '" ++ @typeName(T) ++ "'");
2282 },
2283 .Union => {
2284 if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
2285 return value.jsonStringify(options, context, Errors, output);
2286 }
2287
2288 const info = @typeInfo(T).Union;
2289 if (info.tag_type) |UnionTagType| {
2290 inline for (info.fields) |u_field| {
2291 if (@enumToInt(@as(UnionTagType, value)) == u_field.enum_field.?.value) {
2292 return try stringify(@field(value, u_field.name), options, context, Errors, output);
2293 }
2294 }
2295 } else {
2296 @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'");
2297 }
2298 },
2299 .Struct => |S| {
2300 if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
2301 return value.jsonStringify(options, context, Errors, output);
2302 }
2303
2304 try output(context, "{");
2305 comptime var field_output = false;
2306 inline for (S.fields) |Field, field_i| {
2307 // don't include void fields
2308 if (Field.field_type == void) continue;
2309
2310 if (!field_output) {
2311 field_output = true;
2312 } else {
2313 try output(context, ",");
2314 }
2315
2316 try stringify(Field.name, options, context, Errors, output);
2317 try output(context, ":");
2318 try stringify(@field(value, Field.name), options, context, Errors, output);
2319 }
2320 try output(context, "}");
2321 return;
2322 },
2323 .Pointer => |ptr_info| switch (ptr_info.size) {
2324 .One => {
2325 // TODO: avoid loops?
2326 return try stringify(value.*, options, context, Errors, output);
2327 },
2328 // TODO: .Many when there is a sentinel (waiting for https://github.com/ziglang/zig/pull/3972)
2329 .Slice => {
2330 if (ptr_info.child == u8 and std.unicode.utf8ValidateSlice(value)) {
2331 try output(context, "\"");
2332 var i: usize = 0;
2333 while (i < value.len) : (i += 1) {
2334 switch (value[i]) {
2335 // normal ascii characters
2336 0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => try output(context, value[i .. i + 1]),
2337 // control characters with short escapes
2338 '\\' => try output(context, "\\\\"),
2339 '\"' => try output(context, "\\\""),
2340 '/' => try output(context, "\\/"),
2341 0x8 => try output(context, "\\b"),
2342 0xC => try output(context, "\\f"),
2343 '\n' => try output(context, "\\n"),
2344 '\r' => try output(context, "\\r"),
2345 '\t' => try output(context, "\\t"),
2346 else => {
2347 const ulen = std.unicode.utf8ByteSequenceLength(value[i]) catch unreachable;
2348 const codepoint = std.unicode.utf8Decode(value[i .. i + ulen]) catch unreachable;
2349 if (codepoint <= 0xFFFF) {
2350 // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF),
2351 // then it may be represented as a six-character sequence: a reverse solidus, followed
2352 // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point.
2353 try output(context, "\\u");
2354 try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, context, Errors, output);
2355 } else {
2356 // To escape an extended character that is not in the Basic Multilingual Plane,
2357 // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair.
2358 const high = @intCast(u16, (codepoint - 0x10000) >> 10) + 0xD800;
2359 const low = @intCast(u16, codepoint & 0x3FF) + 0xDC00;
2360 try output(context, "\\u");
2361 try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, context, Errors, output);
2362 try output(context, "\\u");
2363 try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, context, Errors, output);
2364 }
2365 i += ulen - 1;
2366 },
2367 }
2368 }
2369 try output(context, "\"");
2370 return;
2371 }
2372
2373 try output(context, "[");
2374 for (value) |x, i| {
2375 if (i != 0) {
2376 try output(context, ",");
2377 }
2378 try stringify(x, options, context, Errors, output);
2379 }
2380 try output(context, "]");
2381 return;
2382 },
2383 else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
2384 },
2385 .Array => |info| {
2386 return try stringify(value[0..], options, context, Errors, output);
2387 },
2388 else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
2389 }
2390 unreachable;
2391}
2392
2393fn teststringify(expected: []const u8, value: var) !void {
2394 const TestStringifyContext = struct {
2395 expected_remaining: []const u8,
2396 fn testStringifyWrite(context: *@This(), bytes: []const u8) !void {
2397 if (context.expected_remaining.len < bytes.len) {
2398 std.debug.warn(
2399 \\====== expected this output: =========
2400 \\{}
2401 \\======== instead found this: =========
2402 \\{}
2403 \\======================================
2404 , .{
2405 context.expected_remaining,
2406 bytes,
2407 });
2408 return error.TooMuchData;
2409 }
2410 if (!mem.eql(u8, context.expected_remaining[0..bytes.len], bytes)) {
2411 std.debug.warn(
2412 \\====== expected this output: =========
2413 \\{}
2414 \\======== instead found this: =========
2415 \\{}
2416 \\======================================
2417 , .{
2418 context.expected_remaining[0..bytes.len],
2419 bytes,
2420 });
2421 return error.DifferentData;
2422 }
2423 context.expected_remaining = context.expected_remaining[bytes.len..];
2424 }
2425 };
2426 var buf: [100]u8 = undefined;
2427 var context = TestStringifyContext{ .expected_remaining = expected };
2428 try stringify(value, StringifyOptions{}, &context, error{
2429 TooMuchData,
2430 DifferentData,
2431 }, TestStringifyContext.testStringifyWrite);
2432 if (context.expected_remaining.len > 0) return error.NotEnoughData;
2433}
2434
2435test "stringify basic types" {
2436 try teststringify("false", false);
2437 try teststringify("true", true);
2438 try teststringify("null", @as(?u8, null));
2439 try teststringify("null", @as(?*u32, null));
2440 try teststringify("42", 42);
2441 try teststringify("4.2e+01", 42.0);
2442 try teststringify("42", @as(u8, 42));
2443 try teststringify("42", @as(u128, 42));
2444 try teststringify("4.2e+01", @as(f32, 42));
2445 try teststringify("4.2e+01", @as(f64, 42));
2446}
2447
2448test "stringify string" {
2449 try teststringify("\"hello\"", "hello");
2450 try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r");
2451 try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}");
2452 try teststringify("\"with unicode\\u0080\"", "with unicode\u{80}");
2453 try teststringify("\"with unicode\\u00ff\"", "with unicode\u{FF}");
2454 try teststringify("\"with unicode\\u0100\"", "with unicode\u{100}");
2455 try teststringify("\"with unicode\\u0800\"", "with unicode\u{800}");
2456 try teststringify("\"with unicode\\u8000\"", "with unicode\u{8000}");
2457 try teststringify("\"with unicode\\ud799\"", "with unicode\u{D799}");
2458 try teststringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}");
2459 try teststringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}");
2460}
2461
2462test "stringify tagged unions" {
2463 try teststringify("42", union(enum) {
2464 Foo: u32,
2465 Bar: bool,
2466 }{ .Foo = 42 });
2467}
2468
2469test "stringify struct" {
2470 try teststringify("{\"foo\":42}", struct {
2471 foo: u32,
2472 }{ .foo = 42 });
2473}
2474
2475test "stringify struct with void field" {
2476 try teststringify("{\"foo\":42}", struct {
2477 foo: u32,
2478 bar: void = {},
2479 }{ .foo = 42 });
2480}
2481
2482test "stringify array of structs" {
2483 const MyStruct = struct {
2484 foo: u32,
2485 };
2486 try teststringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
2487 MyStruct{ .foo = 42 },
2488 MyStruct{ .foo = 100 },
2489 MyStruct{ .foo = 1000 },
2490 });
2491}
2492
2493test "stringify struct with custom stringifier" {
2494 try teststringify("[\"something special\",42]", struct {
2495 foo: u32,
2496 const Self = @This();
2497 pub fn jsonStringify(
2498 value: Self,
2499 options: StringifyOptions,
2500 context: var,
2501 comptime Errors: type,
2502 comptime output: fn (@TypeOf(context), []const u8) Errors!void,
2503 ) !void {
2504 try output(context, "[\"something special\",");
2505 try stringify(42, options, context, Errors, output);
2506 try output(context, "]");
2507 }
2508 }{ .foo = 42 });
2509}