diff --git a/include/boost/redis/request.hpp b/include/boost/redis/request.hpp index ca6f9952..fc3160c0 100644 --- a/include/boost/redis/request.hpp +++ b/include/boost/redis/request.hpp @@ -10,7 +10,6 @@ #include #include -#include #include #include @@ -34,11 +33,9 @@ struct request_access; * * @code * request r; - * r.push("HELLO", 3); - * r.push("FLUSHALL"); - * r.push("PING"); - * r.push("PING", "key"); - * r.push("QUIT"); + * r.push("SET", "k1", "some_value"); + * r.push("SET", "k2", "other_value"); + * r.push("GET", "k3"); * @endcode * * Uses a `std::string` for internal storage. @@ -146,14 +143,14 @@ public: * * @code * request req; - * req.push("SET", "key", "some string", "EX", "2"); + * req.push("SET", "key", "some string", "EX", 2); * @endcode * * This will add a `SET` command with value `"some string"` and an * expiration of 2 seconds. * - * Command arguments should either be convertible to `std::string_view` - * or support the `boost_redis_to_bulk` function. + * Command arguments should either be convertible to `std::string_view`, + * integral types, or support the `boost_redis_to_bulk` function. * This function is a customization point that must be made available * using ADL and must have the following signature: * @@ -165,7 +162,7 @@ public: * See cpp20_serialization.cpp * * @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`. - * @param args Command arguments. Non-string types will be converted to string by calling `boost_redis_to_bulk` on each argument. + * @param args Command arguments. `args` is allowed to be empty. * @tparam Ts Types of the command arguments. * */ @@ -196,21 +193,36 @@ public: * req.push_range("HSET", "key", map.cbegin(), map.cend()); * @endcode * - * Command arguments should either be convertible to `std::string_view` - * or support the `boost_redis_to_bulk` function. - * This function is a customization point that must be made available - * using ADL and must have the following signature: + * This will generate the following command: * * @code - * void boost_redis_to_bulk(std::string& to, T const& t); + * HSET key key1 value1 key2 value2 key3 value3 * @endcode - * + * + * *If the passed range is empty, no command is added* and this + * function becomes a no-op. + * + * The value type of the passed range should satisfy one of the following: + * + * @li The type is convertible to `std::string_view`. One argument is added + * per element in the range. + * @li The type is an integral type. One argument is added + * per element in the range. + * @li The type supports the `boost_redis_to_bulk` function. One argument is added + * per element in the range. This function is a customization point that must be made available + * using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`. + * @li The type is a `std::pair` instantiation, with both arguments supporting one of + * the points above. Two arguments are added per element in the range. + * Nested pairs are not allowed. + * @li The type is a `std::tuple` instantiation, with every argument supporting + * one of the points above. N arguments are added per element in the range, + * with N being the tuple size. Nested tuples are not allowed. + * * @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`. * @param key The command key. It will be added as the first argument to the command. * @param begin Iterator to the begin of the range. * @param end Iterator to the end of the range. - * @tparam ForwardIterator A forward iterator with an element type that is convertible to `std::string_view` - * or supports `boost_redis_to_bulk`. + * @tparam ForwardIterator A forward iterator with an element type that supports one of the points above. * * See cpp20_serialization.cpp */ @@ -249,23 +261,38 @@ public: * { "channel1" , "channel2" , "channel3" }; * * request req; - * req.push("SUBSCRIBE", std::cbegin(channels), std::cend(channels)); + * req.push("SUBSCRIBE", channels.cbegin(), channels.cend()); * @endcode * - * Command arguments should either be convertible to `std::string_view` - * or support the `boost_redis_to_bulk` function. - * This function is a customization point that must be made available - * using ADL and must have the following signature: + * This will generate the following command: * * @code - * void boost_redis_to_bulk(std::string& to, T const& t); + * SUBSCRIBE channel1 channel2 channel3 * @endcode + * + * *If the passed range is empty, no command is added* and this + * function becomes a no-op. + * + * The value type of the passed range should satisfy one of the following: + * + * @li The type is convertible to `std::string_view`. One argument is added + * per element in the range. + * @li The type is an integral type. One argument is added + * per element in the range. + * @li The type supports the `boost_redis_to_bulk` function. One argument is added + * per element in the range. This function is a customization point that must be made available + * using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`. + * @li The type is a `std::pair` instantiation, with both arguments supporting one of + * the points above. Two arguments are added per element in the range. + * Nested pairs are not allowed. + * @li The type is a `std::tuple` instantiation, with every argument supporting + * one of the points above. N arguments are added per element in the range, + * with N being the tuple size. Nested tuples are not allowed. * * @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`. * @param begin Iterator to the begin of the range. * @param end Iterator to the end of the range. - * @tparam ForwardIterator A forward iterator with an element type that is convertible to `std::string_view` - * or supports `boost_redis_to_bulk`. + * @tparam ForwardIterator A forward iterator with an element type that supports one of the points above. * * See cpp20_serialization.cpp */ @@ -296,13 +323,31 @@ public: * * Equivalent to the overload taking a range of begin and end * iterators. + * + * *If the passed range is empty, no command is added* and this + * function becomes a no-op. + * + * The value type of the passed range should satisfy one of the following: + * + * @li The type is convertible to `std::string_view`. One argument is added + * per element in the range. + * @li The type is an integral type. One argument is added + * per element in the range. + * @li The type supports the `boost_redis_to_bulk` function. One argument is added + * per element in the range. This function is a customization point that must be made available + * using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`. + * @li The type is a `std::pair` instantiation, with both arguments supporting one of + * the points above. Two arguments are added per element in the range. + * Nested pairs are not allowed. + * @li The type is a `std::tuple` instantiation, with every argument supporting + * one of the points above. N arguments are added per element in the range, + * with N being the tuple size. Nested tuples are not allowed. * * @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`. * @param key The command key. It will be added as the first argument to the command. * @param range Range containing the command arguments. * @tparam Range A type that can be passed to `std::begin()` and `std::end()` to obtain - * iterators. The range elements should be convertible to `std::string_view` - * or support `boost_redis_to_bulk`. + * iterators. */ template void push_range( @@ -320,12 +365,30 @@ public: * * Equivalent to the overload taking a range of begin and end * iterators. + * + * *If the passed range is empty, no command is added* and this + * function becomes a no-op. + * + * The value type of the passed range should satisfy one of the following: + * + * @li The type is convertible to `std::string_view`. One argument is added + * per element in the range. + * @li The type is an integral type. One argument is added + * per element in the range. + * @li The type supports the `boost_redis_to_bulk` function. One argument is added + * per element in the range. This function is a customization point that must be made available + * using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`. + * @li The type is a `std::pair` instantiation, with both arguments supporting one of + * the points above. Two arguments are added per element in the range. + * Nested pairs are not allowed. + * @li The type is a `std::tuple` instantiation, with every argument supporting + * one of the points above. N arguments are added per element in the range, + * with N being the tuple size. Nested tuples are not allowed. * * @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`. * @param range Range containing the command arguments. * @tparam Range A type that can be passed to `std::begin()` and `std::end()` to obtain - * iterators. The range elements should be convertible to `std::string_view` - * or support `boost_redis_to_bulk`. + * iterators. */ template void push_range( diff --git a/include/boost/redis/resp3/serialization.hpp b/include/boost/redis/resp3/serialization.hpp index 72de1b28..28835925 100644 --- a/include/boost/redis/resp3/serialization.hpp +++ b/include/boost/redis/resp3/serialization.hpp @@ -16,9 +16,6 @@ #include #include -// NOTE: Consider detecting tuples in the type in the parameter pack -// to calculate the header size correctly. - namespace boost::redis::resp3 { /** @brief Adds a bulk to the request. @@ -35,6 +32,10 @@ namespace boost::redis::resp3 { * } * @endcode * + * The function must add exactly one bulk string RESP3 node. + * If you're using `boost_redis_to_bulk` with a string argument, + * you're safe. + * * @param payload Storage on which data will be copied into. * @param data Data that will be serialized and stored in `payload`. */ @@ -100,6 +101,11 @@ struct bulk_counter> { static constexpr auto size = 2U; }; +template +struct bulk_counter> { + static constexpr auto size = sizeof...(T); +}; + void add_blob(std::string& payload, std::string_view blob); void add_separator(std::string& payload); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a7ab3518..d73055a3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,6 +35,7 @@ endmacro() # Unit tests make_test(test_low_level) make_test(test_request) +make_test(test_serialization) make_test(test_low_level_sync_sans_io) make_test(test_any_adapter) make_test(test_log_to_file) diff --git a/test/Jamfile b/test/Jamfile index b6676bf6..5ed7d798 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -52,6 +52,7 @@ lib redis_test_common local tests = test_low_level test_request + test_serialization test_low_level_sync_sans_io test_any_adapter test_log_to_file diff --git a/test/test_request.cpp b/test/test_request.cpp index 4be2425e..63b59ca3 100644 --- a/test/test_request.cpp +++ b/test/test_request.cpp @@ -14,8 +14,6 @@ using boost::redis::request; -// TODO: Serialization. - namespace { void test_push_no_args() diff --git a/test/test_serialization.cpp b/test/test_serialization.cpp new file mode 100644 index 00000000..18185ac4 --- /dev/null +++ b/test/test_serialization.cpp @@ -0,0 +1,193 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com), +// Ruben Perez Hidalgo (rubenperez038 at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +using boost::redis::request; + +namespace other { + +struct my_struct { + int value; +}; + +void boost_redis_to_bulk(std::string& to, my_struct value) +{ + boost::redis::resp3::boost_redis_to_bulk(to, value.value); +} + +} // namespace other + +namespace { + +// --- Strings --- +void test_string_view() +{ + request req; + req.push("GET", std::string_view("key")); + BOOST_TEST_EQ(req.payload(), "*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n"); +} + +void test_string() +{ + std::string s{"k1"}; + const std::string s2{"k2"}; + request req; + req.push("GET", s, s2, std::string("k3")); + BOOST_TEST_EQ(req.payload(), "*4\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\nk2\r\n$2\r\nk3\r\n"); +} + +void test_c_string() +{ + request req; + req.push("GET", "k1", static_cast("k2")); + BOOST_TEST_EQ(req.payload(), "*3\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\nk2\r\n"); +} + +void test_string_empty() +{ + request req; + req.push("GET", std::string_view{}); + BOOST_TEST_EQ(req.payload(), "*2\r\n$3\r\nGET\r\n$0\r\n\r\n"); +} + +// --- Integers --- +void test_signed_ints() +{ + request req; + req.push("GET", static_cast(20), static_cast(-42), -1, 80l, 200ll); + BOOST_TEST_EQ( + req.payload(), + "*6\r\n$3\r\nGET\r\n$2\r\n20\r\n$3\r\n-42\r\n$2\r\n-1\r\n$2\r\n80\r\n$3\r\n200\r\n"); +} + +void test_unsigned_ints() +{ + request req; + req.push( + "GET", + static_cast(20), + static_cast(42), + 50u, + 80ul, + 200ull); + BOOST_TEST_EQ( + req.payload(), + "*6\r\n$3\r\nGET\r\n$2\r\n20\r\n$2\r\n42\r\n$2\r\n50\r\n$2\r\n80\r\n$3\r\n200\r\n"); +} + +// We don't overflow for big ints +void test_signed_ints_minmax() +{ + using lims = std::numeric_limits; + request req; + req.push("GET", (lims::min)(), (lims::max)()); + BOOST_TEST_EQ( + req.payload(), + "*3\r\n$3\r\nGET\r\n$20\r\n-9223372036854775808\r\n$19\r\n9223372036854775807\r\n"); +} + +void test_unsigned_ints_max() +{ + request req; + req.push("GET", (std::numeric_limits::max)()); + BOOST_TEST_EQ(req.payload(), "*2\r\n$3\r\nGET\r\n$20\r\n18446744073709551615\r\n"); +} + +// Custom type +void test_custom() +{ + request req; + req.push("GET", other::my_struct{42}); + BOOST_TEST_EQ(req.payload(), "*2\r\n$3\r\nGET\r\n$2\r\n42\r\n"); +} + +// --- Pairs and tuples (only supported in the range versions) --- +// Nested structures are not supported (compile time error) +void test_pair() +{ + std::vector> vec{ + {"k1", 42} + }; + request req; + req.push_range("GET", vec); + BOOST_TEST_EQ(req.payload(), "*3\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\n42\r\n"); +} + +void test_pair_custom() +{ + std::vector> vec{ + {"k1", {42}} + }; + request req; + req.push_range("GET", vec); + BOOST_TEST_EQ(req.payload(), "*3\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\n42\r\n"); +} + +void test_tuple() +{ + std::vector> vec{ + {"k1", 42, 1} + }; + request req; + req.push_range("GET", vec); + BOOST_TEST_EQ(req.payload(), "*4\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\n42\r\n$1\r\n1\r\n"); +} + +void test_tuple_custom() +{ + std::vector> vec{ + {"k1", {42}} + }; + request req; + req.push_range("GET", vec); + BOOST_TEST_EQ(req.payload(), "*3\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\n42\r\n"); +} + +void test_tuple_empty() +{ + std::vector> vec{{}}; + request req; + req.push_range("GET", vec); + BOOST_TEST_EQ(req.payload(), "*1\r\n$3\r\nGET\r\n"); +} + +} // namespace + +int main() +{ + test_string_view(); + test_string(); + test_c_string(); + test_string_empty(); + + test_signed_ints(); + test_unsigned_ints(); + test_signed_ints_minmax(); + test_unsigned_ints_max(); + + test_custom(); + + test_pair(); + test_pair_custom(); + test_tuple(); + test_tuple_custom(); + test_tuple_empty(); + + return boost::report_errors(); +}