diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 3992a1b..1f27def 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -17,6 +17,7 @@ add_subdirectory(Logging) # Layer 2 add_subdirectory(Core) # Layer 3 +add_subdirectory(Network) add_subdirectory(Scene) # Layer 4 add_subdirectory(Physics) diff --git a/Engine/Networking/CMakeLists.txt b/Engine/Network/CMakeLists.txt similarity index 63% rename from Engine/Networking/CMakeLists.txt rename to Engine/Network/CMakeLists.txt index 8d8505c..9ef8502 100644 --- a/Engine/Networking/CMakeLists.txt +++ b/Engine/Network/CMakeLists.txt @@ -1,5 +1,6 @@ setup_module( - NAME networking + NAME network TARGET_DEPS logging utils core + ENABLE_TESTS FATAL_ERROR ) diff --git a/Engine/Network/include/Network.hpp b/Engine/Network/include/Network.hpp new file mode 100644 index 0000000..80cd13a --- /dev/null +++ b/Engine/Network/include/Network.hpp @@ -0,0 +1,5 @@ +// Copyright 2024 Stone-Engine + +#pragma once + +#include "Network/ObjectPool.hpp" diff --git a/Engine/Network/include/Network/CommandExecutor.hpp b/Engine/Network/include/Network/CommandExecutor.hpp new file mode 100644 index 0000000..5c95823 --- /dev/null +++ b/Engine/Network/include/Network/CommandExecutor.hpp @@ -0,0 +1,24 @@ +// Copyright 2024 Stone-Engine + +#pragma once + +#include + +namespace Stone::Network { + +template +class CommandExecutor { +public: + using Command = std::function &)>; + + CommandExecutor() = default; + CommandExecutor(const CommandExecutor &) = delete; + + virtual ~CommandExecutor() = default; + + void execute(const Command &command, const std::shared_ptr &object) { + command(object); + } +}; + +} // namespace Stone::Network diff --git a/Engine/Network/include/Network/Dispatcher/JsonRpcDispatcher.hpp b/Engine/Network/include/Network/Dispatcher/JsonRpcDispatcher.hpp new file mode 100644 index 0000000..acfc864 --- /dev/null +++ b/Engine/Network/include/Network/Dispatcher/JsonRpcDispatcher.hpp @@ -0,0 +1,85 @@ +// Copyright 2024 Stone-Engine + +#pragma once + +#include "Utils/Json.hpp" +#include "Utils/SigSlot.hpp" + +#include + +#define JSONRPC_ID "id" +#define JSONRPC_METHOD "method" +#define JSONRPC_PARAMS "params" +#define JSONRPC_RESULT "result" +#define JSONRPC_ERROR "error" + +namespace Stone::Network { + +class JsonRpcDispatcher { +public: + using Id = int; + using Method = std::string; + using Params = Json::Value; + using Result = Json::Value; + using Error = std::string; + + using SuccessCallback = std::function; + using FailureCallback = std::function; + + using SyncRequestHandler = std::function; + using AsyncRequestHandler = std::function &)>; + using RequestHandler = std::function; + + using NotificationSignal = Signal; + + using ResponseCallbacks = std::pair; + + JsonRpcDispatcher() = default; + JsonRpcDispatcher(const JsonRpcDispatcher &other) = default; + + virtual ~JsonRpcDispatcher() = default; + + bool registerRequestHandler(const Method &method, const SyncRequestHandler &syncHandler); + bool registerRequestHandler(const Method &method, const AsyncRequestHandler &asyncHandler); + bool registerRequestHandler(const Method &method, const RequestHandler &handler); + + bool hasRequestHandler(const Method &method) const; + + NotificationSignal &getNotificationSignal(const Method &method); + + bool hasNotificationSignal(const Method &method) const; + + bool sendRequest(std::ostream &output, const Method &method, const Params ¶ms, + const ResponseCallbacks &callbacks, float timeout = 10.0f); + + bool sendNotification(std::ostream &output, const Method &method, const Params ¶ms); + + bool handleString(const std::string &message, std::ostream &output); + bool handleStream(std::istream &stream, std::ostream &output); + bool handleJsonArray(const Json::Array &message, std::ostream &output); + bool handleJsonObject(const Json::Object &message, std::ostream &output); + + bool handleRequest(Id id, const Method &method, const Params ¶ms, std::ostream &output); + bool handleNotification(const Method &method, const Params ¶ms); + bool handleResponseSuccess(Id id, const Result &result); + bool handleResponseError(Id id, const Error &error); + + void cleanup(); + void cleanupTimedOutPendingRequests(); + void cleanupEmptyNotificationSignals(); + +private: + std::unordered_map _requestHandlers; + std::unordered_map> _notificationSignals; + + struct PendingResponse { + ResponseCallbacks callbacks; + std::chrono::steady_clock::time_point expiration; + }; + std::unordered_map _pendingRequests; + Id _nextId = 0; + + bool _sendErrorMessage = false; +}; + +} // namespace Stone::Network diff --git a/Engine/Network/include/Network/NetworkObject.hpp b/Engine/Network/include/Network/NetworkObject.hpp new file mode 100644 index 0000000..25eb5a5 --- /dev/null +++ b/Engine/Network/include/Network/NetworkObject.hpp @@ -0,0 +1,36 @@ +// Copyright 2024 Stone-Engine + +#include "Core/Object.hpp" +#include "Network/ObjectPool.hpp" +#include "Utils/SigSlot.hpp" + +namespace Stone::Network { + +class NetworkObject : public Core::Object { + STONE_OBJECT(NetworkObject) + +public: + NetworkObject() = default; + NetworkObject(const NetworkObject &other) = default; + + ~NetworkObject() override = default; + + void writeToJson(Json::Object &json) const override; + + ObjectPool::Id getPoolId() const; + + Signal onReceivingData; + Signal onSendingData; + +protected: + virtual void receiveData(const std::uint8_t *data, std::size_t size); + + virtual void sendData(const std::uint8_t *data, std::size_t &size) const; + +private: + ObjectPool::Id _poolId = 0; + + friend class NetworkSession; +}; + +} // namespace Stone::Network diff --git a/Engine/Network/include/Network/NetworkSession.hpp b/Engine/Network/include/Network/NetworkSession.hpp new file mode 100644 index 0000000..eb50067 --- /dev/null +++ b/Engine/Network/include/Network/NetworkSession.hpp @@ -0,0 +1,26 @@ +// Copyright 2024 Stone-Engine + +#pragma once + +#include "Network/NetworkObject.hpp" +#include "Network/ObjectPool.hpp" + +namespace Stone::Network { + +class NetworkSession { +public: + NetworkSession() = default; + + ObjectPool &getObjectPool() { + return _objectPool; + } + +private: + // TODO: Add CommunicationDevice class, which will be used to send and receive data + + // TODO: Add CommandExecutor class, which will be used to execute commands + + ObjectPool _objectPool; +}; + +} // namespace Stone::Network diff --git a/Engine/Network/include/Network/ObjectPool.hpp b/Engine/Network/include/Network/ObjectPool.hpp new file mode 100644 index 0000000..14f305c --- /dev/null +++ b/Engine/Network/include/Network/ObjectPool.hpp @@ -0,0 +1,71 @@ +// Copyright 2024 Stone-Engine + +#pragma once + +#include +#include + +namespace Stone::Network { + +template +class ObjectPool { +public: + using Id = unsigned int; + + ObjectPool() : _lastUsableId(1), _objects() { + _incrementLastUsableId(); + }; + + ObjectPool(const ObjectPool &) = delete; + + virtual ~ObjectPool() = default; + + Id add(const std::shared_ptr &object) { + Id id = _makeUsableId(); + _objects[id] = object; + return id; + } + + void set(Id id, const std::shared_ptr &object) { + if (id >= _objects.size()) { + _objects.resize(id + 1); + } + _objects[id] = object; + _incrementLastUsableId(); + } + + std::shared_ptr get(Id id) const { + return _objects[id].lock(); + } + + const std::weak_ptr &weak_get(Id id) const { + return _objects[id]; + } + + void refreshId() { + _lastUsableId = 1; + _incrementLastUsableId(); + } + +private: + Id _makeUsableId() { + Id id = _lastUsableId++; + _incrementLastUsableId(); + return id; + } + + void _incrementLastUsableId() { + while (_lastUsableId < _objects.size() && !_objects[_lastUsableId].expired()) { + ++_lastUsableId; + } + if (_lastUsableId >= _objects.size()) { + _objects.resize(_lastUsableId + 1); + } + } + + Id _lastUsableId; // The last id that is directly usable for the next object + + std::vector> _objects; // The objects in the pool +}; + +} // namespace Stone::Network diff --git a/Engine/Network/src/Network/Dispatcher/JsonRpcDispatcher.cpp b/Engine/Network/src/Network/Dispatcher/JsonRpcDispatcher.cpp new file mode 100644 index 0000000..1e8c427 --- /dev/null +++ b/Engine/Network/src/Network/Dispatcher/JsonRpcDispatcher.cpp @@ -0,0 +1,280 @@ +// Copyright 2024 Stone-Engine + +#include "Network/Dispatcher/JsonRpcDispatcher.hpp" + +#include + +#define MAX_REQUEST_LOOP 1000000000 +#define ERROR_HANDLER_THROWN "Unknown error occurred" +#define ERROR_FAILED_TO_SEND "Failed to send request" +#define ERROR_INVALID_JSON "Failed to parse JSON" +#define ERROR_INVALID_MESSAGE "Invalid JSON-RPC message format" + +namespace Stone::Network { + +bool JsonRpcDispatcher::registerRequestHandler(const Method &method, const SyncRequestHandler &syncHandler) { + return registerRequestHandler( + method, [syncHandler](const Params ¶ms, const SuccessCallback &success, const FailureCallback &failure) { + try { + success(syncHandler(params)); + } catch (const Error &err) { + failure(err); + } catch (const std::exception &e) { + failure(e.what()); + } catch (...) { + failure(ERROR_HANDLER_THROWN); + } + }); +} + +bool JsonRpcDispatcher::registerRequestHandler(const Method &method, const AsyncRequestHandler &asyncHandler) { + return registerRequestHandler( + method, [asyncHandler](const Params ¶ms, const SuccessCallback &success, const FailureCallback &failure) { + std::promise promise; + std::future future = promise.get_future(); + asyncHandler(params, promise); + future.wait(); + try { + success(future.get()); + } catch (const Error &err) { + failure(err); + } catch (const std::exception &e) { + failure(e.what()); + } catch (...) { + failure(ERROR_HANDLER_THROWN); + } + }); +} + +bool JsonRpcDispatcher::registerRequestHandler(const Method &method, const RequestHandler &handler) { + auto it = _requestHandlers.find(method); + if (it != _requestHandlers.end()) { + return false; // Handler already registered + } + _requestHandlers[method] = handler; + return true; +} + +bool JsonRpcDispatcher::hasRequestHandler(const Method &method) const { + return _requestHandlers.find(method) != _requestHandlers.end(); +} + +JsonRpcDispatcher::NotificationSignal &JsonRpcDispatcher::getNotificationSignal(const Method &method) { + if (auto it = _notificationSignals.find(method); it != _notificationSignals.end()) { + return *it->second; + } + auto it = _notificationSignals.emplace(method, std::make_unique()); + return *it.first->second; +} + +bool JsonRpcDispatcher::hasNotificationSignal(const Method &method) const { + return _notificationSignals.find(method) != _notificationSignals.end(); +} + +bool JsonRpcDispatcher::sendRequest(std::ostream &output, const Method &method, const Params ¶ms, + const ResponseCallbacks &callbacks, float timeout) { + _nextId++; + if (_nextId >= MAX_REQUEST_LOOP) + _nextId = 1; + Json::Value request = Json::object({ + { JSONRPC_ID, Json::number(_nextId)}, + {JSONRPC_METHOD, Json::string(method)}, + }); + if (!params.isNull()) + request[JSONRPC_PARAMS] = params; + _pendingRequests[_nextId] = { + callbacks, + std::chrono::steady_clock::now() + std::chrono::milliseconds(static_cast(timeout * 1000)) // + }; + output << request; + if (output.fail() || output.bad()) { + handleResponseError(_nextId, ERROR_FAILED_TO_SEND); + return false; + } + return true; +} + +bool JsonRpcDispatcher::sendNotification(std::ostream &output, const Method &method, const Params ¶ms) { + output << Json::object({ + {JSONRPC_METHOD, Json::string(method)}, + {JSONRPC_PARAMS, params}, + }); + return !(output.fail() || output.bad()); +} + +bool JsonRpcDispatcher::handleString(const std::string &message, std::ostream &output) { + std::stringstream stream(message); + return handleStream(stream, output); +} + +bool JsonRpcDispatcher::handleStream(std::istream &stream, std::ostream &output) { + Json::Value jsonValue; + try { + Json::parseStream(stream, jsonValue); + } catch (const std::exception &e) { + if (_sendErrorMessage) { + output << Json::object({ + { JSONRPC_ERROR, ERROR_INVALID_JSON}, + {JSONRPC_PARAMS, Json::string(e.what())}, + }); + } + return false; + } + if (jsonValue.is()) { + return handleJsonObject(jsonValue.get(), output); + } else if (jsonValue.is()) { + return handleJsonArray(jsonValue.get(), output); + } + + if (_sendErrorMessage) { + output << Json::object({ + { JSONRPC_ERROR, ERROR_INVALID_MESSAGE}, + {JSONRPC_PARAMS, jsonValue}, + }); + } + + return false; +} + +bool JsonRpcDispatcher::handleJsonArray(const Json::Array &message, std::ostream &output) { + for (const auto &item : message) { + if (item.is()) { + handleJsonObject(item.get(), output); + } else { + if (_sendErrorMessage) { + output << Json::object({ + { JSONRPC_ERROR, ERROR_INVALID_MESSAGE}, + {JSONRPC_PARAMS, item}, + }); + } + } + } + return true; +} + +bool JsonRpcDispatcher::handleJsonObject(const Json::Object &message, std::ostream &output) { + const auto &idPtr = message.find(JSONRPC_ID); + const bool hasId = idPtr != message.end() && idPtr->second.is(); + const Id id = hasId ? static_cast(idPtr->second.get()) : 0; + + const auto &methodPtr = message.find(JSONRPC_METHOD); + const bool hasMethod = methodPtr != message.end() && methodPtr->second.is(); + + if (hasMethod) { + const auto ¶ms = message.find(JSONRPC_PARAMS); + + if (hasId) + return handleRequest(id, methodPtr->second.get(), + params == message.end() ? Json::null() : params->second, output); + else + return handleNotification(methodPtr->second.get(), + params == message.end() ? Json::null() : params->second); + } else { + if (hasId) { + const auto &resultPtr = message.find(JSONRPC_RESULT); + const bool hasResult = resultPtr != message.end() && !resultPtr->second.isNull(); + + const auto &errorPtr = message.find(JSONRPC_ERROR); + const bool hasError = errorPtr != message.end() && errorPtr->second.is(); + + if (hasResult && !hasError) { + return handleResponseSuccess(id, resultPtr->second); + } else if (!hasResult && hasError) { + return handleResponseError(id, errorPtr->second.get()); + } + if (_sendErrorMessage) { + output << Json::object({ + { JSONRPC_ERROR, ERROR_INVALID_MESSAGE}, + {JSONRPC_PARAMS, message}, + }); + } + } + } + return false; +} + +bool JsonRpcDispatcher::handleRequest(Id id, const Method &method, const Params ¶ms, std::ostream &output) { + auto it = _requestHandlers.find(method); + if (it != _requestHandlers.end()) { + it->second( + params, + [&output, id](const Result &result) { + output << Json::object({ + { JSONRPC_ID, Json::number(id)}, + {JSONRPC_RESULT, result}, + }); + }, + [&output, id](const Error &error) { + output << Json::object({ + { JSONRPC_ID, Json::number(id)}, + {JSONRPC_ERROR, error}, + }); + }); + return true; + } + return false; +} + +bool JsonRpcDispatcher::handleNotification(const Method &method, const Params ¶ms) { + bool used = false; + auto it = _notificationSignals.find(method); + if (it != _notificationSignals.end()) { + (*it->second)(params); + used = true; + } + auto handlerIt = _requestHandlers.find(method); + if (handlerIt != _requestHandlers.end()) { + handlerIt->second(params, [](const Result &) {}, [](const Error &) {}); + used = true; + } + return used; +} + +bool JsonRpcDispatcher::handleResponseSuccess(Id id, const Result &result) { + auto it = _pendingRequests.find(id); + if (it != _pendingRequests.end()) { + it->second.callbacks.first(result); + _pendingRequests.erase(it); + return true; + } + return false; +} + +bool JsonRpcDispatcher::handleResponseError(Id id, const Error &error) { + auto it = _pendingRequests.find(id); + if (it != _pendingRequests.end()) { + it->second.callbacks.second(error); + _pendingRequests.erase(it); + return true; + } + return false; +} + +void JsonRpcDispatcher::cleanup() { + cleanupTimedOutPendingRequests(); + cleanupEmptyNotificationSignals(); +} + +void JsonRpcDispatcher::cleanupTimedOutPendingRequests() { + for (auto it = _pendingRequests.begin(); it != _pendingRequests.end();) { + if (it->second.expiration < std::chrono::steady_clock::now()) { + it->second.callbacks.second("request timed out"); + it = _pendingRequests.erase(it); + } else { + ++it; + } + } +} + +void JsonRpcDispatcher::cleanupEmptyNotificationSignals() { + for (auto it = _notificationSignals.begin(); it != _notificationSignals.end();) { + if (!it->second->isBound()) { + it = _notificationSignals.erase(it); + } else { + ++it; + } + } +} + + +} // namespace Stone::Network diff --git a/Engine/Network/src/Network/NetworkObject.cpp b/Engine/Network/src/Network/NetworkObject.cpp new file mode 100644 index 0000000..a08d3dc --- /dev/null +++ b/Engine/Network/src/Network/NetworkObject.cpp @@ -0,0 +1,27 @@ +// Copyright 2024 Stone-Engine + +#include "Network/NetworkObject.hpp" + +namespace Stone::Network { + + +void NetworkObject::writeToJson(Json::Object &json) const { + Core::Object::writeToJson(json); + + json["poolId"] = Json::number(_poolId); +} + +ObjectPool::Id NetworkObject::getPoolId() const { + return _poolId; +} + +void NetworkObject::receiveData(const std::uint8_t *data, std::size_t size) { + onReceivingData.broadcast(*this, data, size); +} + +void NetworkObject::sendData(const std::uint8_t *data, std::size_t &size) const { + onSendingData.broadcast(*this, data, size); +} + + +} // namespace Stone::Network diff --git a/Engine/Network/src/Network/ObjectPool.cpp b/Engine/Network/src/Network/ObjectPool.cpp new file mode 100644 index 0000000..a81c4fd --- /dev/null +++ b/Engine/Network/src/Network/ObjectPool.cpp @@ -0,0 +1,3 @@ +// Copyright 2024 Stone-Engine + +#include "Network/ObjectPool.hpp" diff --git a/Engine/Network/test/test_JsonRpcDispatcher.cpp b/Engine/Network/test/test_JsonRpcDispatcher.cpp new file mode 100644 index 0000000..5c0df9b --- /dev/null +++ b/Engine/Network/test/test_JsonRpcDispatcher.cpp @@ -0,0 +1,325 @@ +#include "Network/Dispatcher/JsonRpcDispatcher.hpp" + +#include + +using namespace Stone::Network; + +TEST(JsonRpcDispatcher, HandleRequestThatIgnoreParams) { + JsonRpcDispatcher dispatcher; + std::stringstream out; + Json::Value outJson; + + int zeValue = 0; + + dispatcher.registerRequestHandler("incValue", [&zeValue](const Json::Value ¶ms) { + (void)params; + zeValue++; + return Json::null(); + }); + + EXPECT_EQ(zeValue, 0); + + EXPECT_TRUE(dispatcher.handleString(R"({"method": "incValue"})", out)); + EXPECT_EQ(zeValue, 1); + EXPECT_STREQ(out.str().c_str(), ""); + + EXPECT_TRUE(dispatcher.handleString(R"({"method": "incValue", "params": null})", out)); + EXPECT_EQ(zeValue, 2); + EXPECT_STREQ(out.str().c_str(), ""); + + EXPECT_TRUE(dispatcher.handleString(R"({"method": "incValue", "params": {}})", out)); + EXPECT_EQ(zeValue, 3); + EXPECT_STREQ(out.str().c_str(), ""); + + EXPECT_TRUE(dispatcher.handleString(R"({"id": 0, "method": "incValue", "params": {}})", out)); + EXPECT_EQ(zeValue, 4); + EXPECT_STRNE(out.str().c_str(), ""); + + out = std::stringstream(); + EXPECT_STREQ(out.str().c_str(), ""); + + EXPECT_TRUE(dispatcher.handleString(R"({"id": 1, "method": "incValue", "params": {}})", out)); + EXPECT_EQ(zeValue, 5); + EXPECT_STRNE(out.str().c_str(), ""); +} + +TEST(JsonRpcDispatcher, HandleRequestUsingNumberParam) { + JsonRpcDispatcher dispatcher; + std::stringstream out; + Json::Value outJson; + + int zeValue = 0; + + dispatcher.registerRequestHandler("incValue", [&zeValue](const Json::Value ¶ms) { + if (!params.is()) + throw std::runtime_error("invalid params type"); + zeValue += params.get(); + return Json::number(zeValue); + }); + + EXPECT_EQ(zeValue, 0); + + EXPECT_TRUE(dispatcher.handleString(R"({"id": 0, "method": "incValue"})", out)); + ASSERT_EQ(zeValue, 0); + EXPECT_STRNE(out.str().c_str(), ""); + ASSERT_NO_THROW(out >> outJson); + ASSERT_TRUE(outJson.is()); + EXPECT_STREQ(outJson.get()["error"].get().c_str(), "invalid params type"); + out = std::stringstream(); + EXPECT_STREQ(out.str().c_str(), ""); + + EXPECT_TRUE(dispatcher.handleString(R"({"method": "incValue", "params": 12})", out)); + EXPECT_EQ(zeValue, 12); + EXPECT_STREQ(out.str().c_str(), ""); + + EXPECT_TRUE(dispatcher.handleString(R"({"id": 1, "method": "incValue", "params": 6})", out)); + EXPECT_EQ(zeValue, 18); + EXPECT_STRNE(out.str().c_str(), ""); + ASSERT_NO_THROW(out >> outJson); + ASSERT_TRUE(outJson.is()); + EXPECT_EQ(outJson.get()["id"].get(), 1); + EXPECT_EQ(outJson.get()["result"].get(), 18); + EXPECT_EQ(outJson.get().find("error"), outJson.get().end()); +} + + +TEST(JsonRpcDispatcher, HandleNotificationThatIgnoreParams) { + JsonRpcDispatcher dispatcher; + std::stringstream out; + + int zeValue = 0; + auto incValue = [&zeValue](const Json::Value ¶ms) { + (void)params; + zeValue++; + }; + Stone::Slot incSlot(incValue); + + dispatcher.getNotificationSignal("incValue").bind(incSlot); + + EXPECT_EQ(zeValue, 0); + + EXPECT_TRUE(dispatcher.handleString(R"({"method": "incValue"})", out)); + EXPECT_EQ(zeValue, 1); + + EXPECT_TRUE(dispatcher.handleString(R"({"method": "incValue", "params": null})", out)); + EXPECT_EQ(zeValue, 2); + + EXPECT_TRUE(dispatcher.handleString(R"({"method": "incValue", "params": {}})", out)); + EXPECT_EQ(zeValue, 3); +} + +TEST(JsonRpcDispatcher, HandleNotificationUsingParams) { + JsonRpcDispatcher dispatcher; + std::stringstream out; + + int zeValue = 0; + auto incValue = [&zeValue](const Json::Value ¶ms) { + if (!params.is()) + return; + zeValue += params.get(); + }; + Stone::Slot incSlot(incValue); + + dispatcher.getNotificationSignal("incValue").bind(incSlot); + + EXPECT_EQ(zeValue, 0); + + EXPECT_TRUE(dispatcher.handleString(R"({"method": "incValue"})", out)); + EXPECT_EQ(zeValue, 0); + + EXPECT_TRUE(dispatcher.handleString(R"({"method": "incValue", "params": 2})", out)); + EXPECT_EQ(zeValue, 2); + + EXPECT_TRUE(dispatcher.handleString(R"({"method": "incValue", "params": [2]})", out)); + EXPECT_EQ(zeValue, 2); +} + +TEST(JsonRpcDispatcher, CleanupNotificationSignal) { + JsonRpcDispatcher dispatcher; + + { + Stone::Slot incSlot([](const Json::Value &) {}); + + dispatcher.getNotificationSignal("incValue").bind(incSlot); + + EXPECT_TRUE(dispatcher.hasNotificationSignal("incValue")); + dispatcher.cleanupEmptyNotificationSignals(); + EXPECT_TRUE(dispatcher.hasNotificationSignal("incValue")); + } + + dispatcher.cleanupEmptyNotificationSignals(); + EXPECT_FALSE(dispatcher.hasNotificationSignal("incValue")); +} + +TEST(JsonRpcDispatcher, SendRequestWithParams) { + JsonRpcDispatcher dispatcher; + std::stringstream out; + Json::Value outJson; + + dispatcher.sendRequest(out, "setValue", Json::number(12), + {[](const Json::Value &result) { (void)result; }, + [](const std::string &error) { + (void)error; + }}); + + ASSERT_NO_THROW(out >> outJson); + ASSERT_TRUE(outJson.is()); + ASSERT_NE(outJson.get().find("id"), outJson.get().end()); + ASSERT_NE(outJson.get().find("method"), outJson.get().end()); + ASSERT_NE(outJson.get().find("params"), outJson.get().end()); + + EXPECT_EQ(outJson.get()["method"], Json::string("setValue")); + EXPECT_EQ(outJson.get()["params"], Json::number(12)); + + int firstId = outJson.get()["id"].get(); + + dispatcher.sendRequest(out, "getValue", Json::null(), + {[](const Json::Value &result) { (void)result; }, + [](const std::string &error) { + (void)error; + }}); + + ASSERT_NO_THROW(out >> outJson); + ASSERT_TRUE(outJson.is()); + ASSERT_NE(outJson.get().find("id"), outJson.get().end()); + ASSERT_NE(outJson.get().find("method"), outJson.get().end()); + + EXPECT_EQ(outJson.get()["method"], Json::string("getValue")); + + int secondId = outJson.get()["id"].get(); + EXPECT_NE(firstId, secondId); +} + +TEST(JsonRpcDispatcher, SendRequestWithParamsAndReceiveResponse) { + JsonRpcDispatcher dispatcher; + std::stringstream out; + Json::Value outJson; + + int zeValue = 0; + std::string receivedError = ""; + + { + EXPECT_TRUE(dispatcher.sendRequest( // + out, "getValue", Json::null(), + {[&zeValue](const Json::Value &result) { + if (result.is()) { + zeValue = result.get(); + } + }, + [&receivedError](const std::string &error) { + receivedError = error; + }})); + + ASSERT_NO_THROW(out >> outJson); + ASSERT_TRUE(outJson.is()); + ASSERT_NE(outJson.get().find("id"), outJson.get().end()); + ASSERT_NE(outJson.get().find("method"), outJson.get().end()); + + ASSERT_TRUE(outJson.get()["id"].is()); + EXPECT_EQ(outJson.get()["method"], Json::string("getValue")); + + int requestId = outJson.get()["id"].get(); + + EXPECT_EQ(zeValue, 0); + + auto response = Json::Object({ + { "id", Json::number(requestId)}, + {"result", Json::number(12)}, + }); + EXPECT_TRUE(dispatcher.handleJsonObject(response, out)); + + EXPECT_EQ(zeValue, 12); + EXPECT_EQ(receivedError, ""); + + zeValue = 0; + + auto secondResponse = Json::Object({ + { "id", Json::number(requestId)}, + {"result", Json::number(17)}, + }); + + EXPECT_FALSE(dispatcher.handleJsonObject(secondResponse, out)); + + EXPECT_NE(zeValue, 17); + } + + { + zeValue = 10; + + EXPECT_TRUE(dispatcher.sendRequest( // + out, "getValue", Json::null(), + {[&zeValue](const Json::Value &result) { + if (result.is()) { + zeValue = result.get(); + } + }, + [&zeValue, &receivedError](const std::string &error) { + // + zeValue = 0; + receivedError = error; + }})); + + ASSERT_NO_THROW(out >> outJson); + ASSERT_TRUE(outJson.is()); + ASSERT_NE(outJson.get().find("id"), outJson.get().end()); + ASSERT_NE(outJson.get().find("method"), outJson.get().end()); + + ASSERT_TRUE(outJson.get()["id"].is()); + EXPECT_EQ(outJson.get()["method"], Json::string("getValue")); + + int requestId = outJson.get()["id"].get(); + + auto response = Json::Object({ + { "id", Json::number(requestId)}, + {"error", Json::string("pas envie cette fois")}, + }); + EXPECT_TRUE(dispatcher.handleJsonObject(response, out)); + + EXPECT_EQ(zeValue, 0); + EXPECT_EQ(receivedError, "pas envie cette fois"); + } +} + +TEST(JsonRpcDispatcher, HandleRequestWithTimeout) { + JsonRpcDispatcher dispatcher; + std::stringstream out; + + bool errorReceived = false; + + dispatcher.sendRequest( // + out, "getValue", Json::null(), + {[](const Json::Value &result) { (void)result; }, + [&errorReceived](const std::string &error) { + (void)error; + errorReceived = true; + }}, + 0.005f); + // Timeout after 5ms + + dispatcher.cleanupTimedOutPendingRequests(); + EXPECT_FALSE(errorReceived); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + dispatcher.cleanupTimedOutPendingRequests(); + EXPECT_FALSE(errorReceived); + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + dispatcher.cleanupTimedOutPendingRequests(); + EXPECT_TRUE(errorReceived); +} + +TEST(JsonRpcDispatcher, SendNotification) { + JsonRpcDispatcher dispatcher; + std::stringstream out; + Json::Value outJson; + + EXPECT_TRUE(dispatcher.sendNotification(out, "sayHello", Json::array({Json::number(12), Json::string("world")}))); + + EXPECT_NO_THROW(out >> outJson); + ASSERT_TRUE(outJson.is()); + ASSERT_EQ(outJson["method"], Json::string("sayHello")); + ASSERT_TRUE(outJson["params"].is()); + ASSERT_EQ(outJson["params"].get().size(), 2); + EXPECT_EQ(outJson["params"][0], Json::number(12)); + EXPECT_EQ(outJson["params"][1], Json::string("world")); +} diff --git a/Engine/Network/test/test_ObjectPool.cpp b/Engine/Network/test/test_ObjectPool.cpp new file mode 100644 index 0000000..a59bffa --- /dev/null +++ b/Engine/Network/test/test_ObjectPool.cpp @@ -0,0 +1,101 @@ +#include "Network/ObjectPool.hpp" + +#include + +using namespace Stone::Network; + +using PoolId = Stone::Network::ObjectPool::Id; + +TEST(ObjectPool, AddObject) { + ObjectPool pool; + auto object = std::make_shared(42); + PoolId id = pool.add(object); + auto result = pool.get(id); + ASSERT_EQ(*result, 42); +} + +TEST(ObjectPool, AddMultipleObjects) { + ObjectPool pool; + auto object1 = std::make_shared(42); + auto object2 = std::make_shared(43); + PoolId id1 = pool.add(object1); + PoolId id2 = pool.add(object2); + auto result1 = pool.get(id1); + auto result2 = pool.get(id2); + ASSERT_EQ(*result1, 42); + ASSERT_EQ(*result2, 43); +} + +TEST(ObjectPool, SetObject) { + ObjectPool pool; + auto object = std::make_shared(42); + PoolId id = pool.add(object); + auto result = pool.get(id); + ASSERT_EQ(*result, 42); + + auto newObject = std::make_shared(43); + pool.set(id, newObject); + auto newResult = pool.get(id); + ASSERT_EQ(*newResult, 43); +} + +TEST(ObjectPool, SetMultipleObjects) { + ObjectPool pool; + auto object1 = std::make_shared(42); + auto object2 = std::make_shared(43); + PoolId id1 = pool.add(object1); + PoolId id2 = pool.add(object2); + auto result1 = pool.get(id1); + auto result2 = pool.get(id2); + ASSERT_EQ(*result1, 42); + ASSERT_EQ(*result2, 43); + + auto newObject1 = std::make_shared(44); + auto newObject2 = std::make_shared(45); + pool.set(id1, newObject1); + pool.set(id2, newObject2); + auto newResult1 = pool.get(id1); + auto newResult2 = pool.get(id2); + ASSERT_EQ(*newResult1, 44); + ASSERT_EQ(*newResult2, 45); +} + +TEST(ObjectPool, RefreshId) { + ObjectPool pool; + + auto object1 = std::make_shared(42); + auto object2 = std::make_shared(43); + PoolId id1 = pool.add(object1); + PoolId id2 = pool.add(object2); + ASSERT_EQ(id1, 1); + ASSERT_EQ(id2, 2); + + { + auto result1 = pool.get(id1); + auto result2 = pool.get(id2); + ASSERT_EQ(*result1, 42); + ASSERT_EQ(*result2, 43); + } + + object1.reset(); + object2.reset(); + ASSERT_EQ(pool.get(id1), nullptr); + ASSERT_EQ(pool.get(id2), nullptr); + + auto object3 = std::make_shared(44); + PoolId id3 = pool.add(object3); + ASSERT_EQ(id3, 3); + + { + auto result3 = pool.get(id3); + ASSERT_EQ(*result3, 44); + } + + pool.refreshId(); + auto newObject1 = std::make_shared(44); + auto newObject2 = std::make_shared(45); + PoolId newId1 = pool.add(newObject1); + PoolId newId2 = pool.add(newObject2); + ASSERT_EQ(newId1, 1); + ASSERT_EQ(newId2, 2); +} diff --git a/Engine/Networking/include/Networking.hpp b/Engine/Networking/include/Networking.hpp deleted file mode 100644 index 6f70f09..0000000 --- a/Engine/Networking/include/Networking.hpp +++ /dev/null @@ -1 +0,0 @@ -#pragma once diff --git a/Engine/Networking/src/library.cpp b/Engine/Networking/src/library.cpp deleted file mode 100644 index 8cfbf65..0000000 --- a/Engine/Networking/src/library.cpp +++ /dev/null @@ -1,2 +0,0 @@ -void lib() { -} diff --git a/Engine/Utils/include/Utils/Json.hpp b/Engine/Utils/include/Utils/Json.hpp index f1b094d..d6b4c7b 100644 --- a/Engine/Utils/include/Utils/Json.hpp +++ b/Engine/Utils/include/Utils/Json.hpp @@ -12,16 +12,20 @@ struct Value; using Object = std::unordered_map; using Array = std::vector; +using String = std::string; +using Number = double; +using Boolean = bool; +using Null = std::nullptr_t; struct Value { - std::variant value; + std::variant value; Value() : value(nullptr) { } - template , T>>> + template , T>>> Value(T &&val) : value(std::forward(val)) { } @@ -46,6 +50,14 @@ struct Value { void serialize(std::ostream &stream) const; std::string serialize() const; + + bool operator==(const Value &other) const; + bool operator!=(const Value &other) const; + + Value &operator[](int index); + const Value &operator[](int index) const; + Value &operator[](const std::string &key); + const Value &operator[](const std::string &key) const; }; void parseStream(std::istream &input, Value &out); @@ -54,9 +66,9 @@ void parseFile(const std::string &path, Value &out); Value object(const Object &obj = {}); Value array(const Array &arr = {}); -Value string(const std::string &str = ""); -Value number(double num = 0.0); -Value boolean(bool b = false); +Value string(const String &str = ""); +Value number(Number num = 0.0); +Value boolean(Boolean b = false); Value null(); @@ -126,10 +138,10 @@ class Serializer { void operator()(const Object &obj); void operator()(const Array &arr); - void operator()(const std::string &str); - void operator()(double num); - void operator()(bool b); - void operator()(std::nullptr_t); + void operator()(const String &str); + void operator()(Number num); + void operator()(Boolean b); + void operator()(Null); private: std::ostream &_stream; diff --git a/Engine/Utils/include/Utils/SigSlot.hpp b/Engine/Utils/include/Utils/SigSlot.hpp index e5a9f1c..b9065ef 100644 --- a/Engine/Utils/include/Utils/SigSlot.hpp +++ b/Engine/Utils/include/Utils/SigSlot.hpp @@ -152,6 +152,15 @@ struct Signal { } } + /** + * @brief Checks if a slot is bound to this Signal. + * + * @return True if a slot is bound to this Signal, false otherwise. + */ + bool isBound() const { + return !_slots.empty(); + } + private: std::unordered_set *> _slots; ///< The set of slots bound to this Signal. }; diff --git a/Engine/Utils/src/Utils/Json.cpp b/Engine/Utils/src/Utils/Json.cpp index b9d2573..0f4b91a 100644 --- a/Engine/Utils/src/Utils/Json.cpp +++ b/Engine/Utils/src/Utils/Json.cpp @@ -18,6 +18,30 @@ std::string Value::serialize() const { return ss.str(); } +bool Value::operator==(const Value &other) const { + return value == other.value; +} + +bool Value::operator!=(const Value &other) const { + return !(*this == other); +} + +Value &Value::operator[](int index) { + return get()[index]; +} + +const Value &Value::operator[](int index) const { + return get()[index]; +} + +Value &Value::operator[](const std::string &key) { + return get()[key]; +} + +const Value &Value::operator[](const std::string &key) const { + return get().find(key)->second; +} + void parseStream(std::istream &input, Value &out) { Parser parser(input); parser.parse(out); @@ -44,15 +68,15 @@ Value array(const Array &arr) { return {arr}; } -Value string(const std::string &str) { +Value string(const String &str) { return {str}; } -Value number(double num) { +Value number(Number num) { return {num}; } -Value boolean(bool b) { +Value boolean(Boolean b) { return {b}; } @@ -278,19 +302,19 @@ static std::string _escape_string(const std::string &str) { return result; } -void Serializer::operator()(const std::string &str) { +void Serializer::operator()(const String &str) { _stream << "\"" << _escape_string(str) << "\""; } -void Serializer::operator()(double num) { +void Serializer::operator()(Number num) { _stream << num; } -void Serializer::operator()(bool b) { +void Serializer::operator()(Boolean b) { _stream << (b ? "true" : "false"); } -void Serializer::operator()(std::nullptr_t) { +void Serializer::operator()(Null) { _stream << "null"; } diff --git a/Engine/Utils/test/test_Json.cpp b/Engine/Utils/test/test_Json.cpp index 6dc98dc..df3b679 100644 --- a/Engine/Utils/test/test_Json.cpp +++ b/Engine/Utils/test/test_Json.cpp @@ -2,6 +2,55 @@ #include +TEST(Json, GetContent) { + std::string jsonString = R"({"name": "John", "age": 30, "isStudent": false})"; + + Json::Value json = Json::object({ + { "name", Json::string("John")}, + { "age", Json::number(30)}, + {"isStudent", Json::boolean(false)}, + { "scores", Json::array({Json::number(85.5), Json::number(92.0), Json::number(78.5)})}, + { "address", Json::object({{"city", Json::string("New York")}, {"zip", Json::string("10001")}})}, + }); + + ASSERT_TRUE(json.is()); + + Json::Object &obj = json.get(); + ASSERT_TRUE(obj["name"].is()); + EXPECT_EQ(obj["name"].get(), "John"); + + ASSERT_TRUE(obj["age"].is()); + EXPECT_EQ(obj["age"].get(), 30); + + ASSERT_TRUE(obj["isStudent"].is()); + EXPECT_EQ(obj["isStudent"].get(), false); + + ASSERT_TRUE(obj["scores"].is()); + auto scores = obj["scores"].get(); + ASSERT_EQ(scores.size(), 3); + ASSERT_TRUE(scores[0].is()); + ASSERT_EQ(scores[0].get(), 85.5); + ASSERT_TRUE(scores[1].is()); + ASSERT_EQ(scores[1].get(), 92.0); + ASSERT_TRUE(scores[2].is()); + ASSERT_EQ(scores[2].get(), 78.5); + + ASSERT_TRUE(obj["address"].is()); + auto address = obj["address"].get(); + ASSERT_TRUE(address["city"].is()); + ASSERT_EQ(address["city"].get(), "New York"); + ASSERT_TRUE(address["zip"].is()); + ASSERT_EQ(address["zip"].get(), "10001"); + + const Json::Value constJson = json; + ASSERT_TRUE(constJson.is()); + ASSERT_TRUE(constJson["name"].is()); + EXPECT_EQ(constJson["name"].get(), "John"); + + EXPECT_EQ(constJson["address"]["city"].get(), "New York"); + ASSERT_EQ(constJson["scores"][1].get(), 92.0); +} + TEST(Json, ParseEmptyObject) { std::string jsonString = "{}"; @@ -21,14 +70,14 @@ TEST(Json, ParseSimpleObject) { ASSERT_TRUE(json.is()); Json::Object obj = json.get(); - ASSERT_TRUE(obj["name"].is()); - ASSERT_EQ(obj["name"].get(), "John"); + ASSERT_TRUE(obj["name"].is()); + ASSERT_EQ(obj["name"].get(), "John"); - ASSERT_TRUE(obj["age"].is()); - ASSERT_EQ(obj["age"].get(), 30); + ASSERT_TRUE(obj["age"].is()); + ASSERT_EQ(obj["age"].get(), 30); - ASSERT_TRUE(obj["isStudent"].is()); - ASSERT_EQ(obj["isStudent"].get(), false); + ASSERT_TRUE(obj["isStudent"].is()); + ASSERT_EQ(obj["isStudent"].get(), false); } TEST(Json, ParseArray) { @@ -42,14 +91,14 @@ TEST(Json, ParseArray) { Json::Array arr = json.get(); ASSERT_EQ(arr.size(), 4); - ASSERT_TRUE(arr[0].is()); - ASSERT_EQ(arr[0].get(), 1); + ASSERT_TRUE(arr[0].is()); + ASSERT_EQ(arr[0].get(), 1); - ASSERT_TRUE(arr[1].is()); - ASSERT_EQ(arr[1].get(), "two"); + ASSERT_TRUE(arr[1].is()); + ASSERT_EQ(arr[1].get(), "two"); - ASSERT_TRUE(arr[2].is()); - ASSERT_EQ(arr[2].get(), true); + ASSERT_TRUE(arr[2].is()); + ASSERT_EQ(arr[2].get(), true); ASSERT_TRUE(arr[3].isNull()); } @@ -66,11 +115,11 @@ TEST(Json, ParseNestedObject) { ASSERT_TRUE(obj["person"].is()); Json::Object personObj = obj["person"].get(); - ASSERT_TRUE(personObj["name"].is()); - ASSERT_EQ(personObj["name"].get(), "John"); + ASSERT_TRUE(personObj["name"].is()); + ASSERT_EQ(personObj["name"].get(), "John"); - ASSERT_TRUE(personObj["age"].is()); - ASSERT_EQ(personObj["age"].get(), 30); + ASSERT_TRUE(personObj["age"].is()); + ASSERT_EQ(personObj["age"].get(), 30); } TEST(Json, MalformedJsonThrowsException) { @@ -130,14 +179,14 @@ TEST(JsonSerializer, SerializeSimpleObject) { auto obj = json.get(); - ASSERT_TRUE(obj["name"].is()); - ASSERT_EQ(obj["name"].get(), "John"); + ASSERT_TRUE(obj["name"].is()); + ASSERT_EQ(obj["name"].get(), "John"); - ASSERT_TRUE(obj["age"].is()); - ASSERT_EQ(obj["age"].get(), 30); + ASSERT_TRUE(obj["age"].is()); + ASSERT_EQ(obj["age"].get(), 30); - ASSERT_TRUE(obj["isStudent"].is()); - ASSERT_EQ(obj["isStudent"].get(), false); + ASSERT_TRUE(obj["isStudent"].is()); + ASSERT_EQ(obj["isStudent"].get(), false); } TEST(JsonSerializer, SerializeArray) { @@ -158,14 +207,14 @@ TEST(JsonSerializer, SerializeArray) { auto obj = json.get(); ASSERT_EQ(obj.size(), 4); - ASSERT_TRUE(obj[0].is()); - ASSERT_EQ(obj[0].get(), 1); + ASSERT_TRUE(obj[0].is()); + ASSERT_EQ(obj[0].get(), 1); - ASSERT_TRUE(obj[1].is()); - ASSERT_EQ(obj[1].get(), "two"); + ASSERT_TRUE(obj[1].is()); + ASSERT_EQ(obj[1].get(), "two"); - ASSERT_TRUE(obj[2].is()); - ASSERT_EQ(obj[2].get(), true); + ASSERT_TRUE(obj[2].is()); + ASSERT_EQ(obj[2].get(), true); ASSERT_TRUE(obj[3].isNull()); } @@ -193,11 +242,11 @@ TEST(JsonSerializer, SerializeNestedObject) { auto personObj = obj["person"].get(); - ASSERT_TRUE(personObj["name"].is()); - ASSERT_EQ(personObj["name"].get(), "John"); + ASSERT_TRUE(personObj["name"].is()); + ASSERT_EQ(personObj["name"].get(), "John"); - ASSERT_TRUE(personObj["age"].is()); - ASSERT_EQ(personObj["age"].get(), 30); + ASSERT_TRUE(personObj["age"].is()); + ASSERT_EQ(personObj["age"].get(), 30); } TEST(JsonSerializer, SerializeComplexObject) { @@ -226,26 +275,26 @@ TEST(JsonSerializer, SerializeComplexObject) { ASSERT_TRUE(json.is()); auto obj = json.get(); - ASSERT_TRUE(obj["name"].is()); - ASSERT_EQ(obj["name"].get(), "John"); + ASSERT_TRUE(obj["name"].is()); + ASSERT_EQ(obj["name"].get(), "John"); - ASSERT_TRUE(obj["age"].is()); - ASSERT_EQ(obj["age"].get(), 30); + ASSERT_TRUE(obj["age"].is()); + ASSERT_EQ(obj["age"].get(), 30); - ASSERT_TRUE(obj["isStudent"].is()); - ASSERT_EQ(obj["isStudent"].get(), false); + ASSERT_TRUE(obj["isStudent"].is()); + ASSERT_EQ(obj["isStudent"].get(), false); ASSERT_TRUE(obj["scores"].is()); auto scores = obj["scores"].get(); ASSERT_EQ(scores.size(), 3); - ASSERT_EQ(scores[0].get(), 85.5); - ASSERT_EQ(scores[1].get(), 92); - ASSERT_EQ(scores[2].get(), 78.5); + ASSERT_EQ(scores[0].get(), 85.5); + ASSERT_EQ(scores[1].get(), 92); + ASSERT_EQ(scores[2].get(), 78.5); ASSERT_TRUE(obj["address"].is()); auto address = obj["address"].get(); - ASSERT_EQ(address["city"].get(), "New York"); - ASSERT_EQ(address["zip"].get(), "10001"); + ASSERT_EQ(address["city"].get(), "New York"); + ASSERT_EQ(address["zip"].get(), "10001"); } TEST(JsonSerializer, SerializeStringWithSpecialCharacters) { @@ -268,14 +317,14 @@ TEST(JsonSerializer, SerializeStringWithSpecialCharacters) { ASSERT_TRUE(json.is()); auto obj = json.get(); - ASSERT_TRUE(obj["name"].is()); - ASSERT_EQ(obj["name"].get(), "John \"Doe\""); + ASSERT_TRUE(obj["name"].is()); + ASSERT_EQ(obj["name"].get(), "John \"Doe\""); - ASSERT_TRUE(obj["city"].is()); - ASSERT_EQ(obj["city"].get(), "New\nYork"); + ASSERT_TRUE(obj["city"].is()); + ASSERT_EQ(obj["city"].get(), "New\nYork"); - ASSERT_TRUE(obj["zip"].is()); - ASSERT_EQ(obj["zip"].get(), "1000\b1"); + ASSERT_TRUE(obj["zip"].is()); + ASSERT_EQ(obj["zip"].get(), "1000\b1"); } TEST(JsonLexer, NextToken) { @@ -382,8 +431,8 @@ TEST(JsonParser, ParseJsonFromStream) { EXPECT_TRUE(json.is()); Json::Object &obj = json.get(); - EXPECT_TRUE(obj["name"].is()); - EXPECT_EQ(obj["name"].get(), "John"); + EXPECT_TRUE(obj["name"].is()); + EXPECT_EQ(obj["name"].get(), "John"); } { @@ -406,9 +455,9 @@ TEST(JsonParser, ParseJsonFromStream) { Json::Array &arr = json.get(); ASSERT_EQ(arr.size(), 3); - ASSERT_EQ(arr[0].get(), 1); - ASSERT_EQ(arr[1].get(), 2); - ASSERT_EQ(arr[2].get(), 3); + ASSERT_EQ(arr[0].get(), 1); + ASSERT_EQ(arr[1].get(), 2); + ASSERT_EQ(arr[2].get(), 3); } { @@ -418,7 +467,7 @@ TEST(JsonParser, ParseJsonFromStream) { EXPECT_TRUE(json.is()); Json::Object obj = json.get(); - EXPECT_TRUE(obj["name"].is()); - EXPECT_EQ(obj["name"].get(), "Eric"); + EXPECT_TRUE(obj["name"].is()); + EXPECT_EQ(obj["name"].get(), "Eric"); } }