From 6605bd3bb5717e815d85bc93a95dc800aef0f804 Mon Sep 17 00:00:00 2001 From: Dominik Tugend Date: Fri, 22 Jul 2022 17:47:20 +0200 Subject: [PATCH 1/2] Allow to send / receive nlohman::json objects - Closes jsonrpcx/json-rpc-cxx#35 - purpose: this way we have more possibilities, e.g. seriazling from / to streams. --- include/jsonrpccxx/batchclient.hpp | 22 ++++++-- include/jsonrpccxx/client.hpp | 47 +++++++++++++--- include/jsonrpccxx/common.hpp | 8 +++ include/jsonrpccxx/iclientjsonconnector.hpp | 14 +++++ include/jsonrpccxx/server.hpp | 59 ++++++++++++--------- test/client.cpp | 6 +++ 6 files changed, 117 insertions(+), 39 deletions(-) create mode 100644 include/jsonrpccxx/iclientjsonconnector.hpp diff --git a/include/jsonrpccxx/batchclient.hpp b/include/jsonrpccxx/batchclient.hpp index 727fe05..e0cec4b 100644 --- a/include/jsonrpccxx/batchclient.hpp +++ b/include/jsonrpccxx/batchclient.hpp @@ -83,19 +83,31 @@ namespace jsonrpccxx { std::vector nullIds; }; - class BatchClient : public JsonRpcClient { + template + class BatchClientBase : public TBase { public: - explicit BatchClient(IClientConnector &connector) : JsonRpcClient(connector, version::v2) {} + explicit BatchClientBase(TConnector &connector) : TBase(connector, version::v2) {} + BatchResponse BatchCall(const BatchRequest &request) { try { - json response = json::parse(connector.Send(request.Build().dump())); + json response = GetConnector().Send(request.Build()); if (!response.is_array()) { throw JsonRpcException(parse_error, std::string("invalid JSON response from server: expected array")); } return BatchResponse(std::move(response)); - } catch (json::parse_error &e) { - throw JsonRpcException(parse_error, std::string("invalid JSON response from server: ") + e.what()); + } catch(const json::parse_error & e) { + throw JsonRpcException::fromJsonParseError(e); } } }; + + class BatchClientJson : public BatchClientBase { + public: + explicit BatchClientJson(IClientJsonConnector &connector) : BatchClientBase(connector) {} + }; + + class BatchClient : public BatchClientBase { + public: + explicit BatchClient(IClientConnector &connector) : BatchClientBase(connector) {} + }; } diff --git a/include/jsonrpccxx/client.hpp b/include/jsonrpccxx/client.hpp index 3241d15..9a0b5b3 100644 --- a/include/jsonrpccxx/client.hpp +++ b/include/jsonrpccxx/client.hpp @@ -1,5 +1,6 @@ #pragma once #include "common.hpp" +#include "iclientjsonconnector.hpp" #include "iclientconnector.hpp" #include #include @@ -17,10 +18,10 @@ namespace jsonrpccxx { json result; }; - class JsonRpcClient { + class JsonRpcClientBase { public: - JsonRpcClient(IClientConnector &connector, version v) : connector(connector), v(v) {} - virtual ~JsonRpcClient() = default; + JsonRpcClientBase(version v) : v(v) {} + virtual ~JsonRpcClientBase() = default; template T CallMethod(const id_type &id, const std::string &name) { return call_method(id, name, json::object()).result.get(); } @@ -33,7 +34,7 @@ namespace jsonrpccxx { void CallNotificationNamed(const std::string &name, const named_parameter ¶ms = {}) { call_notification(name, params); } protected: - IClientConnector &connector; + virtual IClientJsonConnector &GetConnector() = 0; private: version v; @@ -56,7 +57,7 @@ namespace jsonrpccxx { j["params"] = nullptr; } try { - json response = json::parse(connector.Send(j.dump())); + json response = GetConnector().Send(j); if (has_key_type(response, "error", json::value_t::object)) { throw JsonRpcException::fromJson(response["error"]); } else if (has_key_type(response, "error", json::value_t::string)) { @@ -69,8 +70,8 @@ namespace jsonrpccxx { return JsonRpcResponse{response["id"].get(), response["result"].get()}; } throw JsonRpcException(internal_error, R"(invalid server response: neither "result" nor "error" fields found)"); - } catch (json::parse_error &e) { - throw JsonRpcException(parse_error, std::string("invalid JSON response from server: ") + e.what()); + } catch (const json::parse_error &e) { + throw JsonRpcException::fromJsonParseError(e); } } @@ -86,7 +87,37 @@ namespace jsonrpccxx { } else if (v == version::v1) { j["params"] = nullptr; } - connector.Send(j.dump()); + GetConnector().Send(j); } }; + + class JsonRpcClientJson + : public JsonRpcClientBase { + public: + JsonRpcClientJson(IClientJsonConnector &connector, version v) : JsonRpcClientBase(v), connector(connector) {} + + protected: + virtual IClientJsonConnector &GetConnector() override { return connector; } + + private: + IClientJsonConnector &connector; + }; + + class JsonRpcClient + : public JsonRpcClientBase + , private IClientJsonConnector { + public: + JsonRpcClient(IClientConnector &connector, version v) : JsonRpcClientBase(v), connector(connector) {} + + protected: + IClientConnector &connector; + virtual IClientJsonConnector &GetConnector() override { return *this; } + + private: + + virtual json IClientJsonConnector::Send(const json &request) { + return json::parse(connector.Send(request.dump())); + } + }; + } // namespace jsonrpccxx diff --git a/include/jsonrpccxx/common.hpp b/include/jsonrpccxx/common.hpp index f269b4d..1087583 100644 --- a/include/jsonrpccxx/common.hpp +++ b/include/jsonrpccxx/common.hpp @@ -59,10 +59,18 @@ namespace jsonrpccxx { return JsonRpcException(internal_error, R"(invalid error response: "code" (negative number) and "message" (string) are required)"); } + static inline JsonRpcException fromJsonParseError(const json::parse_error &value) { + return JsonRpcException(parse_error, std::string("invalid JSON response from server: ") + value.what()); + } + private: int code; std::string message; json data; std::string err; }; + + static inline json create_parse_error_response_v2(const json::parse_error &e) { + return json{{"id", nullptr}, {"error", {{"code", parse_error}, {"message", std::string("parse error: ") + e.what()}}}, {"jsonrpc", "2.0"}}; + } } // namespace jsonrpccxx diff --git a/include/jsonrpccxx/iclientjsonconnector.hpp b/include/jsonrpccxx/iclientjsonconnector.hpp new file mode 100644 index 0000000..676820f --- /dev/null +++ b/include/jsonrpccxx/iclientjsonconnector.hpp @@ -0,0 +1,14 @@ +#pragma once +#include "common.hpp" + +namespace jsonrpccxx { + class IClientJsonConnector { + public: + virtual ~IClientJsonConnector() = default; + + /*** + * @throws json::parse_error In case repsonse can not be parsed. + */ + virtual json Send(const json &request) = 0; + }; +} diff --git a/include/jsonrpccxx/server.hpp b/include/jsonrpccxx/server.hpp index d3a6315..202b56b 100644 --- a/include/jsonrpccxx/server.hpp +++ b/include/jsonrpccxx/server.hpp @@ -31,35 +31,41 @@ namespace jsonrpccxx { JsonRpc2Server() = default; ~JsonRpc2Server() override = default; - std::string HandleRequest(const std::string &requestString) override { - try { - json request = json::parse(requestString); - if (request.is_array()) { - json result = json::array(); - for (json &r : request) { - json res = this->HandleSingleRequest(r); - if (!res.is_null()) { - result.push_back(std::move(res)); - } - } - return result.dump(); - } else if (request.is_object()) { - json res = HandleSingleRequest(request); + /*** + * @remark If you encounter a json::parse_error, you can reply with create_parse_error_response_v2. + */ + json HandleRequest(const json &request) { + if (request.is_array()) { + json result = json::array(); + for (const json &r : request) { + json res = this->HandleSingleRequest(r); if (!res.is_null()) { - return res.dump(); - } else { - return ""; + result.push_back(std::move(res)); } + } + return result.dump(); + } else if (request.is_object()) { + json res = HandleSingleRequest(request); + if (!res.is_null()) { + return res.dump(); } else { - return json{{"id", nullptr}, {"error", {{"code", invalid_request}, {"message", "invalid request: expected array or object"}}}, {"jsonrpc", "2.0"}}.dump(); + return ""; } + } else { + return json{{"id", nullptr}, {"error", {{"code", invalid_request}, {"message", "invalid request: expected array or object"}}}, {"jsonrpc", "2.0"}}.dump(); + } + } + + std::string HandleRequest(const std::string &requestString) override { + try { + return HandleRequest(json::parse(requestString)); } catch (json::parse_error &e) { - return json{{"id", nullptr}, {"error", {{"code", parse_error}, {"message", std::string("parse error: ") + e.what()}}}, {"jsonrpc", "2.0"}}.dump(); + return create_parse_error_response_v2(e).dump(); } } private: - json HandleSingleRequest(json &request) { + json HandleSingleRequest(const json &request) { json id = nullptr; if (valid_id(request)) { id = request["id"]; @@ -79,7 +85,7 @@ namespace jsonrpccxx { } } - json ProcessSingleRequest(json &request) { + json ProcessSingleRequest(const json &request) { if (!has_key_type(request, "jsonrpc", json::value_t::string) || request["jsonrpc"] != "2.0") { throw JsonRpcException(invalid_request, R"(invalid request: missing jsonrpc field set to "2.0")"); } @@ -92,18 +98,19 @@ namespace jsonrpccxx { if (has_key(request, "params") && !(request["params"].is_array() || request["params"].is_object() || request["params"].is_null())) { throw JsonRpcException(invalid_request, "invalid request: params field must be an array, object or null"); } - if (!has_key(request, "params") || has_key_type(request, "params", json::value_t::null)) { - request["params"] = json::array(); - } + const json params = (!has_key(request, "params") || has_key_type(request, "params", json::value_t::null)) + ? json::array() + : request["params"] + ; if (!has_key(request, "id")) { try { - dispatcher.InvokeNotification(request["method"], request["params"]); + dispatcher.InvokeNotification(request["method"], params); return json(); } catch (std::exception &) { return json(); } } else { - return {{"jsonrpc", "2.0"}, {"id", request["id"]}, {"result", dispatcher.InvokeMethod(request["method"], request["params"])}}; + return {{"jsonrpc", "2.0"}, {"id", request["id"]}, {"result", dispatcher.InvokeMethod(request["method"], params)}}; } } }; diff --git a/test/client.cpp b/test/client.cpp index b94f459..e0accb9 100644 --- a/test/client.cpp +++ b/test/client.cpp @@ -152,6 +152,12 @@ TEST_CASE_FIXTURE(F, "v2_method_result_empty") { c.VerifyMethodRequest(version::v2, "some.method_1", "1"); } +TEST_CASE_FIXTURE(F, "v2_method_result_parse_error") { + c.raw_response = "abcdef"; + REQUIRE_THROWS_WITH(clientV2.CallMethod("1", "some.method_1", {}), "-32700: invalid JSON response from server: [json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'a'"); + c.VerifyMethodRequest(version::v2, "some.method_1", "1"); +} + /* TEST_CASE_FIXTURE(F, "v1_method_result_empty") { c.raw_response = "{}"; From e8d2cff46a10dcd4887e52bf0f5891364df9a051 Mon Sep 17 00:00:00 2001 From: Dominik Tugend Date: Sat, 23 Jul 2022 13:47:37 +0200 Subject: [PATCH 2/2] Fix Linux / gcc compilation failing --- include/jsonrpccxx/batchclient.hpp | 2 +- include/jsonrpccxx/client.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/jsonrpccxx/batchclient.hpp b/include/jsonrpccxx/batchclient.hpp index e0cec4b..e21d5e8 100644 --- a/include/jsonrpccxx/batchclient.hpp +++ b/include/jsonrpccxx/batchclient.hpp @@ -90,7 +90,7 @@ namespace jsonrpccxx { BatchResponse BatchCall(const BatchRequest &request) { try { - json response = GetConnector().Send(request.Build()); + json response = this->GetConnector().Send(request.Build()); if (!response.is_array()) { throw JsonRpcException(parse_error, std::string("invalid JSON response from server: expected array")); } diff --git a/include/jsonrpccxx/client.hpp b/include/jsonrpccxx/client.hpp index 9a0b5b3..b633130 100644 --- a/include/jsonrpccxx/client.hpp +++ b/include/jsonrpccxx/client.hpp @@ -115,7 +115,7 @@ namespace jsonrpccxx { private: - virtual json IClientJsonConnector::Send(const json &request) { + virtual json Send(const json &request) { return json::parse(connector.Send(request.dump())); } };