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 spdlog::debug("RpcEndpoint sending typed method call: {}", method);
152 nlohmann::json json_params;
153 try {
154 json_params = params;
155 } catch (const nlohmann::json::exception &ex) {
156 spdlog::error(
157 "RpcEndpoint failed to convert parameters to JSON: {}", ex.what());
159 RpcErrorCode::kClientSerializationError,
160 "RpcEndpoint failed to convert parameters to JSON: " +
161 std::string(ex.what()));
162 }
163
164 auto result = co_await RpcEndpoint::SendMethodCall(method, json_params);
165 if (!result) {
166 co_return result;
167 }
168
169 try {
170 co_return result->template get<ResultType>();
171 } catch (const nlohmann::json::exception &ex) {
172 spdlog::error("RpcEndpoint failed to convert result: {}", ex.what());
174 RpcErrorCode::kClientDeserializationError,
175 "RpcEndpoint failed to convert result: " + std::string(ex.what()));
176 }
177}
178
179template <typename ParamsType>
180auto RpcEndpoint::SendNotification(std::string method, ParamsType params)
181 -> asio::awaitable<std::expected<void, RpcError>>
183{
184 spdlog::debug("RpcEndpoint sending typed notification: {}", method);
185 nlohmann::json json_params;
186 try {
187 json_params = params;
188 } catch (const nlohmann::json::exception &ex) {
189 spdlog::error(
190 "RpcEndpoint failed to convert notification parameters: {}", ex.what());
192 RpcErrorCode::kClientSerializationError,
193 "RpcEndpoint failed to convert notification parameters: " +
194 std::string(ex.what()));
195 }
196
197 auto result = co_await RpcEndpoint::SendNotification(method, json_params);
198 if (!result) {
199 co_return result;
200 }
201
202 co_return Ok();
203}
204
205template <typename ParamsType, typename ResultType>
207 std::string method,
208 std::function<asio::awaitable<ResultType>(ParamsType)> handler)
210{
211 // Create a handler object and store its function object
212 // NOTE: We use a shared_ptr to ensure the handler object stays alive
213 // throughout the entire lifetime of any coroutine that might use it. This
214 // prevents the use-after-free issues that can occur with lambda captures in
215 // coroutines.
216 auto typed_handler =
217 std::make_shared<TypedMethodHandler<ParamsType, ResultType>>(
218 std::move(handler));
219
220 // Register a lambda that calls the handler object
221 RegisterMethodCall(
222 method,
223 [handler = std::move(typed_handler)](
224 std::optional<nlohmann::json> params) { return (*handler)(params); });
225}
226
227template <typename ParamsType, typename ResultType, typename ErrorType>
229 std::string method,
230 std::function<
231 asio::awaitable<std::expected<ResultType, ErrorType>>(ParamsType)>
232 handler)
234{
235 auto typed_handler =
236 std::make_shared<TypedMethodHandler<ParamsType, ResultType, ErrorType>>(
237 std::move(handler));
238
239 RegisterMethodCall(
240 method,
241 [handler = std::move(typed_handler)](
242 std::optional<nlohmann::json> params) { return (*handler)(params); });
243}
244
245template <typename ParamsType, typename ErrorType>
247 std::string method,
248 std::function<asio::awaitable<std::expected<void, ErrorType>>(ParamsType)>
249 handler)
251{
252 // Create a handler object and store its function object
253 // NOTE: Using a class-based approach with shared_ptr ownership guarantees
254 // the handler remains valid even when coroutines are suspended and resumed,
255 // which is safer than direct lambda captures that may go out of scope.
256 auto typed_handler =
257 std::make_shared<TypedNotificationHandler<ParamsType, ErrorType>>(
258 std::move(handler));
259
260 // Register a lambda that calls the handler object
261 RegisterNotification(
262 method,
263 [handler = std::move(typed_handler)](
264 std::optional<nlohmann::json> params) { return (*handler)(params); });
265}
266
267} // 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.