authorgravatar for quae@daurnimator.comdaurnimator <quae@daurnimator.com> 2019-12-29 17:46:32+11:00
committergravatar for quae@daurnimator.comdaurnimator <quae@daurnimator.com> 2020-02-19 23:16:35+11:00
logd989396a3473836a9e2e269b2be9675574ead389
tree0bfe0ff21e33d9b188f9f766de420902a4886700
parent5a2060482042129e64af2b6281f8e4bbf1f2fe6e
signature Commit is signed but in an unrecognized format.

std: add json.parse to automatically decode json into a struct


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

lib/std/json.zig+552
...@@ -19,6 +19,74 @@ const StringEscapes = union(enum) {...@@ -19,6 +19,74 @@ const StringEscapes = union(enum) {
19 },19 },
20};20};
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
22/// A single token slice into the parent string.90/// A single token slice into the parent string.
23///91///
24/// Use `token.slice()` on the input at the current position to get the current slice.92/// Use `token.slice()` on the input at the current position to get the current slice.
...@@ -1201,6 +1269,490 @@ pub const Value = union(enum) {...@@ -1201,6 +1269,490 @@ pub const Value = union(enum) {
1201 }1269 }
1202};1270};
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 var fail_alloc = testing.FailingAllocator.init(testing.allocator, 1);
1661 const options = ParseOptions{ .allocator = &fail_alloc.allocator };
1662 const T = union(enum) {
1663 int: i32,
1664 float: f64,
1665 string: []const u8,
1666 };
1667 // use a string with unicode escape so we know result can't be a reference to global constant
1668 const r = try parse(T, &TokenStream.init("\"with\\u0105unicode\""), options);
1669 testing.expectEqual(@TagType(T).string, @as(@TagType(T), r));
1670 testing.expectEqualSlices(u8, "withąunicode", r.string);
1671 testing.expectEqual(@as(usize, 0), fail_alloc.deallocations);
1672 parseFree(T, r, options);
1673 testing.expectEqual(@as(usize, 1), fail_alloc.deallocations);
1674}
1675
1676test "parse into struct with no fields" {
1677 const T = struct {};
1678 testing.expectEqual(T{}, try parse(T, &TokenStream.init("{}"), ParseOptions{}));
1679}
1680
1681test "parse into struct with misc fields" {
1682 @setEvalBranchQuota(10000);
1683 const options = ParseOptions{ .allocator = testing.allocator };
1684 const T = struct {
1685 int: i64,
1686 float: f64,
1687 @"with\\escape": bool,
1688 @"withąunicode😂": bool,
1689 language: []const u8,
1690 optional: ?bool,
1691 default_field: i32 = 42,
1692 static_array: [3]f64,
1693 dynamic_array: []f64,
1694
1695 const Bar = struct {
1696 nested: []const u8,
1697 };
1698 complex: Bar,
1699
1700 const Baz = struct {
1701 foo: []const u8,
1702 };
1703 veryComplex: []Baz,
1704
1705 const Union = union(enum) {
1706 x: u8,
1707 float: f64,
1708 string: []const u8,
1709 };
1710 a_union: Union,
1711 };
1712 const r = try parse(T, &TokenStream.init(
1713 \\{
1714 \\ "int": 420,
1715 \\ "float": 3.14,
1716 \\ "with\\escape": true,
1717 \\ "with\u0105unicode\ud83d\ude02": false,
1718 \\ "language": "zig",
1719 \\ "optional": null,
1720 \\ "static_array": [66.6, 420.420, 69.69],
1721 \\ "dynamic_array": [66.6, 420.420, 69.69],
1722 \\ "complex": {
1723 \\ "nested": "zig"
1724 \\ },
1725 \\ "veryComplex": [
1726 \\ {
1727 \\ "foo": "zig"
1728 \\ }, {
1729 \\ "foo": "rocks"
1730 \\ }
1731 \\ ],
1732 \\ "a_union": 100000
1733 \\}
1734 ), options);
1735 defer parseFree(T, r, options);
1736 testing.expectEqual(@as(i64, 420), r.int);
1737 testing.expectEqual(@as(f64, 3.14), r.float);
1738 testing.expectEqual(true, r.@"with\\escape");
1739 testing.expectEqual(false, r.@"withąunicode😂");
1740 testing.expectEqualSlices(u8, "zig", r.language);
1741 testing.expectEqual(@as(?bool, null), r.optional);
1742 testing.expectEqual(@as(i32, 42), r.default_field);
1743 testing.expectEqual(@as(f64, 66.6), r.static_array[0]);
1744 testing.expectEqual(@as(f64, 420.420), r.static_array[1]);
1745 testing.expectEqual(@as(f64, 69.69), r.static_array[2]);
1746 testing.expectEqual(@as(usize, 3), r.dynamic_array.len);
1747 testing.expectEqual(@as(f64, 66.6), r.dynamic_array[0]);
1748 testing.expectEqual(@as(f64, 420.420), r.dynamic_array[1]);
1749 testing.expectEqual(@as(f64, 69.69), r.dynamic_array[2]);
1750 testing.expectEqualSlices(u8, r.complex.nested, "zig");
1751 testing.expectEqualSlices(u8, "zig", r.veryComplex[0].foo);
1752 testing.expectEqualSlices(u8, "rocks", r.veryComplex[1].foo);
1753 testing.expectEqual(T.Union{ .float = 100000 }, r.a_union);
1754}
1755
1204/// A non-stream JSON parser which constructs a tree of Value's.1756/// A non-stream JSON parser which constructs a tree of Value's.
1205pub const Parser = struct {1757pub const Parser = struct {
1206 allocator: *Allocator,1758 allocator: *Allocator,