From 9fda30bda4a30eca8bede62dd50d1bdbe5512ea7 Mon Sep 17 00:00:00 2001 From: Ian-Flury Date: Wed, 23 Jul 2025 08:59:35 -0700 Subject: [PATCH 1/2] Add byte container concepts and support for them in `rfl::cbor::read()` --- include/rfl.hpp | 1 + include/rfl/cbor/read.hpp | 4 +- include/rfl/concepts.hpp | 65 +++++++++++++++++ tests/cbor/test_read_byte_containers.cpp | 90 ++++++++++++++++++++++++ tests/generic/test_byte_containers.cpp | 54 ++++++++++++++ 5 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 include/rfl/concepts.hpp create mode 100644 tests/cbor/test_read_byte_containers.cpp create mode 100644 tests/generic/test_byte_containers.cpp diff --git a/include/rfl.hpp b/include/rfl.hpp index fb4fea60..ac5e7eac 100644 --- a/include/rfl.hpp +++ b/include/rfl.hpp @@ -50,6 +50,7 @@ #include "rfl/apply.hpp" #include "rfl/as.hpp" #include "rfl/comparisons.hpp" +#include "rfl/concepts.hpp" #include "rfl/default.hpp" #include "rfl/define_literal.hpp" #include "rfl/define_named_tuple.hpp" diff --git a/include/rfl/cbor/read.hpp b/include/rfl/cbor/read.hpp index 47a3246a..52a27cf8 100644 --- a/include/rfl/cbor/read.hpp +++ b/include/rfl/cbor/read.hpp @@ -7,6 +7,7 @@ #include #include +#include "../concepts.hpp" #include "../Processors.hpp" #include "../internal/wrap_in_rfl_array_t.hpp" #include "Parser.hpp" @@ -19,8 +20,7 @@ using InputVarType = typename Reader::InputVarType; /// Parses an object from CBOR using reflection. template -Result> read(const std::vector& _bytes) { - // TODO: Use a non-throwing decode_cbor(), pending https://github.com/danielaparker/jsoncons/issues/615 +Result> read(const ContiguousByteContainer auto& _bytes) { try { auto val = jsoncons::cbor::decode_cbor(_bytes); auto r = Reader(); diff --git a/include/rfl/concepts.hpp b/include/rfl/concepts.hpp new file mode 100644 index 00000000..d752f06d --- /dev/null +++ b/include/rfl/concepts.hpp @@ -0,0 +1,65 @@ +#ifndef RFL_CONCEPTS_HPP_ +#define RFL_CONCEPTS_HPP_ + +#include +#include +#include +#include +#include + +namespace rfl { + +/// @brief Concept for byte-like types that can be used in contiguous containers +/// Includes char, signed char, unsigned char, std::byte, and uint8_t +template +concept ByteLike = std::same_as || + std::same_as || + std::same_as || + std::same_as || + std::same_as; + +/// @brief Concept for containers with a contiguous sequence of byte-like types +/// Requires: +/// - Container has a value_type that is byte-like +/// - Container provides data() method returning a pointer to contiguous memory +/// - Container provides size() method returning the number of elements +/// - Container supports range-based for loops (begin/end) +template +concept ContiguousByteContainer = requires(const Container& c) { + typename Container::value_type; + { c.data() } -> std::convertible_to; + { c.size() } -> std::convertible_to; + { c.begin() } -> std::input_iterator; + { c.end() } -> std::input_iterator; + requires ByteLike; + requires std::contiguous_iterator; +}; + +/// @brief Concept for mutable containers with a contiguous sequence of byte-like types +/// Extends ContiguousByteContainer with mutable access requirements +template +concept MutableContiguousByteContainer = ContiguousByteContainer && requires(Container& c) { + { c.data() } -> std::convertible_to; + { c.begin() } -> std::output_iterator; + { c.end() } -> std::output_iterator; +}; + +/// @brief Concept for back-insertable byte containers (like std::vector) +/// Useful for containers that can grow dynamically during serialization +template +concept BackInsertableByteContainer = ContiguousByteContainer && requires(Container& c, typename Container::value_type v) { + c.push_back(v); + c.reserve(std::size_t{}); + { c.capacity() } -> std::convertible_to; +}; + +/// @brief Concept for byte spans or views (read-only, non-owning containers) +/// Includes std::span, std::string_view when used with char data, etc. +template +concept ByteSpanLike = ContiguousByteContainer && + std::is_trivially_copyable_v && + std::is_trivially_destructible_v; + +} // namespace rfl + +#endif // RFL_CONCEPTS_HPP_ diff --git a/tests/cbor/test_read_byte_containers.cpp b/tests/cbor/test_read_byte_containers.cpp new file mode 100644 index 00000000..ae428512 --- /dev/null +++ b/tests/cbor/test_read_byte_containers.cpp @@ -0,0 +1,90 @@ +#include +#include +#include + +#include + +// Make sure things still compile when +// rfl.hpp is included after rfl/cbor.hpp. +#include + +namespace test_read_byte_containers +{ + +struct TestBox +{ + int length; + int width; + int height; +}; + +// TODO: Apparently the jsoncons trait is_byte_sequence is not working for std::span. +// TEST(cbor, test_read_from_char_span) +// { + +// TestBox b = { +// .length = 1, +// .width = 2, +// .height = 3, +// }; + + +// std::vector rfl_buffer = rfl::cbor::write(s); + +// // I don't want to be forced to copy to a std::vector if that's not what data strucure I use. +// // So, allow reading from a std::span +// std::span span(rfl_buffer.data(), rfl_buffer.size()); + +// auto result = rfl::cbor::read(span); +// EXPECT_TRUE(result); +// EXPECT_EQ(result->one, 1); +// EXPECT_EQ(result->two, 2); +// } + +TEST(cbor, test_read_from_byte_view) +{ + TestBox b = { + .length = 1, + .width = 2, + .height = 3, + }; + + // TODO: Write directly into desired container, once rfl::cbor::write supports it. + std::vector rfl_buffer = rfl::cbor::write(b); + + std::array my_buffer; + std::transform(rfl_buffer.begin(), rfl_buffer.end(), my_buffer.begin(), + [](char c) { return static_cast(c); }); + + std::basic_string_view byte_view(my_buffer.data(), rfl_buffer.size()); + + auto result = rfl::cbor::read(byte_view); + EXPECT_TRUE(result); + EXPECT_EQ(result->length, 1); + EXPECT_EQ(result->width, 2); + EXPECT_EQ(result->height, 3); +} + +TEST(cbor, test_read_from_uint8_array) +{ + TestBox b = { + .length = 4, + .width = 5, + .height = 6, + }; + + // TODO: Write directly into desired container, once rfl::cbor::write supports it. + std::vector rfl_buffer = rfl::cbor::write(b); + + std::array my_buffer; + std::transform(rfl_buffer.begin(), rfl_buffer.end(), my_buffer.begin(), + [](char c) { return static_cast(c); }); + + auto result = rfl::cbor::read(my_buffer); + EXPECT_TRUE(result); + EXPECT_EQ(result->length, 4); + EXPECT_EQ(result->width, 5); + EXPECT_EQ(result->height, 6); +} + +} // namespace test_read_byte_containers \ No newline at end of file diff --git a/tests/generic/test_byte_containers.cpp b/tests/generic/test_byte_containers.cpp new file mode 100644 index 00000000..7af46a8b --- /dev/null +++ b/tests/generic/test_byte_containers.cpp @@ -0,0 +1,54 @@ +#include +#include +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_byte_container_concepts { + +TEST(generic, test_byte_container_concepts) { + // Test various container types + std::vector vec_uint8{1, 2, 3, 4}; + std::vector vec_char{'a', 'b', 'c'}; + std::array arr_uint8{1, 2, 3, 4}; + std::string str_data{"hello"}; + std::string_view str_view{"world"}; + std::array arr_byte{std::byte{5}, std::byte{4}, std::byte{3}, std::byte{2}, std::byte{1}}; + std::span byte_span = arr_byte; + + // Test with concepts + static_assert(rfl::ByteLike); + static_assert(rfl::ByteLike); + static_assert(rfl::ByteLike); + static_assert(rfl::ByteLike); + static_assert(!rfl::ByteLike); + static_assert(!rfl::ByteLike); + + static_assert(rfl::ContiguousByteContainer); + static_assert(rfl::ContiguousByteContainer); + static_assert(rfl::ContiguousByteContainer); + static_assert(rfl::ContiguousByteContainer); + static_assert(rfl::ContiguousByteContainer); + static_assert(rfl::ContiguousByteContainer); + static_assert(rfl::ContiguousByteContainer); + + static_assert(rfl::MutableContiguousByteContainer); + static_assert(rfl::MutableContiguousByteContainer); + static_assert(rfl::MutableContiguousByteContainer); + static_assert(rfl::MutableContiguousByteContainer); + static_assert(!rfl::MutableContiguousByteContainer); + static_assert(rfl::MutableContiguousByteContainer); + static_assert(rfl::MutableContiguousByteContainer); + + static_assert(rfl::BackInsertableByteContainer); + static_assert(rfl::BackInsertableByteContainer); + static_assert(!rfl::BackInsertableByteContainer); + static_assert(rfl::BackInsertableByteContainer); + static_assert(!rfl::BackInsertableByteContainer); + static_assert(!rfl::BackInsertableByteContainer); + static_assert(!rfl::BackInsertableByteContainer); +} + +} // namespace test_byte_container_concepts From 8a75449552a1993e6b1e0c50cc07cb910bc67245 Mon Sep 17 00:00:00 2001 From: Ian-Flury Date: Tue, 22 Jul 2025 09:54:59 -0700 Subject: [PATCH 2/2] Add byte container support for ubjson. --- include/rfl/ubjson/read.hpp | 3 +- tests/ubjson/test_read_byte_containers.cpp | 62 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/ubjson/test_read_byte_containers.cpp diff --git a/include/rfl/ubjson/read.hpp b/include/rfl/ubjson/read.hpp index 521bf70c..5d50cf03 100644 --- a/include/rfl/ubjson/read.hpp +++ b/include/rfl/ubjson/read.hpp @@ -7,6 +7,7 @@ #include #include +#include "../concepts.hpp" #include "../Processors.hpp" #include "../internal/wrap_in_rfl_array_t.hpp" #include "Parser.hpp" @@ -19,7 +20,7 @@ using InputVarType = typename Reader::InputVarType; /// Parses an object from UBJSON using reflection. template -Result> read(const std::vector& _bytes) { +Result> read(const ContiguousByteContainer auto& _bytes) { // TODO: Use a non-throwing decode_ubjson(), pending https://github.com/danielaparker/jsoncons/issues/615 try { auto val = jsoncons::ubjson::decode_ubjson(_bytes); diff --git a/tests/ubjson/test_read_byte_containers.cpp b/tests/ubjson/test_read_byte_containers.cpp new file mode 100644 index 00000000..ee78a3fe --- /dev/null +++ b/tests/ubjson/test_read_byte_containers.cpp @@ -0,0 +1,62 @@ +#include +#include +#include + +#include + +// Make sure things still compile when +// rfl.hpp is included after rfl/ubjson.hpp. +#include + +namespace test_read_byte_containers +{ + +struct TestBall +{ + float radius; + float mass; +}; + +TEST(ubjson, test_read_from_byte_view) +{ + TestBall b = { + .radius = 1.5f, + .mass = 2.5f, + }; + + // TODO: Write directly into desired container, once rfl::ubjson::write supports it. + std::vector rfl_buffer = rfl::ubjson::write(b); + + std::array my_buffer; + std::transform(rfl_buffer.begin(), rfl_buffer.end(), my_buffer.begin(), + [](char c) { return static_cast(c); }); + + std::basic_string_view byte_view(my_buffer.data(), rfl_buffer.size()); + + auto result = rfl::ubjson::read(byte_view); + EXPECT_TRUE(result); + EXPECT_EQ(result->radius, 1.5f); + EXPECT_EQ(result->mass, 2.5f); +} + +TEST(ubjson, test_read_from_uint8_array) +{ + TestBall b = { + .radius = 4.5f, + .mass = 5.5f, + }; + + // TODO: Write directly into desired container, once rfl::ubjson::write supports it. + std::vector rfl_buffer = rfl::ubjson::write(b); + + std::array my_buffer; + std::transform(rfl_buffer.begin(), rfl_buffer.end(), my_buffer.begin(), + [](char c) { return static_cast(c); }); + + auto result = rfl::ubjson::read(my_buffer); + EXPECT_TRUE(result); + EXPECT_EQ(result->radius, 4.5f); + EXPECT_EQ(result->mass, 5.5f); +} + +} // namespace test_read_byte_containers