Commit b51414da authored by Dominik Charousset's avatar Dominik Charousset

Add new deserializer for reading JSON input

parent 9a120d9a
......@@ -156,6 +156,7 @@ caf_add_component(
src/ipv6_address.cpp
src/ipv6_endpoint.cpp
src/ipv6_subnet.cpp
src/json_reader.cpp
src/json_writer.cpp
src/load_inspector.cpp
src/local_actor.cpp
......@@ -295,6 +296,7 @@ caf_add_component(
ipv6_address
ipv6_endpoint
ipv6_subnet
json_reader
json_writer
load_inspector
logger
......
......@@ -40,6 +40,20 @@ public:
using data_type
= std::variant<null_t, int64_t, double, bool, string_view, array, object>;
static constexpr size_t null_index = 0;
static constexpr size_t integer_index = 1;
static constexpr size_t double_index = 2;
static constexpr size_t bool_index = 3;
static constexpr size_t string_index = 4;
static constexpr size_t array_index = 5;
static constexpr size_t object_index = 6;
data_type data;
};
......
......@@ -47,6 +47,53 @@ void print_escaped(Buffer& buf, string_view str) {
buf.push_back('"');
}
template <class Buffer>
void print_unescaped(Buffer& buf, string_view str) {
buf.reserve(buf.size() + str.size());
auto i = str.begin();
auto e = str.end();
while (i != e) {
switch (*i) {
default:
buf.push_back(*i);
++i;
break;
case '\\':
if (++i != e) {
switch (*i) {
case '"':
buf.push_back('"');
break;
case '\\':
buf.push_back('\\');
break;
case 'b':
buf.push_back('\b');
break;
case 'f':
buf.push_back('\f');
break;
case 'n':
buf.push_back('\n');
break;
case 'r':
buf.push_back('\r');
break;
case 't':
buf.push_back('\t');
break;
case 'v':
buf.push_back('\v');
break;
default:
buf.push_back('?');
}
++i;
}
}
}
}
template <class Buffer>
void print(Buffer& buf, none_t) {
using namespace caf::literals;
......
......@@ -145,6 +145,13 @@ public:
int compare(uint8_t code, type_id_t category) const noexcept;
// -- modifiers --------------------------------------------------------------
/// Reverts this error to "not an error" as if calling `*this = error{}`.
void reset() noexcept {
data_.reset();
}
// -- static convenience functions -------------------------------------------
/// @cond PRIVATE
......
// 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 "caf/deserializer.hpp"
#include "caf/detail/core_export.hpp"
#include "caf/detail/json.hpp"
#include "caf/string_view.hpp"
#include <variant>
namespace caf {
/// Deserializes an inspectable object from a JSON-formatted string.
class CAF_CORE_EXPORT json_reader : public deserializer {
public:
// -- member types -----------------------------------------------------------
using super = deserializer;
struct sequence {
detail::json::array::const_iterator pos;
detail::json::array::const_iterator end;
bool at_end() const noexcept {
return pos == end;
}
auto& current() {
return *pos;
}
void advance() {
++pos;
}
};
struct members {
detail::json::object::const_iterator pos;
detail::json::object::const_iterator end;
bool at_end() const noexcept {
return pos == end;
}
auto& current() {
return *pos;
}
void advance() {
++pos;
}
};
using json_key = string_view;
using value_type
= std::variant<const detail::json::value*, const detail::json::object*,
detail::json::null_t, json_key, sequence, members>;
using stack_allocator
= detail::monotonic_buffer_resource::allocator<value_type>;
using stack_type = std::vector<value_type, stack_allocator>;
/// Denotes the type at the current position.
enum class position {
value,
object,
null,
key,
sequence,
members,
past_the_end,
invalid,
};
// -- constructors, destructors, and assignment operators --------------------
json_reader();
explicit json_reader(actor_system& sys);
explicit json_reader(execution_unit* ctx);
json_reader(const json_reader&) = delete;
json_reader& operator=(const json_reader&) = delete;
~json_reader() override;
// -- modifiers --------------------------------------------------------------
/// Removes any loaded JSON data and reclaims all memory resources.
void reset();
/// Parses @p json_text into an internal representation. After loading the
/// JSON input, the reader is ready for attempting to deserialize inspectable
/// objects.
/// @warning The internal data structure keeps pointers into @p json_text.
/// Hence, the buffer pointed to by the string view must remain valid
/// until either destroying this reader or calling `reset`.
/// @note Implicitly calls `reset`.
bool load(string_view json_text);
/// Reverts the state of the reader back to where it was after calling `load`.
/// @post The reader is ready for attempting to deserialize another
/// inspectable object.
void revert();
// -- overrides --------------------------------------------------------------
bool fetch_next_object_type(type_id_t& type) override;
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(std::string& x) override;
bool value(std::u16string& x) override;
bool value(std::u32string& x) override;
bool value(span<byte> x) override;
private:
[[nodiscard]] position pos() const noexcept;
template <bool PopOrAdvanceOnSuccess, class F>
bool consume(const char* fun_name, F f);
template <class T>
bool integer(T& x);
template <position P>
auto& top() noexcept {
return std::get<static_cast<size_t>(P)>(st_->back());
}
template <position P>
const auto& top() const noexcept {
return std::get<static_cast<size_t>(P)>(st_->back());
}
void pop() {
st_->pop_back();
}
template <class T>
void push(T&& x) {
st_->emplace_back(std::forward<T>(x));
}
detail::monotonic_buffer_resource buf_;
stack_type* st_ = nullptr;
detail::json::value* root_ = nullptr;
};
} // namespace caf
......@@ -42,7 +42,7 @@ public:
template <class... Ts>
void emplace_error(Ts&&... xs) {
err_ = make_error(xs...);
err_ = make_error(std::forward<Ts>(xs)...);
}
const error& get_error() const noexcept {
......
......@@ -42,7 +42,7 @@ public:
template <class... Ts>
void emplace_error(Ts&&... xs) {
err_ = make_error(xs...);
err_ = make_error(std::forward<Ts>(xs)...);
}
const error& get_error() const noexcept {
......
// 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_reader.hpp"
#include "caf/detail/bounds_checker.hpp"
#include "caf/detail/print.hpp"
namespace {
static constexpr const char class_name[] = "caf::json_reader";
caf::string_view pretty_name(caf::json_reader::position pos) {
switch (pos) {
default:
return "invalid input";
case caf::json_reader::position::value:
return "json::value";
case caf::json_reader::position::object:
return "json::object";
case caf::json_reader::position::null:
return "null";
case caf::json_reader::position::key:
return "json::key";
case caf::json_reader::position::sequence:
return "json::array";
case caf::json_reader::position::members:
return "json::members";
}
}
std::string type_clash(caf::string_view want, caf::string_view got) {
std::string result = "type clash: expected ";
result.insert(result.end(), want.begin(), want.end());
result += ", got ";
result.insert(result.end(), got.begin(), got.end());
return result;
}
std::string type_clash(caf::string_view want, caf::json_reader::position got) {
return type_clash(want, pretty_name(got));
}
std::string type_clash(caf::json_reader::position want,
caf::json_reader::position got) {
return type_clash(pretty_name(want), pretty_name(got));
}
std::string type_clash(caf::string_view want,
const caf::detail::json::value& got) {
using namespace caf::literals;
using caf::detail::json::value;
switch (got.data.index()) {
case value::integer_index:
return type_clash(want, "json::integer"_sv);
case value::double_index:
return type_clash(want, "json::real"_sv);
case value::bool_index:
return type_clash(want, "json::boolean"_sv);
case value::string_index:
return type_clash(want, "json::string"_sv);
case value::array_index:
return type_clash(want, "json::array"_sv);
case value::object_index:
return type_clash(want, "json::object"_sv);
default:
return type_clash(want, "json::null"_sv);
}
}
const caf::detail::json::member*
find_member(const caf::detail::json::object* obj, caf::string_view key) {
for (const auto& member : *obj)
if (member.key == key)
return &member;
return nullptr;
}
} // namespace
#define FN_DECL static constexpr const char* fn = __func__
#define INVALID_AND_PAST_THE_END_CASES \
case position::invalid: \
emplace_error(sec::runtime_error, class_name, fn, \
"found an invalid position"); \
return false; \
case position::past_the_end: \
emplace_error(sec::runtime_error, class_name, fn, \
"tried reading past the end"); \
return false;
#define SCOPE(expected_position) \
if (auto got = pos(); got != expected_position) { \
emplace_error(sec::runtime_error, class_name, __func__, \
type_clash(expected_position, got)); \
return false; \
}
namespace caf {
// -- constructors, destructors, and assignment operators ----------------------
json_reader::json_reader() : super() {
has_human_readable_format_ = true;
}
json_reader::json_reader(actor_system& sys) : super(sys) {
has_human_readable_format_ = true;
}
json_reader::json_reader(execution_unit* ctx) : super(ctx) {
has_human_readable_format_ = true;
}
json_reader::~json_reader() {
// nop
}
// -- modifiers --------------------------------------------------------------
void json_reader::reset() {
buf_.reclaim();
st_ = nullptr;
}
bool json_reader::load(string_view json_text) {
reset();
string_parser_state ps{json_text.begin(), json_text.end()};
root_ = detail::json::parse(ps, &buf_);
if (ps.code != pec::success) {
err_ = make_error(ps);
st_ = nullptr;
return false;
} else {
err_.reset();
detail::monotonic_buffer_resource::allocator<stack_type> alloc{&buf_};
st_ = new (alloc.allocate(1)) stack_type(stack_allocator{&buf_});
st_->reserve(16);
st_->emplace_back(root_);
return true;
}
}
void json_reader::revert() {
if (st_) {
CAF_ASSERT(root_ != nullptr);
err_.reset();
st_->clear();
st_->emplace_back(root_);
}
}
// -- interface functions ------------------------------------------------------
bool json_reader::fetch_next_object_type(type_id_t& type) {
FN_DECL;
return consume<false>(fn, [this, &type](const detail::json::value& val) {
if (val.data.index() == detail::json::value::object_index) {
auto& obj = get<detail::json::object>(val.data);
if (auto mem_ptr = find_member(&obj, "@type")) {
if (mem_ptr->val->data.index() == detail::json::value::string_index) {
auto str = std::get<string_view>(mem_ptr->val->data);
if (auto id = query_type_id(str); id != invalid_type_id) {
type = id;
return true;
} else {
std::string what = "found an unknown @type: ";
what.insert(what.end(), str.begin(), str.end());
emplace_error(sec::runtime_error, class_name, fn, std::move(what));
return false;
}
} else {
emplace_error(sec::runtime_error, class_name, fn,
"expected a string argument to @type");
return false;
}
} else {
emplace_error(sec::runtime_error, class_name, fn,
"found no @type member");
return false;
}
} else {
emplace_error(sec::runtime_error, class_name, fn,
type_clash("json::object", val));
return false;
}
});
}
bool json_reader::begin_object(type_id_t, string_view) {
FN_DECL;
return consume<false>(fn, [this](const detail::json::value& val) {
if (val.data.index() == detail::json::value::object_index) {
push(&get<detail::json::object>(val.data));
return true;
} else {
emplace_error(sec::runtime_error, class_name, fn,
type_clash("json::object", val));
return false;
}
});
}
bool json_reader::end_object() {
FN_DECL;
SCOPE(position::object);
pop();
auto current_pos = pos();
switch (current_pos) {
INVALID_AND_PAST_THE_END_CASES
case position::value:
pop();
return true;
case position::sequence:
top<position::sequence>().advance();
return true;
default:
emplace_error(sec::runtime_error, class_name, __func__,
type_clash("json::value or json::array", current_pos));
return false;
}
}
bool json_reader::begin_field(string_view name) {
SCOPE(position::object);
if (auto member = find_member(top<position::object>(), name)) {
push(member->val);
return true;
} else {
emplace_error(sec::runtime_error, class_name, __func__,
"no such field: " + to_string(name));
return false;
}
}
bool json_reader::begin_field(string_view name, bool& is_present) {
SCOPE(position::object);
if (auto member = find_member(top<position::object>(), name);
member != nullptr
&& member->val->data.index() != detail::json::value::null_index) {
push(member->val);
is_present = true;
} else {
is_present = false;
}
return true;
}
bool json_reader::begin_field(string_view, span<const type_id_t>, size_t&) {
emplace_error(sec::runtime_error, class_name, __func__,
"variant fields are currently not supported for JSON input");
return false;
}
bool json_reader::begin_field(string_view, bool&, span<const type_id_t>,
size_t&) {
emplace_error(sec::runtime_error, class_name, __func__,
"variant fields are currently not supported for JSON input");
return false;
}
bool json_reader::end_field() {
SCOPE(position::object);
// Note: no pop() here, because the value(s) were already consumed.
return true;
}
bool json_reader::begin_tuple(size_t size) {
size_t list_size = 0;
if (begin_sequence(list_size)) {
if (list_size == size) {
return true;
} else {
std::string msg;
msg += "expected tuple of size ";
detail::print(msg, size);
msg += ", got a list of size ";
detail::print(msg, list_size);
emplace_error(sec::conversion_failed, class_name, __func__,
std::move(msg));
return false;
}
} else {
return false;
}
}
bool json_reader::end_tuple() {
return end_sequence();
}
bool json_reader::begin_key_value_pair() {
SCOPE(position::members);
if (auto& xs = top<position::members>(); !xs.at_end()) {
auto& current = xs.current();
push(current.val);
push(current.key);
return true;
} else {
emplace_error(sec::runtime_error, class_name, __func__,
"tried reading a JSON::object sequentially past its end");
return false;
}
}
bool json_reader::end_key_value_pair() {
SCOPE(position::members);
top<position::members>().advance();
return true;
}
bool json_reader::begin_sequence(size_t& size) {
FN_DECL;
return consume<false>(fn, [this, &size](const detail::json::value& val) {
if (val.data.index() == detail::json::value::array_index) {
auto& ls = get<detail::json::array>(val.data);
size = ls.size();
push(sequence{ls.begin(), ls.end()});
return true;
} else {
emplace_error(sec::runtime_error, class_name, fn,
type_clash("json::array", val));
return false;
}
});
}
bool json_reader::end_sequence() {
SCOPE(position::sequence);
if (top<position::sequence>().at_end()) {
pop();
return true;
} else {
emplace_error(sec::runtime_error, class_name, __func__,
"failed to consume all elements from json::array");
return false;
}
}
bool json_reader::begin_associative_array(size_t& size) {
FN_DECL;
return consume<false>(fn, [this, &size](const detail::json::value& val) {
if (val.data.index() == detail::json::value::object_index) {
auto& obj = get<detail::json::object>(val.data);
size = obj.size();
push(members{obj.begin(), obj.end()});
return true;
} else {
emplace_error(sec::runtime_error, class_name, fn,
type_clash("json::object", val));
return false;
}
});
}
bool json_reader::end_associative_array() {
SCOPE(position::members);
if (top<position::members>().at_end()) {
pop();
return true;
} else {
emplace_error(sec::runtime_error, class_name, __func__,
"failed to consume all elements in an associative array");
return false;
}
}
bool json_reader::value(byte& x) {
auto tmp = uint8_t{0};
if (value(tmp)) {
x = static_cast<byte>(tmp);
return true;
} else {
return false;
}
}
bool json_reader::value(bool& x) {
FN_DECL;
return consume<true>(fn, [this, &x](const detail::json::value& val) {
if (val.data.index() == detail::json::value::bool_index) {
x = std::get<bool>(val.data);
return true;
} else {
emplace_error(sec::runtime_error, class_name, fn,
type_clash("json::boolean", val));
return false;
}
});
}
template <bool PopOrAdvanceOnSuccess, class F>
bool json_reader::consume(const char* fn, F f) {
auto current_pos = pos();
switch (current_pos) {
INVALID_AND_PAST_THE_END_CASES
case position::value:
if (f(*top<position::value>())) {
if constexpr (PopOrAdvanceOnSuccess)
pop();
return true;
} else {
return false;
}
case position::key:
if (f(detail::json::value{top<position::key>()})) {
if constexpr (PopOrAdvanceOnSuccess)
pop();
return true;
} else {
return false;
}
case position::sequence:
if (auto& ls = top<position::sequence>(); !ls.at_end()) {
auto& curr = ls.current();
if constexpr (PopOrAdvanceOnSuccess)
ls.advance();
return f(curr);
} else {
emplace_error(sec::runtime_error, class_name, fn,
"tried reading a json::array past the end");
return false;
}
default:
emplace_error(sec::runtime_error, class_name, fn,
type_clash("json::value", current_pos));
return false;
}
}
template <class T>
bool json_reader::integer(T& x) {
static constexpr const char* fn = "value";
return consume<true>(fn, [this, &x](const detail::json::value& val) {
if (val.data.index() == detail::json::value::integer_index) {
auto i64 = std::get<int64_t>(val.data);
if (detail::bounds_checker<T>::check(i64)) {
x = static_cast<T>(i64);
return true;
} else {
emplace_error(sec::runtime_error, class_name, fn,
"integer out of bounds");
return false;
}
} else {
emplace_error(sec::runtime_error, class_name, fn,
type_clash("json::integer", val));
return false;
}
});
}
bool json_reader::value(int8_t& x) {
return integer(x);
}
bool json_reader::value(uint8_t& x) {
return integer(x);
}
bool json_reader::value(int16_t& x) {
return integer(x);
}
bool json_reader::value(uint16_t& x) {
return integer(x);
}
bool json_reader::value(int32_t& x) {
return integer(x);
}
bool json_reader::value(uint32_t& x) {
return integer(x);
}
bool json_reader::value(int64_t& x) {
return integer(x);
}
bool json_reader::value(uint64_t& x) {
return integer(x);
}
bool json_reader::value(float& x) {
auto tmp = 0.0;
if (value(tmp)) {
x = static_cast<float>(x);
return true;
} else {
return false;
}
}
bool json_reader::value(double& x) {
FN_DECL;
return consume<true>(fn, [this, &x](const detail::json::value& val) {
if (val.data.index() == detail::json::value::double_index) {
x = std::get<double>(val.data);
return true;
} else {
emplace_error(sec::runtime_error, class_name, fn,
type_clash("json::real", val));
return false;
}
});
}
bool json_reader::value(long double& x) {
auto tmp = 0.0;
if (value(tmp)) {
x = static_cast<long double>(x);
return true;
} else {
return false;
}
}
bool json_reader::value(std::string& x) {
FN_DECL;
return consume<true>(fn, [this, &x](const detail::json::value& val) {
if (val.data.index() == detail::json::value::string_index) {
detail::print_unescaped(x, std::get<string_view>(val.data));
return true;
} else {
emplace_error(sec::runtime_error, class_name, fn,
type_clash("json::string", val));
return false;
}
});
}
bool json_reader::value(std::u16string&) {
emplace_error(sec::runtime_error, class_name, __func__,
"u16string support not implemented yet");
return false;
}
bool json_reader::value(std::u32string&) {
emplace_error(sec::runtime_error, class_name, __func__,
"u32string support not implemented yet");
return false;
}
bool json_reader::value(span<byte>) {
emplace_error(sec::runtime_error, class_name, __func__,
"byte span support not implemented yet");
return false;
}
json_reader::position json_reader::pos() const noexcept {
if (st_ == nullptr)
return position::invalid;
else if (st_->empty())
return position::past_the_end;
else
return static_cast<position>(st_->back().index());
}
} // 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_reader
#include "caf/json_reader.hpp"
#include "core-test.hpp"
#include "caf/dictionary.hpp"
using namespace caf;
namespace {
struct fixture {
template <class T>
void add_test_case(string_view input, T val) {
auto f = [this, input, obj{std::move(val)}]() -> bool {
auto tmp = T{};
auto res = CHECK(reader.load(input)) // parse JSON
&& CHECK(reader.apply(tmp)); // deserialize object
if (res) {
if constexpr (std::is_same<T, message>::value)
res = CHECK_EQ(to_string(tmp), to_string(obj));
else
res = CHECK_EQ(tmp, obj);
}
if (!res)
MESSAGE("rejected input: " << input);
return res;
};
test_cases.emplace_back(std::move(f));
}
template <class T, class... Ts>
std::vector<T> ls(Ts... xs) {
return {std::move(xs)...};
}
template <class T>
using dict = dictionary<T>;
fixture();
json_reader reader;
std::vector<std::function<bool()>> test_cases;
};
fixture::fixture() {
add_test_case(R"_(true)_", true);
add_test_case(R"_(false)_", false);
add_test_case(R"_([true, false])_", ls<bool>(true, false));
add_test_case(R"_(42)_", int32_t{42});
add_test_case(R"_([1, 2, 3])_", ls<int32_t>(1, 2, 3));
add_test_case(R"_(2.0)_", 2.0);
add_test_case(R"_([2.0, 4.0, 8.0])_", ls<double>(2.0, 4.0, 8.0));
add_test_case(R"_("hello \"world\"!")_", std::string{R"_(hello "world"!)_"});
add_test_case(R"_(["hello", "world"])_", ls<std::string>("hello", "world"));
add_test_case(R"_({"a": 1, "b": 2})_", my_request(1, 2));
add_test_case(R"_({"a": 1, "b": 2})_", dict<int>({{"a", 1}, {"b", 2}}));
add_test_case(R"_([{"@type": "my_request", "a": 1, "b": 2}])_",
make_message(my_request(1, 2)));
}
} // namespace
CAF_TEST_FIXTURE_SCOPE(json_reader_tests, fixture)
CAF_TEST(json baselines) {
size_t baseline_index = 0;
detail::monotonic_buffer_resource resource;
for (auto& f : test_cases) {
MESSAGE("test case at index " << baseline_index++);
if (!f())
if (auto reason = reader.get_error())
MESSAGE("JSON reader stopped due to: " << reason);
}
}
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