| ... | @@ -1686,3 +1686,269 @@ test "string copy option" { | ... | @@ -1686,3 +1686,269 @@ test "string copy option" { |
| 1686 | } | 1686 | } |
| 1687 | testing.expect(found_nocopy); | 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 | } |