Skip to content

Commit 1096192

Browse files
committed
Initial commit
0 parents  commit 1096192

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+36191
-0
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
## [Unreleased]
8+
- Initial implementation of 1.0 and 2.0 server
9+
- Initial implementation of 1.0 and 2.0 client
10+
- Test suite for server and client

CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
cmake_minimum_required(VERSION 3.12)
2+
set(CMAKE_CXX_STANDARD 17)
3+
project(libjson-rpc-cpp VERSION 2.0.0 LANGUAGES CXX)
4+
5+
add_executable(jsonrpccpp-test src/client.hpp src/dispatcher.hpp src/main.cpp src/client.test.cpp src/typemapper.test.cpp src/dispatcher.test.cpp src/server.test.cpp)
6+
target_include_directories(jsonrpccpp-test PUBLIC vendor src)
7+
8+
enable_testing()
9+
add_test(NAME test COMMAND jsonrpccpp-test)

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Code style
2+
Please make sure your editor picks up the [.clang-format](.clang-format) file.

LICENSE

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Copyright (C) 2019 Peter Spiess-Knafl <[email protected]>
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
this software and associated documentation files (the "Software"), to deal in the
5+
Software without restriction, including without limitation the rights to
6+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
the Software, and to permit persons to whom the Software is furnished to do so,
8+
subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
14+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
15+
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
16+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
17+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
18+
OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
**THIS IS A WORK IN PROGRESS PROJECT**
2+
3+
#json-rpc-cxx
4+
![json-rpc-cxx-icon](doc/icon.png)
5+
6+
A [JSON-RPC](https://www.jsonrpc.org/) (1.0 & 2.0) framework implemented in C++17 using the [nlohmann's json for modern C++](https://github.com/nlohmann/json).
7+
8+
- JSON-RPC 1.0 and 2.0 compliant client
9+
- JSON-RCP 1.0 and 2.0 compliant server
10+
- Transport agnostic interfaces
11+
- Compile time type mapping (using [nlohmann's arbitrary type conversion](https://github.com/nlohmann/json#arbitrary-types-conversions))
12+
- Runtime type checking
13+
- Cross-platform (Windows, Linux, OSX)
14+
15+
## Usage
16+
TBD
17+
18+
### Installation
19+
TBD, probably vcpkg and header only
20+
### Examples
21+
Examples can be found in [src/examples](src/examples)
22+
23+
## Design goals
24+
- Easy to use interface
25+
- Type safety where possible
26+
- Avoid errors at compile time where possible
27+
- Test driven development
28+
- Choose expressiveness over speed
29+
- Minimal dependencies
30+
31+
## License
32+
This framework is licensed under [MIT](LICENSE).
33+
34+
### Dependencies
35+
- [nlohmann's JSON for modern C++](https://github.com/nlohmann/json) is licensed under MIT.
36+
- [Catch](https://github.com/catchorg/Catch2) is licensed under BSL-1.0.
37+
38+
## Developer information
39+
- [CONTRIBUTING.md](CONTRIBUTING.md)
40+
- [CHANGELOG.md](CHANGELOG.md)

doc/icon.png

11.2 KB
Loading

doc/icon.svg

Lines changed: 111 additions & 0 deletions
Loading

src/client.hpp

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#pragma once
2+
#include "common.hpp"
3+
#include <exception>
4+
#include <nlohmann/json.hpp>
5+
#include <string>
6+
#include <variant>
7+
8+
namespace jsonrpccpp {
9+
enum class version { v1, v2 };
10+
enum class param_type { by_position, by_name };
11+
12+
typedef std::vector<json> positional_parameter;
13+
typedef std::map<std::string, json> named_parameter;
14+
typedef std::variant<int, std::string> id_type;
15+
16+
struct JsonRpcResponse {
17+
std::string id;
18+
json result;
19+
};
20+
21+
class IClientConnector {
22+
public:
23+
virtual ~IClientConnector() = default;
24+
virtual std::string Send(const std::string &request) = 0;
25+
};
26+
27+
//TODO: add batch calls
28+
29+
class JsonRpcClient {
30+
public:
31+
JsonRpcClient(IClientConnector &connector, version v) : connector(connector), v(v) {}
32+
virtual ~JsonRpcClient() = default;
33+
34+
JsonRpcResponse CallMethod(const id_type &id, const std::string &name, const positional_parameter &params = {}) { return call_method(id, name, params); }
35+
JsonRpcResponse CallMethodNamed(const id_type &id, const std::string &name, const named_parameter &params = {}) { return call_method(id, name, params); }
36+
37+
void CallNotification(const std::string &name, const positional_parameter &params = {}) { call_notification(name, params); }
38+
void CallNotificationNamed(const std::string &name, const named_parameter &params = {}) { call_notification(name, params); }
39+
40+
protected:
41+
IClientConnector &connector;
42+
43+
private:
44+
version v;
45+
static inline bool has_key(const json &v, const std::string &key) { return v.find(key) != v.end(); }
46+
static inline bool has_key_type(const json &v, const std::string &key, json::value_t type) { return has_key(v, key) && v.at(key).type() == type; }
47+
48+
inline JsonRpcException get_error(const json &value) {
49+
bool has_code = has_key_type(value, "code", json::value_t::number_integer);
50+
bool has_message = has_key_type(value, "message", json::value_t::string);
51+
bool has_data = has_key(value, "data");
52+
if (has_code && has_message) {
53+
if (has_data) {
54+
return JsonRpcException(value["code"], value["message"], value["data"].get<json>());
55+
} else {
56+
return JsonRpcException(value["code"], value["message"]);
57+
}
58+
}
59+
return JsonRpcException(-32603, R"(invalid error response: "code" (negative number) and "message" (string) are required)");
60+
}
61+
62+
JsonRpcResponse call_method(const id_type &id, const std::string &name, const json &params) {
63+
json j = {{"method", name}};
64+
if (std::get_if<int>(&id) != nullptr) {
65+
j["id"] = std::get<int>(id);
66+
} else {
67+
j["id"] = std::get<std::string>(id);
68+
}
69+
if (v == version::v2) {
70+
j["jsonrpc"] = "2.0";
71+
}
72+
if (!params.empty() && !params.is_null()) {
73+
j["params"] = params;
74+
} else if (v == version::v1) {
75+
j["params"] = nullptr;
76+
}
77+
try {
78+
json response = json::parse(connector.Send(j.dump()));
79+
if (has_key_type(response, "error", json::value_t::object)) {
80+
throw get_error(response["error"]);
81+
}
82+
if (has_key(response, "result") && has_key(response, "id")) {
83+
return JsonRpcResponse{response["id"].get<std::string>(), response["result"].get<json>()};
84+
}
85+
throw JsonRpcException(-32603, R"(invalid server response: neither "result" nor "error" fields found)");
86+
} catch (json::parse_error &e) {
87+
throw JsonRpcException(-32700, std::string("invalid JSON response from server: ") + e.what());
88+
}
89+
}
90+
91+
void call_notification(const std::string &name, const nlohmann::json &params) {
92+
nlohmann::json j = {{"method", name}};
93+
if (v == version::v2) {
94+
j["jsonrpc"] = "2.0";
95+
} else {
96+
j["id"] = nullptr;
97+
}
98+
if (!params.empty() && !params.is_null()) {
99+
j["params"] = params;
100+
} else if (v == version::v1) {
101+
j["params"] = nullptr;
102+
}
103+
connector.Send(j.dump());
104+
}
105+
};
106+
107+
class BatchRequest {
108+
public:
109+
BatchRequest() : call(json::array()) {}
110+
111+
BatchRequest& AddMethodCall(const id_type &id, const std::string& name, const positional_parameter &params = {}) {
112+
json request = {{"method", name}, {"params", params}, {"jsonrpc", "2.0"}};
113+
if (std::get_if<int>(&id) != nullptr) {
114+
request["id"] = std::get<int>(id);
115+
} else {
116+
request["id"] = std::get<std::string>(id);
117+
}
118+
call.push_back(request);
119+
return *this;
120+
}
121+
122+
BatchRequest& AddNamedMethodCall(const id_type &id, const std::string& name, const named_parameter &params = {}) {
123+
json request = {{"method", name}, {"params", params}, {"jsonrpc", "2.0"}};
124+
if (std::get_if<int>(&id) != nullptr) {
125+
request["id"] = std::get<int>(id);
126+
} else {
127+
request["id"] = std::get<std::string>(id);
128+
}
129+
call.push_back(request);
130+
return *this;
131+
}
132+
133+
BatchRequest& AddNotificationCall(const std::string& name, const positional_parameter &params = {}) {
134+
call.push_back({{"method", name}, {"params", params}, {"jsonrpc", "2.0"}});
135+
return *this;
136+
}
137+
138+
BatchRequest& AddNamedNotificationCall(const std::string& name, const named_parameter &params = {}) {
139+
call.push_back({{"method", name}, {"params", params}, {"jsonrpc", "2.0"}});
140+
return *this;
141+
}
142+
143+
const json& Build() const {
144+
return call;
145+
}
146+
private:
147+
json call;
148+
};
149+
150+
class BatchResponse {
151+
public:
152+
BatchResponse(std::map<json, json>&& results, std::map<json, JsonRpcException> &&errors) {
153+
this->errors = errors;
154+
this->results = results;
155+
}
156+
157+
template<typename T>
158+
T GetResult(const json& id) {
159+
if (results.find(id) != results.end()) {
160+
//TOOD: catch type conversion error
161+
return results[id].get<T>();
162+
} else if (errors.find(id) != errors.end()) {
163+
throw errors[id];
164+
}
165+
throw JsonRpcException(-32700, std::string("no result found for id ") + id.dump());
166+
}
167+
168+
private:
169+
std::map<json, json> results;
170+
std::map<json, JsonRpcException> errors;
171+
};
172+
173+
class JsonRpc2Client : public JsonRpcClient {
174+
public:
175+
JsonRpc2Client(IClientConnector &connector) : JsonRpcClient(connector, version::v2) {}
176+
177+
BatchResponse BatchCall(const BatchRequest& request) {
178+
try {
179+
json response = json::parse(connector.Send(request.Build().dump()));
180+
if (!response.is_array()) {
181+
throw JsonRpcException(-32700, std::string("invalid JSON response from server: expected array"));
182+
}
183+
std::map<json, json> results;
184+
std::map<json, JsonRpcException> errors;
185+
186+
return BatchResponse(std::move(results), std::move(errors));
187+
} catch (json::parse_error &e) {
188+
throw JsonRpcException(-32700, std::string("invalid JSON response from server: ") + e.what());
189+
}
190+
}
191+
192+
};
193+
} // namespace jsonrpccpp

0 commit comments

Comments
 (0)