Skip to content

Commit d910557

Browse files
authored
Merge pull request #201 from anarthal/feature/type-erased-response
Adds a type-erased response adapter to the public API
2 parents 26197e8 + 5089eef commit d910557

File tree

10 files changed

+218
-14
lines changed

10 files changed

+218
-14
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/* Copyright (c) 2018-2023 Marcelo Zimbres Silva ([email protected])
2+
*
3+
* Distributed under the Boost Software License, Version 1.0. (See
4+
* accompanying file LICENSE.txt)
5+
*/
6+
7+
#ifndef BOOST_REDIS_ANY_ADAPTER_HPP
8+
#define BOOST_REDIS_ANY_ADAPTER_HPP
9+
10+
11+
#include <boost/redis/resp3/node.hpp>
12+
#include <boost/redis/adapter/adapt.hpp>
13+
#include <boost/system/error_code.hpp>
14+
#include <cstddef>
15+
#include <functional>
16+
#include <string_view>
17+
#include <type_traits>
18+
19+
namespace boost::redis {
20+
21+
namespace detail {
22+
23+
// Forward decl
24+
template <class Executor>
25+
class connection_base;
26+
27+
}
28+
29+
/** @brief A type-erased reference to a response.
30+
* @ingroup high-level-api
31+
*
32+
* A type-erased response adapter. It can be executed using @ref connection::async_exec.
33+
* Using this type instead of raw response references enables separate compilation.
34+
*
35+
* Given a response object `resp` that can be passed to `async_exec`, the following two
36+
* statements have the same effect:
37+
* ```
38+
* co_await conn.async_exec(req, resp);
39+
* co_await conn.async_exec(req, any_response(resp));
40+
* ```
41+
*/
42+
class any_adapter
43+
{
44+
using fn_type = std::function<void(std::size_t, resp3::basic_node<std::string_view> const&, system::error_code&)>;
45+
46+
struct impl_t {
47+
fn_type adapt_fn;
48+
std::size_t supported_response_size;
49+
} impl_;
50+
51+
template <class T>
52+
static auto create_impl(T& resp) -> impl_t
53+
{
54+
using namespace boost::redis::adapter;
55+
auto adapter = boost_redis_adapt(resp);
56+
std::size_t size = adapter.get_supported_response_size();
57+
return { std::move(adapter), size };
58+
}
59+
60+
template <class Executor>
61+
friend class detail::connection_base;
62+
63+
public:
64+
/**
65+
* @brief Constructor.
66+
*
67+
* Creates a type-erased response adapter from `resp` by calling
68+
* `boost_redis_adapt`. `T` must be a valid Redis response type.
69+
* Any type passed to @ref connection::async_exec qualifies.
70+
*
71+
* This object stores a reference to `resp`, which must be kept alive
72+
* while `*this` is being used.
73+
*/
74+
template <class T, class = std::enable_if_t<!std::is_same_v<T, any_adapter>>>
75+
explicit any_adapter(T& resp) : impl_(create_impl(resp)) {}
76+
};
77+
78+
}
79+
80+
#endif

include/boost/redis/connection.hpp

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#ifndef BOOST_REDIS_CONNECTION_HPP
88
#define BOOST_REDIS_CONNECTION_HPP
99

10+
#include <boost/redis/adapter/any_adapter.hpp>
1011
#include <boost/redis/detail/connection_base.hpp>
1112
#include <boost/redis/logger.hpp>
1213
#include <boost/redis/config.hpp>
@@ -17,7 +18,7 @@
1718
#include <boost/asio/any_completion_handler.hpp>
1819

1920
#include <chrono>
20-
#include <memory>
21+
#include <cstddef>
2122
#include <limits>
2223

