Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/rfl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions include/rfl/cbor/read.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <jsoncons_ext/cbor/decode_cbor.hpp>
#include <string>

#include "../concepts.hpp"
#include "../Processors.hpp"
#include "../internal/wrap_in_rfl_array_t.hpp"
#include "Parser.hpp"
Expand All @@ -19,8 +20,7 @@ using InputVarType = typename Reader::InputVarType;

/// Parses an object from CBOR using reflection.
template <class T, class... Ps>
Result<internal::wrap_in_rfl_array_t<T>> read(const std::vector<char>& _bytes) {
// TODO: Use a non-throwing decode_cbor(), pending https://github.com/danielaparker/jsoncons/issues/615
Result<internal::wrap_in_rfl_array_t<T>> read(const ContiguousByteContainer auto& _bytes) {
try {
auto val = jsoncons::cbor::decode_cbor<jsoncons::json>(_bytes);
auto r = Reader();
Expand Down
65 changes: 65 additions & 0 deletions include/rfl/concepts.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#ifndef RFL_CONCEPTS_HPP_
#define RFL_CONCEPTS_HPP_

#include <concepts>
#include <type_traits>
#include <cstdint>
#include <cstddef>
#include <iterator>

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<typename T>
concept ByteLike = std::same_as<T, char> ||
std::same_as<T, signed char> ||
std::same_as<T, unsigned char> ||
std::same_as<T, std::uint8_t> ||
std::same_as<T, std::byte>;

/// @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<typename Container>
concept ContiguousByteContainer = requires(const Container& c) {
typename Container::value_type;
{ c.data() } -> std::convertible_to<const typename Container::value_type*>;
{ c.size() } -> std::convertible_to<std::size_t>;
{ c.begin() } -> std::input_iterator;
{ c.end() } -> std::input_iterator;
requires ByteLike<typename Container::value_type>;
requires std::contiguous_iterator<decltype(c.begin())>;
};

/// @brief Concept for mutable containers with a contiguous sequence of byte-like types
/// Extends ContiguousByteContainer with mutable access requirements
template<typename Container>
concept MutableContiguousByteContainer = ContiguousByteContainer<Container> && requires(Container& c) {
{ c.data() } -> std::convertible_to<typename Container::value_type*>;
{ c.begin() } -> std::output_iterator<typename Container::value_type>;
{ c.end() } -> std::output_iterator<typename Container::value_type>;
Comment on lines +41 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The MutableContiguousByteContainer concept extends ContiguousByteContainer and adds requirements for mutable access. However, the checks for std::output_iterator on c.begin() and c.end() might be redundant and semantically confusing, as one cannot typically write to an end iterator. Relying on the non-const data() method on line 42 might be a clearer way to ensure mutability.

};

/// @brief Concept for back-insertable byte containers (like std::vector<uint8_t>)
/// Useful for containers that can grow dynamically during serialization
template<typename Container>
concept BackInsertableByteContainer = ContiguousByteContainer<Container> && requires(Container& c, typename Container::value_type v) {
c.push_back(v);
c.reserve(std::size_t{});
{ c.capacity() } -> std::convertible_to<std::size_t>;
};

/// @brief Concept for byte spans or views (read-only, non-owning containers)
/// Includes std::span<const uint8_t>, std::string_view when used with char data, etc.
template<typename Container>
concept ByteSpanLike = ContiguousByteContainer<Container> &&
std::is_trivially_copyable_v<Container> &&
std::is_trivially_destructible_v<Container>;

} // namespace rfl

#endif // RFL_CONCEPTS_HPP_
3 changes: 2 additions & 1 deletion include/rfl/ubjson/read.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <jsoncons_ext/ubjson/decode_ubjson.hpp>
#include <string>

#include "../concepts.hpp"
#include "../Processors.hpp"
#include "../internal/wrap_in_rfl_array_t.hpp"
#include "Parser.hpp"
Expand All @@ -19,7 +20,7 @@ using InputVarType = typename Reader::InputVarType;

/// Parses an object from UBJSON using reflection.
template <class T, class... Ps>
Result<internal::wrap_in_rfl_array_t<T>> read(const std::vector<char>& _bytes) {
Result<internal::wrap_in_rfl_array_t<T>> 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<jsoncons::json>(_bytes);
Expand Down
90 changes: 90 additions & 0 deletions tests/cbor/test_read_byte_containers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include <algorithm>
#include <array>
#include <rfl/cbor.hpp>

#include <gtest/gtest.h>

// Make sure things still compile when
// rfl.hpp is included after rfl/cbor.hpp.
#include <rfl.hpp>

namespace test_read_byte_containers
{

struct TestBox
{
int length;
int width;
int height;
};

// TODO: Apparently the jsoncons trait is_byte_sequence<T> is not working for std::span.
// TEST(cbor, test_read_from_char_span)
// {

// TestBox b = {
// .length = 1,
// .width = 2,
// .height = 3,
// };


// std::vector<char> rfl_buffer = rfl::cbor::write(s);

// // I don't want to be forced to copy to a std::vector<char> if that's not what data strucure I use.
// // So, allow reading from a std::span
// std::span<char> span(rfl_buffer.data(), rfl_buffer.size());

// auto result = rfl::cbor::read<TestBox>(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<char> rfl_buffer = rfl::cbor::write(b);

std::array<std::byte, 64> my_buffer;
std::transform(rfl_buffer.begin(), rfl_buffer.end(), my_buffer.begin(),
[](char c) { return static_cast<std::byte>(c); });

std::basic_string_view<std::byte> byte_view(my_buffer.data(), rfl_buffer.size());

auto result = rfl::cbor::read<TestBox>(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<char> rfl_buffer = rfl::cbor::write(b);

std::array<std::uint8_t, 64> my_buffer;
std::transform(rfl_buffer.begin(), rfl_buffer.end(), my_buffer.begin(),
[](char c) { return static_cast<std::uint8_t>(c); });

auto result = rfl::cbor::read<TestBox>(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
54 changes: 54 additions & 0 deletions tests/generic/test_byte_containers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#include <vector>
#include <array>
#include <string>
#include <cassert>
#include <rfl/concepts.hpp>

#include "write_and_read.hpp"

namespace test_byte_container_concepts {

TEST(generic, test_byte_container_concepts) {
// Test various container types
std::vector<std::uint8_t> vec_uint8{1, 2, 3, 4};
std::vector<char> vec_char{'a', 'b', 'c'};
std::array<std::uint8_t, 4> arr_uint8{1, 2, 3, 4};
std::string str_data{"hello"};
std::string_view str_view{"world"};
std::array<std::byte, 5> arr_byte{std::byte{5}, std::byte{4}, std::byte{3}, std::byte{2}, std::byte{1}};
std::span<std::byte> byte_span = arr_byte;

// Test with concepts
static_assert(rfl::ByteLike<std::uint8_t>);
static_assert(rfl::ByteLike<char>);
static_assert(rfl::ByteLike<unsigned char>);
static_assert(rfl::ByteLike<std::byte>);
static_assert(!rfl::ByteLike<int>);
static_assert(!rfl::ByteLike<float>);

static_assert(rfl::ContiguousByteContainer<decltype(vec_uint8)>);
static_assert(rfl::ContiguousByteContainer<decltype(vec_char)>);
static_assert(rfl::ContiguousByteContainer<decltype(arr_uint8)>);
static_assert(rfl::ContiguousByteContainer<decltype(str_data)>);
static_assert(rfl::ContiguousByteContainer<decltype(str_view)>);
static_assert(rfl::ContiguousByteContainer<decltype(arr_byte)>);
static_assert(rfl::ContiguousByteContainer<decltype(byte_span)>);

static_assert(rfl::MutableContiguousByteContainer<decltype(vec_uint8)>);
static_assert(rfl::MutableContiguousByteContainer<decltype(vec_char)>);
static_assert(rfl::MutableContiguousByteContainer<decltype(arr_uint8)>);
static_assert(rfl::MutableContiguousByteContainer<decltype(str_data)>);
static_assert(!rfl::MutableContiguousByteContainer<decltype(str_view)>);
static_assert(rfl::MutableContiguousByteContainer<decltype(arr_byte)>);
static_assert(rfl::MutableContiguousByteContainer<decltype(byte_span)>);

static_assert(rfl::BackInsertableByteContainer<decltype(vec_uint8)>);
static_assert(rfl::BackInsertableByteContainer<decltype(vec_char)>);
static_assert(!rfl::BackInsertableByteContainer<decltype(arr_uint8)>);
static_assert(rfl::BackInsertableByteContainer<decltype(str_data)>);
static_assert(!rfl::BackInsertableByteContainer<decltype(str_view)>);
static_assert(!rfl::BackInsertableByteContainer<decltype(arr_byte)>);
static_assert(!rfl::BackInsertableByteContainer<decltype(byte_span)>);
}

} // namespace test_byte_container_concepts
62 changes: 62 additions & 0 deletions tests/ubjson/test_read_byte_containers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#include <algorithm>
#include <array>
#include <rfl/ubjson.hpp>

#include <gtest/gtest.h>

// Make sure things still compile when
// rfl.hpp is included after rfl/ubjson.hpp.
#include <rfl.hpp>

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<char> rfl_buffer = rfl::ubjson::write(b);

std::array<std::byte, 64> my_buffer;
std::transform(rfl_buffer.begin(), rfl_buffer.end(), my_buffer.begin(),
[](char c) { return static_cast<std::byte>(c); });

std::basic_string_view<std::byte> byte_view(my_buffer.data(), rfl_buffer.size());

auto result = rfl::ubjson::read<TestBall>(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<char> rfl_buffer = rfl::ubjson::write(b);

std::array<std::uint8_t, 64> my_buffer;
std::transform(rfl_buffer.begin(), rfl_buffer.end(), my_buffer.begin(),
[](char c) { return static_cast<std::uint8_t>(c); });

auto result = rfl::ubjson::read<TestBall>(my_buffer);
EXPECT_TRUE(result);
EXPECT_EQ(result->radius, 4.5f);
EXPECT_EQ(result->mass, 5.5f);
}

} // namespace test_read_byte_containers
Loading