| ... | ... | @@ -1686,3 +1686,269 @@ test "string copy option" { |
| 1686 | 1686 | } |
| 1687 | 1687 | testing.expect(found_nocopy); |
| 1688 | 1688 | } |
| 1689 | |
| 1690 | pub const StringifyOptions = struct { |
| 1691 | // TODO: indentation options? |
| 1692 | // TODO: make escaping '/' in strings optional? |
| 1693 | // TODO: allow picking if []u8 is string or array? |
| 1694 | }; |
| 1695 | |
| 1696 | pub fn stringify( |
| 1697 | value: var, |
| 1698 | options: StringifyOptions, |
| 1699 | context: var, |
| 1700 | comptime Errors: type, |
| 1701 | comptime output: fn (@TypeOf(context), []const u8) Errors!void, |
| 1702 | ) Errors!void { |
| 1703 | const T = @TypeOf(value); |
| 1704 | switch (@typeInfo(T)) { |
| 1705 | .Float, .ComptimeFloat => { |
| 1706 | return std.fmt.formatFloatScientific(value, std.fmt.FormatOptions{}, context, Errors, output); |
| 1707 | }, |
| 1708 | .Int, .ComptimeInt => { |
| 1709 | return std.fmt.formatIntValue(value, "", std.fmt.FormatOptions{}, context, Errors, output); |
| 1710 | }, |
| 1711 | .Bool => { |
| 1712 | return output(context, if (value) "true" else "false"); |
| 1713 | }, |
| 1714 | .Optional => { |
| 1715 | if (value) |payload| { |
| 1716 | return try stringify(payload, options, context, Errors, output); |
| 1717 | } else { |
| 1718 | return output(context, "null"); |
| 1719 | } |
| 1720 | }, |
| 1721 | .Enum => { |
| 1722 | if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { |
| 1723 | return value.jsonStringify(options, context, Errors, output); |
| 1724 | } |
| 1725 | |
| 1726 | @compileError("Unable to stringify enum '" ++ @typeName(T) ++ "'"); |
| 1727 | }, |
| 1728 | .Union => { |
| 1729 | if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { |
| 1730 | return value.jsonStringify(options, context, Errors, output); |
| 1731 | } |
| 1732 | |
| 1733 | const info = @typeInfo(T).Union; |
| 1734 | if (info.tag_type) |UnionTagType| { |
| 1735 | inline for (info.fields) |u_field| { |
| 1736 | if (@enumToInt(@as(UnionTagType, value)) == u_field.enum_field.?.value) { |
| 1737 | return try stringify(@field(value, u_field.name), options, context, Errors, output); |
| 1738 | } |
| 1739 | } |
| 1740 | } else { |
| 1741 | @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'"); |
| 1742 | } |
| 1743 | }, |
| 1744 | .Struct => |S| { |
| 1745 | if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { |
| 1746 | return value.jsonStringify(options, context, Errors, output); |
| 1747 | } |
| 1748 | |
| 1749 | try output(context, "{"); |
| 1750 | comptime var field_output = false; |
| 1751 | inline for (S.fields) |Field, field_i| { |
| 1752 | // don't include void fields |
| 1753 | if (Field.field_type == void) continue; |
| 1754 | |
| 1755 | if (!field_output) { |
| 1756 | field_output = true; |
| 1757 | } else { |
| 1758 | try output(context, ","); |
| 1759 | } |
| 1760 | |
| 1761 | try stringify(Field.name, options, context, Errors, output); |
| 1762 | try output(context, ":"); |
| 1763 | try stringify(@field(value, Field.name), options, context, Errors, output); |
| 1764 | } |
| 1765 | try output(context, "}"); |
| 1766 | return; |
| 1767 | }, |
| 1768 | .Pointer => |ptr_info| switch (ptr_info.size) { |
| 1769 | .One => { |
| 1770 | // TODO: avoid loops? |
| 1771 | return try stringify(value.*, options, context, Errors, output); |
| 1772 | }, |
| 1773 | // TODO: .Many when there is a sentinel (waiting for https://github.com/ziglang/zig/pull/3972) |
| 1774 | .Slice => { |
| 1775 | if (ptr_info.child == u8 and std.unicode.utf8ValidateSlice(value)) { |
| 1776 | try output(context, "\""); |
| 1777 | var i: usize = 0; |
| 1778 | while (i < value.len) : (i += 1) { |
| 1779 | switch (value[i]) { |
| 1780 | // normal ascii characters |
| 1781 | 0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => try output(context, value[i .. i + 1]), |
| 1782 | // control characters with short escapes |
| 1783 | '\\' => try output(context, "\\\\"), |
| 1784 | '\"' => try output(context, "\\\""), |
| 1785 | '/' => try output(context, "\\/"), |
| 1786 | 0x8 => try output(context, "\\b"), |
| 1787 | 0xC => try output(context, "\\f"), |
| 1788 | '\n' => try output(context, "\\n"), |
| 1789 | '\r' => try output(context, "\\r"), |
| 1790 | '\t' => try output(context, "\\t"), |
| 1791 | else => { |
| 1792 | const ulen = std.unicode.utf8ByteSequenceLength(value[i]) catch unreachable; |
| 1793 | const codepoint = std.unicode.utf8Decode(value[i .. i + ulen]) catch unreachable; |
| 1794 | if (codepoint <= 0xFFFF) { |
| 1795 | // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF), |
| 1796 | // then it may be represented as a six-character sequence: a reverse solidus, followed |
| 1797 | // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point. |
| 1798 | try output(context, "\\u"); |
| 1799 | try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, context, Errors, output); |
| 1800 | } else { |
| 1801 | // To escape an extended character that is not in the Basic Multilingual Plane, |
| 1802 | // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair. |
| 1803 | const high = @intCast(u16, (codepoint - 0x10000) >> 10) + 0xD800; |
| 1804 | const low = @intCast(u16, codepoint & 0x3FF) + 0xDC00; |
| 1805 | try output(context, "\\u"); |
| 1806 | try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, context, Errors, output); |
| 1807 | try output(context, "\\u"); |
| 1808 | try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, context, Errors, output); |
| 1809 | } |
| 1810 | i += ulen - 1; |
| 1811 | }, |
| 1812 | } |
| 1813 | } |
| 1814 | try output(context, "\""); |
| 1815 | return; |
| 1816 | } |
| 1817 | |
| 1818 | try output(context, "["); |
| 1819 | for (value) |x, i| { |
| 1820 | if (i != 0) { |
| 1821 | try output(context, ","); |
| 1822 | } |
| 1823 | try stringify(x, options, context, Errors, output); |
| 1824 | } |
| 1825 | try output(context, "]"); |
| 1826 | return; |
| 1827 | }, |
| 1828 | else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"), |
| 1829 | }, |
| 1830 | .Array => |info| { |
| 1831 | return try stringify(value[0..], options, context, Errors, output); |
| 1832 | }, |
| 1833 | else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"), |
| 1834 | } |
| 1835 | unreachable; |
| 1836 | } |
| 1837 | |
| 1838 | fn teststringify(expected: []const u8, value: var) !void { |
| 1839 | const TestStringifyContext = struct { |
| 1840 | expected_remaining: []const u8, |
| 1841 | fn testStringifyWrite(context: *@This(), bytes: []const u8) !void { |
| 1842 | if (context.expected_remaining.len < bytes.len) { |
| 1843 | std.debug.warn( |
| 1844 | \\====== expected this output: ========= |
| 1845 | \\{} |
| 1846 | \\======== instead found this: ========= |
| 1847 | \\{} |
| 1848 | \\====================================== |
| 1849 | , .{ |
| 1850 | context.expected_remaining, |
| 1851 | bytes, |
| 1852 | }); |
| 1853 | return error.TooMuchData; |
| 1854 | } |
| 1855 | if (!mem.eql(u8, context.expected_remaining[0..bytes.len], bytes)) { |
| 1856 | std.debug.warn( |
| 1857 | \\====== expected this output: ========= |
| 1858 | \\{} |
| 1859 | \\======== instead found this: ========= |
| 1860 | \\{} |
| 1861 | \\====================================== |
| 1862 | , .{ |
| 1863 | context.expected_remaining[0..bytes.len], |
| 1864 | bytes, |
| 1865 | }); |
| 1866 | return error.DifferentData; |
| 1867 | } |
| 1868 | context.expected_remaining = context.expected_remaining[bytes.len..]; |
| 1869 | } |
| 1870 | }; |
| 1871 | var buf: [100]u8 = undefined; |
| 1872 | var context = TestStringifyContext{ .expected_remaining = expected }; |
| 1873 | try stringify(value, StringifyOptions{}, &context, error{ |
| 1874 | TooMuchData, |
| 1875 | DifferentData, |
| 1876 | }, TestStringifyContext.testStringifyWrite); |
| 1877 | if (context.expected_remaining.len > 0) return error.NotEnoughData; |
| 1878 | } |
| 1879 | |
| 1880 | test "stringify basic types" { |
| 1881 | try teststringify("false", false); |
| 1882 | try teststringify("true", true); |
| 1883 | try teststringify("null", @as(?u8, null)); |
| 1884 | try teststringify("null", @as(?*u32, null)); |
| 1885 | try teststringify("42", 42); |
| 1886 | try teststringify("4.2e+01", 42.0); |
| 1887 | try teststringify("42", @as(u8, 42)); |
| 1888 | try teststringify("42", @as(u128, 42)); |
| 1889 | try teststringify("4.2e+01", @as(f32, 42)); |
| 1890 | try teststringify("4.2e+01", @as(f64, 42)); |
| 1891 | } |
| 1892 | |
| 1893 | test "stringify string" { |
| 1894 | try teststringify("\"hello\"", "hello"); |
| 1895 | try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r"); |
| 1896 | try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}"); |
| 1897 | try teststringify("\"with unicode\\u0080\"", "with unicode\u{80}"); |
| 1898 | try teststringify("\"with unicode\\u00ff\"", "with unicode\u{FF}"); |
| 1899 | try teststringify("\"with unicode\\u0100\"", "with unicode\u{100}"); |
| 1900 | try teststringify("\"with unicode\\u0800\"", "with unicode\u{800}"); |
| 1901 | try teststringify("\"with unicode\\u8000\"", "with unicode\u{8000}"); |
| 1902 | try teststringify("\"with unicode\\ud799\"", "with unicode\u{D799}"); |
| 1903 | try teststringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}"); |
| 1904 | try teststringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}"); |
| 1905 | } |
| 1906 | |
| 1907 | test "stringify tagged unions" { |
| 1908 | try teststringify("42", union(enum) { |
| 1909 | Foo: u32, |
| 1910 | Bar: bool, |
| 1911 | }{ .Foo = 42 }); |
| 1912 | } |
| 1913 | |
| 1914 | test "stringify struct" { |
| 1915 | try teststringify("{\"foo\":42}", struct { |
| 1916 | foo: u32, |
| 1917 | }{ .foo = 42 }); |
| 1918 | } |
| 1919 | |
| 1920 | test "stringify struct with void field" { |
| 1921 | try teststringify("{\"foo\":42}", struct { |
| 1922 | foo: u32, |
| 1923 | bar: void = {}, |
| 1924 | }{ .foo = 42 }); |
| 1925 | } |
| 1926 | |
| 1927 | test "stringify array of structs" { |
| 1928 | const MyStruct = struct { |
| 1929 | foo: u32, |
| 1930 | }; |
| 1931 | try teststringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{ |
| 1932 | MyStruct{ .foo = 42 }, |
| 1933 | MyStruct{ .foo = 100 }, |
| 1934 | MyStruct{ .foo = 1000 }, |
| 1935 | }); |
| 1936 | } |
| 1937 | |
| 1938 | test "stringify struct with custom stringifier" { |
| 1939 | try teststringify("[\"something special\",42]", struct { |
| 1940 | foo: u32, |
| 1941 | const Self = @This(); |
| 1942 | pub fn jsonStringify( |
| 1943 | value: Self, |
| 1944 | options: StringifyOptions, |
| 1945 | context: var, |
| 1946 | comptime Errors: type, |
| 1947 | comptime output: fn (@TypeOf(context), []const u8) Errors!void, |
| 1948 | ) !void { |
| 1949 | try output(context, "[\"something special\","); |
| 1950 | try stringify(42, options, context, Errors, output); |
| 1951 | try output(context, "]"); |
| 1952 | } |
| 1953 | }{ .foo = 42 }); |
| 1954 | } |