diff options
Diffstat (limited to 'lib/std')
| -rw-r--r-- | lib/std/json.zig | 266 |
1 files changed, 266 insertions, 0 deletions
diff --git a/lib/std/json.zig b/lib/std/json.zig index b04861eaae..37ee20f2a2 100644 --- a/lib/std/json.zig +++ b/lib/std/json.zig @@ -1686,3 +1686,269 @@ test "string copy option" { } testing.expect(found_nocopy); } + +pub const StringifyOptions = struct { + // TODO: indentation options? + // TODO: make escaping '/' in strings optional? + // TODO: allow picking if []u8 is string or array? +}; + +pub fn stringify( + value: var, + options: StringifyOptions, + context: var, + comptime Errors: type, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, +) Errors!void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .Float, .ComptimeFloat => { + return std.fmt.formatFloatScientific(value, std.fmt.FormatOptions{}, context, Errors, output); + }, + .Int, .ComptimeInt => { + return std.fmt.formatIntValue(value, "", std.fmt.FormatOptions{}, context, Errors, output); + }, + .Bool => { + return output(context, if (value) "true" else "false"); + }, + .Optional => { + if (value) |payload| { + return try stringify(payload, options, context, Errors, output); + } else { + return output(context, "null"); + } + }, + .Enum => { + if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { + return value.jsonStringify(options, context, Errors, output); + } + + @compileError("Unable to stringify enum '" ++ @typeName(T) ++ "'"); + }, + .Union => { + if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { + return value.jsonStringify(options, context, Errors, output); + } + + const info = @typeInfo(T).Union; + if (info.tag_type) |UnionTagType| { + inline for (info.fields) |u_field| { + if (@enumToInt(@as(UnionTagType, value)) == u_field.enum_field.?.value) { + return try stringify(@field(value, u_field.name), options, context, Errors, output); + } + } + } else { + @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'"); + } + }, + .Struct => |S| { + if (comptime std.meta.trait.hasFn("jsonStringify")(T)) { + return value.jsonStringify(options, context, Errors, output); + } + + try output(context, "{"); + comptime var field_output = false; + inline for (S.fields) |Field, field_i| { + // don't include void fields + if (Field.field_type == void) continue; + + if (!field_output) { + field_output = true; + } else { + try output(context, ","); + } + + try stringify(Field.name, options, context, Errors, output); + try output(context, ":"); + try stringify(@field(value, Field.name), options, context, Errors, output); + } + try output(context, "}"); + return; + }, + .Pointer => |ptr_info| switch (ptr_info.size) { + .One => { + // TODO: avoid loops? + return try stringify(value.*, options, context, Errors, output); + }, + // TODO: .Many when there is a sentinel (waiting for https://github.com/ziglang/zig/pull/3972) + .Slice => { + if (ptr_info.child == u8 and std.unicode.utf8ValidateSlice(value)) { + try output(context, "\""); + var i: usize = 0; + while (i < value.len) : (i += 1) { + switch (value[i]) { + // normal ascii characters + 0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => try output(context, value[i .. i + 1]), + // control characters with short escapes + '\\' => try output(context, "\\\\"), + '\"' => try output(context, "\\\""), + '/' => try output(context, "\\/"), + 0x8 => try output(context, "\\b"), + 0xC => try output(context, "\\f"), + '\n' => try output(context, "\\n"), + '\r' => try output(context, "\\r"), + '\t' => try output(context, "\\t"), + else => { + const ulen = std.unicode.utf8ByteSequenceLength(value[i]) catch unreachable; + const codepoint = std.unicode.utf8Decode(value[i .. i + ulen]) catch unreachable; + if (codepoint <= 0xFFFF) { + // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF), + // then it may be represented as a six-character sequence: a reverse solidus, followed + // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point. + try output(context, "\\u"); + try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, context, Errors, output); + } else { + // To escape an extended character that is not in the Basic Multilingual Plane, + // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair. + const high = @intCast(u16, (codepoint - 0x10000) >> 10) + 0xD800; + const low = @intCast(u16, codepoint & 0x3FF) + 0xDC00; + try output(context, "\\u"); + try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, context, Errors, output); + try output(context, "\\u"); + try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, context, Errors, output); + } + i += ulen - 1; + }, + } + } + try output(context, "\""); + return; + } + + try output(context, "["); + for (value) |x, i| { + if (i != 0) { + try output(context, ","); + } + try stringify(x, options, context, Errors, output); + } + try output(context, "]"); + return; + }, + else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"), + }, + .Array => |info| { + return try stringify(value[0..], options, context, Errors, output); + }, + else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"), + } + unreachable; +} + +fn teststringify(expected: []const u8, value: var) !void { + const TestStringifyContext = struct { + expected_remaining: []const u8, + fn testStringifyWrite(context: *@This(), bytes: []const u8) !void { + if (context.expected_remaining.len < bytes.len) { + std.debug.warn( + \\====== expected this output: ========= + \\{} + \\======== instead found this: ========= + \\{} + \\====================================== + , .{ + context.expected_remaining, + bytes, + }); + return error.TooMuchData; + } + if (!mem.eql(u8, context.expected_remaining[0..bytes.len], bytes)) { + std.debug.warn( + \\====== expected this output: ========= + \\{} + \\======== instead found this: ========= + \\{} + \\====================================== + , .{ + context.expected_remaining[0..bytes.len], + bytes, + }); + return error.DifferentData; + } + context.expected_remaining = context.expected_remaining[bytes.len..]; + } + }; + var buf: [100]u8 = undefined; + var context = TestStringifyContext{ .expected_remaining = expected }; + try stringify(value, StringifyOptions{}, &context, error{ + TooMuchData, + DifferentData, + }, TestStringifyContext.testStringifyWrite); + if (context.expected_remaining.len > 0) return error.NotEnoughData; +} + +test "stringify basic types" { + try teststringify("false", false); + try teststringify("true", true); + try teststringify("null", @as(?u8, null)); + try teststringify("null", @as(?*u32, null)); + try teststringify("42", 42); + try teststringify("4.2e+01", 42.0); + try teststringify("42", @as(u8, 42)); + try teststringify("42", @as(u128, 42)); + try teststringify("4.2e+01", @as(f32, 42)); + try teststringify("4.2e+01", @as(f64, 42)); +} + +test "stringify string" { + try teststringify("\"hello\"", "hello"); + try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r"); + try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}"); + try teststringify("\"with unicode\\u0080\"", "with unicode\u{80}"); + try teststringify("\"with unicode\\u00ff\"", "with unicode\u{FF}"); + try teststringify("\"with unicode\\u0100\"", "with unicode\u{100}"); + try teststringify("\"with unicode\\u0800\"", "with unicode\u{800}"); + try teststringify("\"with unicode\\u8000\"", "with unicode\u{8000}"); + try teststringify("\"with unicode\\ud799\"", "with unicode\u{D799}"); + try teststringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}"); + try teststringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}"); +} + +test "stringify tagged unions" { + try teststringify("42", union(enum) { + Foo: u32, + Bar: bool, + }{ .Foo = 42 }); +} + +test "stringify struct" { + try teststringify("{\"foo\":42}", struct { + foo: u32, + }{ .foo = 42 }); +} + +test "stringify struct with void field" { + try teststringify("{\"foo\":42}", struct { + foo: u32, + bar: void = {}, + }{ .foo = 42 }); +} + +test "stringify array of structs" { + const MyStruct = struct { + foo: u32, + }; + try teststringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{ + MyStruct{ .foo = 42 }, + MyStruct{ .foo = 100 }, + MyStruct{ .foo = 1000 }, + }); +} + +test "stringify struct with custom stringifier" { + try teststringify("[\"something special\",42]", struct { + foo: u32, + const Self = @This(); + pub fn jsonStringify( + value: Self, + options: StringifyOptions, + context: var, + comptime Errors: type, + comptime output: fn (@TypeOf(context), []const u8) Errors!void, + ) !void { + try output(context, "[\"something special\","); + try stringify(42, options, context, Errors, output); + try output(context, "]"); + } + }{ .foo = 42 }); +} |
