| ... | @@ -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,493 @@ pub const Value = union(enum) { | ... | @@ -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 | /// A non-stream JSON parser which constructs a tree of Value's. | 1759 | /// A non-stream JSON parser which constructs a tree of Value's. |
| 1205 | pub const Parser = struct { | 1760 | pub const Parser = struct { |
| 1206 | allocator: *Allocator, | 1761 | allocator: *Allocator, |
| ... | @@ -1686,3 +2241,269 @@ test "string copy option" { | ... | @@ -1686,3 +2241,269 @@ test "string copy option" { |
| 1686 | } | 2241 | } |
| 1687 | testing.expect(found_nocopy); | 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 | } |