Commit 07e53a43 authored by Dominik Charousset's avatar Dominik Charousset

Add JSON reader/writer to serialization tests

parent 74034160
......@@ -77,6 +77,33 @@ parse(string_parser_state& ps, T& x) {
return parse(ps, reinterpret_cast<squashed_type&>(x));
}
// When parsing regular integers, a "071" is 57 because the parser reads it as
// an octal number. This wrapper forces the parser to ignore leading zeros
// instead and always read numbers as decimals.
template <class Integer>
struct zero_padded_integer {
Integer val = 0;
};
template <class Integer>
void parse(string_parser_state& ps, zero_padded_integer<Integer>& x) {
x.val = 0;
ps.skip_whitespaces();
if (ps.at_end()) {
// Let the actual parser implementation set an appropriate error code.
parse(ps, x.val);
} else {
// Skip all leading zeros and then dispatch to the matching parser.
auto c = ps.current();
auto j = ps.i + 1;
while (c == '0' && j != ps.e && isdigit(*j)) {
c = ps.next();
++j;
}
parse(ps, x.val);
}
}
// -- floating point types -----------------------------------------------------
CAF_CORE_EXPORT void parse(string_parser_state& ps, float& x);
......@@ -111,7 +138,7 @@ CAF_CORE_EXPORT void parse(string_parser_state&, dictionary<config_value>&);
template <class... Ts>
bool parse_sequence(string_parser_state& ps, Ts&&... xs) {
auto parse_one = [&](auto& x) {
auto parse_one = [&ps](auto& x) {
parse(ps, x);
return ps.code <= pec::trailing_character;
};
......@@ -162,13 +189,14 @@ void parse(string_parser_state& ps,
namespace sc = std::chrono;
using value_type = sc::time_point<sc::system_clock, Duration>;
// Parse ISO 8601 as generated by detail::print.
int32_t year = 0;
int32_t month = 0;
int32_t day = 0;
int32_t hour = 0;
int32_t minute = 0;
int32_t second = 0;
int32_t milliseconds = 0;
using int_wrapper = zero_padded_integer<int32_t>;
auto year = int_wrapper{};
auto month = int_wrapper{};
auto day = int_wrapper{};
auto hour = int_wrapper{};
auto minute = int_wrapper{};
auto second = int_wrapper{};
auto milliseconds = int_wrapper{};
if (!parse_sequence(ps,
// YYYY-MM-DD
year, literal{{"-"}}, month, literal{{"-"}}, day,
......@@ -183,19 +211,19 @@ void parse(string_parser_state& ps,
return;
// Fill tm record.
tm record;
record.tm_sec = second;
record.tm_min = minute;
record.tm_hour = hour;
record.tm_mday = day;
record.tm_mon = month - 1; // months since January
record.tm_year = year - 1900; // years since 1900
record.tm_wday = -1; // n/a
record.tm_yday = -1; // n/a
record.tm_isdst = -1; // n/a
record.tm_sec = second.val;
record.tm_min = minute.val;
record.tm_hour = hour.val;
record.tm_mday = day.val;
record.tm_mon = month.val - 1; // months since January
record.tm_year = year.val - 1900; // years since 1900
record.tm_wday = -1; // n/a
record.tm_yday = -1; // n/a
record.tm_isdst = -1; // n/a
// Convert to chrono time and add the milliseconds.
auto tstamp = sc::system_clock::from_time_t(mktime(&record));
auto since_epoch = sc::duration_cast<Duration>(tstamp.time_since_epoch());
since_epoch += sc::milliseconds{milliseconds};
since_epoch += sc::milliseconds{milliseconds.val};
x = value_type{since_epoch};
}
......
......@@ -764,10 +764,7 @@ struct inspector_access<
detail::print(str, x);
return str;
};
auto set = [&x](std::string str) {
auto err = detail::parse(str, x);
return !err;
};
auto set = [&x](std::string str) { return detail::parse(str, x); };
return f.apply(get, set);
} else {
using rep_type = typename Duration::rep;
......
......@@ -24,7 +24,8 @@ public:
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).
member, /// A single key-value pair.
key, /// The key of a field.
array, /// Contains any number of elements.
string, /// A character sequence (terminal type).
number, /// An integer or floating point (terminal type).
......@@ -161,9 +162,6 @@ private:
// 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();
......
......@@ -95,10 +95,18 @@ obj_consumer val_consumer::begin_object() {
void read_value(string_parser_state& ps, val_consumer consumer);
void read_json_null(string_parser_state& ps, val_consumer consumer) {
template <class Consumer>
void read_json_null_or_nan(string_parser_state& ps, Consumer consumer) {
enum { nil, is_null, is_nan };
auto res_type = nil;
auto g = make_scope_guard([&] {
if (ps.code <= pec::trailing_character)
consumer.value(json::null_t{});
if (ps.code <= pec::trailing_character) {
CAF_ASSERT(res_type != nil);
if (res_type == is_null)
consumer.value(json::null_t{});
else
consumer.value(std::numeric_limits<double>::quiet_NaN());
}
});
// clang-format off
start();
......@@ -108,12 +116,16 @@ void read_json_null(string_parser_state& ps, val_consumer consumer) {
}
state(has_n) {
transition(has_nu, 'u')
transition(has_na, 'a')
}
state(has_nu) {
transition(has_nul, 'l')
}
state(has_nul) {
transition(done, 'l')
transition(done, 'l', res_type = is_null)
}
state(has_na) {
transition(done, 'n', res_type = is_nan)
}
term_state(done) {
transition(init, " \t\n")
......@@ -228,7 +240,7 @@ void read_value(string_parser_state& ps, val_consumer consumer) {
transition(init, " \t\n")
fsm_epsilon(read_json_string(ps, consumer), done, '"')
fsm_epsilon(read_bool(ps, consumer), done, "ft")
fsm_epsilon(read_json_null(ps, consumer), done, "n")
fsm_epsilon(read_json_null_or_nan(ps, consumer), done, "n")
fsm_epsilon(read_number(ps, consumer), done, "+-.0123456789")
fsm_epsilon(read_json_object(ps, consumer.begin_object()), done, '{')
fsm_epsilon(read_json_array(ps, consumer.begin_array()), done, '[')
......
......@@ -125,7 +125,7 @@ bool json_reader::load(string_view json_text) {
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);
set_error(make_error(ps));
st_ = nullptr;
return false;
} else {
......@@ -500,7 +500,7 @@ bool json_reader::value(uint64_t& x) {
bool json_reader::value(float& x) {
auto tmp = 0.0;
if (value(tmp)) {
x = static_cast<float>(x);
x = static_cast<float>(tmp);
return true;
} else {
return false;
......@@ -524,7 +524,7 @@ bool json_reader::value(double& x) {
bool json_reader::value(long double& x) {
auto tmp = 0.0;
if (value(tmp)) {
x = static_cast<long double>(x);
x = static_cast<long double>(tmp);
return true;
} else {
return false;
......
......@@ -11,6 +11,8 @@ namespace caf {
namespace {
static constexpr const char class_name[] = "caf::json_writer";
constexpr bool can_morph(json_writer::type from, json_writer::type to) {
return from == json_writer::type::element && to != json_writer::type::member;
}
......@@ -31,11 +33,10 @@ template <class T>
bool json_writer::number(T x) {
switch (top()) {
case type::element:
unsafe_morph(type::number);
detail::print(buf_, x);
pop();
return true;
case type::member:
unsafe_morph(type::element);
case type::key:
add('"');
detail::print(buf_, x);
add("\": ");
......@@ -80,8 +81,12 @@ void json_writer::reset() {
bool json_writer::begin_object(type_id_t, string_view name) {
auto add_type_annotation = [this, name] {
CAF_ASSERT(top() == type::key);
add(R"_("@type": )_");
pop();
CAF_ASSERT(top() == type::element);
detail::print_escaped(buf_, name);
pop();
return true;
};
return begin_associative_array(0) // Put opening paren, ...
......@@ -96,9 +101,11 @@ bool json_writer::end_object() {
bool json_writer::begin_field(string_view name) {
if (begin_key_value_pair()) {
CAF_ASSERT(top() == type::key);
detail::print_escaped(buf_, name);
add(": ");
unsafe_morph(type::element);
pop();
CAF_ASSERT(top() == type::element);
return true;
} else {
return false;
......@@ -107,12 +114,15 @@ bool json_writer::begin_field(string_view name) {
bool json_writer::begin_field(string_view name, bool is_present) {
if (begin_key_value_pair()) {
CAF_ASSERT(top() == type::key);
detail::print_escaped(buf_, name);
add(": ");
if (is_present)
unsafe_morph(type::element);
else
pop();
CAF_ASSERT(top() == type::element);
if (!is_present) {
add("null");
pop();
}
return true;
} else {
return false;
......@@ -129,7 +139,7 @@ bool json_writer::begin_field(string_view name, bool is_present,
}
bool json_writer::end_field() {
return pop_if_next(type::object);
return end_key_value_pair();
}
bool json_writer::begin_tuple(size_t size) {
......@@ -142,14 +152,24 @@ bool json_writer::end_tuple() {
bool json_writer::begin_key_value_pair() {
sep();
return push_if(type::object, type::member);
auto t = top();
switch (t) {
case type::object:
push(type::member);
push(type::element);
push(type::key);
return true;
default: {
std::string str = "expected object, found ";
str += json_type_name(t);
emplace_error(sec::runtime_error, class_name, __func__, std::move(str));
return false;
}
}
}
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);
return pop_if(type::member);
}
bool json_writer::begin_sequence(size_t) {
......@@ -184,18 +204,14 @@ bool json_writer::end_sequence() {
bool json_writer::begin_associative_array(size_t) {
switch (top()) {
default:
emplace_error(sec::runtime_error,
emplace_error(sec::runtime_error, class_name, __func__,
"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:
sep();
push(type::object);
break;
}
......@@ -210,8 +226,8 @@ bool json_writer::end_associative_array() {
--indentation_level_;
nl();
add('}');
if (top() == type::array)
sep();
if (!stack_.empty())
stack_.back().filled = true;
return true;
} else {
return false;
......@@ -223,7 +239,30 @@ bool json_writer::value(byte x) {
}
bool json_writer::value(bool x) {
return number(x);
auto add_str = [this, x] {
if (x)
add("true");
else
add("false");
};
switch (top()) {
case type::element:
add_str();
pop();
return true;
case type::key:
add('"');
add_str();
add("\": ");
return true;
case type::array:
sep();
add_str();
return true;
default:
fail(type::boolean);
return false;
}
}
bool json_writer::value(int8_t x) {
......@@ -273,13 +312,13 @@ bool json_writer::value(long double x) {
bool json_writer::value(string_view x) {
switch (top()) {
case type::element:
unsafe_morph(type::string);
detail::print_escaped(buf_, x);
pop();
return true;
case type::member:
unsafe_morph(type::element);
case type::key:
detail::print_escaped(buf_, x);
add(": ");
pop();
return true;
case type::array:
sep();
......@@ -306,18 +345,19 @@ bool json_writer::value(const std::u32string&) {
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('"');
pop();
return true;
case type::member:
case type::key:
unsafe_morph(type::element);
add('"');
detail::append_hex(buf_, reinterpret_cast<const void*>(x.data()),
x.size());
add("\": ");
pop();
return true;
case type::array:
sep();
......@@ -342,7 +382,7 @@ void json_writer::init() {
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.
// Placeholder for what is to come.
push();
}
......@@ -358,20 +398,6 @@ 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()) {
......@@ -389,12 +415,12 @@ bool json_writer::pop_if(type t) {
stack_.pop_back();
return true;
} else {
std::string str = "pop_if failed: expected = ";
std::string str = "pop_if failed: expected ";
str += json_type_name(t);
if (stack_.empty()) {
str += ", found an empty stack";
} else {
str += ", found = ";
str += ", found ";
str += json_type_name(stack_.back().t);
}
emplace_error(sec::runtime_error, std::move(str));
......@@ -403,17 +429,19 @@ bool json_writer::pop_if(type t) {
}
bool json_writer::pop_if_next(type t) {
if (stack_.size() > 1 && stack_[stack_.size() - 2] == t) {
if (stack_.size() > 1
&& (stack_[stack_.size() - 2] == t
|| can_morph(stack_[stack_.size() - 2].t, t))) {
stack_.pop_back();
return true;
} else {
std::string str = "pop_if_next failed: expected = ";
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 += ", found ";
str += json_type_name(stack_[stack_.size() - 2].t);
}
emplace_error(sec::runtime_error, std::move(str));
......@@ -469,7 +497,8 @@ void json_writer::nl() {
}
void json_writer::sep() {
CAF_ASSERT(top() == type::object || top() == type::array);
CAF_ASSERT(top() == type::element || top() == type::object
|| top() == type::array);
if (stack_.back().filled) {
if (indentation_factor_ > 0) {
add(",\n");
......
......@@ -297,6 +297,45 @@ bool inspect(Inspector& f, dummy_enum& x) {
return f.apply(get, set);
}
struct point {
int x;
int y;
};
[[maybe_unused]] constexpr bool operator==(point a, point b) noexcept {
return a.x == b.x && a.y == b.y;
}
[[maybe_unused]] constexpr bool operator!=(point a, point b) noexcept {
return !(a == b);
}
template <class Inspector>
bool inspect(Inspector& f, point& x) {
return f.object(x).fields(f.field("x", x.x), f.field("y", x.y));
}
struct rectangle {
point top_left;
point bottom_right;
};
template <class Inspector>
bool inspect(Inspector& f, rectangle& x) {
return f.object(x).fields(f.field("top-left", x.top_left),
f.field("bottom-right", x.bottom_right));
}
[[maybe_unused]] constexpr bool operator==(const rectangle& x,
const rectangle& y) noexcept {
return x.top_left == y.top_left && x.bottom_right == y.bottom_right;
}
[[maybe_unused]] constexpr bool operator!=(const rectangle& x,
const rectangle& y) noexcept {
return !(x == y);
}
// -- type IDs for for all unit test suites ------------------------------------
#define ADD_TYPE_ID(type) CAF_ADD_TYPE_ID(core_test, type)
......@@ -319,7 +358,9 @@ CAF_BEGIN_TYPE_ID_BLOCK(core_test, caf::first_custom_type_id)
ADD_TYPE_ID((int_actor))
ADD_TYPE_ID((level))
ADD_TYPE_ID((my_request))
ADD_TYPE_ID((point))
ADD_TYPE_ID((raw_struct))
ADD_TYPE_ID((rectangle))
ADD_TYPE_ID((s1))
ADD_TYPE_ID((s2))
ADD_TYPE_ID((s3))
......
......@@ -58,36 +58,6 @@ struct fixture {
std::vector<std::function<bool()>> test_cases;
};
struct point {
int x;
int y;
};
bool operator==(point a, point b) noexcept {
return std::tie(a.x, a.y) == std::tie(b.x, b.y);
}
template <class Inspector>
bool inspect(Inspector& f, point& x) {
return f.object(x).fields(f.field("x", x.x), f.field("y", x.y));
}
struct rectangle {
point top_left;
point bottom_right;
};
template <class Inspector>
bool inspect(Inspector& f, rectangle& x) {
return f.object(x).fields(f.field("top-left", x.top_left),
f.field("bottom-right", x.bottom_right));
}
bool operator==(const rectangle& x, const rectangle& y) noexcept {
return std::tie(x.top_left, x.bottom_right)
== std::tie(y.top_left, y.bottom_right);
}
fixture::fixture() {
using i32_list = std::vector<int32_t>;
using str_list = std::vector<std::string>;
......
......@@ -138,4 +138,37 @@ SCENARIO("the JSON writer converts simple structs to strings") {
}
}
SCENARIO("the JSON writer converts nested structs to strings") {
GIVEN("a rectangle object") {
auto x = rectangle{{100, 200}, {10, 20}};
WHEN("converting it to JSON with indentation factor 0") {
THEN("the JSON output is a single line") {
std::string out
= R"({"@type": "rectangle", )"
R"("top-left": {"@type": "point", "x": 100, "y": 200}, )"
R"("bottom-right": {"@type": "point", "x": 10, "y": 20}})";
CHECK_EQ(to_json_string(x, 0), out);
}
}
WHEN("converting it to JSON with indentation factor 2") {
THEN("the JSON output uses multiple lines") {
std::string out = R"_({
"@type": "rectangle",
"top-left": {
"@type": "point",
"x": 100,
"y": 200
},
"bottom-right": {
"@type": "point",
"x": 10,
"y": 20
}
})_";
CHECK_EQ(to_json_string(x, 2), out);
}
}
}
}
CAF_TEST_FIXTURE_SCOPE_END()
......@@ -42,6 +42,8 @@
#include "caf/detail/stringification_inspector.hpp"
#include "caf/detail/type_traits.hpp"
#include "caf/event_based_actor.hpp"
#include "caf/json_reader.hpp"
#include "caf/json_writer.hpp"
#include "caf/message.hpp"
#include "caf/message_handler.hpp"
#include "caf/proxy_registry.hpp"
......@@ -143,6 +145,10 @@ struct fixture : test_coordinator_fixture<> {
message msg;
message recursive;
json_writer jwriter;
json_reader jreader;
template <class... Ts>
byte_buffer serialize(const Ts&... xs) {
byte_buffer buf;
......@@ -161,12 +167,39 @@ struct fixture : test_coordinator_fixture<> {
CAF_FAIL("deserialization failed: " << source.get_error());
}
template <class... Ts>
std::string serialize_json(const Ts&... xs) {
jwriter.reset();
if (!(jwriter.apply(xs) && ...))
CAF_FAIL("JSON serialization failed: "
<< jwriter.get_error()
<< ", data: " << deep_to_string(std::forward_as_tuple(xs...)));
auto str = jwriter.str();
return std::string{str.begin(), str.end()};
}
template <class... Ts>
void deserialize_json(const std::string& str, Ts&... xs) {
if (!jreader.load(str))
CAF_FAIL("JSON loading failed: " << jreader.get_error()
<< "\n input: " << str);
if (!(jreader.apply(xs) && ...))
CAF_FAIL("JSON deserialization failed: " << jreader.get_error()
<< "\n input: " << str);
}
// serializes `x` and then deserializes and returns the serialized value
template <class T>
T roundtrip(T x) {
T result;
deserialize(serialize(x), result);
return result;
T roundtrip(T x, bool enable_json = true) {
auto r1 = T{};
deserialize(serialize(x), r1);
if (enable_json) {
auto r2 = T{};
deserialize_json(serialize_json(x), r2);
if (!CAF_CHECK_EQUAL(r1, r2))
CAF_MESSAGE("generated JSON: " << serialize_json(x));
}
return r1;
}
// converts `x` to a message, serialize it, then deserializes it, and
......@@ -184,7 +217,7 @@ struct fixture : test_coordinator_fixture<> {
return result.get_as<T>(0);
}
fixture() {
fixture() : jwriter(sys), jreader(sys) {
rs.str.assign(std::string(str.rbegin(), str.rend()));
msg = make_message(i32, i64, ts, te, str, rs);
config_value::dictionary dict;
......@@ -217,12 +250,20 @@ struct is_message {
} // namespace
#define CHECK_RT(val) CAF_CHECK_EQUAL(val, roundtrip(val))
#define CHECK_RT(val) \
do { \
CAF_MESSAGE(#val); \
CAF_CHECK_EQUAL(val, roundtrip(val)); \
} while (false)
#define CHECK_PRED_RT(pred, value) CAF_CHECK(pred(roundtrip(value)))
#define CHECK_PRED_RT(pred, value) \
do { \
CAF_MESSAGE(#pred "(" #value ")"); \
CAF_CHECK(pred(roundtrip(value, false))); \
} while (false)
#define CHECK_SIGN_RT(value) \
CAF_CHECK_EQUAL(std::signbit(roundtrip(value)), std::signbit(value))
CAF_CHECK_EQUAL(std::signbit(roundtrip(value, false)), std::signbit(value))
#define CHECK_MSG_RT(val) CAF_CHECK_EQUAL(val, msg_roundtrip(val))
......@@ -328,7 +369,7 @@ CAF_TEST(messages) {
deserialize(buf2, y);
CAF_CHECK_EQUAL(to_string(msg), to_string(y));
CAF_CHECK(is_message(y).equal(i32, i64, ts, te, str, rs));
CAF_CHECK_EQUAL(to_string(recursive), to_string(roundtrip(recursive)));
CAF_CHECK_EQUAL(to_string(recursive), to_string(roundtrip(recursive, false)));
}
CAF_TEST(multiple_messages) {
......@@ -370,11 +411,11 @@ CAF_TEST(variant_with_tree_types) {
CAF_MESSAGE("deserializing into a non-empty vector overrides any content");
using test_variant = variant<int, double, std::string>;
test_variant x{42};
CAF_CHECK_EQUAL(x, roundtrip(x));
CAF_CHECK_EQUAL(x, roundtrip(x, false));
x = 12.34;
CAF_CHECK_EQUAL(x, roundtrip(x));
CAF_CHECK_EQUAL(x, roundtrip(x, false));
x = std::string{"foobar"};
CAF_CHECK_EQUAL(x, roundtrip(x));
CAF_CHECK_EQUAL(x, roundtrip(x, false));
}
// -- our vector<bool> serialization packs into an uint64_t. Hence, the
......@@ -456,10 +497,10 @@ CAF_TEST(serializers handle actor handles) {
[](int i) { return i; },
};
});
CAF_CHECK_EQUAL(dummy, roundtrip(dummy));
CAF_CHECK_EQUAL(dummy, roundtrip(dummy, false));
CAF_CHECK_EQUAL(dummy, msg_roundtrip(dummy));
std::vector<strong_actor_ptr> wrapped{actor_cast<strong_actor_ptr>(dummy)};
CAF_CHECK_EQUAL(wrapped, roundtrip(wrapped));
CAF_CHECK_EQUAL(wrapped, roundtrip(wrapped, false));
CAF_CHECK_EQUAL(wrapped, msg_roundtrip(wrapped));
anon_send_exit(dummy, exit_reason::user_shutdown);
}
......
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