JSON-RPC 2.0
JSON-RPC 2.0 Modern C++ Library
Loading...
Searching...
No Matches
endpoint.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <atomic>
4#include <functional>
5#include <memory>
6#include <optional>
7#include <string>
8#include <unordered_map>
9
10#include <asio.hpp>
11#include <nlohmann/json.hpp>
12#include <spdlog/spdlog.h>
13
20
21namespace jsonrpc::endpoint {
22
25
27 public:
28 explicit RpcEndpoint(
29 asio::any_io_executor executor,
30 std::unique_ptr<transport::Transport> transport,
31 std::shared_ptr<spdlog::logger> logger = nullptr);
32
33 static auto CreateClient(
34 asio::any_io_executor executor,
35 std::unique_ptr<transport::Transport> transport)
36 -> asio::awaitable<std::expected<std::unique_ptr<RpcEndpoint>, RpcError>>;
37
38 RpcEndpoint(const RpcEndpoint &) = delete;
40 auto operator=(const RpcEndpoint &) -> RpcEndpoint & = delete;
41 auto operator=(RpcEndpoint &&) -> RpcEndpoint & = delete;
42
43 ~RpcEndpoint() = default;
44
45 auto Start() -> asio::awaitable<std::expected<void, RpcError>>;
46
47 auto WaitForShutdown() -> asio::awaitable<std::expected<void, RpcError>>;
48
49 auto Shutdown() -> asio::awaitable<std::expected<void, RpcError>>;
50
51 [[nodiscard]] auto IsRunning() const -> bool {
52 return is_running_.load();
53 }
54
55 auto Logger() -> std::shared_ptr<spdlog::logger> {
56 return logger_;
57 }
58
59 auto SendMethodCall(
60 std::string method, std::optional<nlohmann::json> params = std::nullopt)
61 -> asio::awaitable<std::expected<nlohmann::json, RpcError>>;
62
63 template <typename ParamsType, typename ResultType>
64 auto SendMethodCall(std::string method, ParamsType params)
65 -> asio::awaitable<std::expected<ResultType, RpcError>>
66 requires(
68
70 std::string method, std::optional<nlohmann::json> params = std::nullopt)
71 -> asio::awaitable<std::expected<void, RpcError>>;
72
73 template <typename ParamsType>
74 auto SendNotification(std::string method, ParamsType params)
75 -> asio::awaitable<std::expected<void, RpcError>>
77
79 std::string method, typename Dispatcher::MethodCallHandler handler);
80
81 template <typename ParamsType, typename ResultType>
83 std::string method,
84 std::function<asio::awaitable<ResultType>(ParamsType)> handler)
86
87 template <typename ParamsType, typename ResultType, typename ErrorType>
89 std::string method,
90 std::function<
91 asio::awaitable<std::expected<ResultType, ErrorType>>(ParamsType)>
92 handler)
94
96 std::string method, typename Dispatcher::NotificationHandler handler);
97
98 template <typename ParamsType, typename ErrorType>
100 std::string method,
101 std::function<asio::awaitable<std::expected<void, ErrorType>>(ParamsType)>
102 handler)
104
105 [[nodiscard]] auto HasPendingRequests() const -> bool;
106
107 private:
108 void StartMessageProcessing();
109
110 auto ProcessMessagesLoop(asio::cancellation_slot slot)
111 -> asio::awaitable<void>;
112
113 auto HandleMessage(std::string message)
114 -> asio::awaitable<std::expected<void, RpcError>>;
115
116 auto HandleResponse(Response response)
117 -> asio::awaitable<std::expected<void, RpcError>>;
118
119 auto GetNextRequestId() -> int64_t {
120 return next_request_id_++;
121 }
122
123 std::shared_ptr<spdlog::logger> logger_;
124
125 asio::any_io_executor executor_;
126
127 std::unique_ptr<transport::Transport> transport_;
128
129 Dispatcher dispatcher_;
130
131 std::unordered_map<int64_t, std::shared_ptr<PendingRequest>>
132 pending_requests_;
133
134 std::atomic<bool> is_running_{false};
135
136 asio::strand<asio::any_io_executor> endpoint_strand_;
137
138 std::atomic<int64_t> next_request_id_{0};
139
140 asio::cancellation_signal cancel_signal_;
141
142 asio::awaitable<void> message_loop_;
143};
144
145template <typename ParamsType, typename ResultType>
146auto RpcEndpoint::SendMethodCall(std::string method, ParamsType params)
147 -> asio::awaitable<std::expected<ResultType, RpcError>>
148 requires(
150{
151 nlohmann::json json_params;
152 try {
153 json_params = params;
154 } catch (const nlohmann::json::exception &ex) {
155 logger_->error(
156 "RpcEndpoint failed to convert parameters to JSON: {}", ex.what());
158 RpcErrorCode::kClientSerializationError,
159 "RpcEndpoint failed to convert parameters to JSON: " +
160 std::string(ex.what()));
161 }
162
163 auto result = co_await RpcEndpoint::SendMethodCall(method, json_params);
164 if (!result) {
165 co_return result;
166 }
167
168 try {
169 co_return result->template get<ResultType>();
170 } catch (const nlohmann::json::exception &ex) {
171 logger_->error("RpcEndpoint failed to convert result: {}", ex.what());
173 RpcErrorCode::kClientDeserializationError,
174 "RpcEndpoint failed to convert result: " + std::string(ex.what()));
175 }
176}
177
178template <typename ParamsType>
179auto RpcEndpoint::SendNotification(std::string method, ParamsType params)
180 -> asio::awaitable<std::expected<void, RpcError>>
182{
183 nlohmann::json json_params;
184 try {
185 json_params = params;
186 } catch (const nlohmann::json::exception &ex) {
187 logger_->error(
188 "RpcEndpoint failed to convert notification parameters: {}", ex.what());
190 RpcErrorCode::kClientSerializationError,
191 "RpcEndpoint failed to convert notification parameters: " +
192 std::string(ex.what()));
193 }
194
195 auto result = co_await RpcEndpoint::SendNotification(method, json_params);
196 if (!result) {
197 co_return result;
198 }
199
200 co_return Ok();
201}
202
203template <typename ParamsType, typename ResultType>
205 std::string method,
206 std::function<asio::awaitable<ResultType>(ParamsType)> handler)
208{
209 // Create a handler object and store its function object
210 // NOTE: We use a shared_ptr to ensure the handler object stays alive
211 // throughout the entire lifetime of any coroutine that might use it. This
212 // prevents the use-after-free issues that can occur with lambda captures in
213 // coroutines.
214 auto typed_handler =
215 std::make_shared<TypedMethodHandler<ParamsType, ResultType>>(
216 std::move(handler));
217
218 // Register a lambda that calls the handler object
219 RegisterMethodCall(
220 method,
221 [handler = std::move(typed_handler)](
222 std::optional<nlohmann::json> params) { return (*handler)(params); });
223}
224
225template <typename ParamsType, typename ResultType, typename ErrorType>
227 std::string method,
228 std::function<
229 asio::awaitable<std::expected<ResultType, ErrorType>>(ParamsType)>
230 handler)
232{
233 auto typed_handler =
234 std::make_shared<TypedMethodHandler<ParamsType, ResultType, ErrorType>>(
235 std::move(handler));
236
237 RegisterMethodCall(
238 method,
239 [handler = std::move(typed_handler)](
240 std::optional<nlohmann::json> params) { return (*handler)(params); });
241}
242
243template <typename ParamsType, typename ErrorType>
245 std::string method,
246 std::function<asio::awaitable<std::expected<void, ErrorType>>(ParamsType)>
247 handler)
249{
250 // Create a handler object and store its function object
251 // NOTE: Using a class-based approach with shared_ptr ownership guarantees
252 // the handler remains valid even when coroutines are suspended and resumed,
253 // which is safer than direct lambda captures that may go out of scope.
254 auto typed_handler =
255 std::make_shared<TypedNotificationHandler<ParamsType, ErrorType>>(
256 std::move(handler));
257
258 // Register a lambda that calls the handler object
259 RegisterNotification(
260 method,
261 [handler = std::move(typed_handler)](
262 std::optional<nlohmann::json> params) { return (*handler)(params); });
263}
264
265} // namespace jsonrpc::endpoint
std::function< asio::awaitable< nlohmann::json >( const std::optional< nlohmann::json > &)> MethodCallHandler
std::function< asio::awaitable< void >( const std::optional< nlohmann::json > &)> NotificationHandler
auto operator=(RpcEndpoint &&) -> RpcEndpoint &=delete
auto operator=(const RpcEndpoint &) -> RpcEndpoint &=delete
static auto CreateClient(asio::any_io_executor executor, std::unique_ptr< transport::Transport > transport) -> asio::awaitable< std::expected< std::unique_ptr< RpcEndpoint >, RpcError > >
Definition endpoint.cpp:27
RpcEndpoint(asio::any_io_executor executor, std::unique_ptr< transport::Transport > transport, std::shared_ptr< spdlog::logger > logger=nullptr)
Definition endpoint.cpp:16
auto HasPendingRequests() const -> bool
Definition endpoint.cpp:174
void RegisterNotification(std::string method, typename Dispatcher::NotificationHandler handler)
Definition endpoint.cpp:169
auto Shutdown() -> asio::awaitable< std::expected< void, RpcError > >
Definition endpoint.cpp:78
RpcEndpoint(const RpcEndpoint &)=delete
void RegisterMethodCall(std::string method, typename Dispatcher::MethodCallHandler handler)
Definition endpoint.cpp:164
auto IsRunning() const -> bool
Definition endpoint.hpp:51
auto WaitForShutdown() -> asio::awaitable< std::expected< void, RpcError > >
Definition endpoint.cpp:64
RpcEndpoint(RpcEndpoint &&)=delete
auto SendMethodCall(std::string method, std::optional< nlohmann::json > params=std::nullopt) -> asio::awaitable< std::expected< nlohmann::json, RpcError > >
Definition endpoint.cpp:110
auto Start() -> asio::awaitable< std::expected< void, RpcError > >
Definition endpoint.cpp:42
auto SendNotification(std::string method, std::optional< nlohmann::json > params=std::nullopt) -> asio::awaitable< std::expected< void, RpcError > >
Definition endpoint.cpp:143
auto Logger() -> std::shared_ptr< spdlog::logger >
Definition endpoint.hpp:55
static auto UnexpectedFromCode(RpcErrorCode code, std::string message="") -> std::unexpected< RpcError >
Definition error.hpp:101
auto Ok() -> std::expected< void, RpcError >
Definition error.hpp:111
Type-safe handlers for JSON-RPC method calls and notifications.