diff --git a/.github/workflows/website-build.yml b/.github/workflows/website-build.yml index f1015c9eb..f40f41b82 100644 --- a/.github/workflows/website-build.yml +++ b/.github/workflows/website-build.yml @@ -38,6 +38,7 @@ jobs: -DSOURCEMETA_CORE_JSONSCHEMA:BOOL=OFF -DSOURCEMETA_CORE_JSONPOINTER:BOOL=OFF -DSOURCEMETA_CORE_YAML:BOOL=OFF + -DSOURCEMETA_CORE_JSONRPC:BOOL=OFF -DSOURCEMETA_CORE_SEMVER:BOOL=OFF -DSOURCEMETA_CORE_GZIP:BOOL=OFF -DSOURCEMETA_CORE_HTML:BOOL=OFF diff --git a/.github/workflows/website-deploy.yml b/.github/workflows/website-deploy.yml index b9f3e4a66..f8f1e35a5 100644 --- a/.github/workflows/website-deploy.yml +++ b/.github/workflows/website-deploy.yml @@ -48,6 +48,7 @@ jobs: -DSOURCEMETA_CORE_JSONSCHEMA:BOOL=OFF -DSOURCEMETA_CORE_JSONPOINTER:BOOL=OFF -DSOURCEMETA_CORE_YAML:BOOL=OFF + -DSOURCEMETA_CORE_JSONRPC:BOOL=OFF -DSOURCEMETA_CORE_SEMVER:BOOL=OFF -DSOURCEMETA_CORE_GZIP:BOOL=OFF -DSOURCEMETA_CORE_HTML:BOOL=OFF diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b29bab01..7d37f11d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,7 @@ option(SOURCEMETA_CORE_JSONSCHEMA "Build the Sourcemeta Core JSON Schema library option(SOURCEMETA_CORE_JSONPOINTER "Build the Sourcemeta Core JSON Pointer library" ON) option(SOURCEMETA_CORE_JSONL "Build the Sourcemeta Core JSONL library" ON) option(SOURCEMETA_CORE_YAML "Build the Sourcemeta Core YAML library" ON) +option(SOURCEMETA_CORE_JSONRPC "Build the Sourcemeta Core JSON-RPC library" ON) option(SOURCEMETA_CORE_SEMVER "Build the Sourcemeta Core SemVer library" ON) option(SOURCEMETA_CORE_GZIP "Build the Sourcemeta Core GZIP library" ON) option(SOURCEMETA_CORE_HTML "Build the Sourcemeta Core HTML library" ON) @@ -164,6 +165,10 @@ if(SOURCEMETA_CORE_YAML) add_subdirectory(src/core/yaml) endif() +if(SOURCEMETA_CORE_JSONRPC) + add_subdirectory(src/core/jsonrpc) +endif() + if(SOURCEMETA_CORE_SEMVER) add_subdirectory(src/core/semver) endif() @@ -300,6 +305,10 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/yaml) endif() + if(SOURCEMETA_CORE_JSONRPC) + add_subdirectory(test/jsonrpc) + endif() + if(SOURCEMETA_CORE_SEMVER) add_subdirectory(test/semver) endif() diff --git a/config.cmake.in b/config.cmake.in index ccfee7085..ed2c352c2 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -24,6 +24,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonpointer) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonschema) list(APPEND SOURCEMETA_CORE_COMPONENTS yaml) + list(APPEND SOURCEMETA_CORE_COMPONENTS jsonrpc) list(APPEND SOURCEMETA_CORE_COMPONENTS semver) list(APPEND SOURCEMETA_CORE_COMPONENTS gzip) list(APPEND SOURCEMETA_CORE_COMPONENTS html) @@ -122,6 +123,13 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonpointer.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_yaml.cmake") + elseif(component STREQUAL "jsonrpc") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonrpc.cmake") elseif(component STREQUAL "semver") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_semver.cmake") diff --git a/src/core/jsonrpc/CMakeLists.txt b/src/core/jsonrpc/CMakeLists.txt new file mode 100644 index 000000000..436c7595c --- /dev/null +++ b/src/core/jsonrpc/CMakeLists.txt @@ -0,0 +1,8 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jsonrpc + SOURCES jsonrpc.cc) + +target_link_libraries(sourcemeta_core_jsonrpc PUBLIC sourcemeta::core::json) + +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME jsonrpc) +endif() diff --git a/src/core/jsonrpc/include/sourcemeta/core/jsonrpc.h b/src/core/jsonrpc/include/sourcemeta/core/jsonrpc.h new file mode 100644 index 000000000..3ac86a8ae --- /dev/null +++ b/src/core/jsonrpc/include/sourcemeta/core/jsonrpc.h @@ -0,0 +1,312 @@ +#ifndef SOURCEMETA_CORE_JSONRPC_H_ +#define SOURCEMETA_CORE_JSONRPC_H_ + +#ifndef SOURCEMETA_CORE_JSONRPC_EXPORT +#include +#endif + +#include + +#include // std::int64_t +#include // std::optional, std::nullopt +#include // std::string_view + +/// @defgroup jsonrpc JSON-RPC +/// @brief An implementation of the JSON-RPC 2.0 specification. +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +namespace sourcemeta::core { + +/// @ingroup jsonrpc +/// The pre-defined JSON-RPC 2.0 error code for invalid JSON received by the +/// server. +constexpr std::int64_t JSONRPC_CODE_PARSE = -32700; + +/// @ingroup jsonrpc +/// The pre-defined JSON-RPC 2.0 error code for a malformed request envelope. +constexpr std::int64_t JSONRPC_CODE_INVALID_REQUEST = -32600; + +/// @ingroup jsonrpc +/// The pre-defined JSON-RPC 2.0 error code for unknown methods. +constexpr std::int64_t JSONRPC_CODE_METHOD_NOT_FOUND = -32601; + +/// @ingroup jsonrpc +/// The pre-defined JSON-RPC 2.0 error code for invalid method parameters. +constexpr std::int64_t JSONRPC_CODE_INVALID_PARAMS = -32602; + +/// @ingroup jsonrpc +/// The pre-defined JSON-RPC 2.0 error code for internal server errors. +constexpr std::int64_t JSONRPC_CODE_INTERNAL = -32603; + +/// @ingroup jsonrpc +/// The lower bound of the JSON-RPC 2.0 reserved range for +/// implementation-defined server errors. +constexpr std::int64_t JSONRPC_CODE_SERVER_ERROR_MIN = -32099; + +/// @ingroup jsonrpc +/// The upper bound of the JSON-RPC 2.0 reserved range for +/// implementation-defined server errors. +constexpr std::int64_t JSONRPC_CODE_SERVER_ERROR_MAX = -32000; + +/// @ingroup jsonrpc +/// Check whether the given code lies within the JSON-RPC 2.0 reserved range +/// for implementation-defined server errors. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// assert(sourcemeta::core::jsonrpc_is_server_error(-32050)); +/// assert(!sourcemeta::core::jsonrpc_is_server_error(-32603)); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_is_server_error(const std::int64_t code) -> bool; + +/// @ingroup jsonrpc +/// Extract the request identifier from a JSON-RPC 2.0 envelope. Returns a +/// pointer to the identifier (string, number, or null per the specification) +/// or `nullptr` when the field is missing or not one of those types. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto request{sourcemeta::core::parse_json( +/// R"({ "jsonrpc": "2.0", "id": 7, "method": "ping" })")}; +/// const auto *identifier{sourcemeta::core::jsonrpc_request_id(request)}; +/// assert(identifier != nullptr); +/// assert(identifier->to_integer() == 7); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_request_id(const sourcemeta::core::JSON &request) + -> const sourcemeta::core::JSON *; + +/// @ingroup jsonrpc +/// Check whether the given JSON value is a well-formed JSON-RPC 2.0 request. +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto request{sourcemeta::core::parse_json( +/// R"({ "jsonrpc": "2.0", "id": 1, "method": "ping" })")}; +/// assert(sourcemeta::core::jsonrpc_is_request(request)); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_is_request(const sourcemeta::core::JSON &request) -> bool; + +/// @ingroup jsonrpc +/// Extract the method name from a JSON-RPC 2.0 envelope, or an empty view +/// when the method field is missing or not a string. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto request{sourcemeta::core::parse_json( +/// R"({ "jsonrpc": "2.0", "id": 1, "method": "ping" })")}; +/// assert(sourcemeta::core::jsonrpc_method(request) == "ping"); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_method(const sourcemeta::core::JSON &request) -> std::string_view; + +/// @ingroup jsonrpc +/// Extract the params from a JSON-RPC 2.0 envelope, or `nullptr` when the +/// value is missing or not an object or array. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto request{sourcemeta::core::parse_json(R"JSON({ +/// "jsonrpc": "2.0", +/// "id": 1, +/// "method": "subtract", +/// "params": [ 42, 23 ] +/// })JSON")}; +/// const auto *parameters{sourcemeta::core::jsonrpc_params(request)}; +/// assert(parameters != nullptr); +/// assert(parameters->is_array()); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_params(const sourcemeta::core::JSON &request) + -> const sourcemeta::core::JSON *; + +/// @ingroup jsonrpc +/// Check whether the given JSON value is a well-formed JSON-RPC 2.0 +/// notification (a request without an identifier). For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto request{sourcemeta::core::parse_json( +/// R"({ "jsonrpc": "2.0", "method": "notifications/initialized" })")}; +/// assert(sourcemeta::core::jsonrpc_is_notification(request)); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_is_notification(const sourcemeta::core::JSON &request) -> bool; + +/// @ingroup jsonrpc +/// Construct a successful JSON-RPC 2.0 response envelope with the given +/// identifier and result. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// #include +/// +/// const auto identifier{sourcemeta::core::JSON{1}}; +/// auto result{sourcemeta::core::JSON::make_object()}; +/// result.assign("foo", sourcemeta::core::JSON{42}); +/// const auto envelope{ +/// sourcemeta::core::jsonrpc_make_success(identifier, std::move(result))}; +/// assert(envelope.at("id").to_integer() == 1); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_make_success(const sourcemeta::core::JSON &identifier, + sourcemeta::core::JSON result) + -> sourcemeta::core::JSON; + +/// @ingroup jsonrpc +/// Construct a successful JSON-RPC 2.0 response envelope with the given +/// identifier and an empty object result. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto identifier{sourcemeta::core::JSON{1}}; +/// const auto envelope{ +/// sourcemeta::core::jsonrpc_make_success_empty(identifier)}; +/// assert(envelope.at("result").is_object()); +/// assert(envelope.at("result").empty()); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_make_success_empty(const sourcemeta::core::JSON &identifier) + -> sourcemeta::core::JSON; + +/// @ingroup jsonrpc +/// Construct a JSON-RPC 2.0 error response envelope. Passing `nullptr` for the +/// identifier writes an `"id": null` member, as the specification requires +/// when the originating request could not be parsed. The optional data +/// carries implementation-specific extra information. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto identifier{sourcemeta::core::JSON{1}}; +/// const auto envelope{sourcemeta::core::jsonrpc_make_error( +/// &identifier, -32000, "Server error")}; +/// assert(envelope.at("error").at("code").to_integer() == -32000); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_make_error(const sourcemeta::core::JSON *identifier, + const std::int64_t code, const std::string_view message, + std::optional data = + std::nullopt) -> sourcemeta::core::JSON; + +/// @ingroup jsonrpc +/// Get the canonical JSON-RPC 2.0 parse-error envelope. The returned reference +/// is to a static instance that lives for the duration of the program. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto &envelope{sourcemeta::core::jsonrpc_make_error_parse()}; +/// assert(envelope.at("error").at("code").to_integer() == -32700); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_make_error_parse() -> const sourcemeta::core::JSON &; + +/// @ingroup jsonrpc +/// Construct a JSON-RPC 2.0 invalid-request error envelope, optionally +/// carrying the offending request identifier. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto envelope{sourcemeta::core::jsonrpc_make_error_invalid_request()}; +/// assert(envelope.at("error").at("code").to_integer() == -32600); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_make_error_invalid_request( + const sourcemeta::core::JSON *identifier = nullptr) + -> sourcemeta::core::JSON; + +/// @ingroup jsonrpc +/// Construct a JSON-RPC 2.0 method-not-found error envelope for the given +/// request identifier. For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto identifier{sourcemeta::core::JSON{2}}; +/// const auto envelope{ +/// sourcemeta::core::jsonrpc_make_error_method_not_found(identifier)}; +/// assert(envelope.at("error").at("code").to_integer() == -32601); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_make_error_method_not_found( + const sourcemeta::core::JSON &identifier) -> sourcemeta::core::JSON; + +/// @ingroup jsonrpc +/// Construct a JSON-RPC 2.0 invalid-params error envelope for the given +/// request identifier, optionally carrying implementation-specific data. For +/// example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// const auto identifier{sourcemeta::core::JSON{"req-7"}}; +/// const auto envelope{ +/// sourcemeta::core::jsonrpc_make_error_invalid_params(identifier)}; +/// assert(envelope.at("error").at("code").to_integer() == -32602); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_make_error_invalid_params( + const sourcemeta::core::JSON &identifier, + std::optional data = std::nullopt) + -> sourcemeta::core::JSON; + +/// @ingroup jsonrpc +/// Construct a JSON-RPC 2.0 internal-error envelope, optionally carrying the +/// offending request identifier. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto envelope{sourcemeta::core::jsonrpc_make_error_internal()}; +/// assert(envelope.at("error").at("code").to_integer() == -32603); +/// ``` +SOURCEMETA_CORE_JSONRPC_EXPORT +auto jsonrpc_make_error_internal(const sourcemeta::core::JSON *identifier = + nullptr) -> sourcemeta::core::JSON; + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/jsonrpc/jsonrpc.cc b/src/core/jsonrpc/jsonrpc.cc new file mode 100644 index 000000000..8aa10ade4 --- /dev/null +++ b/src/core/jsonrpc/jsonrpc.cc @@ -0,0 +1,198 @@ +#include + +#include + +#include // std::int64_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move + +namespace { + +const auto JSONRPC_HASH_ID{ + sourcemeta::core::JSON::make_object().as_object().hash("id")}; +const auto JSONRPC_HASH_JSONRPC{ + sourcemeta::core::JSON::make_object().as_object().hash("jsonrpc")}; +const auto JSONRPC_HASH_METHOD{ + sourcemeta::core::JSON::make_object().as_object().hash("method")}; +const auto JSONRPC_HASH_RESULT{ + sourcemeta::core::JSON::make_object().as_object().hash("result")}; +const auto JSONRPC_HASH_ERROR{ + sourcemeta::core::JSON::make_object().as_object().hash("error")}; +const auto JSONRPC_HASH_CODE{ + sourcemeta::core::JSON::make_object().as_object().hash("code")}; +const auto JSONRPC_HASH_MESSAGE{ + sourcemeta::core::JSON::make_object().as_object().hash("message")}; +const auto JSONRPC_HASH_DATA{ + sourcemeta::core::JSON::make_object().as_object().hash("data")}; +const auto JSONRPC_HASH_PARAMS{ + sourcemeta::core::JSON::make_object().as_object().hash("params")}; + +} // namespace + +namespace sourcemeta::core { + +auto jsonrpc_is_server_error(const std::int64_t code) -> bool { + return code >= JSONRPC_CODE_SERVER_ERROR_MIN && + code <= JSONRPC_CODE_SERVER_ERROR_MAX; +} + +auto jsonrpc_request_id(const sourcemeta::core::JSON &request) + -> const sourcemeta::core::JSON * { + if (!request.is_object()) { + return nullptr; + } + const auto *identifier{request.try_at("id", JSONRPC_HASH_ID)}; + if (identifier == nullptr) { + return nullptr; + } + if (!identifier->is_string() && !identifier->is_number() && + !identifier->is_null()) { + return nullptr; + } + return identifier; +} + +auto jsonrpc_is_request(const sourcemeta::core::JSON &request) -> bool { + if (!request.is_object()) { + return false; + } + const auto *jsonrpc_field{request.try_at("jsonrpc", JSONRPC_HASH_JSONRPC)}; + if (jsonrpc_field == nullptr || !jsonrpc_field->is_string() || + jsonrpc_field->to_string() != "2.0" || + jsonrpc_request_id(request) == nullptr) { + return false; + } + const auto *parameters_field{request.try_at("params", JSONRPC_HASH_PARAMS)}; + if (parameters_field != nullptr && !parameters_field->is_object() && + !parameters_field->is_array()) { + return false; + } + const auto *method_field{request.try_at("method", JSONRPC_HASH_METHOD)}; + return method_field != nullptr && method_field->is_string(); +} + +auto jsonrpc_method(const sourcemeta::core::JSON &request) -> std::string_view { + if (!request.is_object()) { + return {}; + } + const auto *method_field{request.try_at("method", JSONRPC_HASH_METHOD)}; + if (method_field == nullptr || !method_field->is_string()) { + return {}; + } + return method_field->to_string(); +} + +auto jsonrpc_params(const sourcemeta::core::JSON &request) + -> const sourcemeta::core::JSON * { + if (!request.is_object()) { + return nullptr; + } + const auto *parameters{request.try_at("params", JSONRPC_HASH_PARAMS)}; + if (parameters == nullptr || + (!parameters->is_object() && !parameters->is_array())) { + return nullptr; + } + return parameters; +} + +auto jsonrpc_is_notification(const sourcemeta::core::JSON &request) -> bool { + if (!request.is_object()) { + return false; + } + const auto *jsonrpc_field{request.try_at("jsonrpc", JSONRPC_HASH_JSONRPC)}; + if (jsonrpc_field == nullptr || !jsonrpc_field->is_string() || + jsonrpc_field->to_string() != "2.0" || + request.try_at("id", JSONRPC_HASH_ID) != nullptr) { + return false; + } + const auto *parameters_field{request.try_at("params", JSONRPC_HASH_PARAMS)}; + if (parameters_field != nullptr && !parameters_field->is_object() && + !parameters_field->is_array()) { + return false; + } + const auto *method_field{request.try_at("method", JSONRPC_HASH_METHOD)}; + return method_field != nullptr && method_field->is_string(); +} + +auto jsonrpc_make_success(const sourcemeta::core::JSON &identifier, + sourcemeta::core::JSON result) + -> sourcemeta::core::JSON { + auto envelope{sourcemeta::core::JSON::make_object()}; + envelope.assign_assume_new(std::string{"jsonrpc"}, + sourcemeta::core::JSON{"2.0"}, + JSONRPC_HASH_JSONRPC); + envelope.assign_assume_new( + std::string{"id"}, sourcemeta::core::JSON{identifier}, JSONRPC_HASH_ID); + envelope.assign_assume_new(std::string{"result"}, std::move(result), + JSONRPC_HASH_RESULT); + return envelope; +} + +auto jsonrpc_make_success_empty(const sourcemeta::core::JSON &identifier) + -> sourcemeta::core::JSON { + return jsonrpc_make_success(identifier, + sourcemeta::core::JSON::make_object()); +} + +auto jsonrpc_make_error(const sourcemeta::core::JSON *identifier, + const std::int64_t code, const std::string_view message, + std::optional data) + -> sourcemeta::core::JSON { + auto envelope{sourcemeta::core::JSON::make_object()}; + envelope.assign_assume_new(std::string{"jsonrpc"}, + sourcemeta::core::JSON{"2.0"}, + JSONRPC_HASH_JSONRPC); + envelope.assign_assume_new(std::string{"id"}, + identifier != nullptr + ? sourcemeta::core::JSON{*identifier} + : sourcemeta::core::JSON{nullptr}, + JSONRPC_HASH_ID); + auto error{sourcemeta::core::JSON::make_object()}; + error.assign_assume_new(std::string{"code"}, sourcemeta::core::JSON{code}, + JSONRPC_HASH_CODE); + error.assign_assume_new(std::string{"message"}, + sourcemeta::core::JSON{message}, + JSONRPC_HASH_MESSAGE); + if (data.has_value()) { + error.assign_assume_new(std::string{"data"}, std::move(data.value()), + JSONRPC_HASH_DATA); + } + envelope.assign_assume_new(std::string{"error"}, std::move(error), + JSONRPC_HASH_ERROR); + return envelope; +} + +auto jsonrpc_make_error_parse() -> const sourcemeta::core::JSON & { + static const auto envelope{ + jsonrpc_make_error(nullptr, JSONRPC_CODE_PARSE, "Parse error")}; + return envelope; +} + +auto jsonrpc_make_error_invalid_request( + const sourcemeta::core::JSON *identifier) -> sourcemeta::core::JSON { + return jsonrpc_make_error(identifier, JSONRPC_CODE_INVALID_REQUEST, + "Invalid Request"); +} + +auto jsonrpc_make_error_method_not_found( + const sourcemeta::core::JSON &identifier) -> sourcemeta::core::JSON { + return jsonrpc_make_error(&identifier, JSONRPC_CODE_METHOD_NOT_FOUND, + "Method not found"); +} + +auto jsonrpc_make_error_invalid_params( + const sourcemeta::core::JSON &identifier, + std::optional data) -> sourcemeta::core::JSON { + return jsonrpc_make_error(&identifier, JSONRPC_CODE_INVALID_PARAMS, + "Invalid params", std::move(data)); +} + +auto jsonrpc_make_error_internal(const sourcemeta::core::JSON *identifier) + -> sourcemeta::core::JSON { + return jsonrpc_make_error(identifier, JSONRPC_CODE_INTERNAL, + "Internal error"); +} + +} // namespace sourcemeta::core diff --git a/test/jsonrpc/CMakeLists.txt b/test/jsonrpc/CMakeLists.txt new file mode 100644 index 000000000..1d8e992ed --- /dev/null +++ b/test/jsonrpc/CMakeLists.txt @@ -0,0 +1,5 @@ +sourcemeta_googletest(NAMESPACE sourcemeta PROJECT core NAME jsonrpc + SOURCES jsonrpc_test.cc) + +target_link_libraries(sourcemeta_core_jsonrpc_unit + PRIVATE sourcemeta::core::jsonrpc) diff --git a/test/jsonrpc/jsonrpc_test.cc b/test/jsonrpc/jsonrpc_test.cc new file mode 100644 index 000000000..190d5a2d1 --- /dev/null +++ b/test/jsonrpc/jsonrpc_test.cc @@ -0,0 +1,577 @@ +#include + +#include + +#include + +#include // std::int64_t + +TEST(JSONRPC, code_constants) { + EXPECT_EQ(sourcemeta::core::JSONRPC_CODE_PARSE, + static_cast(-32700)); + EXPECT_EQ(sourcemeta::core::JSONRPC_CODE_INVALID_REQUEST, + static_cast(-32600)); + EXPECT_EQ(sourcemeta::core::JSONRPC_CODE_METHOD_NOT_FOUND, + static_cast(-32601)); + EXPECT_EQ(sourcemeta::core::JSONRPC_CODE_INVALID_PARAMS, + static_cast(-32602)); + EXPECT_EQ(sourcemeta::core::JSONRPC_CODE_INTERNAL, + static_cast(-32603)); + EXPECT_EQ(sourcemeta::core::JSONRPC_CODE_SERVER_ERROR_MIN, + static_cast(-32099)); + EXPECT_EQ(sourcemeta::core::JSONRPC_CODE_SERVER_ERROR_MAX, + static_cast(-32000)); +} + +TEST(JSONRPC, is_server_error_lower_bound) { + EXPECT_TRUE(sourcemeta::core::jsonrpc_is_server_error(-32099)); +} + +TEST(JSONRPC, is_server_error_upper_bound) { + EXPECT_TRUE(sourcemeta::core::jsonrpc_is_server_error(-32000)); +} + +TEST(JSONRPC, is_server_error_inside_range) { + EXPECT_TRUE(sourcemeta::core::jsonrpc_is_server_error(-32050)); +} + +TEST(JSONRPC, is_server_error_below_range) { + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_server_error(-32100)); +} + +TEST(JSONRPC, is_server_error_above_range) { + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_server_error(-31999)); +} + +TEST(JSONRPC, is_server_error_predefined_internal) { + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_server_error(-32603)); +} + +TEST(JSONRPC, is_server_error_positive_application_code) { + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_server_error(1)); +} + +TEST(JSONRPC, request_id_integer) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 7, "method": "ping" })")}; + const auto *identifier{sourcemeta::core::jsonrpc_request_id(request)}; + ASSERT_NE(identifier, nullptr); + EXPECT_TRUE(identifier->is_integer()); + EXPECT_EQ(identifier->to_integer(), 7); +} + +TEST(JSONRPC, request_id_string) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": "abc", "method": "ping" })")}; + const auto *identifier{sourcemeta::core::jsonrpc_request_id(request)}; + ASSERT_NE(identifier, nullptr); + EXPECT_TRUE(identifier->is_string()); + EXPECT_EQ(identifier->to_string(), "abc"); +} + +TEST(JSONRPC, request_id_missing) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "method": "ping" })")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_request_id(request), nullptr); +} + +TEST(JSONRPC, request_id_null) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": null, "method": "ping" })")}; + const auto *identifier{sourcemeta::core::jsonrpc_request_id(request)}; + ASSERT_NE(identifier, nullptr); + EXPECT_TRUE(identifier->is_null()); +} + +TEST(JSONRPC, request_id_floating_point) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1.5, "method": "ping" })")}; + const auto *identifier{sourcemeta::core::jsonrpc_request_id(request)}; + ASSERT_NE(identifier, nullptr); + EXPECT_TRUE(identifier->is_real()); + EXPECT_EQ(identifier->to_real(), 1.5); +} + +TEST(JSONRPC, request_id_boolean) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": true, "method": "ping" })")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_request_id(request), nullptr); +} + +TEST(JSONRPC, request_id_object) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": {}, "method": "ping" })")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_request_id(request), nullptr); +} + +TEST(JSONRPC, request_id_array) { + const auto request{sourcemeta::core::parse_json(R"([ 1, 2, 3 ])")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_request_id(request), nullptr); +} + +TEST(JSONRPC, request_id_primitive) { + const auto request{sourcemeta::core::parse_json(R"(42)")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_request_id(request), nullptr); +} + +TEST(JSONRPC, is_request_valid_integer_id) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1, "method": "ping" })")}; + EXPECT_TRUE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_valid_string_id) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": "x", "method": "ping" })")}; + EXPECT_TRUE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_missing_id) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "method": "ping" })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_null_id) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": null, "method": "ping" })")}; + EXPECT_TRUE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_floating_point_id) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1.5, "method": "ping" })")}; + EXPECT_TRUE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_missing_method) { + const auto request{ + sourcemeta::core::parse_json(R"({ "jsonrpc": "2.0", "id": 1 })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_non_string_method) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1, "method": 5 })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_missing_jsonrpc) { + const auto request{ + sourcemeta::core::parse_json(R"({ "id": 1, "method": "ping" })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_wrong_jsonrpc_version) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "1.0", "id": 1, "method": "ping" })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_jsonrpc_not_string) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": 2, "id": 1, "method": "ping" })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_array) { + const auto request{sourcemeta::core::parse_json(R"([ 1, 2 ])")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_primitive) { + const auto request{sourcemeta::core::parse_json(R"("hello")")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_primitive_params) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1, "method": "ping", "params": "x" })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_null_params) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1, "method": "ping", "params": null })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_request_omitted_params) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1, "method": "ping" })")}; + EXPECT_TRUE(sourcemeta::core::jsonrpc_is_request(request)); +} + +TEST(JSONRPC, is_notification_valid) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "method": "notifications/initialized" })")}; + EXPECT_TRUE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, is_notification_with_id) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1, "method": "ping" })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, is_notification_with_null_id) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": null, "method": "ping" })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, is_notification_missing_method) { + const auto request{sourcemeta::core::parse_json(R"({ "jsonrpc": "2.0" })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, is_notification_non_string_method) { + const auto request{ + sourcemeta::core::parse_json(R"({ "jsonrpc": "2.0", "method": 5 })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, is_notification_missing_jsonrpc) { + const auto request{sourcemeta::core::parse_json(R"({ "method": "ping" })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, is_notification_wrong_jsonrpc_version) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "1.0", "method": "ping" })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, is_notification_array) { + const auto request{sourcemeta::core::parse_json(R"([ 1, 2 ])")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, is_notification_primitive_params) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "method": "ping", "params": "x" })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, is_notification_null_params) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "method": "ping", "params": null })")}; + EXPECT_FALSE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, is_notification_array_params) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "method": "ping", "params": [ 1, 2 ] })")}; + EXPECT_TRUE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, is_notification_object_params) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "method": "ping", "params": { "x": 1 } })")}; + EXPECT_TRUE(sourcemeta::core::jsonrpc_is_notification(request)); +} + +TEST(JSONRPC, method_present) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1, "method": "ping" })")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_method(request), "ping"); +} + +TEST(JSONRPC, method_missing) { + const auto request{ + sourcemeta::core::parse_json(R"({ "jsonrpc": "2.0", "id": 1 })")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_method(request), ""); +} + +TEST(JSONRPC, method_non_string) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1, "method": 5 })")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_method(request), ""); +} + +TEST(JSONRPC, method_on_non_object) { + const auto request{sourcemeta::core::parse_json(R"([ 1, 2 ])")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_method(request), ""); +} + +TEST(JSONRPC, params_object) { + const auto request{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "ping", + "params": { "uri": "http://example.com" } + })JSON")}; + const auto *params{sourcemeta::core::jsonrpc_params(request)}; + ASSERT_NE(params, nullptr); + EXPECT_TRUE(params->is_object()); + EXPECT_EQ(params->at("uri").to_string(), "http://example.com"); +} + +TEST(JSONRPC, params_array) { + const auto request{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "method": "subtract", + "params": [ 42, 23 ] + })JSON")}; + const auto *params{sourcemeta::core::jsonrpc_params(request)}; + ASSERT_NE(params, nullptr); + EXPECT_TRUE(params->is_array()); + EXPECT_EQ(params->size(), 2); +} + +TEST(JSONRPC, params_missing) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1, "method": "ping" })")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_params(request), nullptr); +} + +TEST(JSONRPC, params_primitive) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1, "method": "ping", "params": "x" })")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_params(request), nullptr); +} + +TEST(JSONRPC, params_null) { + const auto request{sourcemeta::core::parse_json( + R"({ "jsonrpc": "2.0", "id": 1, "method": "ping", "params": null })")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_params(request), nullptr); +} + +TEST(JSONRPC, params_on_non_object) { + const auto request{sourcemeta::core::parse_json(R"([ 1, 2 ])")}; + EXPECT_EQ(sourcemeta::core::jsonrpc_params(request), nullptr); +} + +TEST(JSONRPC, make_success_empty) { + const auto identifier{sourcemeta::core::JSON{1}}; + const auto envelope{sourcemeta::core::jsonrpc_make_success_empty(identifier)}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "result": {} + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_success_empty_object_result) { + const auto identifier{sourcemeta::core::JSON{1}}; + const auto envelope{sourcemeta::core::jsonrpc_make_success( + identifier, sourcemeta::core::JSON::make_object())}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "result": {} + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_success_populated_object_result) { + const auto identifier{sourcemeta::core::JSON{"abc"}}; + auto result{sourcemeta::core::JSON::make_object()}; + result.assign("foo", sourcemeta::core::JSON{42}); + const auto envelope{ + sourcemeta::core::jsonrpc_make_success(identifier, std::move(result))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": "abc", + "result": { "foo": 42 } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_success_array_result) { + const auto identifier{sourcemeta::core::JSON{2}}; + const auto envelope{sourcemeta::core::jsonrpc_make_success( + identifier, sourcemeta::core::parse_json(R"([ 1, 2, 3 ])"))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 2, + "result": [ 1, 2, 3 ] + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_with_id_no_data) { + const auto identifier{sourcemeta::core::JSON{1}}; + const auto envelope{sourcemeta::core::jsonrpc_make_error(&identifier, -32000, + "Server error")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32000, + "message": "Server error" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_with_id_and_data) { + const auto identifier{sourcemeta::core::JSON{"req-1"}}; + const auto envelope{sourcemeta::core::jsonrpc_make_error( + &identifier, -32000, "Server error", + sourcemeta::core::JSON{"more details"})}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": "req-1", + "error": { + "code": -32000, + "message": "Server error", + "data": "more details" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_without_id) { + const auto envelope{ + sourcemeta::core::jsonrpc_make_error(nullptr, -32000, "Server error")}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": null, + "error": { + "code": -32000, + "message": "Server error" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_without_id_with_object_data) { + auto data{sourcemeta::core::JSON::make_object()}; + data.assign("hint", sourcemeta::core::JSON{"check input"}); + const auto envelope{sourcemeta::core::jsonrpc_make_error( + nullptr, -32000, "Server error", std::move(data))}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": null, + "error": { + "code": -32000, + "message": "Server error", + "data": { "hint": "check input" } + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_parse) { + const auto &envelope{sourcemeta::core::jsonrpc_make_error_parse()}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": null, + "error": { + "code": -32700, + "message": "Parse error" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_parse_returns_same_reference) { + const auto *first{&sourcemeta::core::jsonrpc_make_error_parse()}; + const auto *second{&sourcemeta::core::jsonrpc_make_error_parse()}; + EXPECT_EQ(first, second); +} + +TEST(JSONRPC, make_error_invalid_request_with_id) { + const auto identifier{sourcemeta::core::JSON{1}}; + const auto envelope{ + sourcemeta::core::jsonrpc_make_error_invalid_request(&identifier)}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32600, + "message": "Invalid Request" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_invalid_request_without_id) { + const auto envelope{sourcemeta::core::jsonrpc_make_error_invalid_request()}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": null, + "error": { + "code": -32600, + "message": "Invalid Request" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_method_not_found) { + const auto identifier{sourcemeta::core::JSON{2}}; + const auto envelope{ + sourcemeta::core::jsonrpc_make_error_method_not_found(identifier)}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 2, + "error": { + "code": -32601, + "message": "Method not found" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_invalid_params) { + const auto identifier{sourcemeta::core::JSON{"req-7"}}; + const auto envelope{ + sourcemeta::core::jsonrpc_make_error_invalid_params(identifier)}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": "req-7", + "error": { + "code": -32602, + "message": "Invalid params" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_invalid_params_with_data) { + const auto identifier{sourcemeta::core::JSON{"req-8"}}; + const auto envelope{sourcemeta::core::jsonrpc_make_error_invalid_params( + identifier, sourcemeta::core::JSON{"abc"})}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": "req-8", + "error": { + "code": -32602, + "message": "Invalid params", + "data": "abc" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_internal_with_id) { + const auto identifier{sourcemeta::core::JSON{3}}; + const auto envelope{ + sourcemeta::core::jsonrpc_make_error_internal(&identifier)}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": 3, + "error": { + "code": -32603, + "message": "Internal error" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} + +TEST(JSONRPC, make_error_internal_without_id) { + const auto envelope{sourcemeta::core::jsonrpc_make_error_internal()}; + const auto expected{sourcemeta::core::parse_json(R"JSON({ + "jsonrpc": "2.0", + "id": null, + "error": { + "code": -32603, + "message": "Internal error" + } + })JSON")}; + EXPECT_EQ(envelope, expected); +} diff --git a/test/packaging/find_package/CMakeLists.txt b/test/packaging/find_package/CMakeLists.txt index 651999e56..b106c4715 100644 --- a/test/packaging/find_package/CMakeLists.txt +++ b/test/packaging/find_package/CMakeLists.txt @@ -26,3 +26,4 @@ target_link_libraries(core_hello PRIVATE sourcemeta::core::markdown) target_link_libraries(core_hello PRIVATE sourcemeta::core::editorschema) target_link_libraries(core_hello PRIVATE sourcemeta::core::options) target_link_libraries(core_hello PRIVATE sourcemeta::core::preprocessor) +target_link_libraries(core_hello PRIVATE sourcemeta::core::jsonrpc) diff --git a/test/packaging/find_package/hello.cc b/test/packaging/find_package/hello.cc index ba602b358..7f0cdfd92 100644 --- a/test/packaging/find_package/hello.cc +++ b/test/packaging/find_package/hello.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include