authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2021-02-01 12:11:36-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2021-02-01 12:11:36-08:00
log66c0fe4f90e5767a608efc77dbcecf2a5c8a5173
treed51fca4a9cf8f08da4362688f780eb32fab7b502
parenta03f9548d3dd32876f99f5b7bdf1d678c5a5b98e
parente0a04e7f6739aa0d81e3102c04af37ab51e550d4
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #7922 from daurnimator/comptime-json-fields

std.json support for comptime fields

1 files changed, 128 insertions(+), 15 deletions(-)

lib/std/json.zig+128-15
...@@ -1374,6 +1374,65 @@ test "Value.jsonStringify" {...@@ -1374,6 +1374,65 @@ test "Value.jsonStringify" {
1374 }1374 }
1375}1375}
13761376
1377/// parse tokens from a stream, returning `false` if they do not decode to `value`
1378fn parsesTo(comptime T: type, value: T, tokens: *TokenStream, options: ParseOptions) !bool {
1379 // TODO: should be able to write this function to not require an allocator
1380 const tmp = try parse(T, tokens, options);
1381 defer parseFree(T, tmp, options);
1382
1383 return parsedEqual(tmp, value);
1384}
1385
1386/// Returns if a value returned by `parse` is deep-equal to another value
1387fn parsedEqual(a: anytype, b: @TypeOf(a)) bool {
1388 switch (@typeInfo(@TypeOf(a))) {
1389 .Optional => {
1390 if (a == null and b == null) return true;
1391 if (a == null or b == null) return false;
1392 return parsedEqual(a.?, b.?);
1393 },
1394 .Union => |unionInfo| {
1395 if (info.tag_type) |UnionTag| {
1396 const tag_a = std.meta.activeTag(a);
1397 const tag_b = std.meta.activeTag(b);
1398 if (tag_a != tag_b) return false;
1399
1400 inline for (info.fields) |field_info| {
1401 if (@field(UnionTag, field_info.name) == tag_a) {
1402 return parsedEqual(@field(a, field_info.name), @field(b, field_info.name));
1403 }
1404 }
1405 return false;
1406 } else {
1407 unreachable;
1408 }
1409 },
1410 .Array => {
1411 for (a) |e, i|
1412 if (!parsedEqual(e, b[i])) return false;
1413 return true;
1414 },
1415 .Struct => |info| {
1416 inline for (info.fields) |field_info| {
1417 if (!parsedEqual(@field(a, field_info.name), @field(b, field_info.name))) return false;
1418 }
1419 return true;
1420 },
1421 .Pointer => |ptrInfo| switch (ptrInfo.size) {
1422 .One => return parsedEqual(a.*, b.*),
1423 .Slice => {
1424 if (a.len != b.len) return false;
1425 for (a) |e, i|
1426 if (!parsedEqual(e, b[i])) return false;
1427 return true;
1428 },
1429 .Many, .C => unreachable,
1430 },
1431 else => return a == b,
1432 }
1433 unreachable;
1434}
1435
1377pub const ParseOptions = struct {1436pub const ParseOptions = struct {
1378 allocator: ?*Allocator = null,1437 allocator: ?*Allocator = null,
13791438
...@@ -1454,6 +1513,8 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:...@@ -1454,6 +1513,8 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:
1454 // Parsing some types won't have OutOfMemory in their1513 // Parsing some types won't have OutOfMemory in their
1455 // error-sets, for the condition to be valid, merge it in.1514 // error-sets, for the condition to be valid, merge it in.
1456 if (@as(@TypeOf(err) || error{OutOfMemory}, err) == error.OutOfMemory) return err;1515 if (@as(@TypeOf(err) || error{OutOfMemory}, err) == error.OutOfMemory) return err;
1516 // Bubble up AllocatorRequired, as it indicates missing option
1517 if (@as(@TypeOf(err) || error{AllocatorRequired}, err) == error.AllocatorRequired) return err;
1457 // otherwise continue through the `inline for`1518 // otherwise continue through the `inline for`
1458 }1519 }
1459 }1520 }
...@@ -1471,7 +1532,7 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:...@@ -1471,7 +1532,7 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:
1471 var fields_seen = [_]bool{false} ** structInfo.fields.len;1532 var fields_seen = [_]bool{false} ** structInfo.fields.len;
1472 errdefer {1533 errdefer {
1473 inline for (structInfo.fields) |field, i| {1534 inline for (structInfo.fields) |field, i| {
1474 if (fields_seen[i]) {1535 if (fields_seen[i] and !field.is_comptime) {
1475 parseFree(field.field_type, @field(r, field.name), options);1536 parseFree(field.field_type, @field(r, field.name), options);
1476 }1537 }
1477 }1538 }
...@@ -1504,7 +1565,13 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:...@@ -1504,7 +1565,13 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:
1504 parseFree(field.field_type, @field(r, field.name), options);1565 parseFree(field.field_type, @field(r, field.name), options);
1505 }1566 }
1506 }1567 }
1507 @field(r, field.name) = try parse(field.field_type, tokens, options);1568 if (field.is_comptime) {
1569 if (!try parsesTo(field.field_type, field.default_value.?, tokens, options)) {
1570 return error.UnexpectedValue;
1571 }
1572 } else {
1573 @field(r, field.name) = try parse(field.field_type, tokens, options);
1574 }
1508 fields_seen[i] = true;1575 fields_seen[i] = true;
1509 found = true;1576 found = true;
1510 break;1577 break;
...@@ -1518,7 +1585,9 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:...@@ -1518,7 +1585,9 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:
1518 inline for (structInfo.fields) |field, i| {1585 inline for (structInfo.fields) |field, i| {
1519 if (!fields_seen[i]) {1586 if (!fields_seen[i]) {
1520 if (field.default_value) |default| {1587 if (field.default_value) |default| {
1521 @field(r, field.name) = default;1588 if (!field.is_comptime) {
1589 @field(r, field.name) = default;
1590 }
1522 } else {1591 } else {
1523 return error.MissingField;1592 return error.MissingField;
1524 }1593 }
...@@ -1731,18 +1800,6 @@ test "parse into tagged union" {...@@ -1731,18 +1800,6 @@ test "parse into tagged union" {
1731 testing.expectEqual(T{ .float = 1.5 }, try parse(T, &TokenStream.init("1.5"), ParseOptions{}));1800 testing.expectEqual(T{ .float = 1.5 }, try parse(T, &TokenStream.init("1.5"), ParseOptions{}));
1732 }1801 }
17331802
1734 { // if union matches string member, fails with NoUnionMembersMatched rather than AllocatorRequired
1735 // Note that this behaviour wasn't necessarily by design, but was
1736 // what fell out of the implementation and may result in interesting
1737 // API breakage if changed
1738 const T = union(enum) {
1739 int: i32,
1740 float: f64,
1741 string: []const u8,
1742 };
1743 testing.expectError(error.NoUnionMembersMatched, parse(T, &TokenStream.init("\"foo\""), ParseOptions{}));
1744 }
1745
1746 { // failing allocations should be bubbled up instantly without trying next member1803 { // failing allocations should be bubbled up instantly without trying next member
1747 var fail_alloc = testing.FailingAllocator.init(testing.allocator, 0);1804 var fail_alloc = testing.FailingAllocator.init(testing.allocator, 0);
1748 const options = ParseOptions{ .allocator = &fail_alloc.allocator };1805 const options = ParseOptions{ .allocator = &fail_alloc.allocator };
...@@ -1772,6 +1829,25 @@ test "parse into tagged union" {...@@ -1772,6 +1829,25 @@ test "parse into tagged union" {
1772 }1829 }
1773}1830}
17741831
1832test "parse union bubbles up AllocatorRequired" {
1833 { // string member first in union (and not matching)
1834 const T = union(enum) {
1835 string: []const u8,
1836 int: i32,
1837 };
1838 testing.expectError(error.AllocatorRequired, parse(T, &TokenStream.init("42"), ParseOptions{}));
1839 }
1840
1841 { // string member not first in union (and matching)
1842 const T = union(enum) {
1843 int: i32,
1844 float: f64,
1845 string: []const u8,
1846 };
1847 testing.expectError(error.AllocatorRequired, parse(T, &TokenStream.init("\"foo\""), ParseOptions{}));
1848 }
1849}
1850
1775test "parseFree descends into tagged union" {1851test "parseFree descends into tagged union" {
1776 var fail_alloc = testing.FailingAllocator.init(testing.allocator, 1);1852 var fail_alloc = testing.FailingAllocator.init(testing.allocator, 1);
1777 const options = ParseOptions{ .allocator = &fail_alloc.allocator };1853 const options = ParseOptions{ .allocator = &fail_alloc.allocator };
...@@ -1789,6 +1865,43 @@ test "parseFree descends into tagged union" {...@@ -1789,6 +1865,43 @@ test "parseFree descends into tagged union" {
1789 testing.expectEqual(@as(usize, 1), fail_alloc.deallocations);1865 testing.expectEqual(@as(usize, 1), fail_alloc.deallocations);
1790}1866}
17911867
1868test "parse with comptime field" {
1869 {
1870 const T = struct {
1871 comptime a: i32 = 0,
1872 b: bool,
1873 };
1874 testing.expectEqual(T{ .a = 0, .b = true }, try parse(T, &TokenStream.init(
1875 \\{
1876 \\ "a": 0,
1877 \\ "b": true
1878 \\}
1879 ), ParseOptions{}));
1880 }
1881
1882 { // string comptime values currently require an allocator
1883 const T = union(enum) {
1884 foo: struct {
1885 comptime kind: []const u8 = "boolean",
1886 b: bool,
1887 },
1888 bar: struct {
1889 comptime kind: []const u8 = "float",
1890 b: f64,
1891 },
1892 };
1893
1894 const r = try std.json.parse(T, &std.json.TokenStream.init(
1895 \\{
1896 \\ "kind": "float",
1897 \\ "b": 1.0
1898 \\}
1899 ), .{
1900 .allocator = std.testing.allocator,
1901 });
1902 }
1903}
1904
1792test "parse into struct with no fields" {1905test "parse into struct with no fields" {
1793 const T = struct {};1906 const T = struct {};
1794 testing.expectEqual(T{}, try parse(T, &TokenStream.init("{}"), ParseOptions{}));1907 testing.expectEqual(T{}, try parse(T, &TokenStream.init("{}"), ParseOptions{}));