authorgravatar for quae@daurnimator.comdaurnimator <quae@daurnimator.com> 2019-12-30 22:35:11+11:00
committergravatar for quae@daurnimator.comdaurnimator <quae@daurnimator.com> 2020-02-19 23:01:12+11:00
log5a2060482042129e64af2b6281f8e4bbf1f2fe6e
tree35efb28e180088a04f9e7f122592cd43d1cf0eb9
parentc5ca0fe237e105b95c8edb7f80da899012e573f8
signaturelock-open Commit is signed but in an unrecognized format.

std: add json.stringify to encode arbitrary values to JSON


1 files changed, 266 insertions(+), 0 deletions(-)

lib/std/json.zig+266
...@@ -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
1690pub 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
1696pub 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
1838fn 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
1880test "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
1893test "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
1907test "stringify tagged unions" {
1908 try teststringify("42", union(enum) {
1909 Foo: u32,
1910 Bar: bool,
1911 }{ .Foo = 42 });
1912}
1913
1914test "stringify struct" {
1915 try teststringify("{\"foo\":42}", struct {
1916 foo: u32,
1917 }{ .foo = 42 });
1918}
1919
1920test "stringify struct with void field" {
1921 try teststringify("{\"foo\":42}", struct {
1922 foo: u32,
1923 bar: void = {},
1924 }{ .foo = 42 });
1925}
1926
1927test "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
1938test "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}