2324
namespace boost::redis {
@@ -256,7 +257,22 @@ class basic_connection {
256257
Response& resp = ignore,
257258
CompletionToken&& token = CompletionToken{})
258259
{
259-
return impl_.async_exec(req, resp, std::forward<CompletionToken>(token));
260+
return impl_.async_exec(req, any_adapter(resp), std::forward<CompletionToken>(token));
261+
}
262+
263+
/** @copydoc async_exec
264+
*
265+
* @details This function uses the type-erased @ref any_adapter class, which
266+
* encapsulates a reference to a response object.
267+
*/
268+
template <class CompletionToken = asio::default_completion_token_t<executor_type>>
269+
auto
270+
async_exec(
271+
request const& req,
272+
any_adapter adapter,
273+
CompletionToken&& token = CompletionToken{})
274+
{
275+
return impl_.async_exec(req, std::move(adapter), std::forward<CompletionToken>(token));
260276
}
261277

262278
/** @brief Cancel operations.
@@ -392,9 +408,21 @@ class connection {
392408

393409
/// Calls `boost::redis::basic_connection::async_exec`.
394410
template <class Response, class CompletionToken>
395-
auto async_exec(request const& req, Response& resp, CompletionToken token)
411+
auto async_exec(request const& req, Response& resp, CompletionToken&& token)
396412
{
397-
return impl_.async_exec(req, resp, std::move(token));
413+
return async_exec(req, any_adapter(resp), std::forward<CompletionToken>(token));
414+
}
415+
416+
/// Calls `boost::redis::basic_connection::async_exec`.
417+
template <class CompletionToken>
418+
auto async_exec(request const& req, any_adapter adapter, CompletionToken&& token)
419+
{
420+
return asio::async_initiate<
421+
CompletionToken, void(boost::system::error_code, std::size_t)>(
422+
[](auto handler, connection* self, request const* req, any_adapter&& adapter)
423+
{
424+
self->async_exec_impl(*req, std::move(adapter), std::move(handler));
425+
}, token, this, &req, std::move(adapter));
398426
}
399427

400428
/// Calls `boost::redis::basic_connection::cancel`.
@@ -435,6 +463,12 @@ class connection {
435463
config const& cfg,
436464
logger l,
437465
asio::any_completion_handler<void(boost::system::error_code)> token);
466+
467+
void
468+
async_exec_impl(
469+
request const& req,
470+
any_adapter&& adapter,
471+
asio::any_completion_handler<void(boost::system::error_code, std::size_t)> token);
438472

439473
basic_connection<executor_type> impl_;
440474
};

include/boost/redis/detail/connection_base.hpp

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#ifndef BOOST_REDIS_CONNECTION_BASE_HPP
88
#define BOOST_REDIS_CONNECTION_BASE_HPP
99

10+
#include <boost/redis/adapter/any_adapter.hpp>
1011
#include <boost/redis/adapter/adapt.hpp>
1112
#include <boost/redis/detail/helper.hpp>
1213
#include <boost/redis/error.hpp>
@@ -30,15 +31,16 @@
3031
#include <boost/asio/read_until.hpp>
3132
#include <boost/asio/buffer.hpp>
3233
#include <boost/asio/experimental/channel.hpp>
34+
#include <boost/asio/associated_immediate_executor.hpp>
3335

3436
#include <algorithm>
3537
#include <array>
3638
#include <chrono>
3739
#include <deque>
3840
#include <memory>
3941
#include <string_view>
40-
#include <type_traits>
4142
#include <functional>
43+
#include <utility>
4244

4345
namespace boost::redis::detail
4446
{
@@ -121,7 +123,9 @@ struct exec_op {
121123
// be stablished.
122124
if (info_->req_->get_config().cancel_if_not_connected && !conn_->is_open()) {
123125
BOOST_ASIO_CORO_YIELD
124-
asio::post(std::move(self));
126+
asio::dispatch(
127+
asio::get_associated_immediate_executor(self, self.get_io_executor()),
128+
std::move(self));
125129
return self.complete(error::not_connected, 0);
126130
}
127131

@@ -440,14 +444,13 @@ class connection_base {
440444
cancel_impl(op);
441445
}
442446

443-
template <class Response, class CompletionToken>
444-
auto async_exec(request const& req, Response& resp, CompletionToken token)
447+
template <class CompletionToken>
448+
auto async_exec(request const& req, any_adapter&& adapter, CompletionToken&& token)
445449
{
446-
using namespace boost::redis::adapter;
447-
auto f = boost_redis_adapt(resp);
448-
BOOST_ASSERT_MSG(req.get_expected_responses() <= f.get_supported_response_size(), "Request and response have incompatible sizes.");
450+
auto& adapter_impl = adapter.impl_;
451+
BOOST_ASSERT_MSG(req.get_expected_responses() <= adapter_impl.supported_response_size, "Request and response have incompatible sizes.");
449452

450-
auto info = std::make_shared<req_info>(req, f, get_executor());
453+
auto info = std::make_shared<req_info>(req, std::move(adapter_impl.adapt_fn), get_executor());
451454

452455
return asio::async_compose
453456
< CompletionToken

include/boost/redis/detail/health_checker.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <boost/redis/request.hpp>
1212
#include <boost/redis/response.hpp>
1313
#include <boost/redis/operation.hpp>
14+
#include <boost/redis/adapter/any_adapter.hpp>
1415
#include <boost/redis/detail/helper.hpp>
1516
#include <boost/redis/config.hpp>
1617
#include <boost/asio/steady_timer.hpp>
@@ -44,7 +45,7 @@ class ping_op {
4445
}
4546

4647
BOOST_ASIO_CORO_YIELD
47-
conn_->async_exec(checker_->req_, checker_->resp_, std::move(self));
48+
conn_->async_exec(checker_->req_, any_adapter(checker_->resp_), std::move(self));
4849
if (ec || is_cancelled(self)) {
4950
logger_.trace("ping_op: error/cancelled (1).");
5051
checker_->wait_timer_.cancel();

include/boost/redis/detail/runner.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#ifndef BOOST_REDIS_RUNNER_HPP
88
#define BOOST_REDIS_RUNNER_HPP
99

10+
#include <boost/redis/adapter/any_adapter.hpp>
1011
#include <boost/redis/detail/health_checker.hpp>
1112
#include <boost/redis/config.hpp>
1213
#include <boost/redis/response.hpp>
@@ -47,7 +48,7 @@ struct hello_op {
4748
runner_->add_hello();
4849

4950
BOOST_ASIO_CORO_YIELD
50-
conn_->async_exec(runner_->hello_req_, runner_->hello_resp_, std::move(self));
51+
conn_->async_exec(runner_->hello_req_, any_adapter(runner_->hello_resp_), std::move(self));
5152
logger_.on_hello(ec, runner_->hello_resp_);
5253

5354
if (ec || runner_->has_error_in_response() || is_cancelled(self)) {

include/boost/redis/impl/connection.ipp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
#include <boost/redis/connection.hpp>
8+
#include <cstddef>
89

910
namespace boost::redis {
1011

@@ -31,6 +32,15 @@ connection::async_run_impl(
3132
impl_.async_run(cfg, l, std::move(token));
3233
}
3334

35+
void
36+
connection::async_exec_impl(
37+
request const& req,
38+
any_adapter&& adapter,
39+
asio::any_completion_handler<void(boost::system::error_code, std::size_t)> token)
40+
{
41+
impl_.async_exec(req, std::move(adapter), std::move(token));
42+
}
43+
3444
void connection::cancel(operation op)
3545
{
3646
impl_.cancel(op);

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ make_test(test_conn_exec_cancel 20)
4747
make_test(test_conn_exec_cancel2 20)
4848
make_test(test_conn_echo_stress 20)
4949
make_test(test_conn_run_cancel 20)
50+
make_test(test_any_adapter 17)
5051
make_test(test_issue_50 20)
5152
make_test(test_issue_181 17)
5253

test/Jamfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ local tests =
5151
test_low_level
5252
test_request
5353
test_run
54+
test_any_adapter
5455
;
5556

5657
# Build and run the tests

test/test_any_adapter.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva ([email protected])
2+
*
3+
* Distributed under the Boost Software License, Version 1.0. (See
4+
* accompanying file LICENSE.txt)
5+
*/
6+
7+
#include <boost/redis/ignore.hpp>
8+
#include <boost/redis/response.hpp>
9+
#include <boost/redis/adapter/any_adapter.hpp>
10+
#include <string>
11+
#define BOOST_TEST_MODULE any_adapter
12+
#include <boost/test/included/unit_test.hpp>
13+
14+
using boost::redis::generic_response;
15+
using boost::redis::response;
16+
using boost::redis::ignore;
17+
using boost::redis::any_adapter;
18+
19+
BOOST_AUTO_TEST_CASE(any_adapter_response_types)
20+
{
21+
// any_adapter can be used with any supported responses
22+
response<int> r1;
23+
response<int, std::string> r2;
24+
generic_response r3;
25+
26+
BOOST_CHECK_NO_THROW(any_adapter{r1});
27+
BOOST_CHECK_NO_THROW(any_adapter{r2});
28+
BOOST_CHECK_NO_THROW(any_adapter{r3});
29+
BOOST_CHECK_NO_THROW(any_adapter{ignore});
30+
}
31+
32+
BOOST_AUTO_TEST_CASE(any_adapter_copy_move)
33+
{
34+
// any_adapter can be copied/moved
35+
response<int, std::string> r;
36+
any_adapter ad1 {r};
37+
38+
// copy constructor
39+
any_adapter ad2 {ad1};
40+
41+
// move constructor
42+
any_adapter ad3 {std::move(ad2)};
43+
44+
// copy assignment
45+
BOOST_CHECK_NO_THROW(ad2 = ad1);
46+
47+
// move assignment
48+
BOOST_CHECK_NO_THROW(ad2 = std::move(ad1));
49+
}

test/test_conn_exec.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
* accompanying file LICENSE.txt)
55
*/
66

