2
0
mirror of https://github.com/boostorg/redis.git synced 2026-01-19 04:42:09 +00:00

Fixes std::tuple serialization and adds tests (#363)

* Fixes a problem that caused passing ranges containing tuples into `request::push_range` to generate invalid commands.
* Adds test_serialization
* Updates request reference docs to reflect the requirements of the types passed to push and push_range

close #360
This commit is contained in:
Anarthal (Rubén Pérez)
2025-12-02 11:20:01 +01:00
committed by GitHub
parent 755d14a10d
commit 6005ebd04a
6 changed files with 297 additions and 35 deletions

View File

@@ -10,7 +10,6 @@
#include <boost/redis/resp3/serialization.hpp>
#include <boost/redis/resp3/type.hpp>
#include <algorithm>
#include <string>
#include <tuple>
@@ -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
*/
@@ -297,12 +324,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 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 <class Range>
void push_range(
@@ -321,11 +366,29 @@ 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 <class Range>
void push_range(

View File

@@ -16,9 +16,6 @@
#include <string>
#include <tuple>
// 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<std::pair<T, U>> {
static constexpr auto size = 2U;
};
template <class... T>
struct bulk_counter<std::tuple<T...>> {
static constexpr auto size = sizeof...(T);
};
void add_blob(std::string& payload, std::string_view blob);
void add_separator(std::string& payload);

View File

@@ -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)

View File

@@ -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

View File

@@ -14,8 +14,6 @@
using boost::redis::request;
// TODO: Serialization.
namespace {
void test_push_no_args()

193
test/test_serialization.cpp Normal file
View File

@@ -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 <boost/redis/request.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/core/lightweight_test.hpp>
#include <cstdint>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
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<const char*>("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<signed char>(20), static_cast<short>(-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<unsigned char>(20),
static_cast<unsigned short>(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<std::int64_t>;
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<std::uint64_t>::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<std::pair<std::string_view, int>> 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<std::pair<std::string_view, other::my_struct>> 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<std::tuple<std::string_view, int, unsigned char>> 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<std::tuple<std::string_view, other::my_struct>> 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<std::tuple<>> 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();
}