From 3ed0ff34b37bb1481d0a49bac40efadfa7fde01b Mon Sep 17 00:00:00 2001 From: Rob Hogan Date: Wed, 6 Mar 2024 09:27:50 -0800 Subject: [PATCH] Add helpers for formatting JSON CDP responses (#43340) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/43340 Adds convenience methods `jsonResult`, `jsonError` and `jsonNotification` for more ergonomic construction of CDP JSON responses. Note that CDP is *loosely* based on [JSON-RPC 2.0](https://www.jsonrpc.org/specification), but differs for example in the omission of `"jsonrpc": "2.0"`. Before: ``` frontendChannel_(folly::toJson(folly::dynamic::object("id", req.id)( "error", folly::dynamic::object("code", -32602)( "message", "executionContextName is mutually exclusive with executionContextId")))); ``` After: ``` frontendChannel_(cdp::jsonError( req.id, cdp::ErrorCode::InvalidParams, "executionContextName is mutually exclusive with executionContextId")); ``` Changelog: [Internal] Reviewed By: motiz88 Differential Revision: D54202854 fbshipit-source-id: 76a407ae39ff9c2ec79bcaddb6cd4d494afb7693 --- .../jsinspector-modern/CdpJson.cpp | 61 +++++++++ .../ReactCommon/jsinspector-modern/CdpJson.h | 124 ++++++++++++++++++ .../FallbackRuntimeAgentDelegate.cpp | 19 ++- .../jsinspector-modern/HostAgent.cpp | 40 +++--- .../jsinspector-modern/HostAgent.h | 1 - .../jsinspector-modern/HostTarget.cpp | 17 +-- .../jsinspector-modern/InstanceAgent.cpp | 11 +- .../jsinspector-modern/InstanceAgent.h | 2 +- .../jsinspector-modern/Parsing.cpp | 32 ----- .../ReactCommon/jsinspector-modern/Parsing.h | 69 ---------- .../jsinspector-modern/RuntimeAgent.cpp | 38 +++--- .../jsinspector-modern/RuntimeAgent.h | 3 +- .../jsinspector-modern/RuntimeAgentDelegate.h | 2 +- 13 files changed, 241 insertions(+), 178 deletions(-) create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/CdpJson.cpp create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/CdpJson.h delete mode 100644 packages/react-native/ReactCommon/jsinspector-modern/Parsing.cpp delete mode 100644 packages/react-native/ReactCommon/jsinspector-modern/Parsing.h diff --git a/packages/react-native/ReactCommon/jsinspector-modern/CdpJson.cpp b/packages/react-native/ReactCommon/jsinspector-modern/CdpJson.cpp new file mode 100644 index 00000000000000..d91d3f3678a38c --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/CdpJson.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "CdpJson.h" + +#include +#include + +namespace facebook::react::jsinspector_modern::cdp { + +PreparsedRequest preparse(std::string_view message) { + folly::dynamic parsed = folly::parseJson(message); + return PreparsedRequest{ + .id = parsed["id"].getInt(), + .method = parsed["method"].getString(), + .params = parsed.count("params") != 0u ? parsed["params"] : nullptr}; +} + +std::string PreparsedRequest::toJson() const { + folly::dynamic obj = folly::dynamic::object; + obj["id"] = id; + obj["method"] = method; + if (params != nullptr) { + obj["params"] = params; + } + return folly::toJson(obj); +} + +std::string jsonError( + std::optional id, + ErrorCode code, + std::optional message) { + auto dynamicError = folly::dynamic::object("code", static_cast(code)); + if (message) { + dynamicError("message", *message); + } + return folly::toJson( + (id ? folly::dynamic::object("id", *id) + : folly::dynamic::object( + "id", nullptr))("error", std::move(dynamicError))); +} + +std::string jsonResult(RequestId id, const folly::dynamic& result) { + return folly::toJson(folly::dynamic::object("id", id)("result", result)); +} + +std::string jsonNotification( + std::string_view method, + std::optional params) { + auto dynamicNotification = folly::dynamic::object("method", method); + if (params) { + dynamicNotification("params", *params); + } + return folly::toJson(std::move(dynamicNotification)); +} + +} // namespace facebook::react::jsinspector_modern::cdp diff --git a/packages/react-native/ReactCommon/jsinspector-modern/CdpJson.h b/packages/react-native/ReactCommon/jsinspector-modern/CdpJson.h new file mode 100644 index 00000000000000..b77e40f7ef7cfb --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/CdpJson.h @@ -0,0 +1,124 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react::jsinspector_modern::cdp { + +using RequestId = long long; + +/** + * Error codes to be used in CDP responses. + * https://www.jsonrpc.org/specification#error_object + */ +enum class ErrorCode { + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603 + /* -32000 to -32099: Implementation-defined server errors. */ +}; + +/** + * An incoming CDP request that has been parsed into a more usable form. + */ +struct PreparsedRequest { + public: + /** + * The ID of the request. + */ + RequestId id{}; + + /** + * The name of the method being invoked. + */ + std::string method; + + /** + * The parameters passed to the method, if any. + */ + folly::dynamic params; + + /** + * Equality operator, useful for unit tests + */ + inline bool operator==(const PreparsedRequest& rhs) const { + return id == rhs.id && method == rhs.method && params == rhs.params; + } + + std::string toJson() const; +}; + +/** + * Parse a JSON-encoded CDP request into its constituent parts. + * \throws ParseError If the input cannot be parsed. + * \throws TypeError If the input does not conform to the expected format. + */ +PreparsedRequest preparse(std::string_view message); + +/** + * A type error that may be thrown while preparsing a request, or while + * accessing dynamic params on a request. + */ +using TypeError = folly::TypeError; + +/** + * A parse error that may be thrown while preparsing a request. + */ +using ParseError = folly::json::parse_error; + +/** + * Helper functions for creating CDP (loosely JSON-RPC) messages of various + * types, returning a JSON string ready for sending over the wire. + */ + +/** + * Returns a JSON-formatted string representing an error. + * + * {"id": , "error": { "code": , "message": }} + * + * \param id Request ID. Mandatory, null only if the request omitted it or + * could not be parsed. + * \param code Integer code from cdp::ErrorCode. + * \param message Optional, brief human-readable error message. + */ +std::string jsonError( + std::optional id, + ErrorCode code, + std::optional message = std::nullopt); + +/** + * Returns a JSON-formatted string representing a successful response. + * + * {"id": , "result": } + * + * \param id The id of the request that this response corresponds to. + * \param result Result payload, defaulting to {}. + */ +std::string jsonResult( + RequestId id, + const folly::dynamic& result = folly::dynamic::object()); + +/** + * Returns a JSON-formatted string representing a unilateral notifcation. + * + * {"method": , "params": } + * + * \param method Notification (aka "event") method. + * \param params Optional payload pbject. + */ +std::string jsonNotification( + std::string_view method, + std::optional params = std::nullopt); + +} // namespace facebook::react::jsinspector_modern::cdp diff --git a/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeAgentDelegate.cpp b/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeAgentDelegate.cpp index 75f74e5f285302..5cab36048b67ff 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeAgentDelegate.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/FallbackRuntimeAgentDelegate.cpp @@ -54,17 +54,16 @@ void FallbackRuntimeAgentDelegate::sendFallbackRuntimeWarning() { } void FallbackRuntimeAgentDelegate::sendWarningLogEntry(std::string_view text) { - frontendChannel_( - folly::toJson(folly::dynamic::object("method", "Log.entryAdded")( - "params", + frontendChannel_(cdp::jsonNotification( + "Log.entryAdded", + folly::dynamic::object( + "entry", folly::dynamic::object( - "entry", - folly::dynamic::object( - "timestamp", - duration_cast( - system_clock::now().time_since_epoch()) - .count())("source", "other")( - "level", "warning")("text", text))))); + "timestamp", + duration_cast( + system_clock::now().time_since_epoch()) + .count())("source", "other")( + "level", "warning")("text", text)))); } } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp index 9d26aa4d7bb003..2415586f099a07 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +#include "CdpJson.h" + #include #include #include @@ -98,33 +100,27 @@ void HostAgent::handleRequest(const cdp::PreparsedRequest& req) { } if (shouldSendOKResponse) { - folly::dynamic res = folly::dynamic::object("id", req.id)( - "result", folly::dynamic::object()); - std::string json = folly::toJson(res); - frontendChannel_(json); + frontendChannel_(cdp::jsonResult(req.id)); return; } - folly::dynamic res = folly::dynamic::object("id", req.id)( - "error", - folly::dynamic::object("code", -32601)( - "message", req.method + " not implemented yet")); - std::string json = folly::toJson(res); - frontendChannel_(json); + frontendChannel_(cdp::jsonError( + req.id, + cdp::ErrorCode::MethodNotFound, + req.method + " not implemented yet")); } void HostAgent::sendInfoLogEntry(std::string_view text) { - frontendChannel_( - folly::toJson(folly::dynamic::object("method", "Log.entryAdded")( - "params", + frontendChannel_(cdp::jsonNotification( + "Log.entryAdded", + folly::dynamic::object( + "entry", folly::dynamic::object( - "entry", - folly::dynamic::object( - "timestamp", - duration_cast( - system_clock::now().time_since_epoch()) - .count())("source", "other")( - "level", "info")("text", text))))); + "timestamp", + duration_cast( + system_clock::now().time_since_epoch()) + .count())("source", "other")( + "level", "info")("text", text)))); } void HostAgent::setCurrentInstanceAgent( @@ -140,9 +136,7 @@ void HostAgent::setCurrentInstanceAgent( // Because we can only have a single instance, we can report all contexts // as cleared. - folly::dynamic contextsCleared = - folly::dynamic::object("method", "Runtime.executionContextsCleared"); - frontendChannel_(folly::toJson(contextsCleared)); + frontendChannel_(cdp::jsonNotification("Runtime.executionContextsCleared")); } if (instanceAgent_) { // TODO: Send Runtime.executionContextCreated here - at the moment we expect diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h index 6830e0b4521166..aec355bc3ce284 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.h @@ -12,7 +12,6 @@ #include #include -#include #include #include diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp index 4817e4d575754c..2bbb8d78e74b00 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostTarget.cpp @@ -6,11 +6,11 @@ */ #include "HostTarget.h" +#include "CdpJson.h" #include "HostAgent.h" #include "InspectorInterfaces.h" #include "InspectorUtilities.h" #include "InstanceTarget.h" -#include "Parsing.h" #include "SessionState.h" #include @@ -53,14 +53,12 @@ class HostTargetSession { try { request = cdp::preparse(message); } catch (const cdp::ParseError& e) { - frontendChannel_(folly::toJson(folly::dynamic::object("id", nullptr)( - "error", - folly::dynamic::object("code", -32700)("message", e.what())))); + frontendChannel_( + cdp::jsonError(std::nullopt, cdp::ErrorCode::ParseError, e.what())); return; } catch (const cdp::TypeError& e) { - frontendChannel_(folly::toJson(folly::dynamic::object("id", nullptr)( - "error", - folly::dynamic::object("code", -32600)("message", e.what())))); + frontendChannel_(cdp::jsonError( + std::nullopt, cdp::ErrorCode::InvalidRequest, e.what())); return; } @@ -69,9 +67,8 @@ class HostTargetSession { try { hostAgent_.handleRequest(request); } catch (const cdp::TypeError& e) { - frontendChannel_(folly::toJson(folly::dynamic::object("id", request.id)( - "error", - folly::dynamic::object("code", -32600)("message", e.what())))); + frontendChannel_( + cdp::jsonError(request.id, cdp::ErrorCode::InvalidRequest, e.what())); return; } } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp index fdb57b0b4c7dcc..a68d82a479b222 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp @@ -6,6 +6,7 @@ */ #include +#include "CdpJson.h" #include "RuntimeTarget.h" namespace facebook::react::jsinspector_modern { @@ -49,9 +50,8 @@ void InstanceAgent::setCurrentRuntime(RuntimeTarget* runtimeTarget) { if (previousContext.uniqueId.has_value()) { params["executionContextUniqueId"] = *previousContext.uniqueId; } - folly::dynamic contextDestroyed = folly::dynamic::object( - "method", "Runtime.executionContextDestroyed")("params", params); - frontendChannel_(folly::toJson(contextDestroyed)); + frontendChannel_( + cdp::jsonNotification("Runtime.executionContextDestroyed", params)); } maybeSendExecutionContextCreatedNotification(); } @@ -66,9 +66,8 @@ void InstanceAgent::maybeSendExecutionContextCreatedNotification() { if (newContext.uniqueId.has_value()) { params["uniqueId"] = *newContext.uniqueId; } - folly::dynamic contextCreated = folly::dynamic::object( - "method", "Runtime.executionContextCreated")("params", params); - frontendChannel_(folly::toJson(contextCreated)); + frontendChannel_( + cdp::jsonNotification("Runtime.executionContextCreated", params)); } } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.h index 70b7fe8d5e5c6a..167c4c2205bea9 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.h @@ -7,11 +7,11 @@ #pragma once +#include "CdpJson.h" #include "RuntimeTarget.h" #include "SessionState.h" #include -#include #include #include diff --git a/packages/react-native/ReactCommon/jsinspector-modern/Parsing.cpp b/packages/react-native/ReactCommon/jsinspector-modern/Parsing.cpp deleted file mode 100644 index 99b233aa9ca728..00000000000000 --- a/packages/react-native/ReactCommon/jsinspector-modern/Parsing.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#include -#include -#include - -namespace facebook::react::jsinspector_modern::cdp { - -PreparsedRequest preparse(std::string_view message) { - folly::dynamic parsed = folly::parseJson(message); - return PreparsedRequest{ - .id = parsed["id"].getInt(), - .method = parsed["method"].getString(), - .params = parsed.count("params") ? parsed["params"] : nullptr}; -} - -std::string PreparsedRequest::toJson() const { - folly::dynamic obj = folly::dynamic::object; - obj["id"] = id; - obj["method"] = method; - if (params != nullptr) { - obj["params"] = params; - } - return folly::toJson(obj); -} - -} // namespace facebook::react::jsinspector_modern::cdp diff --git a/packages/react-native/ReactCommon/jsinspector-modern/Parsing.h b/packages/react-native/ReactCommon/jsinspector-modern/Parsing.h deleted file mode 100644 index bd446fb1971448..00000000000000 --- a/packages/react-native/ReactCommon/jsinspector-modern/Parsing.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -#pragma once - -#include -#include -#include -#include - -namespace facebook::react::jsinspector_modern { - -namespace cdp { -using RequestId = long long; - -/** - * An incoming CDP request that has been parsed into a more usable form. - */ -struct PreparsedRequest { - public: - /** - * The ID of the request. - */ - RequestId id; - - /** - * The name of the method being invoked. - */ - std::string method; - - /** - * The parameters passed to the method, if any. - */ - folly::dynamic params; - - /** - * Equality operator, useful for unit tests - */ - inline bool operator==(const PreparsedRequest& rhs) const { - return id == rhs.id && method == rhs.method && params == rhs.params; - } - - std::string toJson() const; -}; - -/** - * Parse a JSON-encoded CDP request into its constituent parts. - * \throws ParseError If the input cannot be parsed. - * \throws TypeError If the input does not conform to the expected format. - */ -PreparsedRequest preparse(std::string_view message); - -/** - * A type error that may be thrown while preparsing a request, or while - * accessing dynamic params on a request. - */ -using TypeError = folly::TypeError; - -/** - * A parse error that may be thrown while preparsing a request. - */ -using ParseError = folly::json::parse_error; -} // namespace cdp - -} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp index efac621026d638..0172ff47d13355 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp @@ -42,21 +42,20 @@ bool RuntimeAgent::handleRequest(const cdp::PreparsedRequest& req) { auto executionContextId = req.params["executionContextId"].getInt(); if (executionContextId < (int64_t)std::numeric_limits::min() || executionContextId > (int64_t)std::numeric_limits::max()) { - frontendChannel_(folly::toJson(folly::dynamic::object("id", req.id)( - "error", - folly::dynamic::object("code", -32602)( - "message", "Invalid execution context id")))); + frontendChannel_(cdp::jsonError( + req.id, + cdp::ErrorCode::InvalidParams, + "Invalid execution context id")); return true; } contextSelector = ExecutionContextSelector::byId((int32_t)executionContextId); if (req.params.count("executionContextName")) { - frontendChannel_(folly::toJson(folly::dynamic::object("id", req.id)( - "error", - folly::dynamic::object("code", -32602)( - "message", - "executionContextName is mutually exclusive with executionContextId")))); + frontendChannel_(cdp::jsonError( + req.id, + cdp::ErrorCode::InvalidParams, + "executionContextName is mutually exclusive with executionContextId")); return true; } } else if (req.params.count("executionContextName")) { @@ -68,10 +67,7 @@ bool RuntimeAgent::handleRequest(const cdp::PreparsedRequest& req) { } sessionState_.subscribedBindings[bindingName].insert(contextSelector); - folly::dynamic res = folly::dynamic::object("id", req.id)( - "result", folly::dynamic::object()); - std::string json = folly::toJson(res); - frontendChannel_(json); + frontendChannel_(cdp::jsonResult(req.id)); return true; } @@ -83,10 +79,7 @@ bool RuntimeAgent::handleRequest(const cdp::PreparsedRequest& req) { // if the subscription is targeted by context name. sessionState_.subscribedBindings.erase(req.params["name"].getString()); - folly::dynamic res = folly::dynamic::object("id", req.id)( - "result", folly::dynamic::object()); - std::string json = folly::toJson(res); - frontendChannel_(json); + frontendChannel_(cdp::jsonResult(req.id)); return true; } @@ -113,12 +106,11 @@ void RuntimeAgent::notifyBindingCalled( return; } - frontendChannel_( - folly::toJson(folly::dynamic::object("method", "Runtime.bindingCalled")( - "params", - folly::dynamic::object( - "executionContextId", executionContextDescription_.id)( - "name", bindingName)("payload", payload)))); + frontendChannel_(cdp::jsonNotification( + "Runtime.bindingCalled", + folly::dynamic::object( + "executionContextId", executionContextDescription_.id)( + "name", bindingName)("payload", payload))); } RuntimeAgent::ExportedState RuntimeAgent::getExportedState() { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h index 9a96760e565fbb..4cc01bc1bd0d9a 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h @@ -7,12 +7,11 @@ #pragma once +#include "CdpJson.h" #include "InspectorInterfaces.h" #include "RuntimeAgentDelegate.h" #include "RuntimeTarget.h" -#include - namespace facebook::react::jsinspector_modern { class RuntimeTargetController; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgentDelegate.h b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgentDelegate.h index d7042d60d48c24..d5e62d1b576c2c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgentDelegate.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgentDelegate.h @@ -7,7 +7,7 @@ #pragma once -#include +#include "CdpJson.h" namespace facebook::react::jsinspector_modern {