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
signature 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" {
13741374 }
13751375}
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
13771436pub const ParseOptions = struct {
13781437 allocator: ?*Allocator = null,
13791438
......@@ -1454,6 +1513,8 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:
14541513 // Parsing some types won't have OutOfMemory in their
14551514 // error-sets, for the condition to be valid, merge it in.
14561515 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;
14571518 // otherwise continue through the `inline for`
14581519 }
14591520 }
......@@ -1471,7 +1532,7 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:
14711532 var fields_seen = [_]bool{false} ** structInfo.fields.len;
14721533 errdefer {
14731534 inline for (structInfo.fields) |field, i| {
1474 if (fields_seen[i]) {
1535 if (fields_seen[i] and !field.is_comptime) {
14751536 parseFree(field.field_type, @field(r, field.name), options);
14761537 }
14771538 }
......@@ -1504,7 +1565,13 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:
15041565 parseFree(field.field_type, @field(r, field.name), options);
15051566 }
15061567 }
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 }
15081575 fields_seen[i] = true;
15091576 found = true;
15101577 break;
......@@ -1518,7 +1585,9 @@ fn parseInternal(comptime T: type, token: Token, tokens: *TokenStream, options:
15181585 inline for (structInfo.fields) |field, i| {
15191586 if (!fields_seen[i]) {
15201587 if (field.default_value) |default| {
1521 @field(r, field.name) = default;
1588 if (!field.is_comptime) {
1589 @field(r, field.name) = default;
1590 }
15221591 } else {
15231592 return error.MissingField;
15241593 }
......@@ -1731,18 +1800,6 @@ test "parse into tagged union" {
17311800 testing.expectEqual(T{ .float = 1.5 }, try parse(T, &TokenStream.init("1.5"), ParseOptions{}));
17321801 }
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
17461803 { // failing allocations should be bubbled up instantly without trying next member
17471804 var fail_alloc = testing.FailingAllocator.init(testing.allocator, 0);
17481805 const options = ParseOptions{ .allocator = &fail_alloc.allocator };
......@@ -1772,6 +1829,25 @@ test "parse into tagged union" {
17721829 }
17731830}
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
17751851test "parseFree descends into tagged union" {
17761852 var fail_alloc = testing.FailingAllocator.init(testing.allocator, 1);
17771853 const options = ParseOptions{ .allocator = &fail_alloc.allocator };
......@@ -1789,6 +1865,43 @@ test "parseFree descends into tagged union" {
17891865 testing.expectEqual(@as(usize, 1), fail_alloc.deallocations);
17901866}
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
17921905test "parse into struct with no fields" {
17931906 const T = struct {};
17941907 testing.expectEqual(T{}, try parse(T, &TokenStream.init("{}"), ParseOptions{}));