const std = @import("std");
const mem = std.mem;
const assert = std.debug.assert;
pub const StringifyOptions = struct {
pub const Whitespace = struct {
indent_level: usize = 0,
indent: union(enum) {
space: u8,
tab: void,
none: void,
} = .{ .space = 4 },
separator: bool = true,
pub fn outputIndent(
whitespace: @This(),
out_stream: anytype,
) @TypeOf(out_stream).Error!void {
var char: u8 = undefined;
var n_chars: usize = undefined;
switch (whitespace.indent) {
.space => |n_spaces| {
char = ' ';
n_chars = n_spaces;
},
.tab => {
char = '\t';
n_chars = 1;
},
.none => return,
}
try out_stream.writeByte('\n');
n_chars *= whitespace.indent_level;
try out_stream.writeByteNTimes(char, n_chars);
}
};
whitespace: Whitespace = .{ .indent = .none, .separator = false },
emit_null_optional_fields: bool = true,
string: StringOptions = StringOptions{ .String = .{} },
pub const StringOptions = union(enum) {
Array,
String: StringOutputOptions,
const StringOutputOptions = struct {
escape_solidus: bool = false,
escape_unicode: bool = false,
};
};
};
fn outputUnicodeEscape(
codepoint: u21,
out_stream: anytype,
) !void {
if (codepoint <= 0xFFFF) {
try out_stream.writeAll("\\u");
try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream);
} else {
assert(codepoint <= 0x10FFFF);
const high = @as(u16, @intCast((codepoint - 0x10000) >> 10)) + 0xD800;
const low = @as(u16, @intCast(codepoint & 0x3FF)) + 0xDC00;
try out_stream.writeAll("\\u");
try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream);
try out_stream.writeAll("\\u");
try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream);
}
}
pub fn encodeJsonString(string: []const u8, options: StringifyOptions, writer: anytype) !void {
try writer.writeByte('\"');
try encodeJsonStringChars(string, options, writer);
try writer.writeByte('\"');
}
pub fn encodeJsonStringChars(chars: []const u8, options: StringifyOptions, writer: anytype) !void {
var i: usize = 0;
while (i < chars.len) : (i += 1) {
switch (chars[i]) {
0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => |c| try writer.writeByte(c),
'\\' => try writer.writeAll("\\\\"),
'\"' => try writer.writeAll("\\\""),
'/' => {
if (options.string.String.escape_solidus) {
try writer.writeAll("\\/");
} else {
try writer.writeByte('/');
}
},
0x8 => try writer.writeAll("\\b"),
0xC => try writer.writeAll("\\f"),
'\n' => try writer.writeAll("\\n"),
'\r' => try writer.writeAll("\\r"),
'\t' => try writer.writeAll("\\t"),
else => {
const ulen = std.unicode.utf8ByteSequenceLength(chars[i]) catch unreachable;
if (ulen == 1 or options.string.String.escape_unicode) {
const codepoint = std.unicode.utf8Decode(chars[i..][0..ulen]) catch unreachable;
try outputUnicodeEscape(codepoint, writer);
} else {
try writer.writeAll(chars[i..][0..ulen]);
}
i += ulen - 1;
},
}
}
}
pub fn stringify(
value: anytype,
options: StringifyOptions,
out_stream: anytype,
) !void {
const T = @TypeOf(value);
switch (@typeInfo(T)) {
.Float, .ComptimeFloat => {
return std.fmt.formatFloatScientific(value, std.fmt.FormatOptions{}, out_stream);
},
.Int, .ComptimeInt => {
return std.fmt.formatIntValue(value, "", std.fmt.FormatOptions{}, out_stream);
},
.Bool => {
return out_stream.writeAll(if (value) "true" else "false");
},
.Null => {
return out_stream.writeAll("null");
},
.Optional => {
if (value) |payload| {
return try stringify(payload, options, out_stream);
} else {
return try stringify(null, options, out_stream);
}
},
.Enum => {
if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
return value.jsonStringify(options, out_stream);
}
@compileError("Unable to stringify enum '" ++ @typeName(T) ++ "'");
},
.Union => {
if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
return value.jsonStringify(options, out_stream);
}
const info = @typeInfo(T).Union;
if (info.tag_type) |UnionTagType| {
try out_stream.writeByte('{');
var child_options = options;
child_options.whitespace.indent_level += 1;
inline for (info.fields) |u_field| {
if (value == @field(UnionTagType, u_field.name)) {
try child_options.whitespace.outputIndent(out_stream);
try encodeJsonString(u_field.name, options, out_stream);
try out_stream.writeByte(':');
if (child_options.whitespace.separator) {
try out_stream.writeByte(' ');
}
if (u_field.type == void) {
try out_stream.writeAll("{}");
} else {
try stringify(@field(value, u_field.name), child_options, out_stream);
}
break;
}
} else {
unreachable;
}
try options.whitespace.outputIndent(out_stream);
try out_stream.writeByte('}');
return;
} else {
@compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'");
}
},
.Struct => |S| {
if (comptime std.meta.trait.hasFn("jsonStringify")(T)) {
return value.jsonStringify(options, out_stream);
}
try out_stream.writeByte(if (S.is_tuple) '[' else '{');
var field_output = false;
var child_options = options;
child_options.whitespace.indent_level += 1;
inline for (S.fields) |Field| {
if (Field.type == void) continue;
var emit_field = true;
if (@typeInfo(Field.type) == .Optional) {
if (options.emit_null_optional_fields == false) {
if (@field(value, Field.name) == null) {
emit_field = false;
}
}
}
if (emit_field) {
if (!field_output) {
field_output = true;
} else {
try out_stream.writeByte(',');
}
try child_options.whitespace.outputIndent(out_stream);
if (!S.is_tuple) {
try encodeJsonString(Field.name, options, out_stream);
try out_stream.writeByte(':');
if (child_options.whitespace.separator) {
try out_stream.writeByte(' ');
}
}
try stringify(@field(value, Field.name), child_options, out_stream);
}
}
if (field_output) {
try options.whitespace.outputIndent(out_stream);
}
try out_stream.writeByte(if (S.is_tuple) ']' else '}');
return;
},
.ErrorSet => return stringify(@as([]const u8, @errorName(value)), options, out_stream),
.Pointer => |ptr_info| switch (ptr_info.size) {
.One => switch (@typeInfo(ptr_info.child)) {
.Array => {
const Slice = []const std.meta.Elem(ptr_info.child);
return stringify(@as(Slice, value), options, out_stream);
},
else => {
return stringify(value.*, options, out_stream);
},
},
.Many, .Slice => {
if (ptr_info.size == .Many and ptr_info.sentinel == null)
@compileError("unable to stringify type '" ++ @typeName(T) ++ "' without sentinel");
const slice = if (ptr_info.size == .Many) mem.span(value) else value;
if (ptr_info.child == u8 and options.string == .String and std.unicode.utf8ValidateSlice(slice)) {
try encodeJsonString(slice, options, out_stream);
return;
}
try out_stream.writeByte('[');
var child_options = options;
child_options.whitespace.indent_level += 1;
for (slice, 0..) |x, i| {
if (i != 0) {
try out_stream.writeByte(',');
}
try child_options.whitespace.outputIndent(out_stream);
try stringify(x, child_options, out_stream);
}
if (slice.len != 0) {
try options.whitespace.outputIndent(out_stream);
}
try out_stream.writeByte(']');
return;
},
else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
},
.Array => return stringify(&value, options, out_stream),
.Vector => |info| {
const array: [info.len]info.child = value;
return stringify(&array, options, out_stream);
},
else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
}
unreachable;
}
pub fn stringifyAlloc(allocator: std.mem.Allocator, value: anytype, options: StringifyOptions) ![]const u8 {
var list = std.ArrayList(u8).init(allocator);
errdefer list.deinit();
try stringify(value, options, list.writer());
return list.toOwnedSlice();
}
test {
_ = @import("./stringify_test.zig");
}