7+
#include <boost/redis/adapter/any_adapter.hpp>
78
#include <boost/redis/connection.hpp>
89
#include <boost/system/errc.hpp>
910
#include <boost/asio/detached.hpp>
11+
#include <string>
1012
#define BOOST_TEST_MODULE conn-exec
1113
#include <boost/test/included/unit_test.hpp>
1214
#include <iostream>
@@ -191,3 +193,25 @@ BOOST_AUTO_TEST_CASE(large_number_of_concurrent_requests_issue_170)
191193
BOOST_CHECK_EQUAL(counter, repeat);
192194
}
193195

196+
BOOST_AUTO_TEST_CASE(exec_any_adapter)
197+
{
198+
// Executing an any_adapter object works
199+
request req;
200+
req.push("PING", "PONG");
201+
response<std::string> res;
202+
203+
net::io_context ioc;
204+
205+
auto conn = std::make_shared<connection>(ioc);
206+
207+
conn->async_exec(req, boost::redis::any_adapter(res), [&](auto ec, auto){
208+
BOOST_TEST(!ec);
209+
conn->cancel();
210+
});
211+
212+
run(conn);
213+
ioc.run();
214+
215+
BOOST_TEST(std::get<0>(res).value() == "PONG");
216+
}
217+

0 commit comments

Comments
 (0)