mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 22:04:21 +00:00
315 lines
13 KiB
Zig
315 lines
13 KiB
Zig
const std = @import("std");
|
|
const mem = std.mem;
|
|
const assert = std.debug.assert;
|
|
|
|
pub const StringifyOptions = struct {
|
|
pub const Whitespace = struct {
|
|
/// How many indentation levels deep are we?
|
|
indent_level: usize = 0,
|
|
|
|
/// What character(s) should be used for indentation?
|
|
indent: union(enum) {
|
|
space: u8,
|
|
tab: void,
|
|
none: void,
|
|
} = .{ .space = 4 },
|
|
|
|
/// After a colon, should whitespace be inserted?
|
|
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);
|
|
}
|
|
};
|
|
|
|
/// Controls the whitespace emitted
|
|
whitespace: Whitespace = .{ .indent = .none, .separator = false },
|
|
|
|
/// Should optional fields with null value be written?
|
|
emit_null_optional_fields: bool = true,
|
|
|
|
string: StringOptions = StringOptions{ .String = .{} },
|
|
|
|
/// Should []u8 be serialised as a string? or an array?
|
|
pub const StringOptions = union(enum) {
|
|
Array,
|
|
String: StringOutputOptions,
|
|
|
|
/// String output options
|
|
const StringOutputOptions = struct {
|
|
/// Should '/' be escaped in strings?
|
|
escape_solidus: bool = false,
|
|
|
|
/// Should unicode characters be escaped in strings?
|
|
escape_unicode: bool = false,
|
|
};
|
|
};
|
|
};
|
|
|
|
fn outputUnicodeEscape(
|
|
codepoint: u21,
|
|
out_stream: anytype,
|
|
) !void {
|
|
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 out_stream.writeAll("\\u");
|
|
try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream);
|
|
} else {
|
|
assert(codepoint <= 0x10FFFF);
|
|
// 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 = @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);
|
|
}
|
|
}
|
|
|
|
/// Write `string` to `writer` as a JSON encoded string.
|
|
pub fn encodeJsonString(string: []const u8, options: StringifyOptions, writer: anytype) !void {
|
|
try writer.writeByte('\"');
|
|
try encodeJsonStringChars(string, options, writer);
|
|
try writer.writeByte('\"');
|
|
}
|
|
|
|
/// Write `chars` to `writer` as JSON encoded string characters.
|
|
pub fn encodeJsonStringChars(chars: []const u8, options: StringifyOptions, writer: anytype) !void {
|
|
var i: usize = 0;
|
|
while (i < chars.len) : (i += 1) {
|
|
switch (chars[i]) {
|
|
// normal ascii character
|
|
0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => |c| try writer.writeByte(c),
|
|
// only 2 characters that *must* be escaped
|
|
'\\' => try writer.writeAll("\\\\"),
|
|
'\"' => try writer.writeAll("\\\""),
|
|
// solidus is optional to escape
|
|
'/' => {
|
|
if (options.string.String.escape_solidus) {
|
|
try writer.writeAll("\\/");
|
|
} else {
|
|
try writer.writeByte('/');
|
|
}
|
|
},
|
|
// control characters with short escapes
|
|
// TODO: option to switch between unicode and 'short' forms?
|
|
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;
|
|
// control characters (only things left with 1 byte length) should always be printed as unicode escapes
|
|
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;
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// If `value` has a method called `jsonStringify`, this will call that method instead of the
|
|
/// default implementation, passing it the `options` and `out_stream` parameters.
|
|
pub fn stringify(
|
|
value: anytype,
|
|
options: StringifyOptions,
|
|
out_stream: anytype,
|
|
) @TypeOf(out_stream).Error!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);
|
|
}
|
|
|
|
return try encodeJsonString(@tagName(value), options, out_stream);
|
|
},
|
|
.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; // No active tag?
|
|
}
|
|
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| {
|
|
// don't include void fields
|
|
if (Field.type == void) continue;
|
|
|
|
var emit_field = true;
|
|
|
|
// don't include optional fields that are null when emit_null_optional_fields is set to false
|
|
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 => {
|
|
// TODO: avoid loops?
|
|
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;
|
|
}
|
|
|
|
// Same as `stringify` but accepts an Allocator and stores result in dynamically allocated memory instead of using a Writer.
|
|
// Caller owns returned memory.
|
|
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");
|
|
}
|