diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index 1b11ed0ec..8e8c26af7 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -121,6 +121,7 @@ jobs: echo '```' >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_write >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=field_dispatch >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index 40ab577d1..b90bee2fc 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -63,6 +63,7 @@ jobs: echo '```' >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_write >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=field_dispatch >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index 14ca7b2fc..2178fdce3 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -62,6 +62,7 @@ jobs: echo '```' >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=canada_write >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=field_dispatch >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=licenses_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY diff --git a/benchmarks/all/field_dispatch.hpp b/benchmarks/all/field_dispatch.hpp new file mode 100644 index 000000000..c78090d25 --- /dev/null +++ b/benchmarks/all/field_dispatch.hpp @@ -0,0 +1,76 @@ +#ifndef BENCHMARK_FIELD_DISPATCH_HPP_ +#define BENCHMARK_FIELD_DISPATCH_HPP_ +#include + +#include +#include +namespace field_dispatch { +struct Record2 { + float field0 = 0; + float field1 = 1; + bool operator==(const Record2&) const = default; +}; +struct Record16 { + float field0 = 0; + float field1 = 1; + float field2 = 2; + float field3 = 3; + float field4 = 4; + float field5 = 5; + float field6 = 6; + float field7 = 7; + float field8 = 8; + float field9 = 9; + float field10 = 10; + float field11 = 11; + float field12 = 12; + float field13 = 13; + float field14 = 14; + float field15 = 15; + bool operator==(const Record16&) const = default; +}; +template +struct Data { + std::vector records; + bool operator==(const Data&) const = default; +}; +template +auto make_data(std::size_t size) { + Data data; + data.records.resize(size); + for (std::size_t i = 0; i < size; ++i) { + data.records[i].field0 = static_cast(i) * 0.5f; + data.records[i].field1 = -static_cast(i) * 0.25f; + } + return data; +} +template +void run(benchmark::State& state, Write write, Read read) { + const auto input = make_data(state.range(0)); + const auto bytes = write(input); + { + const auto result = read(bytes); + if (!result) { + state.SkipWithError(result.error().what()); + return; + } + if (*result != input) { + state.SkipWithError("Fields changed during round trip"); + return; + } + } + for (auto _ : state) { + auto result = read(bytes); + if (!result) { + state.SkipWithError(result.error().what()); + break; + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * state.range(0)); + state.SetBytesProcessed(state.iterations() * + static_cast(bytes.size())); + state.counters["serialized_bytes"] = static_cast(bytes.size()); +} +} // namespace field_dispatch +#endif diff --git a/benchmarks/all/field_dispatch_avro.cpp b/benchmarks/all/field_dispatch_avro.cpp new file mode 100644 index 000000000..8dfc2a989 --- /dev/null +++ b/benchmarks/all/field_dispatch_avro.cpp @@ -0,0 +1,20 @@ +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_avro(benchmark::State& state) { + const auto schema = rfl::avro::to_schema>(); + run( + state, [&](const auto& input) { return rfl::avro::write(input, schema); }, + [&](const auto& bytes) { + return rfl::avro::read>(bytes, schema); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_avro, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_avro, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/benchmarks/all/field_dispatch_boost_serialization.cpp b/benchmarks/all/field_dispatch_boost_serialization.cpp new file mode 100644 index 000000000..b99e034b2 --- /dev/null +++ b/benchmarks/all/field_dispatch_boost_serialization.cpp @@ -0,0 +1,20 @@ +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_boost_serialization(benchmark::State& state) { + run( + state, + [&](const auto& input) { return rfl::boost_serialization::write(input); }, + [&](const auto& bytes) { + return rfl::boost_serialization::read>(bytes); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_boost_serialization, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_boost_serialization, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/benchmarks/all/field_dispatch_capnproto.cpp b/benchmarks/all/field_dispatch_capnproto.cpp new file mode 100644 index 000000000..d934e4365 --- /dev/null +++ b/benchmarks/all/field_dispatch_capnproto.cpp @@ -0,0 +1,21 @@ +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_capnproto(benchmark::State& state) { + const auto schema = rfl::capnproto::to_schema>(); + run( + state, + [&](const auto& input) { return rfl::capnproto::write(input, schema); }, + [&](const auto& bytes) { + return rfl::capnproto::read>(bytes, schema); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_capnproto, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_capnproto, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/benchmarks/all/field_dispatch_cereal.cpp b/benchmarks/all/field_dispatch_cereal.cpp new file mode 100644 index 000000000..a1dc6085a --- /dev/null +++ b/benchmarks/all/field_dispatch_cereal.cpp @@ -0,0 +1,19 @@ +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_cereal(benchmark::State& state) { + run( + state, [&](const auto& input) { return rfl::cereal::write(input); }, + [&](const auto& bytes) { + return rfl::cereal::read>(bytes); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_cereal, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_cereal, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/benchmarks/all/field_dispatch_no_field_names.cpp b/benchmarks/all/field_dispatch_no_field_names.cpp new file mode 100644 index 000000000..9f0d89ebf --- /dev/null +++ b/benchmarks/all/field_dispatch_no_field_names.cpp @@ -0,0 +1,57 @@ +#include +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_json_no_field_names(benchmark::State& state) { + run( + state, + [](const auto& input) { + return rfl::json::write(input); + }, + [](const auto& bytes) { + return rfl::json::read, rfl::NoFieldNames, Ps...>(bytes); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_json_no_field_names, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_json_no_field_names, Record2, + rfl::DefaultIfMissing) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_json_no_field_names, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_json_no_field_names, Record16, + rfl::DefaultIfMissing) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +template +void BM_field_dispatch_msgpack_no_field_names(benchmark::State& state) { + run( + state, + [](const auto& input) { + return rfl::msgpack::write(input); + }, + [](const auto& bytes) { + return rfl::msgpack::read, rfl::NoFieldNames, Ps...>( + bytes); + }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_msgpack_no_field_names, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_msgpack_no_field_names, Record2, + rfl::DefaultIfMissing) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_msgpack_no_field_names, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_msgpack_no_field_names, Record16, + rfl::DefaultIfMissing) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/benchmarks/all/field_dispatch_yas.cpp b/benchmarks/all/field_dispatch_yas.cpp new file mode 100644 index 000000000..28357a8bf --- /dev/null +++ b/benchmarks/all/field_dispatch_yas.cpp @@ -0,0 +1,17 @@ +#include + +#include "field_dispatch.hpp" +namespace field_dispatch { +template +void BM_field_dispatch_yas(benchmark::State& state) { + run( + state, [&](const auto& input) { return rfl::yas::write(input); }, + [&](const auto& bytes) { return rfl::yas::read>(bytes); }); +} +BENCHMARK_TEMPLATE(BM_field_dispatch_yas, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_field_dispatch_yas, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace field_dispatch diff --git a/include/rfl/avro/Reader.hpp b/include/rfl/avro/Reader.hpp index 747bb0d0c..75c478439 100644 --- a/include/rfl/avro/Reader.hpp +++ b/include/rfl/avro/Reader.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "../Result.hpp" #include "../always_false.hpp" @@ -274,6 +275,32 @@ struct Reader { return Error(std::string(__FUNCTION__) + " error(" + std::to_string(res) + "): " + avro_strerror()); } + if constexpr (requires { + std::make_integer_sequence{}; + }) { + if (size == ObjectReader::size()) { + std::optional err; + const auto read_field = + [&](std::integral_constant index) { + if (err) { + return; + } + avro_value_t element; + const int result = + avro_value_get_by_index(_obj.val_, i, &element, nullptr); + if (result != 0) { + err = Error(std::string("read_object error(") + + std::to_string(result) + "): " + avro_strerror()); + return; + } + _object_reader.read(index, InputVarType{&element}); + }; + [&](std::integer_sequence) { + (read_field(std::integral_constant{}), ...); + }(std::make_integer_sequence{}); + return err; + } + } for (size_t ix = 0; ix < size; ++ix) { avro_value_t element; res = avro_value_get_by_index(_obj.val_, ix, &element, nullptr); diff --git a/include/rfl/boost_serialization/Reader.hpp b/include/rfl/boost_serialization/Reader.hpp index ec15eb0ae..ce71137d2 100644 --- a/include/rfl/boost_serialization/Reader.hpp +++ b/include/rfl/boost_serialization/Reader.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "../Result.hpp" #include "../always_false.hpp" @@ -181,6 +182,18 @@ struct Reader { template std::optional read_object(const ObjectReader& _object_reader, const InputObjectType& _obj) const noexcept { + if constexpr (requires { + std::make_integer_sequence{}; + }) { + if (_obj.size == ObjectReader::size()) { + [&](std::integer_sequence) { + (_object_reader.read(std::integral_constant{}, + InputVarType{_obj.ar}), + ...); + }(std::make_integer_sequence{}); + return std::nullopt; + } + } for (size_t i = 0; i < _obj.size; ++i) { auto var = InputVarType{_obj.ar}; _object_reader.read(static_cast(i), var); diff --git a/include/rfl/capnproto/Reader.hpp b/include/rfl/capnproto/Reader.hpp index eda33add4..ac4956d3c 100644 --- a/include/rfl/capnproto/Reader.hpp +++ b/include/rfl/capnproto/Reader.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "../Bytestring.hpp" #include "../Result.hpp" @@ -178,8 +179,21 @@ class RFL_API Reader { template std::optional read_object(const ObjectReader& _object_reader, const InputObjectType& _obj) const noexcept { + const auto fields = _obj.val_.getSchema().getFields(); + if constexpr (requires { + std::make_integer_sequence{}; + }) { + if (fields.size() == ObjectReader::size()) { + [&](std::integer_sequence) { + (_object_reader.read(std::integral_constant{}, + InputVarType{_obj.val_.get(fields[Is])}), + ...); + }(std::make_integer_sequence{}); + return std::nullopt; + } + } int i = 0; - for (auto field : _obj.val_.getSchema().getFields()) { + for (auto field : fields) { _object_reader.read(i++, InputVarType{_obj.val_.get(field)}); } return std::nullopt; diff --git a/include/rfl/cereal/Reader.hpp b/include/rfl/cereal/Reader.hpp index 85d1ab09d..3fabeb2e1 100644 --- a/include/rfl/cereal/Reader.hpp +++ b/include/rfl/cereal/Reader.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "../Bytestring.hpp" #include "../Result.hpp" @@ -171,9 +172,10 @@ struct Reader { std::optional read_object(const ObjectReader& _object_reader, const InputObjectType& _obj) const noexcept { try { - for (size_t i = 0; i < ObjectReader::size(); ++i) { - _object_reader.read(i, InputVarType{_obj.archive_}); - } + [&](std::integer_sequence) { + (_object_reader.read(std::integral_constant{}, + InputVarType{_obj.archive_}), ...); + }(std::make_integer_sequence{}); return std::nullopt; } catch (std::exception& e) { return Error(std::string("Cereal object read error: ") + e.what()); diff --git a/include/rfl/parsing/ViewReader.hpp b/include/rfl/parsing/ViewReader.hpp index 9082ac9e3..7c8d798ae 100644 --- a/include/rfl/parsing/ViewReader.hpp +++ b/include/rfl/parsing/ViewReader.hpp @@ -62,6 +62,13 @@ class ViewReader { std::make_integer_sequence()); } + // Positional readers already know the field at compile time. + template + void read(std::integral_constant, const InputVarType& _var) const { + assign_to_matching_field(*r_, i, _var, view_, errors_, found_, set_, + std::integer_sequence()); + } + /** * @brief Returns the size of the view. * diff --git a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp index de57b25d3..e5d20e150 100644 --- a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp @@ -1,6 +1,7 @@ #ifndef RFL_PARSING_VIEWREADERWITHDEFAULTANDSTRIPPEDFIELDNAMES_HPP_ #define RFL_PARSING_VIEWREADERWITHDEFAULTANDSTRIPPEDFIELDNAMES_HPP_ +#include #include #include #include @@ -60,8 +61,7 @@ class ViewReaderWithDefaultAndStrippedFieldNames { << " fields, but got at least one more."; return Error(stream.str()); } - assign_to_field_i(*r_, _var, view_, errors_, i_, - std::make_integer_sequence()); + assign_to_field_i(_var, std::make_integer_sequence()); ++i_; return std::nullopt; } @@ -100,10 +100,17 @@ class ViewReaderWithDefaultAndStrippedFieldNames { } template - static void assign_to_field_i(const R& _r, const auto& _var, auto* _view, - auto* _errors, int _i, - std::integer_sequence) { - (assign_if_field_is_field_i(_r, _var, _view, _errors, _i), ...); + void assign_to_field_i(const InputVarType& _var, + std::integer_sequence) const { + using ReadField = void (*)( + const ViewReaderWithDefaultAndStrippedFieldNames*, const InputVarType&); + static constexpr std::array readers{ + +[](const ViewReaderWithDefaultAndStrippedFieldNames* self, + const InputVarType& var) { + assign_if_field_is_field_i(*self->r_, var, self->view_, + self->errors_, is); + }...}; + readers[i_](this, _var); } // TODO: Unnecessary code duplication. diff --git a/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp index cc776bebc..160641341 100644 --- a/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp @@ -59,8 +59,7 @@ class ViewReaderWithStrippedFieldNames { << " fields, but got at least one more."; return Error(stream.str()); } - assign_to_field_i(*r_, _var, view_, errors_, found_, set_, i_, - std::make_integer_sequence()); + assign_to_field_i(_var, std::make_integer_sequence()); ++i_; return std::nullopt; } @@ -102,11 +101,18 @@ class ViewReaderWithStrippedFieldNames { } template - static void assign_to_field_i(const R& _r, const auto& _var, auto* _view, - auto* _errors, auto* _found, auto* _set, int _i, - std::integer_sequence) { - (assign_if_field_is_field_i(_r, _var, _view, _errors, _found, _set, _i), - ...); + void assign_to_field_i(const InputVarType& _var, + std::integer_sequence) const { + using ReadField = + void (*)(const ViewReaderWithStrippedFieldNames*, const InputVarType&); + static constexpr std::array readers{ + +[](const ViewReaderWithStrippedFieldNames* self, + const InputVarType& var) { + assign_if_field_is_field_i(*self->r_, var, self->view_, + self->errors_, self->found_, + self->set_, is); + }...}; + readers[i_](this, _var); } // TODO: Unnecessary code duplication. diff --git a/include/rfl/yas/Reader.hpp b/include/rfl/yas/Reader.hpp index bf1b652cd..12d729c4f 100644 --- a/include/rfl/yas/Reader.hpp +++ b/include/rfl/yas/Reader.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -163,9 +164,11 @@ struct Reader { std::optional read_object(const ObjectReader& _object_reader, const InputObjectType& _obj) const noexcept { try { - for (std::size_t i = 0; i < ObjectReader::size(); ++i) { - _object_reader.read(i, InputVarType{_obj.ar}); - } + [&](std::integer_sequence) { + (_object_reader.read(std::integral_constant{}, + InputVarType{_obj.ar}), + ...); + }(std::make_integer_sequence{}); return std::nullopt; } catch (std::exception& e) { return Error(std::string("yas object read error: ") + e.what()); diff --git a/tests/avro/test_field_dispatch.cpp b/tests/avro/test_field_dispatch.cpp new file mode 100644 index 000000000..e60874e17 --- /dev/null +++ b/tests/avro/test_field_dispatch.cpp @@ -0,0 +1,23 @@ +#include + +#include "../check_positional_field_dispatch.hpp" +namespace test_positional_field_dispatch { +const auto write_avro = [](const auto& input) { + return rfl::avro::write(input); +}; +const auto read_avro = [](const auto& bytes) { + return rfl::avro::read(bytes); +}; +TEST(avro, field_dispatch_errors_and_cleanup) { + check_errors(write_avro, read_avro); +} +TEST(avro, field_dispatch_empty_object) { check_empty(write_avro, read_avro); } +TEST(avro, field_dispatch_schema_counts) { + const auto schema = rfl::avro::to_schema(); + avro_value_t root; + ASSERT_EQ(avro_generic_value_new(schema.iface(), &root), 0); + const rfl::avro::Reader reader; + check_counts(reader, {&root}, 3); + avro_value_decref(&root); +} +} // namespace test_positional_field_dispatch diff --git a/tests/boost_serialization/test_field_dispatch.cpp b/tests/boost_serialization/test_field_dispatch.cpp new file mode 100644 index 000000000..a4da68e2d --- /dev/null +++ b/tests/boost_serialization/test_field_dispatch.cpp @@ -0,0 +1,27 @@ +#include + +#include "../check_positional_field_dispatch.hpp" +namespace test_positional_field_dispatch { +const auto write_boost_serialization = [](const auto& input) { + return rfl::boost_serialization::write(input); +}; +const auto read_boost_serialization = [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); +}; +TEST(boost_serialization, field_dispatch_errors_and_cleanup) { + check_errors(write_boost_serialization, read_boost_serialization); +} +TEST(boost_serialization, field_dispatch_empty_object) { + check_empty(write_boost_serialization, read_boost_serialization); +} +TEST(boost_serialization, field_dispatch_truncated_cleanup) { + check_truncated(write_boost_serialization, read_boost_serialization); +} +TEST(boost_serialization, field_dispatch_input_counts) { + const rfl::boost_serialization::Reader + reader; + for (const auto count : {0, 2, 3, 4}) { + check_counts(reader, {nullptr, static_cast(count)}, count); + } +} +} // namespace test_positional_field_dispatch diff --git a/tests/boost_serialization/test_no_field_names_dispatch.cpp b/tests/boost_serialization/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..e74824710 --- /dev/null +++ b/tests/boost_serialization/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(boost_serialization, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::boost_serialization::write(value); + }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/capnproto/test_field_dispatch.cpp b/tests/capnproto/test_field_dispatch.cpp new file mode 100644 index 000000000..a343deb21 --- /dev/null +++ b/tests/capnproto/test_field_dispatch.cpp @@ -0,0 +1,26 @@ +#include + +#include "../check_positional_field_dispatch.hpp" +namespace test_positional_field_dispatch { +const auto write_capnproto = [](const auto& input) { + return rfl::capnproto::write(input); +}; +const auto read_capnproto = [](const auto& bytes) { + return rfl::capnproto::read(bytes); +}; +TEST(capnproto, field_dispatch_errors_and_cleanup) { + check_errors(write_capnproto, read_capnproto); +} +TEST(capnproto, field_dispatch_empty_object) { + check_empty(write_capnproto, read_capnproto); +} +TEST(capnproto, field_dispatch_schema_counts) { + const auto schema = rfl::capnproto::to_schema(); + const auto name = rfl::capnproto::get_root_name(); + const auto root_schema = schema.value().getNested(name.c_str()).asStruct(); + capnp::MallocMessageBuilder message; + const auto root = message.initRoot(root_schema); + const rfl::capnproto::Reader reader; + check_counts(reader, {root.asReader()}, 3); +} +} // namespace test_positional_field_dispatch diff --git a/tests/cbor/test_no_field_names_dispatch.cpp b/tests/cbor/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..31c6cd9ca --- /dev/null +++ b/tests/cbor/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(cbor, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::cbor::write(value); + }, + [](const auto& bytes) { + return rfl::cbor::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/cereal/test_field_dispatch.cpp b/tests/cereal/test_field_dispatch.cpp new file mode 100644 index 000000000..d9bcc66cf --- /dev/null +++ b/tests/cereal/test_field_dispatch.cpp @@ -0,0 +1,40 @@ +#include + +#include "../check_positional_field_dispatch.hpp" +namespace test_positional_field_dispatch { +const auto write_cereal = [](const auto& input) { + return rfl::cereal::write(input); +}; +const auto read_cereal = [](const auto& bytes) { + return rfl::cereal::read(bytes); +}; +TEST(cereal, field_dispatch_errors_and_cleanup) { + check_errors(write_cereal, read_cereal); +} +TEST(cereal, field_dispatch_empty_object) { + check_empty(write_cereal, read_cereal); +} +TEST(cereal, field_dispatch_truncated_cleanup) { + check_truncated(write_cereal, read_cereal); +} +TEST(cereal, field_dispatch_static_indices) { + int next = 0; + int static_calls = 0; + const rfl::cereal::Reader reader; + EXPECT_FALSE(reader.read_object( + IndexedReader{{&next}, &static_calls}, + {nullptr})); + EXPECT_EQ(next, 3); + EXPECT_EQ(static_calls, 3); +} +struct CerealRuntimeIndexReader : RuntimeIndexReader { + static constexpr int size() { return 3; } +}; +TEST(cereal, field_dispatch_runtime_index_reader_compatibility) { + int next = 0; + const rfl::cereal::Reader reader; + EXPECT_FALSE( + reader.read_object(CerealRuntimeIndexReader{{&next}}, {nullptr})); + EXPECT_EQ(next, 3); +} +} // namespace test_positional_field_dispatch diff --git a/tests/cereal/test_no_field_names_dispatch.cpp b/tests/cereal/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..64c0d7477 --- /dev/null +++ b/tests/cereal/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(cereal, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::cereal::write(value); + }, + [](const auto& bytes) { + return rfl::cereal::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/check_no_field_names_dispatch.hpp b/tests/check_no_field_names_dispatch.hpp new file mode 100644 index 000000000..1e3c8d2ad --- /dev/null +++ b/tests/check_no_field_names_dispatch.hpp @@ -0,0 +1,30 @@ +#ifndef CHECK_NO_FIELD_NAMES_DISPATCH_HPP_ +#define CHECK_NO_FIELD_NAMES_DISPATCH_HPP_ +#include + +#include +#include +#include + +namespace test_no_field_names_dispatch { +struct Record { + int first; + std::string middle; + int last; + bool operator==(const Record&) const = default; +}; +struct Data { + std::vector records; + bool operator==(const Data&) const = default; +}; +template +void check_roundtrip(Write write, Read read) { + const Data input{{{1, "first", 2}, {-3, "second", 4}}}; + const auto bytes = write(input); + const auto result = read.template operator()(bytes); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(*result, input); + EXPECT_EQ(write(*result), bytes); +} +} // namespace test_no_field_names_dispatch +#endif diff --git a/tests/check_positional_field_dispatch.hpp b/tests/check_positional_field_dispatch.hpp new file mode 100644 index 000000000..3a3a0b0ef --- /dev/null +++ b/tests/check_positional_field_dispatch.hpp @@ -0,0 +1,123 @@ +#ifndef CHECK_POSITIONAL_FIELD_DISPATCH_HPP_ +#define CHECK_POSITIONAL_FIELD_DISPATCH_HPP_ +#include + +#include +#include +#include +namespace test_positional_field_dispatch { +struct Input { + int first; + std::string middle; + int last; +}; + +class TrackedString { + public: + using ReflectionType = std::string; + inline static int live = 0; + + explicit TrackedString(std::string value) : value_(std::move(value)) { + ++live; + } + TrackedString(TrackedString&& other) : value_(std::move(other.value_)) { + ++live; + } + ~TrackedString() { --live; } + const std::string& reflection() const { return value_; } + + private: + std::string value_; +}; + +struct Validated { + rfl::Validator> first; + TrackedString middle; + rfl::Validator> last; +}; + +template +void check_errors(Write write, Read read) { + const auto bytes = write(Input{-1, "constructed between errors", -2}); + const auto result = read.template operator()(bytes); + ASSERT_FALSE(result); + const std::string message = result.error().what(); + EXPECT_NE(message.find("Found 2 errors:"), std::string::npos); + const auto first = message.find("Failed to parse field 'first'"); + const auto last = message.find("Failed to parse field 'last'"); + ASSERT_NE(first, std::string::npos); + ASSERT_NE(last, std::string::npos); + EXPECT_LT(first, last); + EXPECT_EQ(TrackedString::live, 0); +} + +template +void check_truncated(Write write, Read read) { + auto bytes = write(Input{1, "constructed before truncated field", 2}); + bytes.pop_back(); + const auto result = read.template operator()(bytes); + ASSERT_FALSE(result); + EXPECT_NE( + std::string(result.error().what()).find("Failed to parse field 'last'"), + std::string::npos); + EXPECT_EQ(TrackedString::live, 0); +} + +struct Empty {}; +struct WithEmpty { + Empty empty; + int value; +}; +template +void check_empty(Write write, Read read) { + const auto result = + read.template operator()(write(WithEmpty{{}, 42})); + ASSERT_TRUE(result); + EXPECT_EQ(result->value, 42); +} + +template +struct RuntimeIndexReader { + int* next; + void read(int index, const typename R::InputVarType&) const { + EXPECT_EQ(index, (*next)++); + } +}; + +template +struct IndexedReader : RuntimeIndexReader { + int* static_calls; + static constexpr int size() { return N; } + using RuntimeIndexReader::read; + template + void read(std::integral_constant, + const typename R::InputVarType& var) const { + ++*static_calls; + RuntimeIndexReader::read(i, var); + } +}; + +template +void check_count(const R& reader, const typename R::InputObjectType& obj, + int count) { + int next = 0; + int static_calls = 0; + EXPECT_FALSE( + reader.read_object(IndexedReader{{&next}, &static_calls}, obj)); + EXPECT_EQ(next, count); + EXPECT_EQ(static_calls, N == count ? count : 0); +} + +template +void check_counts(const R& reader, const typename R::InputObjectType& obj, + int count) { + check_count<0>(reader, obj, count); + check_count<2>(reader, obj, count); + check_count<3>(reader, obj, count); + check_count<4>(reader, obj, count); + int next = 0; + EXPECT_FALSE(reader.read_object(RuntimeIndexReader{&next}, obj)); + EXPECT_EQ(next, count); +} +} // namespace test_positional_field_dispatch +#endif diff --git a/tests/flexbuffers/test_no_field_names_dispatch.cpp b/tests/flexbuffers/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..dc8ff4e5d --- /dev/null +++ b/tests/flexbuffers/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(flexbuffers, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::flexbuf::write(value); + }, + [](const auto& bytes) { + return rfl::flexbuf::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/generic/test_no_field_names_dispatch.cpp b/tests/generic/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..bc59ebaf0 --- /dev/null +++ b/tests/generic/test_no_field_names_dispatch.cpp @@ -0,0 +1,13 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" + +namespace test_no_field_names_dispatch { +TEST(generic, no_field_names_dispatch_roundtrip) { + const Data input{{{1, "first", 2}, {-3, "second", 4}}}; + const auto value = rfl::to_generic(input); + const auto result = rfl::from_generic(value); + ASSERT_TRUE(result); + EXPECT_EQ(*result, input); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/json/test_no_field_names_dispatch.cpp b/tests/json/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..d66ca1c36 --- /dev/null +++ b/tests/json/test_no_field_names_dispatch.cpp @@ -0,0 +1,79 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(json, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::json::write(value); + }, + [](const auto& bytes) { + return rfl::json::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch + +#include "../check_positional_field_dispatch.hpp" + +namespace test_no_field_names_dispatch { +TEST(json, no_field_names_dispatch_errors_and_cleanup) { + test_positional_field_dispatch::check_errors( + [](const auto& value) { + return rfl::json::write(value); + }, + [](const auto& bytes) { + return rfl::json::read(bytes); + }); +} + +TEST(json, no_field_names_dispatch_missing_and_extra_fields) { + const auto missing = + rfl::json::read(R"([1,"middle"])"); + ASSERT_FALSE(missing); + EXPECT_NE( + std::string(missing.error().what()).find("Field named 'last' not found"), + std::string::npos); + const auto defaults = + rfl::json::read( + R"([1,"middle"])"); + ASSERT_TRUE(defaults); + EXPECT_EQ(defaults->last, 0); + const auto extra = + rfl::json::read(R"([1,"middle",2,3])"); + ASSERT_FALSE(extra); + EXPECT_NE( + std::string(extra.error().what()).find("Expected a maximum of 3 fields"), + std::string::npos); + const auto extra_defaults = + rfl::json::read( + R"([1,"middle",2,3])"); + ASSERT_FALSE(extra_defaults); + EXPECT_EQ(extra_defaults.error().what(), extra.error().what()); +} + +struct Defaults { + rfl::Validator> first = 0; + std::string middle = "default"; + rfl::Validator> last = 0; +}; +TEST(json, no_field_names_dispatch_default_errors) { + const auto result = + rfl::json::read( + R"([-1,"middle",-2])"); + ASSERT_FALSE(result); + const std::string message = result.error().what(); + EXPECT_NE(message.find("Found 2 errors:"), std::string::npos); + EXPECT_LT(message.find("Failed to parse field 'first'"), + message.find("Failed to parse field 'last'")); +} + +struct Empty {}; +TEST(json, no_field_names_dispatch_empty) { + EXPECT_TRUE((rfl::json::read("[]"))); + EXPECT_FALSE((rfl::json::read("[1]"))); + EXPECT_TRUE( + (rfl::json::read("[]"))); + EXPECT_FALSE(( + rfl::json::read("[1]"))); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/msgpack/test_no_field_names_dispatch.cpp b/tests/msgpack/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..b70482dcf --- /dev/null +++ b/tests/msgpack/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(msgpack, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::msgpack::write(value); + }, + [](const auto& bytes) { + return rfl::msgpack::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/ubjson/test_no_field_names_dispatch.cpp b/tests/ubjson/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..391a917c6 --- /dev/null +++ b/tests/ubjson/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(ubjson, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::ubjson::write(value); + }, + [](const auto& bytes) { + return rfl::ubjson::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch diff --git a/tests/yas/test_field_dispatch.cpp b/tests/yas/test_field_dispatch.cpp new file mode 100644 index 000000000..5924e662a --- /dev/null +++ b/tests/yas/test_field_dispatch.cpp @@ -0,0 +1,25 @@ +#include + +#include "../check_positional_field_dispatch.hpp" +namespace test_positional_field_dispatch { +const auto write_yas = [](const auto& input) { return rfl::yas::write(input); }; +const auto read_yas = [](const auto& bytes) { + return rfl::yas::read(bytes); +}; +TEST(yas, field_dispatch_errors_and_cleanup) { + check_errors(write_yas, read_yas); +} +TEST(yas, field_dispatch_empty_object) { check_empty(write_yas, read_yas); } +TEST(yas, field_dispatch_truncated_cleanup) { + check_truncated(write_yas, read_yas); +} +TEST(yas, field_dispatch_static_indices) { + int next = 0; + int static_calls = 0; + const rfl::yas::Reader reader; + EXPECT_FALSE(reader.read_object( + IndexedReader{{&next}, &static_calls}, {nullptr})); + EXPECT_EQ(next, 3); + EXPECT_EQ(static_calls, 3); +} +} // namespace test_positional_field_dispatch diff --git a/tests/yas/test_no_field_names_dispatch.cpp b/tests/yas/test_no_field_names_dispatch.cpp new file mode 100644 index 000000000..d3ef82ca9 --- /dev/null +++ b/tests/yas/test_no_field_names_dispatch.cpp @@ -0,0 +1,14 @@ +#include + +#include "../check_no_field_names_dispatch.hpp" +namespace test_no_field_names_dispatch { +TEST(yas, no_field_names_dispatch_roundtrip) { + check_roundtrip( + [](const auto& value) { + return rfl::yas::write(value); + }, + [](const auto& bytes) { + return rfl::yas::read(bytes); + }); +} +} // namespace test_no_field_names_dispatch