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:
committed by
GitHub
parent
755d14a10d
commit
6005ebd04a
@@ -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
|
||||
*/
|
||||
@@ -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 <class Range>
|
||||
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 <class Range>
|
||||
void push_range(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
|
||||
using boost::redis::request;
|
||||
|
||||
// TODO: Serialization.
|
||||
|
||||
namespace {
|
||||
|
||||
void test_push_no_args()
|
||||
|
||||
193
test/test_serialization.cpp
Normal file
193
test/test_serialization.cpp
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user