diff --git a/README.md b/README.md index 5b71d904..e79474db 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ auto receiver(std::shared_ptr conn) -> asio::awaitable // The response must be consumed without suspending the // coroutine i.e. without the use of async operations. - for (auto const& elem : resp.value().get_view()) + for (auto const& elem : resp.value()) std::cout << elem.value << "\n"; std::cout << std::endl; diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index f8c6e191..ebfbd044 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -133,7 +133,7 @@ auto receiver(std::shared_ptr conn) -> asio::awaitable // The response must be consumed without suspending the // coroutine i.e. without the use of async operations. - for (auto const& elem : resp.value().get_view()) + for (auto const& elem : resp.value()) std::cout << elem.value << "\n"; std::cout << std::endl; diff --git a/example/cpp20_chat_room.cpp b/example/cpp20_chat_room.cpp index 2edb5cea..21aa0c3a 100644 --- a/example/cpp20_chat_room.cpp +++ b/example/cpp20_chat_room.cpp @@ -72,7 +72,7 @@ auto receiver(std::shared_ptr conn) -> awaitable // The response must be consumed without suspending the // coroutine i.e. without the use of async operations. - for (auto const& elem : resp.value().get_view()) + for (auto const& elem : resp.value()) std::cout << elem.value << "\n"; std::cout << std::endl; diff --git a/example/cpp20_subscriber.cpp b/example/cpp20_subscriber.cpp index b8802b03..513f90ac 100644 --- a/example/cpp20_subscriber.cpp +++ b/example/cpp20_subscriber.cpp @@ -72,7 +72,7 @@ auto receiver(std::shared_ptr conn) -> asio::awaitable // The response must be consumed without suspending the // coroutine i.e. without the use of async operations. - for (auto const& elem : resp.value().get_view()) + for (auto const& elem : resp.value()) std::cout << elem.value << "\n"; std::cout << std::endl; diff --git a/include/boost/redis/impl/flat_tree.ipp b/include/boost/redis/impl/flat_tree.ipp index 7ee1aac4..a891f6c8 100644 --- a/include/boost/redis/impl/flat_tree.ipp +++ b/include/boost/redis/impl/flat_tree.ipp @@ -11,10 +11,12 @@ #include #include +#include #include #include #include +#include #include namespace boost::redis::resp3 { @@ -228,15 +230,19 @@ void flat_tree::notify_done() data_tmp_offset_ = data_.size; } +const node_view& flat_tree::at(std::size_t i) const +{ + if (i >= size()) + BOOST_THROW_EXCEPTION(std::out_of_range("flat_tree::at")); + return view_tree_[i]; +} + bool operator==(flat_tree const& a, flat_tree const& b) { // data is already taken into account by comparing the nodes. // Only committed nodes should be taken into account. - auto a_nodes = a.get_view(); - auto b_nodes = b.get_view(); - return a_nodes.size() == b_nodes.size() && - std::equal(a_nodes.begin(), a_nodes.end(), b_nodes.begin()) && - a.total_msgs_ == b.total_msgs_; + return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin()) && + a.get_total_msgs() == b.get_total_msgs(); } } // namespace boost::redis::resp3 diff --git a/include/boost/redis/resp3/flat_tree.hpp b/include/boost/redis/resp3/flat_tree.hpp index 4c6158c5..2783e9ad 100644 --- a/include/boost/redis/resp3/flat_tree.hpp +++ b/include/boost/redis/resp3/flat_tree.hpp @@ -15,6 +15,7 @@ #include #include +#include #include namespace boost::redis { @@ -45,8 +46,12 @@ struct flat_buffer { * to obtain how many responses this object contains. * * Objects are typically created by the user and passed to @ref connection::async_exec - * to be used as response containers. Call @ref get_view to access the actual RESP3 nodes. - * Once populated, `flat_tree` can't be modified, except for @ref clear and assignment. + * to be used as response containers. Once populated, they can be used as a const range + * of @ref resp3::node_view objects. The usual random access range methods (like @ref at, @ref size or + * @ref front) are provided. Once populated, `flat_tree` can't be modified, + * except for @ref clear and assignment. + * + * `flat_tree` models `std::ranges::contiguous_range`. * * A `flat_tree` is conceptually similar to a pair of `std::vector` objects, one holding * @ref resp3::node_view objects, and another owning the the string data that these views @@ -54,6 +59,22 @@ struct flat_buffer { */ class flat_tree { public: + /** + * @brief The type of the iterators returned by @ref begin and @ref end. + * + * It is guaranteed to be a contiguous iterator. While this is currently a pointer, + * users shouldn't rely on this fact, as the exact implementation may change between releases. + */ + using iterator = const node_view*; + + /** + * @brief The type of the iterators returned by @ref rbegin and @ref rend. + * + * As with @ref iterator, users should treat this type as an unspecified + * contiguous iterator type rather than assuming a specific type. + */ + using reverse_iterator = std::reverse_iterator; + /** * @brief Default constructor. * @@ -70,7 +91,7 @@ public: * Constructs a tree by taking ownership of the nodes in `other`. * * @par Object lifetimes - * References to the nodes and strings in `other` remain valid. + * Iterators, pointers and references to the nodes and strings in `other` remain valid. * * @par Exception safety * No-throw guarantee. @@ -95,8 +116,8 @@ public: * `other` is left in a valid but unspecified state. * * @par Object lifetimes - * References to the nodes and strings in `other` remain valid. - * References to the nodes and strings in `*this` are invalidated. + * Iterators, pointers and references to the nodes and strings in `other` remain valid. + * Iterators, pointers and references to the nodes and strings in `*this` are invalidated. * * @par Exception safety * No-throw guarantee. @@ -110,16 +131,137 @@ public: * After the copy, `*this` and `other` have independent lifetimes (usual copy semantics). * * @par Object lifetimes - * References to the nodes and strings in `*this` are invalidated. + * Iterators, pointers and references to the nodes and strings in `*this` are invalidated. * * @par Exception safety * Basic guarantee. Memory allocations might throw. */ flat_tree& operator=(const flat_tree& other); - friend bool operator==(flat_tree const&, flat_tree const&); + /** + * @brief Returns an iterator to the first element of the node range. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns An iterator to the first node. + */ + iterator begin() const noexcept { return data(); } - friend bool operator!=(flat_tree const&, flat_tree const&); + /** + * @brief Returns an iterator past the last element in the node range. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns An iterator past the last element in the node range. + */ + iterator end() const noexcept { return data() + size(); } + + /** + * @brief Returns an iterator to the first element of the reversed node range. + * + * Allows iterating the range of nodes in reverse order. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns An iterator to the first node of the reversed range. + */ + reverse_iterator rbegin() const noexcept { return reverse_iterator{end()}; } + + /** + * @brief Returns an iterator past the last element of the reversed node range. + * + * Allows iterating the range of nodes in reverse order. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns An iterator past the last element of the reversed node range. + */ + reverse_iterator rend() const noexcept { return reverse_iterator{begin()}; } + + /** + * @brief Returns a reference to the node at the specified position (checked access). + * + * @par Exception safety + * Strong guarantee. Throws `std::out_of_range` if `i >= size()`. + * + * @param i Position of the node to return. + * @returns A reference to the node at position `i`. + */ + const node_view& at(std::size_t i) const; + + /** + * @brief Returns a reference to the node at the specified position (unchecked access). + * + * @par Precondition + * `i < size()`. + * + * @par Exception safety + * No-throw guarantee. + * + * @param i Position of the node to return. + * @returns A reference to the node at position `i`. + */ + const node_view& operator[](std::size_t i) const noexcept { return get_view()[i]; } + + /** + * @brief Returns a reference to the first node. + * + * @par Precondition + * `!empty()`. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns A reference to the first node. + */ + const node_view& front() const noexcept { return get_view().front(); } + + /** + * @brief Returns a reference to the last node. + * + * @par Precondition + * `!empty()`. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns A reference to the last node. + */ + const node_view& back() const noexcept { return get_view().back(); } + + /** + * @brief Returns a pointer to the underlying node storage. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns A pointer to the underlying node array. + */ + const node_view* data() const noexcept { return view_tree_.data(); } + + /** + * @brief Checks whether the tree is empty. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns `true` if the tree contains no nodes, `false` otherwise. + */ + bool empty() const noexcept { return size() == 0u; } + + /** + * @brief Returns the number of nodes in the tree. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns The number of nodes. + */ + std::size_t size() const noexcept { return node_tmp_offset_; } /** @brief Reserves capacity for incoming data. * @@ -142,7 +284,7 @@ public: /** @brief Clears the tree so it contains no nodes. * * Calling this function removes every node, making - * @ref get_view return empty and @ref get_total_msgs + * the range contain no nodes, and @ref get_total_msgs * return zero. It does not modify the object's capacity. * * To re-use a `flat_tree` for several requests, @@ -189,17 +331,6 @@ public: */ auto data_capacity() const noexcept -> std::size_t { return data_.capacity; } - /** @brief Returns a vector with the nodes in the tree. - * - * This is the main way to access the contents of the tree. - * - * @par Exception safety - * No-throw guarantee. - * - * @returns The nodes in the tree. - */ - span get_view() const noexcept { return {view_tree_.data(), node_tmp_offset_}; } - /** @brief Returns the number of memory reallocations that took place in the data buffer. * * This function returns how many reallocations in the data buffer were performed and @@ -226,6 +357,7 @@ public: private: template friend class adapter::detail::general_aggregate; + span get_view() const noexcept { return {data(), size()}; } void notify_init(); void notify_done(); diff --git a/test/test_conn_exec.cpp b/test/test_conn_exec.cpp index 22216f9b..715d844f 100644 --- a/test/test_conn_exec.cpp +++ b/test/test_conn_exec.cpp @@ -205,7 +205,7 @@ BOOST_AUTO_TEST_CASE(exec_generic_flat_response) BOOST_TEST_REQUIRE(finished); BOOST_TEST(resp.has_value()); - BOOST_TEST(resp->get_view().front().value == "PONG"); + BOOST_TEST(resp.value().front().value == "PONG"); } } // namespace diff --git a/test/test_conn_push2.cpp b/test/test_conn_push2.cpp index ab188d0d..c76191dd 100644 --- a/test/test_conn_push2.cpp +++ b/test/test_conn_push2.cpp @@ -682,15 +682,15 @@ struct test_pubsub_state_restoration_impl { { // Checks for the expected subscriptions and patterns after restoration std::set seen_channels, seen_patterns; - for (auto it = resp_push.get_view().begin(); it != resp_push.get_view().end();) { + for (auto it = resp_push.begin(); it != resp_push.end();) { // The root element should be a push BOOST_TEST_EQ(it->data_type, type::push); BOOST_TEST_GE(it->aggregate_size, 2u); - BOOST_TEST(++it != resp_push.get_view().end()); + BOOST_TEST(++it != resp_push.end()); // The next element should be the message type std::string_view msg_type = it->value; - BOOST_TEST(++it != resp_push.get_view().end()); + BOOST_TEST(++it != resp_push.end()); // The next element is the channel or pattern if (msg_type == "subscribe") @@ -699,7 +699,7 @@ struct test_pubsub_state_restoration_impl { seen_patterns.insert(it->value); // Skip the rest of the nodes - while (it != resp_push.get_view().end() && it->depth != 0u) + while (it != resp_push.end() && it->depth != 0u) ++it; } diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index 87b7f804..bc8a6545 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -11,19 +11,29 @@ #include #include +#include // for a safe #include #include #include #include "print_node.hpp" #include +#include #include #include +#include +#include #include +#include #include #include #include +#if (__cpp_lib_ranges >= 201911L) && (__cpp_lib_concepts >= 202002L) +#define BOOST_REDIS_TEST_RANGE_CONCEPTS +#include +#endif + using boost::redis::adapter::adapt2; using boost::redis::adapter::result; using boost::redis::resp3::tree; @@ -70,11 +80,7 @@ void check_nodes( boost::span expected, boost::source_location loc = BOOST_CURRENT_LOCATION) { - if (!BOOST_TEST_ALL_EQ( - tree.get_view().begin(), - tree.get_view().end(), - expected.begin(), - expected.end())) + if (!BOOST_TEST_ALL_EQ(tree.begin(), tree.end(), expected.begin(), expected.end())) std::cerr << "Called from " << loc << std::endl; } @@ -1090,6 +1096,233 @@ void test_move_assign_tmp() BOOST_TEST_EQ(t.get_total_msgs(), 2u); } +// --- Iterators --- +// We can obtain iterators using begin() and end() and use them to iterate +void test_iterators() +{ + // Setup + flat_tree t; + add_nodes(t, "+node1\r\n"); + add_nodes(t, ":200\r\n"); + constexpr node_view node1{type::simple_string, 1u, 0u, "node1"}; + constexpr node_view node2{type::number, 1u, 0u, "200"}; + + // These methods are const + const auto& tconst = t; + auto it = tconst.begin(); + auto end = tconst.end(); + + // Iteration using iterators + BOOST_TEST_NE(it, end); + BOOST_TEST_EQ(*it, node1); + BOOST_TEST_NE(++it, end); + BOOST_TEST_EQ(*it, node2); + BOOST_TEST_EQ(++it, end); + + // Iteration using range for + std::vector nodes; + for (const auto& n : t) + nodes.push_back(n); + constexpr std::array expected_nodes{node1, node2}; + BOOST_TEST_ALL_EQ(nodes.begin(), nodes.end(), expected_nodes.begin(), expected_nodes.end()); +} + +// Empty ranges don't cause trouble +void test_iterators_empty() +{ + flat_tree t; + BOOST_TEST_EQ(t.begin(), t.end()); +} + +// Tmp area is not included in the range +// More or less tested with the add_nodes tests +void test_iterators_tmp() +{ + parser p; + flat_tree t; + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n")); + BOOST_TEST_EQ(t.begin(), t.end()); +} + +// The iterator should be contiguous +#ifdef BOOST_REDIS_TEST_RANGE_CONCEPTS +static_assert(std::contiguous_iterator); +#endif + +// --- Reverse iterators --- +// We can obtain iterators using rbegin() and rend() and use them to iterate +void test_reverse_iterators() +{ + // Setup + flat_tree t; + add_nodes(t, "+node1\r\n"); + add_nodes(t, ":200\r\n"); + + // These methods are const + const auto& tconst = t; + + constexpr node_view expected_nodes[] = { + {type::number, 1u, 0u, "200" }, + {type::simple_string, 1u, 0u, "node1"}, + }; + BOOST_TEST_ALL_EQ( + tconst.rbegin(), + tconst.rend(), + std::begin(expected_nodes), + std::end(expected_nodes)); +} + +// Empty ranges don't cause trouble +void test_reverse_iterators_empty() +{ + flat_tree t; + BOOST_TEST(t.rbegin() == t.rend()); +} + +// Tmp area is not included in the range +void test_reverse_iterators_tmp() +{ + parser p; + flat_tree t; + + // Add one full message and a partial one + add_nodes(t, "*1\r\n+node1\r\n"); + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n")); + + // Only the full message appears in the reversed range + constexpr node_view expected_nodes[] = { + {type::simple_string, 1u, 1u, "node1"}, + {type::array, 1u, 0u, "" }, + }; + BOOST_TEST_ALL_EQ(t.rbegin(), t.rend(), std::begin(expected_nodes), std::end(expected_nodes)); +} + +// --- at --- +void test_at() +{ + parser p; + flat_tree t; + + // Add one full message and a partial one + add_nodes(t, "*1\r\n+node1\r\n"); + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n")); + + // Nodes in the range can be accessed with at() + constexpr node_view n0{type::array, 1u, 0u, ""}; + constexpr node_view n1{type::simple_string, 1u, 1u, "node1"}; + BOOST_TEST_EQ(t.at(0u), n0); + BOOST_TEST_EQ(t.at(1u), n1); + + // Nodes in the tmp area are not considered in range + BOOST_TEST_THROWS(t.at(2u), std::out_of_range); + BOOST_TEST_THROWS(t.at(3u), std::out_of_range); + + // Indices out of range throw + BOOST_TEST_THROWS(t.at(4u), std::out_of_range); + BOOST_TEST_THROWS(t.at(5u), std::out_of_range); + BOOST_TEST_THROWS(t.at((std::numeric_limits::max)()), std::out_of_range); +} + +// Empty ranges don't cause trouble +void test_at_empty() +{ + flat_tree t; + BOOST_TEST_THROWS(t.at(0u), std::out_of_range); + BOOST_TEST_THROWS(t.at(2u), std::out_of_range); + BOOST_TEST_THROWS(t.at((std::numeric_limits::max)()), std::out_of_range); +} + +// --- operator[], front, back --- +void test_unchecked_access() +{ + flat_tree t; + add_nodes(t, "*2\r\n+node1\r\n+node2\r\n"); + + constexpr node_view n0{type::array, 2u, 0u, ""}; + constexpr node_view n1{type::simple_string, 1u, 1u, "node1"}; + constexpr node_view n2{type::simple_string, 1u, 1u, "node2"}; + + // operator [] + BOOST_TEST_EQ(t[0u], n0); + BOOST_TEST_EQ(t[1u], n1); + BOOST_TEST_EQ(t[2u], n2); + + // Front and back + BOOST_TEST_EQ(t.front(), n0); + BOOST_TEST_EQ(t.back(), n2); +} + +// --- data --- +void test_data() +{ + flat_tree t; + add_nodes(t, "*1\r\n+node1\r\n"); + + constexpr node_view expected_nodes[] = { + {type::array, 1u, 0u, "" }, + {type::simple_string, 1u, 1u, "node1"}, + }; + + BOOST_TEST_NE(t.data(), nullptr); + BOOST_TEST_ALL_EQ(t.data(), t.data() + 2u, std::begin(expected_nodes), std::end(expected_nodes)); +} + +// Empty ranges don't cause trouble +void test_data_empty() +{ + flat_tree t; + BOOST_TEST_EQ(t.data(), nullptr); +} + +// --- size and empty --- +void test_size() +{ + flat_tree t; + add_nodes(t, "*1\r\n+node1\r\n"); + + BOOST_TEST_EQ(t.size(), 2u); + BOOST_TEST_NOT(t.empty()); +} + +void test_size_empty() +{ + flat_tree t; + + BOOST_TEST_EQ(t.size(), 0u); + BOOST_TEST(t.empty()); +} + +// Tmp area not taken into account +void test_size_tmp() +{ + parser p; + flat_tree t; + + // Add one full message and a partial one + add_nodes(t, "*1\r\n+node1\r\n"); + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n")); + + BOOST_TEST_EQ(t.size(), 2u); + BOOST_TEST_NOT(t.empty()); +} + +void test_size_tmp_only() +{ + parser p; + flat_tree t; + + // Add one partial message + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n")); + + BOOST_TEST_EQ(t.size(), 0u); + BOOST_TEST(t.empty()); +} + +// The range should model contiguous range +#ifdef BOOST_REDIS_TEST_RANGE_CONCEPTS +static_assert(std::ranges::contiguous_range); +#endif + // --- Comparison --- void test_comparison_different() { @@ -1310,6 +1543,27 @@ int main() test_move_assign_both_empty(); test_move_assign_tmp(); + test_iterators(); + test_iterators_empty(); + test_iterators_tmp(); + + test_reverse_iterators(); + test_reverse_iterators_empty(); + test_reverse_iterators_tmp(); + + test_at(); + test_at_empty(); + + test_unchecked_access(); + + test_data(); + test_data_empty(); + + test_size(); + test_size_empty(); + test_size_tmp(); + test_size_tmp_only(); + test_comparison_different(); test_comparison_different_node_types(); test_comparison_equal(); diff --git a/test/test_generic_flat_response.cpp b/test/test_generic_flat_response.cpp index ff9e2b86..b667f5bd 100644 --- a/test/test_generic_flat_response.cpp +++ b/test/test_generic_flat_response.cpp @@ -36,11 +36,7 @@ void test_success() std::vector expected_nodes{ {type::simple_string, 1u, 0u, "hello"}, }; - BOOST_TEST_ALL_EQ( - resp->get_view().begin(), - resp->get_view().end(), - expected_nodes.begin(), - expected_nodes.end()); + BOOST_TEST_ALL_EQ(resp->begin(), resp->end(), expected_nodes.begin(), expected_nodes.end()); } // If an error of any kind appears, we set the overall result to error