From aac14ae16160257acbf721038603960870fcf89b Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 3 Jun 2026 01:49:49 +0800 Subject: [PATCH] feat(automation): JSON-RPC dispatcher envelope + version + error model --- .../GUI/Automation/JsonRpcDispatcher.cpp | 83 +++++++++++++++++++ .../GUI/Automation/JsonRpcDispatcher.hpp | 55 ++++++++++++ tests/automation/CMakeLists.txt | 2 + tests/automation/test_dispatcher.cpp | 44 ++++++++++ 4 files changed, 184 insertions(+) create mode 100644 src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp create mode 100644 src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp create mode 100644 tests/automation/test_dispatcher.cpp diff --git a/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp b/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp new file mode 100644 index 0000000000..22ae6567f1 --- /dev/null +++ b/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp @@ -0,0 +1,83 @@ +#include "JsonRpcDispatcher.hpp" +#include "WidgetSerializer.hpp" +#include "Locator.hpp" +#include +#include + +namespace Slic3r { namespace GUI { namespace Automation { + +JsonRpcDispatcher::JsonRpcDispatcher(IUiBackend& backend) : m_backend(backend) {} + +nlohmann::json JsonRpcDispatcher::make_result(const nlohmann::json& id, nlohmann::json result) { + return { {"jsonrpc","2.0"}, {"id", id}, {"result", std::move(result)} }; +} + +nlohmann::json JsonRpcDispatcher::make_error(const nlohmann::json& id, int code, + const std::string& msg) { + return { {"jsonrpc","2.0"}, {"id", id}, + {"error", { {"code", code}, {"message", msg} }} }; +} + +nlohmann::json JsonRpcDispatcher::m_version(const nlohmann::json&) { + return { {"version", kAutomationVersion}, + {"protocol", "2.0"}, + {"capabilities", nlohmann::json::array({ + "tree.dump","tree.find","widget.get","input.click","input.type", + "input.key","sync.wait_for","app.state","screenshot.window", + "screenshot.viewport3d" })} }; +} + +nlohmann::json JsonRpcDispatcher::dispatch(const nlohmann::json& request) { + nlohmann::json id = request.contains("id") ? request.at("id") : nlohmann::json(nullptr); + + if (!request.is_object() || !request.contains("method") || + !request.at("method").is_string()) { + return make_error(id, kInvalidRequest, "missing or invalid 'method'"); + } + const std::string method = request.at("method").get(); + const nlohmann::json params = + request.contains("params") ? request.at("params") : nlohmann::json::object(); + + try { + if (method == "automation.version") return make_result(id, m_version(params)); + if (method == "tree.dump") return make_result(id, m_tree_dump(params)); + if (method == "tree.find") return make_result(id, m_tree_find(params)); + if (method == "widget.get") return make_result(id, m_widget_get(params)); + if (method == "input.click") return make_result(id, m_input_click(params)); + if (method == "input.type") return make_result(id, m_input_type(params)); + if (method == "input.key") return make_result(id, m_input_key(params)); + if (method == "sync.wait_for") return make_result(id, m_sync_wait_for(params)); + if (method == "app.state") return make_result(id, m_app_state(params)); + if (method == "screenshot.window") return make_result(id, m_screenshot_window(params)); + if (method == "screenshot.viewport3d") return make_result(id, m_screenshot_viewport3d(params)); + return make_error(id, kMethodNotFound, "unknown method: " + method); + } catch (const AutomationError& e) { + return make_error(id, e.code, e.what()); + } catch (const std::exception& e) { + return make_error(id, kInvalidParams, e.what()); + } +} + +std::string JsonRpcDispatcher::handle_request(const std::string& body) { + nlohmann::json req; + try { + req = nlohmann::json::parse(body); + } catch (const std::exception& e) { + return make_error(nullptr, kParseError, std::string("parse error: ") + e.what()).dump(); + } + return dispatch(req).dump(); +} + +// --- method handlers implemented in Tasks 7-10 (stubs throw for now) --- +nlohmann::json JsonRpcDispatcher::m_tree_dump(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } +nlohmann::json JsonRpcDispatcher::m_tree_find(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } +nlohmann::json JsonRpcDispatcher::m_widget_get(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } +nlohmann::json JsonRpcDispatcher::m_input_click(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } +nlohmann::json JsonRpcDispatcher::m_input_type(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } +nlohmann::json JsonRpcDispatcher::m_input_key(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } +nlohmann::json JsonRpcDispatcher::m_sync_wait_for(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } +nlohmann::json JsonRpcDispatcher::m_app_state(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } +nlohmann::json JsonRpcDispatcher::m_screenshot_window(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } +nlohmann::json JsonRpcDispatcher::m_screenshot_viewport3d(const nlohmann::json&){ throw AutomationError(kMethodNotFound, "not implemented"); } + +}}} // namespace diff --git a/src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp b/src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp new file mode 100644 index 0000000000..ae42bb6e7c --- /dev/null +++ b/src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp @@ -0,0 +1,55 @@ +#pragma once +#include "IUiBackend.hpp" +#include +#include + +namespace Slic3r { namespace GUI { namespace Automation { + +// JSON-RPC 2.0 standard error codes. +constexpr int kParseError = -32700; +constexpr int kInvalidRequest = -32600; +constexpr int kMethodNotFound = -32601; +constexpr int kInvalidParams = -32602; +// Application error codes (design spec ยง5). +constexpr int kErrNotFound = 1001; // widget/target not found (or ambiguous) +constexpr int kErrNotActionable = 1002; // disabled / hidden +constexpr int kErrWaitTimeout = 1003; +constexpr int kErrGuiBusy = 1004; // GUI thread timeout +constexpr int kErrScreenshotFail = 1005; +constexpr int kErrDisabled = 1006; + +constexpr const char* kProtocolVersion = "2.0"; +constexpr const char* kAutomationVersion = "1.0.0"; + +class JsonRpcDispatcher { +public: + explicit JsonRpcDispatcher(IUiBackend& backend); + + // Parse a JSON-RPC request body, dispatch, and return the response body. + // Never throws; transport-level/parse errors become JSON-RPC error responses. + std::string handle_request(const std::string& body); + + // For tests: dispatch an already-parsed request object and return the response. + nlohmann::json dispatch(const nlohmann::json& request); + +private: + nlohmann::json make_result(const nlohmann::json& id, nlohmann::json result); + nlohmann::json make_error(const nlohmann::json& id, int code, const std::string& msg); + + // Method handlers (each returns the `result` object or throws AutomationError). + nlohmann::json m_version(const nlohmann::json& params); + nlohmann::json m_tree_dump(const nlohmann::json& params); + nlohmann::json m_tree_find(const nlohmann::json& params); + nlohmann::json m_widget_get(const nlohmann::json& params); + nlohmann::json m_input_click(const nlohmann::json& params); + nlohmann::json m_input_type(const nlohmann::json& params); + nlohmann::json m_input_key(const nlohmann::json& params); + nlohmann::json m_sync_wait_for(const nlohmann::json& params); + nlohmann::json m_app_state(const nlohmann::json& params); + nlohmann::json m_screenshot_window(const nlohmann::json& params); + nlohmann::json m_screenshot_viewport3d(const nlohmann::json& params); + + IUiBackend& m_backend; +}; + +}}} // namespace diff --git a/tests/automation/CMakeLists.txt b/tests/automation/CMakeLists.txt index cf73dd8d4c..122ec4cf22 100644 --- a/tests/automation/CMakeLists.txt +++ b/tests/automation/CMakeLists.txt @@ -4,9 +4,11 @@ add_executable(${_TEST_NAME}_tests automation_tests.cpp test_serializer.cpp test_locator.cpp + test_dispatcher.cpp MockUiBackend.cpp ${CMAKE_SOURCE_DIR}/src/slic3r/GUI/Automation/WidgetSerializer.cpp ${CMAKE_SOURCE_DIR}/src/slic3r/GUI/Automation/Locator.cpp + ${CMAKE_SOURCE_DIR}/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp ) target_link_libraries(${_TEST_NAME}_tests test_common Catch2::Catch2WithMain nlohmann_json) # The nlohmann_json INTERFACE target only exposes deps_src/nlohmann on the include diff --git a/tests/automation/test_dispatcher.cpp b/tests/automation/test_dispatcher.cpp new file mode 100644 index 0000000000..65a1645f7f --- /dev/null +++ b/tests/automation/test_dispatcher.cpp @@ -0,0 +1,44 @@ +#include +#include "slic3r/GUI/Automation/JsonRpcDispatcher.hpp" +#include "MockUiBackend.hpp" + +using namespace Slic3r::GUI::Automation; +using nlohmann::json; + +TEST_CASE("dispatch automation.version", "[automation][rpc]") { + MockUiBackend mock; + JsonRpcDispatcher d(mock); + const json req = {{"jsonrpc","2.0"},{"id",1},{"method","automation.version"}}; + const json resp = d.dispatch(req); + CHECK(resp.at("jsonrpc") == "2.0"); + CHECK(resp.at("id") == 1); + CHECK(resp.at("result").at("version") == kAutomationVersion); + CHECK(resp.at("result").at("protocol") == "2.0"); + CHECK(resp.at("result").at("capabilities").is_array()); +} + +TEST_CASE("unknown method -> -32601", "[automation][rpc]") { + MockUiBackend mock; + JsonRpcDispatcher d(mock); + const json req = {{"jsonrpc","2.0"},{"id",7},{"method","does.not.exist"}}; + const json resp = d.dispatch(req); + CHECK(resp.at("id") == 7); + CHECK(resp.at("error").at("code") == kMethodNotFound); +} + +TEST_CASE("malformed JSON body -> parse error", "[automation][rpc]") { + MockUiBackend mock; + JsonRpcDispatcher d(mock); + const std::string resp = d.handle_request("{not json"); + const json j = json::parse(resp); + CHECK(j.at("error").at("code") == kParseError); + CHECK(j.at("id").is_null()); +} + +TEST_CASE("missing method field -> invalid request", "[automation][rpc]") { + MockUiBackend mock; + JsonRpcDispatcher d(mock); + const json req = {{"jsonrpc","2.0"},{"id",2}}; + const json resp = d.dispatch(req); + CHECK(resp.at("error").at("code") == kInvalidRequest); +}