From 5f9b8aaa955f1a354ea317f8463785f7d08575b6 Mon Sep 17 00:00:00 2001 From: Patric Stout Date: Wed, 6 Sep 2023 13:14:12 +0200 Subject: [PATCH] Codechange: [Script] use nlohmann for Squirrel <-> JSON conversion (#11251) --- src/network/core/config.h | 2 +- src/network/network_admin.cpp | 5 - src/script/api/script_admin.cpp | 70 +++------ src/script/api/script_admin.hpp | 10 +- src/script/api/script_event_types.cpp | 212 +++++++------------------- src/script/api/script_event_types.hpp | 22 +-- src/tests/test_script_admin.cpp | 76 +++++---- 7 files changed, 131 insertions(+), 266 deletions(-) diff --git a/src/network/core/config.h b/src/network/core/config.h index 7657a88d06..e6b4882e2b 100644 --- a/src/network/core/config.h +++ b/src/network/core/config.h @@ -60,7 +60,7 @@ static const uint NETWORK_REVISION_LENGTH = 33; ///< The m static const uint NETWORK_PASSWORD_LENGTH = 33; ///< The maximum length of the password, in bytes including '\0' (must be >= NETWORK_SERVER_ID_LENGTH) static const uint NETWORK_CLIENT_NAME_LENGTH = 25; ///< The maximum length of a client's name, in bytes including '\0' static const uint NETWORK_RCONCOMMAND_LENGTH = 500; ///< The maximum length of a rconsole command, in bytes including '\0' -static const uint NETWORK_GAMESCRIPT_JSON_LENGTH = COMPAT_MTU - 3; ///< The maximum length of a gamescript json string, in bytes including '\0'. Must not be longer than COMPAT_MTU including header (3 bytes) +static const uint NETWORK_GAMESCRIPT_JSON_LENGTH = 9000; ///< The maximum length of a receiving gamescript json string, in bytes including '\0'. static const uint NETWORK_CHAT_LENGTH = 900; ///< The maximum length of a chat message, in bytes including '\0' static const uint NETWORK_CONTENT_FILENAME_LENGTH = 48; ///< The maximum length of a content's filename, in bytes including '\0'. static const uint NETWORK_CONTENT_NAME_LENGTH = 32; ///< The maximum length of a content's name, in bytes including '\0'. diff --git a/src/network/network_admin.cpp b/src/network/network_admin.cpp index ae42156d29..40679352d6 100644 --- a/src/network/network_admin.cpp +++ b/src/network/network_admin.cpp @@ -557,11 +557,6 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::SendConsole(const std::string */ NetworkRecvStatus ServerNetworkAdminSocketHandler::SendGameScript(const std::string_view json) { - /* At the moment we cannot transmit anything larger than MTU. So we limit - * the maximum amount of json data that can be sent. Account also for - * the trailing \0 of the string */ - if (json.size() + 1 >= NETWORK_GAMESCRIPT_JSON_LENGTH) return NETWORK_RECV_STATUS_OKAY; - Packet *p = new Packet(ADMIN_PACKET_SERVER_GAMESCRIPT); p->Send_string(json); diff --git a/src/script/api/script_admin.cpp b/src/script/api/script_admin.cpp index 9f55e1b958..2c30434792 100644 --- a/src/script/api/script_admin.cpp +++ b/src/script/api/script_admin.cpp @@ -16,9 +16,9 @@ #include "../../safeguards.h" -/* static */ bool ScriptAdmin::MakeJSON(HSQUIRRELVM vm, SQInteger index, int max_depth, std::string &data) +/* static */ bool ScriptAdmin::MakeJSON(nlohmann::json &json, HSQUIRRELVM vm, SQInteger index, int depth) { - if (max_depth == 0) { + if (depth == SQUIRREL_MAX_DEPTH) { ScriptLog::Error("Send parameters can only be nested to 25 deep. No data sent."); // SQUIRREL_MAX_DEPTH = 25 return false; } @@ -28,7 +28,7 @@ SQInteger res; sq_getinteger(vm, index, &res); - data = fmt::format("{}", res); + json = res; return true; } @@ -36,63 +36,52 @@ const SQChar *buf; sq_getstring(vm, index, &buf); - size_t len = strlen(buf) + 1; - if (len >= 255) { - ScriptLog::Error("Maximum string length is 254 chars. No data sent."); - return false; - } - - data = fmt::format("\"{}\"", buf); + json = std::string(buf); return true; } case OT_ARRAY: { - data = "[ "; + json = nlohmann::json::array(); - bool first = true; sq_pushnull(vm); while (SQ_SUCCEEDED(sq_next(vm, index - 1))) { - if (!first) data += ", "; - if (first) first = false; + nlohmann::json tmp; - std::string tmp; - - bool res = MakeJSON(vm, -1, max_depth - 1, tmp); + bool res = MakeJSON(tmp, vm, -1, depth + 1); sq_pop(vm, 2); if (!res) { sq_pop(vm, 1); return false; } - data += tmp; + + json.push_back(tmp); } sq_pop(vm, 1); - data += " ]"; return true; } case OT_TABLE: { - data = "{ "; + json = nlohmann::json::object(); - bool first = true; sq_pushnull(vm); while (SQ_SUCCEEDED(sq_next(vm, index - 1))) { - if (!first) data += ", "; - if (first) first = false; + /* Squirrel ensure the key is a string. */ + assert(sq_gettype(vm, -2) == OT_STRING); + const SQChar *buf; + sq_getstring(vm, -2, &buf); + std::string key = std::string(buf); - std::string key; - std::string value; - - /* Store the key + value */ - bool res = MakeJSON(vm, -2, max_depth - 1, key) && MakeJSON(vm, -1, max_depth - 1, value); + nlohmann::json value; + bool res = MakeJSON(value, vm, -1, depth + 1); sq_pop(vm, 2); if (!res) { sq_pop(vm, 1); return false; } - data += key + ": " + value; + + json[key] = value; } sq_pop(vm, 1); - data += " }"; return true; } @@ -100,17 +89,12 @@ SQBool res; sq_getbool(vm, index, &res); - if (res) { - data = "true"; - return true; - } - - data = "false"; + json = res ? true : false; return true; } case OT_NULL: { - data = "null"; + json = nullptr; return true; } @@ -128,19 +112,13 @@ return sq_throwerror(vm, "ScriptAdmin::Send requires a table as first parameter. No data sent."); } - std::string json; - if (!ScriptAdmin::MakeJSON(vm, -1, SQUIRREL_MAX_DEPTH, json)) { + nlohmann::json json; + if (!ScriptAdmin::MakeJSON(json, vm, -1)) { sq_pushinteger(vm, 0); return 1; } - if (json.length() > NETWORK_GAMESCRIPT_JSON_LENGTH) { - ScriptLog::Error("You are trying to send a table that is too large to the AdminPort. No data sent."); - sq_pushinteger(vm, 0); - return 1; - } - - NetworkAdminGameScript(json); + NetworkAdminGameScript(json.dump()); sq_pushinteger(vm, 1); return 1; diff --git a/src/script/api/script_admin.hpp b/src/script/api/script_admin.hpp index 03723784c5..5dea0b0510 100644 --- a/src/script/api/script_admin.hpp +++ b/src/script/api/script_admin.hpp @@ -11,6 +11,7 @@ #define SCRIPT_ADMIN_HPP #include "script_object.hpp" +#include /** * Class that handles communication with the AdminPort. @@ -38,13 +39,14 @@ public: protected: /** - * Convert a Squirrel structure into a JSON string. + * Convert a Squirrel structure into a JSON object. + * @param json The resulting JSON object. * @param vm The VM to operate on. * @param index The index we are currently working for. - * @param max_depth The maximal depth to follow the squirrel struct. - * @param data The resulting json string. + * @param depth The current depth in the squirrel struct. + * @return True iff the conversion was successful. */ - static bool MakeJSON(HSQUIRRELVM vm, SQInteger index, int max_depth, std::string &data); + static bool MakeJSON(nlohmann::json &data, HSQUIRRELVM vm, SQInteger index, int depth = 0); }; #endif /* SCRIPT_ADMIN_HPP */ diff --git a/src/script/api/script_event_types.cpp b/src/script/api/script_event_types.cpp index cea70f8e61..7b7ebcb9b7 100644 --- a/src/script/api/script_event_types.cpp +++ b/src/script/api/script_event_types.cpp @@ -128,14 +128,24 @@ ScriptEventAdminPort::ScriptEventAdminPort(const std::string &json) : { } -#define SKIP_EMPTY(p) while (*(p) == ' ' || *(p) == '\n' || *(p) == '\r') (p)++; -#define RETURN_ERROR(stack) { ScriptLog::Error("Received invalid JSON data from AdminPort."); if (stack != 0) sq_pop(vm, stack); return nullptr; } - SQInteger ScriptEventAdminPort::GetObject(HSQUIRRELVM vm) { - const char *p = this->json.c_str(); + auto json = nlohmann::json::parse(this->json, nullptr, false); + + if (!json.is_object()) { + ScriptLog::Error("The root element in the JSON data from AdminPort has to be an object."); + + sq_pushnull(vm); + return 1; + } + + auto top = sq_gettop(vm); + if (!this->ReadValue(vm, json)) { + /* Rewind the stack, removing anything that might be left on top. */ + sq_settop(vm, top); + + ScriptLog::Error("Received invalid JSON data from AdminPort."); - if (this->ReadTable(vm, p) == nullptr) { sq_pushnull(vm); return 1; } @@ -143,177 +153,59 @@ SQInteger ScriptEventAdminPort::GetObject(HSQUIRRELVM vm) return 1; } -const char *ScriptEventAdminPort::ReadString(HSQUIRRELVM vm, const char *p) +bool ScriptEventAdminPort::ReadValue(HSQUIRRELVM vm, nlohmann::json &json) { - const char *value = p; + switch (json.type()) { + case nlohmann::json::value_t::null: + sq_pushnull(vm); + break; - bool escape = false; - for (;;) { - if (*p == '\\') { - escape = true; - p++; - continue; - } - if (*p == '"' && escape) { - escape = false; - p++; - continue; - } - escape = false; - - if (*p == '"') break; - if (*p == '\0') RETURN_ERROR(0); - - p++; - } - - size_t len = p - value; - sq_pushstring(vm, value, len); - p++; // Step past the end-of-string marker (") - - return p; -} - -const char *ScriptEventAdminPort::ReadTable(HSQUIRRELVM vm, const char *p) -{ - sq_newtable(vm); - - SKIP_EMPTY(p); - if (*p++ != '{') RETURN_ERROR(1); - - for (;;) { - SKIP_EMPTY(p); - if (*p++ != '"') RETURN_ERROR(1); - - p = ReadString(vm, p); - if (p == nullptr) { - sq_pop(vm, 1); - return nullptr; - } - - SKIP_EMPTY(p); - if (*p++ != ':') RETURN_ERROR(2); - - p = this->ReadValue(vm, p); - if (p == nullptr) { - sq_pop(vm, 2); - return nullptr; - } - - sq_rawset(vm, -3); - /* The key (-2) and value (-1) are popped from the stack by squirrel. */ - - SKIP_EMPTY(p); - if (*p == ',') { - p++; - continue; - } - break; - } - - SKIP_EMPTY(p); - if (*p++ != '}') RETURN_ERROR(1); - - return p; -} - -const char *ScriptEventAdminPort::ReadValue(HSQUIRRELVM vm, const char *p) -{ - SKIP_EMPTY(p); - - if (strncmp(p, "false", 5) == 0) { - sq_pushbool(vm, 0); - return p + 5; - } - if (strncmp(p, "true", 4) == 0) { - sq_pushbool(vm, 1); - return p + 4; - } - if (strncmp(p, "null", 4) == 0) { - sq_pushnull(vm); - return p + 4; - } - - switch (*p) { - case '"': { - /* String */ - p = ReadString(vm, ++p); - if (p == nullptr) return nullptr; + case nlohmann::json::value_t::boolean: + sq_pushbool(vm, json.get() ? 1 : 0); + break; + case nlohmann::json::value_t::string: { + auto value = json.get(); + sq_pushstring(vm, value.data(), value.size()); break; } - case '{': { - /* Table */ - p = this->ReadTable(vm, p); - if (p == nullptr) return nullptr; - + case nlohmann::json::value_t::number_integer: + case nlohmann::json::value_t::number_unsigned: + sq_pushinteger(vm, json.get()); break; - } - case '[': { - /* Array */ + case nlohmann::json::value_t::object: + sq_newtable(vm); + + for (auto &[key, value] : json.items()) { + sq_pushstring(vm, key.data(), key.size()); + + if (!this->ReadValue(vm, value)) { + return false; + } + + sq_rawset(vm, -3); + } + break; + + case nlohmann::json::value_t::array: sq_newarray(vm, 0); - /* Empty array? */ - const char *p2 = p + 1; - SKIP_EMPTY(p2); - if (*p2 == ']') { - p = p2 + 1; - break; - } - - while (*p++ != ']') { - p = this->ReadValue(vm, p); - if (p == nullptr) { - sq_pop(vm, 1); - return nullptr; + for (auto &value : json) { + if (!this->ReadValue(vm, value)) { + return false; } + sq_arrayappend(vm, -2); - - SKIP_EMPTY(p); - if (*p == ',') continue; - if (*p == ']') break; - RETURN_ERROR(1); } - - p++; - break; - } - - case '1': case '2': case '3': case '4': case '5': - case '6': case '7': case '8': case '9': case '0': - case '-': { - /* Integer */ - - const char *value = p++; - for (;;) { - switch (*p++) { - case '1': case '2': case '3': case '4': case '5': - case '6': case '7': case '8': case '9': case '0': - continue; - - default: - break; - } - - p--; - break; - } - - int res = atoi(value); - sq_pushinteger(vm, (SQInteger)res); - - break; - } + /* These types are not supported by Squirrel. */ + case nlohmann::json::value_t::number_float: default: - RETURN_ERROR(0); + return false; } - return p; + return true; } - -#undef SKIP_EMPTY -#undef RETURN_ERROR diff --git a/src/script/api/script_event_types.hpp b/src/script/api/script_event_types.hpp index 79902d2668..8ff883ac28 100644 --- a/src/script/api/script_event_types.hpp +++ b/src/script/api/script_event_types.hpp @@ -14,6 +14,8 @@ #include "script_goal.hpp" #include "script_window.hpp" +#include + /** * Event Vehicle Crash, indicating a vehicle of yours is crashed. * It contains the crash site, the crashed vehicle and the reason for the crash. @@ -912,25 +914,11 @@ private: std::string json; ///< The JSON string. /** - * Read a table from a JSON string. + * Convert a JSON part fo Squirrel. * @param vm The VM used. - * @param p The (part of the) JSON string reading. + * @param json The JSON part to convert to Squirrel. */ - const char *ReadTable(HSQUIRRELVM vm, const char *p); - - /** - * Read a value from a JSON string. - * @param vm The VM used. - * @param p The (part of the) JSON string reading. - */ - const char *ReadValue(HSQUIRRELVM vm, const char *p); - - /** - * Read a string from a JSON string. - * @param vm The VM used. - * @param p The (part of the) JSON string reading. - */ - const char *ReadString(HSQUIRRELVM vm, const char *p); + bool ReadValue(HSQUIRRELVM vm, nlohmann::json &json); }; /** diff --git a/src/tests/test_script_admin.cpp b/src/tests/test_script_admin.cpp index b679498921..95e00a3c47 100644 --- a/src/tests/test_script_admin.cpp +++ b/src/tests/test_script_admin.cpp @@ -72,14 +72,14 @@ public: REQUIRE(sq_gettype(vm, -1) == OT_TABLE); /* Feed the snippet into the MakeJSON function. */ - std::string json; - if (!ScriptAdmin::MakeJSON(vm, -1, SQUIRREL_MAX_DEPTH, json)) { + nlohmann::json json; + if (!ScriptAdmin::MakeJSON(json, vm, -1)) { sq_close(vm); return std::nullopt; } sq_close(vm); - return json; + return json.dump(); } /** @@ -111,11 +111,11 @@ public: } REQUIRE(sq_gettype(vm, -1) == OT_TABLE); - std::string squirrel_json; - REQUIRE(ScriptAdmin::MakeJSON(vm, -1, SQUIRREL_MAX_DEPTH, squirrel_json) == true); + nlohmann::json squirrel_json; + REQUIRE(ScriptAdmin::MakeJSON(squirrel_json, vm, -1) == true); sq_close(vm); - return squirrel_json; + return squirrel_json.dump(); } }; @@ -124,18 +124,18 @@ TEST_CASE("Squirrel -> JSON conversion") { TestScriptController controller; - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = null })sq") == R"json({ "test": null })json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = 1 })sq") == R"json({ "test": 1 })json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = -1 })sq") == R"json({ "test": -1 })json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = true })sq") == R"json({ "test": true })json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = "a" })sq") == R"json({ "test": "a" })json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ ] })sq") == R"json({ "test": [ ] })json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ 1 ] })sq") == R"json({ "test": [ 1 ] })json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ 1, "a", true, { test = 1 }, [], null ] })sq") == R"json({ "test": [ 1, "a", true, { "test": 1 }, [ ], null ] })json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { } })sq") == R"json({ "test": { } })json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1 } })sq") == R"json({ "test": { "test": 1 } })json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1, test = 2 } })sq") == R"json({ "test": { "test": 2 } })json"); - CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1, test2 = [ 2 ] } })sq") == R"json({ "test": { "test": 1, "test2": [ 2 ] } })json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = null })sq") == R"json({"test":null})json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = 1 })sq") == R"json({"test":1})json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = -1 })sq") == R"json({"test":-1})json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = true })sq") == R"json({"test":true})json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = "a" })sq") == R"json({"test":"a"})json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ ] })sq") == R"json({"test":[]})json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ 1 ] })sq") == R"json({"test":[1]})json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ 1, "a", true, { test = 1 }, [], null ] })sq") == R"json({"test":[1,"a",true,{"test":1},[],null]})json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { } })sq") == R"json({"test":{}})json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1 } })sq") == R"json({"test":{"test":1}})json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1, test = 2 } })sq") == R"json({"test":{"test":2}})json"); + CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1, test2 = [ 2 ] } })sq") == R"json({"test":{"test":1,"test2":[2]}})json"); /* Cases that should fail, as we cannot convert a class to JSON. */ CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = DummyClass })sq") == std::nullopt); @@ -147,23 +147,33 @@ TEST_CASE("JSON -> Squirrel conversion") { TestScriptController controller; - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": null })json") == R"json({ "test": null })json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": 1 })json") == R"json({ "test": 1 })json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": -1 })json") == R"json({ "test": -1 })json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": true })json") == R"json({ "test": true })json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": "a" })json") == R"json({ "test": "a" })json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [] })json") == R"json({ "test": [ ] })json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1 ] })json") == R"json({ "test": [ 1 ] })json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1, "a", true, { "test": 1 }, [], null ] })json") == R"json({ "test": [ 1, "a", true, { "test": 1 }, [ ], null ] })json"); - // BUG -- This should work, but doesn't. - // CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": {} })json") == R"json({ "test": { } })json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 1 } })json") == R"json({ "test": { "test": 1 } })json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 2 } })json") == R"json({ "test": { "test": 2 } })json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 1, "test2": [ 2 ] } })json") == R"json({ "test": { "test": 1, "test2": [ 2 ] } })json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": null })json") == R"json({"test":null})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": 1 })json") == R"json({"test":1})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": -1 })json") == R"json({"test":-1})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": true })json") == R"json({"test":true})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": "a" })json") == R"json({"test":"a"})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [] })json") == R"json({"test":[]})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1 ] })json") == R"json({"test":[1]})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1, "a", true, { "test": 1 }, [], null ] })json") == R"json({"test":[1,"a",true,{"test":1},[],null]})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": {} })json") == R"json({"test":{}})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 1 } })json") == R"json({"test":{"test":1}})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 2 } })json") == R"json({"test":{"test":2}})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 1, "test2": [ 2 ] } })json") == R"json({"test":{"test":1,"test2":[2]}})json"); /* Check if spaces are properly ignored. */ - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({"test":1})json") == R"json({ "test": 1 })json"); - CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({"test": 1})json") == R"json({ "test": 1 })json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({"test":1})json") == R"json({"test":1})json"); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({"test": 1})json") == R"json({"test":1})json"); + + /* Valid JSON but invalid Squirrel (read: floats). */ + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": 1.1 })json") == std::nullopt); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1, 3, 1.1 ] })json") == std::nullopt); + + /* Root element has to be an object. */ + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( 1 )json") == std::nullopt); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( "a" )json") == std::nullopt); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( [ 1 ] )json") == std::nullopt); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( null )json") == std::nullopt); + CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( true )json") == std::nullopt); /* Cases that should fail, as it is invalid JSON. */ CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({"test":test})json") == std::nullopt);