| ... | @@ -19,6 +19,74 @@ const StringEscapes = union(enum) { | ... | @@ -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 | /// 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 | }; |
| 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 | 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 | |
| | 1676 | test "parse into struct with no fields" { |
| | 1677 | const T = struct {}; |
| | 1678 | testing.expectEqual(T{}, try parse(T, &TokenStream.init("{}"), ParseOptions{})); |
| | 1679 | } |
| | 1680 | |
| | 1681 | test "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. |
| 1205 | pub const Parser = struct { | 1757 | pub const Parser = struct { |
| 1206 | allocator: *Allocator, | 1758 | allocator: *Allocator, |