| ... | ... | @@ -19,6 +19,74 @@ const StringEscapes = union(enum) { |
| 19 | 19 | }, |
| 20 | 20 | }; |
| 21 | 21 | |
| 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 |
| 24 | fn 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 | |
| 74 | test "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 | 90 | /// A single token slice into the parent string. |
| 23 | 91 | /// |
| 24 | 92 | /// Use `token.slice()` on the input at the current position to get the current slice. |
| ... | ... | @@ -1201,6 +1269,493 @@ pub const Value = union(enum) { |
| 1201 | 1269 | } |
| 1202 | 1270 | }; |
| 1203 | 1271 | |
| 1272 | pub 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 | |
| 1283 | fn 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 | |
| 1511 | pub 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` |
| 1518 | pub 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 | |
| 1568 | test "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 | |
| 1582 | test "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 | |
| 1595 | test "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 | |
| 1616 | test "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 | |
| 1659 | test "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 | |
| 1679 | test "parse into struct with no fields" { |
| 1680 | const T = struct {}; |
| 1681 | testing.expectEqual(T{}, try parse(T, &TokenStream.init("{}"), ParseOptions{})); |
| 1682 | } |
| 1683 | |
| 1684 | test "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 | |
| 1204 | 1759 | /// A non-stream JSON parser which constructs a tree of Value's. |
| 1205 | 1760 | pub const Parser = struct { |
| 1206 | 1761 | allocator: *Allocator, |
| ... | ... | @@ -1686,3 +2241,269 @@ test "string copy option" { |
| 1686 | 2241 | } |
| 1687 | 2242 | testing.expect(found_nocopy); |
| 1688 | 2243 | } |
| 2244 | |
| 2245 | pub 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 | |
| 2251 | pub 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 | |
| 2393 | fn 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 | |
| 2435 | test "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 | |
| 2448 | test "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 | |
| 2462 | test "stringify tagged unions" { |
| 2463 | try teststringify("42", union(enum) { |
| 2464 | Foo: u32, |
| 2465 | Bar: bool, |
| 2466 | }{ .Foo = 42 }); |
| 2467 | } |
| 2468 | |
| 2469 | test "stringify struct" { |
| 2470 | try teststringify("{\"foo\":42}", struct { |
| 2471 | foo: u32, |
| 2472 | }{ .foo = 42 }); |
| 2473 | } |
| 2474 | |
| 2475 | test "stringify struct with void field" { |
| 2476 | try teststringify("{\"foo\":42}", struct { |
| 2477 | foo: u32, |
| 2478 | bar: void = {}, |
| 2479 | }{ .foo = 42 }); |
| 2480 | } |
| 2481 | |
| 2482 | test "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 | |
| 2493 | test "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 | } |