zig/lib/std/json/write_stream.zig
May B ea13437ac5
std.json: Support disabling indent (#11823)
Newline Delimited JSON (ndjson) expect compact json without newline inside its content
Add None to StringfyOptions.indent and move newline writeByte inside StringfyOptions.outputIndent
2022-06-29 11:53:01 +02:00

294 lines
9.7 KiB
Zig

const std = @import("../std.zig");
const assert = std.debug.assert;
const maxInt = std.math.maxInt;
const State = enum {
Complete,
Value,
ArrayStart,
Array,
ObjectStart,
Object,
};
/// Writes JSON ([RFC8259](https://tools.ietf.org/html/rfc8259)) formatted data
/// to a stream. `max_depth` is a comptime-known upper bound on the nesting depth.
/// TODO A future iteration of this API will allow passing `null` for this value,
/// and disable safety checks in release builds.
pub fn WriteStream(comptime OutStream: type, comptime max_depth: usize) type {
return struct {
const Self = @This();
pub const Stream = OutStream;
whitespace: std.json.StringifyOptions.Whitespace = std.json.StringifyOptions.Whitespace{
.indent_level = 0,
.indent = .{ .Space = 1 },
},
stream: OutStream,
state_index: usize,
state: [max_depth]State,
pub fn init(stream: OutStream) Self {
var self = Self{
.stream = stream,
.state_index = 1,
.state = undefined,
};
self.state[0] = .Complete;
self.state[1] = .Value;
return self;
}
pub fn beginArray(self: *Self) !void {
assert(self.state[self.state_index] == State.Value); // need to call arrayElem or objectField
try self.stream.writeByte('[');
self.state[self.state_index] = State.ArrayStart;
self.whitespace.indent_level += 1;
}
pub fn beginObject(self: *Self) !void {
assert(self.state[self.state_index] == State.Value); // need to call arrayElem or objectField
try self.stream.writeByte('{');
self.state[self.state_index] = State.ObjectStart;
self.whitespace.indent_level += 1;
}
pub fn arrayElem(self: *Self) !void {
const state = self.state[self.state_index];
switch (state) {
.Complete => unreachable,
.Value => unreachable,
.ObjectStart => unreachable,
.Object => unreachable,
.Array, .ArrayStart => {
if (state == .Array) {
try self.stream.writeByte(',');
}
self.state[self.state_index] = .Array;
self.pushState(.Value);
try self.indent();
},
}
}
pub fn objectField(self: *Self, name: []const u8) !void {
const state = self.state[self.state_index];
switch (state) {
.Complete => unreachable,
.Value => unreachable,
.ArrayStart => unreachable,
.Array => unreachable,
.Object, .ObjectStart => {
if (state == .Object) {
try self.stream.writeByte(',');
}
self.state[self.state_index] = .Object;
self.pushState(.Value);
try self.indent();
try self.writeEscapedString(name);
try self.stream.writeByte(':');
if (self.whitespace.separator) {
try self.stream.writeByte(' ');
}
},
}
}
pub fn endArray(self: *Self) !void {
switch (self.state[self.state_index]) {
.Complete => unreachable,
.Value => unreachable,
.ObjectStart => unreachable,
.Object => unreachable,
.ArrayStart => {
self.whitespace.indent_level -= 1;
try self.stream.writeByte(']');
self.popState();
},
.Array => {
self.whitespace.indent_level -= 1;
try self.indent();
self.popState();
try self.stream.writeByte(']');
},
}
}
pub fn endObject(self: *Self) !void {
switch (self.state[self.state_index]) {
.Complete => unreachable,
.Value => unreachable,
.ArrayStart => unreachable,
.Array => unreachable,
.ObjectStart => {
self.whitespace.indent_level -= 1;
try self.stream.writeByte('}');
self.popState();
},
.Object => {
self.whitespace.indent_level -= 1;
try self.indent();
self.popState();
try self.stream.writeByte('}');
},
}
}
pub fn emitNull(self: *Self) !void {
assert(self.state[self.state_index] == State.Value);
try self.stringify(null);
self.popState();
}
pub fn emitBool(self: *Self, value: bool) !void {
assert(self.state[self.state_index] == State.Value);
try self.stringify(value);
self.popState();
}
pub fn emitNumber(
self: *Self,
/// An integer, float, or `std.math.BigInt`. Emitted as a bare number if it fits losslessly
/// in a IEEE 754 double float, otherwise emitted as a string to the full precision.
value: anytype,
) !void {
assert(self.state[self.state_index] == State.Value);
switch (@typeInfo(@TypeOf(value))) {
.Int => |info| {
if (info.bits < 53) {
try self.stream.print("{}", .{value});
self.popState();
return;
}
if (value < 4503599627370496 and (info.signedness == .unsigned or value > -4503599627370496)) {
try self.stream.print("{}", .{value});
self.popState();
return;
}
},
.ComptimeInt => {
return self.emitNumber(@as(std.math.IntFittingRange(value, value), value));
},
.Float, .ComptimeFloat => if (@floatCast(f64, value) == value) {
try self.stream.print("{}", .{@floatCast(f64, value)});
self.popState();
return;
},
else => {},
}
try self.stream.print("\"{}\"", .{value});
self.popState();
}
pub fn emitString(self: *Self, string: []const u8) !void {
assert(self.state[self.state_index] == State.Value);
try self.writeEscapedString(string);
self.popState();
}
fn writeEscapedString(self: *Self, string: []const u8) !void {
assert(std.unicode.utf8ValidateSlice(string));
try self.stringify(string);
}
/// Writes the complete json into the output stream
pub fn emitJson(self: *Self, json: std.json.Value) Stream.Error!void {
assert(self.state[self.state_index] == State.Value);
try self.stringify(json);
self.popState();
}
fn indent(self: *Self) !void {
assert(self.state_index >= 1);
try self.whitespace.outputIndent(self.stream);
}
fn pushState(self: *Self, state: State) void {
self.state_index += 1;
self.state[self.state_index] = state;
}
fn popState(self: *Self) void {
self.state_index -= 1;
}
fn stringify(self: *Self, value: anytype) !void {
try std.json.stringify(value, std.json.StringifyOptions{
.whitespace = self.whitespace,
}, self.stream);
}
};
}
pub fn writeStream(
out_stream: anytype,
comptime max_depth: usize,
) WriteStream(@TypeOf(out_stream), max_depth) {
return WriteStream(@TypeOf(out_stream), max_depth).init(out_stream);
}
test "json write stream" {
var out_buf: [1024]u8 = undefined;
var slice_stream = std.io.fixedBufferStream(&out_buf);
const out = slice_stream.writer();
var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena_allocator.deinit();
var w = std.json.writeStream(out, 10);
try w.beginObject();
try w.objectField("object");
try w.emitJson(try getJsonObject(arena_allocator.allocator()));
try w.objectField("string");
try w.emitString("This is a string");
try w.objectField("array");
try w.beginArray();
try w.arrayElem();
try w.emitString("Another string");
try w.arrayElem();
try w.emitNumber(@as(i32, 1));
try w.arrayElem();
try w.emitNumber(@as(f32, 3.5));
try w.endArray();
try w.objectField("int");
try w.emitNumber(@as(i32, 10));
try w.objectField("float");
try w.emitNumber(@as(f32, 3.5));
try w.endObject();
const result = slice_stream.getWritten();
const expected =
\\{
\\ "object": {
\\ "one": 1,
\\ "two": 2.0e+00
\\ },
\\ "string": "This is a string",
\\ "array": [
\\ "Another string",
\\ 1,
\\ 3.5e+00
\\ ],
\\ "int": 10,
\\ "float": 3.5e+00
\\}
;
try std.testing.expect(std.mem.eql(u8, expected, result));
}
fn getJsonObject(allocator: std.mem.Allocator) !std.json.Value {
var value = std.json.Value{ .Object = std.json.ObjectMap.init(allocator) };
try value.Object.put("one", std.json.Value{ .Integer = @intCast(i64, 1) });
try value.Object.put("two", std.json.Value{ .Float = 2.0 });
return value;
}