Commit 5b4905fb authored by Dominik Charousset's avatar Dominik Charousset

Add new serializer for generating JSON output

parent 1ed9a0b4
......@@ -154,6 +154,7 @@ caf_add_component(
src/ipv6_address.cpp
src/ipv6_endpoint.cpp
src/ipv6_subnet.cpp
src/json_writer.cpp
src/load_inspector.cpp
src/local_actor.cpp
src/logger.cpp
......@@ -290,6 +291,7 @@ caf_add_component(
ipv6_address
ipv6_endpoint
ipv6_subnet
json_writer
load_inspector
logger
mailbox_element
......
......@@ -18,24 +18,20 @@ enum class hex_format {
lowercase,
};
template <hex_format format = hex_format::uppercase>
void append_hex(std::string& result, const void* vptr, size_t n) {
if (n == 0) {
result += "00";
template <hex_format format = hex_format::uppercase, class Buf = std::string>
void append_hex(Buf& result, const void* vptr, size_t n) {
if (n == 0)
return;
}
auto xs = reinterpret_cast<const uint8_t*>(vptr);
const char* tbl;
if constexpr (format == hex_format::uppercase)
tbl = "0123456789ABCDEF";
else
tbl = "0123456789abcdef";
char buf[3] = {0, 0, 0};
for (size_t i = 0; i < n; ++i) {
auto c = xs[i];
buf[0] = tbl[c >> 4];
buf[1] = tbl[c & 0x0F];
result += buf;
result.push_back(tbl[c >> 4]);
result.push_back(tbl[c & 0x0F]);
}
}
......
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include <vector>
#include "caf/detail/core_export.hpp"
#include "caf/serializer.hpp"
namespace caf {
/// Serializes an inspectable object to a JSON-formatted string.
class CAF_CORE_EXPORT json_writer : public serializer {
public:
// -- member types -----------------------------------------------------------
using super = serializer;
/// Reflects the structure of JSON objects according to ECMA-404. This enum
/// skips types such as `members` or `value` since they are not needed to
/// generate JSON.
enum class type : uint8_t {
element, /// Can morph into any other type except `member`.
object, /// Contains any number of members.
member, /// A single object member (key/value pair).
array, /// Contains any number of elements.
string, /// A character sequence (terminal type).
number, /// An integer or floating point (terminal type).
boolean, /// Either "true" or "false" (terminal type).
null, /// The literal "null" (terminal type).
};
// -- constructors, destructors, and assignment operators --------------------
json_writer();
explicit json_writer(actor_system& sys);
explicit json_writer(execution_unit* ctx);
~json_writer() override;
// -- properties -------------------------------------------------------------
/// Returns a string view into the internal buffer.
/// @warning This view becomes invalid when calling any non-const member
/// function on the writer object.
[[nodiscard]] string_view str() const noexcept {
return {buf_.data(), buf_.size()};
}
/// Returns the current indentation factor.
[[nodiscard]] size_t indentation() const noexcept {
return indentation_factor_;
}
/// Sets the indentation level.
/// @param factor The number of spaces to add to each level of indentation. A
/// value of 0 (the default) disables indentation, printing the
/// entire JSON output into a single line.
void indentation(size_t factor) noexcept {
indentation_factor_ = factor;
}
/// Returns whether the writer generates compact JSON output without any
/// spaces or newlines to separate values.
[[nodiscard]] bool compact() const noexcept {
return indentation_factor_ == 0;
}
// -- modifiers --------------------------------------------------------------
/// Removes all characters from the buffer and restores the writer to its
/// initial state.
/// @warning Invalidates all string views into the buffer.
void reset();
// -- overrides --------------------------------------------------------------
bool begin_object(type_id_t type, string_view name) override;
bool end_object() override;
bool begin_field(string_view) override;
bool begin_field(string_view name, bool is_present) override;
bool begin_field(string_view name, span<const type_id_t> types,
size_t index) override;
bool begin_field(string_view name, bool is_present,
span<const type_id_t> types, size_t index) override;
bool end_field() override;
bool begin_tuple(size_t size) override;
bool end_tuple() override;
bool begin_key_value_pair() override;
bool end_key_value_pair() override;
bool begin_sequence(size_t size) override;
bool end_sequence() override;
bool begin_associative_array(size_t size) override;
bool end_associative_array() override;
bool value(byte x) override;
bool value(bool x) override;
bool value(int8_t x) override;
bool value(uint8_t x) override;
bool value(int16_t x) override;
bool value(uint16_t x) override;
bool value(int32_t x) override;
bool value(uint32_t x) override;
bool value(int64_t x) override;
bool value(uint64_t x) override;
bool value(float x) override;
bool value(double x) override;
bool value(long double x) override;
bool value(string_view x) override;
bool value(const std::u16string& x) override;
bool value(const std::u32string& x) override;
bool value(span<const byte> x) override;
private:
// -- implementation details -------------------------------------------------
template <class T>
bool number(T);
// -- state management -------------------------------------------------------
void init();
// Returns the current top of the stack or `null_literal` if empty.
type top();
// Enters a new level of nesting.
void push(type = type::element);
// Enters a new level of nesting if the current top is `expected`.
bool push_if(type expected, type = type::element);
// Backs up one level of nesting.
bool pop();
// Backs up one level of nesting but checks that current top is `t` before.
bool pop_if(type t);
// Backs up one level of nesting but checks that the top is `t` afterwards.
bool pop_if_next(type t);
// Tries to morph the current top of the stack to t.
bool morph(type t);
// Tries to morph the current top of the stack to t. Stores the previous value
// to `prev`.
bool morph(type t, type& prev);
// Morphs the current top of the stack to t without performing *any* checks.
void unsafe_morph(type t);
// Sets an error reason that the inspector failed to write a t.
void fail(type t);
// -- printing ---------------------------------------------------------------
// Adds a newline unless `compact() == true`.
void nl();
// Adds `c` to the output buffer.
void add(char c) {
buf_.push_back(c);
}
// Adds `str` to the output buffer.
void add(string_view str) {
buf_.insert(buf_.end(), str.begin(), str.end());
}
// Adds a separator to the output buffer unless the current entry is empty.
// The separator is just a comma when in compact mode and otherwise a comma
// followed by a newline.
void sep();
// -- member variables -------------------------------------------------------
// The current level of indentation.
size_t indentation_level_ = 0;
// The number of whitespaces to add per indentation level.
size_t indentation_factor_ = 0;
// Buffer for producing the JSON output.
std::vector<char> buf_;
struct entry {
type t;
bool filled;
friend bool operator==(entry x, type y) noexcept {
return x.t == y;
};
};
// Bookkeeping for where we are in the current object.
std::vector<entry> stack_;
};
} // namespace caf
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#include "caf/json_writer.hpp"
#include "caf/detail/append_hex.hpp"
#include "caf/detail/print.hpp"
namespace caf {
namespace {
constexpr bool can_morph(json_writer::type from, json_writer::type to) {
return from == json_writer::type::element && to != json_writer::type::member;
}
constexpr const char* json_type_names[] = {"element", "object", "member",
"array", "string", "number",
"bool", "null"};
constexpr const char* json_type_name(json_writer::type t) {
return json_type_names[static_cast<uint8_t>(t)];
}
} // namespace
// -- implementation details ---------------------------------------------------
template <class T>
bool json_writer::number(T x) {
switch (top()) {
case type::element:
unsafe_morph(type::number);
detail::print(buf_, x);
return true;
case type::member:
unsafe_morph(type::element);
add('"');
detail::print(buf_, x);
add("\": ");
return true;
case type::array:
sep();
detail::print(buf_, x);
return true;
default:
fail(type::number);
return false;
}
}
// -- constructors, destructors, and assignment operators ----------------------
json_writer::json_writer() : json_writer(nullptr) {
init();
}
json_writer::json_writer(actor_system& sys) : super(sys) {
init();
}
json_writer::json_writer(execution_unit* ctx) : super(ctx) {
init();
}
json_writer::~json_writer() {
// nop
}
// -- modifiers ----------------------------------------------------------------
void json_writer::reset() {
buf_.clear();
stack_.clear();
push();
}
// -- overrides ----------------------------------------------------------------
bool json_writer::begin_object(type_id_t, string_view name) {
auto add_type_annotation = [this, name] {
add(R"_("@type": )_");
detail::print_escaped(buf_, name);
return true;
};
return begin_associative_array(0) // Put opening paren, ...
&& begin_key_value_pair() // ... add implicit @type member, ..
&& add_type_annotation() // ... write content ...
&& end_key_value_pair(); // ... and wait for next field.
}
bool json_writer::end_object() {
return end_associative_array();
}
bool json_writer::begin_field(string_view name) {
if (begin_key_value_pair()) {
detail::print_escaped(buf_, name);
add(": ");
unsafe_morph(type::element);
return true;
} else {
return false;
}
}
bool json_writer::begin_field(string_view name, bool is_present) {
if (begin_key_value_pair()) {
detail::print_escaped(buf_, name);
add(": ");
if (is_present)
unsafe_morph(type::element);
else
add("null");
return true;
} else {
return false;
}
}
bool json_writer::begin_field(string_view name, span<const type_id_t>, size_t) {
return begin_field(name);
}
bool json_writer::begin_field(string_view name, bool is_present,
span<const type_id_t>, size_t) {
return begin_field(name, is_present);
}
bool json_writer::end_field() {
return pop_if_next(type::object);
}
bool json_writer::begin_tuple(size_t size) {
return begin_sequence(size);
}
bool json_writer::end_tuple() {
return end_sequence();
}
bool json_writer::begin_key_value_pair() {
sep();
return push_if(type::object, type::member);
}
bool json_writer::end_key_value_pair() {
// Note: can't check for `type::member` here, because it has been morphed to
// whatever the value type has been. But after popping the current type, there
// still has to be the `object` entry below.
return pop_if_next(type::object);
}
bool json_writer::begin_sequence(size_t) {
switch (top()) {
default:
emplace_error(sec::runtime_error, "unexpected begin_sequence");
return false;
case type::element:
unsafe_morph(type::array);
break;
case type::array:
push(type::array);
break;
}
add('[');
++indentation_level_;
nl();
return true;
}
bool json_writer::end_sequence() {
if (pop_if(type::array)) {
--indentation_level_;
nl();
add(']');
return true;
} else {
return false;
}
}
bool json_writer::begin_associative_array(size_t) {
switch (top()) {
default:
emplace_error(sec::runtime_error,
"unexpected begin_object or begin_associative_array");
return false;
case type::member:
emplace_error(sec::runtime_error,
"unimplemented: json_writer currently does not support "
"complex types as dictionary keys");
return false;
case type::element:
unsafe_morph(type::object);
break;
case type::array:
push(type::object);
break;
}
add('{');
++indentation_level_;
nl();
return true;
}
bool json_writer::end_associative_array() {
if (pop_if(type::object)) {
--indentation_level_;
nl();
add('}');
if (top() == type::array)
sep();
return true;
} else {
return false;
}
}
bool json_writer::value(byte x) {
return number(to_integer<uint8_t>(x));
}
bool json_writer::value(bool x) {
return number(x);
}
bool json_writer::value(int8_t x) {
return number(x);
}
bool json_writer::value(uint8_t x) {
return number(x);
}
bool json_writer::value(int16_t x) {
return number(x);
}
bool json_writer::value(uint16_t x) {
return number(x);
}
bool json_writer::value(int32_t x) {
return number(x);
}
bool json_writer::value(uint32_t x) {
return number(x);
}
bool json_writer::value(int64_t x) {
return number(x);
}
bool json_writer::value(uint64_t x) {
return number(x);
}
bool json_writer::value(float x) {
return number(x);
}
bool json_writer::value(double x) {
return number(x);
}
bool json_writer::value(long double x) {
return number(x);
}
bool json_writer::value(string_view x) {
switch (top()) {
case type::element:
unsafe_morph(type::string);
detail::print_escaped(buf_, x);
return true;
case type::member:
unsafe_morph(type::element);
detail::print_escaped(buf_, x);
add(": ");
return true;
case type::array:
sep();
detail::print_escaped(buf_, x);
return true;
default:
fail(type::string);
return false;
}
}
bool json_writer::value(const std::u16string&) {
emplace_error(sec::unsupported_operation,
"u16string not supported yet by caf::json_writer");
return false;
}
bool json_writer::value(const std::u32string&) {
emplace_error(sec::unsupported_operation,
"u32string not supported yet by caf::json_writer");
return false;
}
bool json_writer::value(span<const byte> x) {
switch (top()) {
case type::element:
unsafe_morph(type::string);
add('"');
detail::append_hex(buf_, reinterpret_cast<const void*>(x.data()),
x.size());
add('"');
return true;
case type::member:
unsafe_morph(type::element);
add('"');
detail::append_hex(buf_, reinterpret_cast<const void*>(x.data()),
x.size());
add("\": ");
return true;
case type::array:
sep();
add('"');
detail::append_hex(buf_, reinterpret_cast<const void*>(x.data()),
x.size());
add('"');
return true;
default:
fail(type::string);
return false;
}
}
// -- state management ---------------------------------------------------------
void json_writer::init() {
has_human_readable_format_ = true;
// Reserve some reasonable storage for the character buffer. JSON grows
// quickly, so we can start at 1kb to avoid a couple of small allocations in
// the beginning.
buf_.reserve(1024);
// Even heavily nested objects should fit into 32 levels of nesting.
stack_.reserve(32);
// The concrete type of the output is yet unknown.
push();
}
json_writer::type json_writer::top() {
if (!stack_.empty())
return stack_.back().t;
else
return type::null;
}
// Enters a new level of nesting.
void json_writer::push(type t) {
stack_.push_back({t, false});
}
bool json_writer::push_if(type expected, type t) {
if (!stack_.empty() && stack_.back() == expected) {
stack_.push_back({t, false});
return true;
} else {
std::string str = "push_if failed: expected = ";
str += json_type_name(expected);
str += ", found = ";
str += json_type_name(t);
emplace_error(sec::runtime_error, std::move(str));
return false;
}
}
// Backs up one level of nesting.
bool json_writer::pop() {
if (!stack_.empty()) {
stack_.pop_back();
return true;
} else {
std::string str = "pop() called with an empty stack: begin/end mismatch";
emplace_error(sec::runtime_error, std::move(str));
return false;
}
}
bool json_writer::pop_if(type t) {
if (!stack_.empty() && stack_.back() == t) {
stack_.pop_back();
return true;
} else {
std::string str = "pop_if failed: expected = ";
str += json_type_name(t);
if (stack_.empty()) {
str += ", found an empty stack";
} else {
str += ", found = ";
str += json_type_name(stack_.back().t);
}
emplace_error(sec::runtime_error, std::move(str));
return false;
}
}
bool json_writer::pop_if_next(type t) {
if (stack_.size() > 1 && stack_[stack_.size() - 2] == t) {
stack_.pop_back();
return true;
} else {
std::string str = "pop_if_next failed: expected = ";
str += json_type_name(t);
if (stack_.size() < 2) {
str += ", found a stack of size ";
detail::print(str, stack_.size());
} else {
str += ", found = ";
str += json_type_name(stack_[stack_.size() - 2].t);
}
emplace_error(sec::runtime_error, std::move(str));
return false;
}
}
// Tries to morph the current top of the stack to t.
bool json_writer::morph(type t) {
type unused;
return morph(t, unused);
}
bool json_writer::morph(type t, type& prev) {
if (!stack_.empty()) {
if (can_morph(stack_.back().t, t)) {
prev = stack_.back().t;
stack_.back().t = t;
return true;
} else {
std::string str = "cannot convert ";
str += json_type_name(stack_.back().t);
str += " to ";
str += json_type_name(t);
emplace_error(sec::runtime_error, std::move(str));
return false;
}
} else {
std::string str = "mismatched begin/end calls on the JSON inspector";
emplace_error(sec::runtime_error, std::move(str));
return false;
}
}
void json_writer::unsafe_morph(type t) {
stack_.back().t = t;
}
void json_writer::fail(type t) {
std::string str = "failed to write a ";
str += json_type_name(t);
str += ": invalid position (begin/end mismatch?)";
emplace_error(sec::runtime_error, std::move(str));
}
// -- printing ---------------------------------------------------------------
void json_writer::nl() {
if (indentation_factor_ > 0) {
buf_.push_back('\n');
buf_.insert(buf_.end(), indentation_factor_ * indentation_level_, ' ');
}
}
void json_writer::sep() {
CAF_ASSERT(top() == type::object || top() == type::array);
if (stack_.back().filled) {
if (indentation_factor_ > 0) {
add(",\n");
buf_.insert(buf_.end(), indentation_factor_ * indentation_level_, ' ');
} else {
add(", ");
}
} else {
stack_.back().filled = true;
}
}
} // namespace caf
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#define CAF_SUITE json_writer
#include "caf/json_writer.hpp"
#include "core-test.hpp"
using namespace caf;
using namespace std::literals::string_literals;
namespace {
struct fixture {
template <class T>
expected<std::string> to_json_string(T&& x, size_t indentation_factor) {
json_writer writer;
writer.indentation(indentation_factor);
if (writer.apply(std::forward<T>(x))) {
auto buf = writer.str();
return {std::string{buf.begin(), buf.end()}};
} else {
MESSAGE("partial JSON output: " << writer.str());
return {writer.get_error()};
}
}
};
} // namespace
CAF_TEST_FIXTURE_SCOPE(json_writer_tests, fixture)
SCENARIO("the JSON writer converts builtin types to strings") {
GIVEN("an integer") {
auto x = 42;
WHEN("converting it to JSON with any indentation factor") {
THEN("the JSON output is the number") {
CHECK_EQ(to_json_string(x, 0), "42"s);
CHECK_EQ(to_json_string(x, 2), "42"s);
}
}
}
GIVEN("a string") {
auto x = R"_(hello "world"!)_"s;
WHEN("converting it to JSON with any indentation factor") {
THEN("the JSON output is the escaped string") {
CHECK_EQ(to_json_string(x, 0), R"_("hello \"world\"!")_"s);
CHECK_EQ(to_json_string(x, 2), R"_("hello \"world\"!")_"s);
}
}
}
GIVEN("a list") {
auto x = std::vector<int>{1, 2, 3};
WHEN("converting it to JSON with indentation factor 0") {
THEN("the JSON output is a single line") {
CHECK_EQ(to_json_string(x, 0), "[1, 2, 3]"s);
}
}
WHEN("converting it to JSON with indentation factor 2") {
THEN("the JSON output uses multiple lines") {
auto out = R"_([
1,
2,
3
])_"s;
CHECK_EQ(to_json_string(x, 2), out);
}
}
}
GIVEN("a dictionary") {
std::map<std::string, std::string> x;
x.emplace("a", "A");
x.emplace("b", "B");
x.emplace("c", "C");
WHEN("converting it to JSON with indentation factor 0") {
THEN("the JSON output is a single line") {
CHECK_EQ(to_json_string(x, 0), R"_({"a": "A", "b": "B", "c": "C"})_"s);
}
}
WHEN("converting it to JSON with indentation factor 2") {
THEN("the JSON output uses multiple lines") {
auto out = R"_({
"a": "A",
"b": "B",
"c": "C"
})_"s;
CHECK_EQ(to_json_string(x, 2), out);
}
}
}
GIVEN("a message") {
auto x = make_message(put_atom_v, "foo", 42);
WHEN("converting it to JSON with indentation factor 0") {
THEN("the JSON output is a single line") {
CHECK_EQ(to_json_string(x, 0),
R"_([{"@type": "caf::put_atom"}, "foo", 42])_"s);
}
}
WHEN("converting it to JSON with indentation factor 2") {
THEN("the JSON output uses multiple lines") {
auto out = R"_([
{
"@type": "caf::put_atom"
},
"foo",
42
])_"s;
CHECK_EQ(to_json_string(x, 2), out);
}
}
}
}
SCENARIO("the JSON writer converts simple structs to strings") {
GIVEN("a dummy_struct object") {
dummy_struct x{10, "foo"};
WHEN("converting it to JSON with indentation factor 0") {
THEN("the JSON output is a single line") {
auto out = R"_({"@type": "dummy_struct", "a": 10, "b": "foo"})_"s;
CHECK_EQ(to_json_string(x, 0), out);
}
}
WHEN("converting it to JSON with indentation factor 2") {
THEN("the JSON output uses multiple lines") {
auto out = R"_({
"@type": "dummy_struct",
"a": 10,
"b": "foo"
})_"s;
CHECK_EQ(to_json_string(x, 2), out);
}
}
}
}
CAF_TEST_FIXTURE_SCOPE_END()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment