feat(automation): JSON-RPC dispatcher envelope + version + error model

This commit is contained in:
SoftFever
2026-06-03 01:49:49 +08:00
parent 94c356845e
commit aac14ae161
4 changed files with 184 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
#include "JsonRpcDispatcher.hpp"
#include "WidgetSerializer.hpp"
#include "Locator.hpp"
#include <chrono>
#include <thread>
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<std::string>();
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

View File

@@ -0,0 +1,55 @@
#pragma once
#include "IUiBackend.hpp"
#include <nlohmann/json.hpp>
#include <string>
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

View File

@@ -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

View File

@@ -0,0 +1,44 @@
#include <catch2/catch_all.hpp>
#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);
}