| ... | @@ -1374,6 +1374,65 @@ test "Value.jsonStringify" { | ... | @@ -1374,6 +1374,65 @@ test "Value.jsonStringify" { |
| 1374 | } | 1374 | } |
| 1375 | } | 1375 | } |
| 1376 | | 1376 | |
| | 1377 | /// parse tokens from a stream, returning `false` if they do not decode to `value` |
| | 1378 | fn 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 |
| | 1387 | fn 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 | |
| 1377 | pub const ParseOptions = struct { | 1436 | pub const ParseOptions = struct { |
| 1378 | allocator: ?*Allocator = null, | 1437 | allocator: ?*Allocator = null, |
| 1379 | | 1438 | |
| ... | @@ -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 their | 1513 | // 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 | } |
| 1733 | | 1802 | |
| 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 member | 1803 | { // 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 | } |
| 1774 | | 1831 | |
| | 1832 | test "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 | |
| 1775 | test "parseFree descends into tagged union" { | 1851 | test "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 | } |
| 1791 | | 1867 | |
| | 1868 | test "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 | |
| 1792 | test "parse into struct with no fields" { | 1905 | test "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{})); |