diff --git a/src/openrct2/core/Json.cpp b/src/openrct2/core/Json.cpp index e996d9e420..eb128106e0 100644 --- a/src/openrct2/core/Json.cpp +++ b/src/openrct2/core/Json.cpp @@ -15,9 +15,8 @@ namespace Json { - json_t* ReadFromFile(const utf8* path, size_t maxSize) + json_t ReadFromFile(const utf8* path, size_t maxSize) { - json_t* json = nullptr; auto fs = OpenRCT2::FileStream(path, OpenRCT2::FILE_MODE_OPEN); size_t fileLength = static_cast(fs.GetLength()); @@ -26,42 +25,101 @@ namespace Json throw IOException("Json file too large."); } - utf8* fileData = Memory::Allocate(fileLength + 1); - fs.Read(fileData, fileLength); - fileData[fileLength] = '\0'; + auto fileData = std::string(static_cast(fileLength) + 1, '\0'); + fs.Read(static_cast(fileData.data()), fileLength); - json_error_t jsonLoadError; - json = json_loads(fileData, 0, &jsonLoadError); - Memory::Free(fileData); + json_t json; - if (json == nullptr) + try { - throw JsonException(&jsonLoadError); + json = json_t::parse(fileData, nullptr, true, true); + } + catch (const json_t::exception& e) + { + throw JsonException(String::Format("Unable to parse JSON file (%s)\n\t%s", path, e.what())); } return json; } - void WriteToFile(const utf8* path, const json_t* json, size_t flags) + void WriteToFile(const utf8* path, const json_t& jsonData, int indentSize) { // Serialise JSON - const char* jsonOutput = json_dumps(json, flags); + std::string jsonOutput = jsonData.dump(indentSize); // Write to file auto fs = OpenRCT2::FileStream(path, OpenRCT2::FILE_MODE_WRITE); - size_t jsonOutputSize = String::SizeOf(jsonOutput); - fs.Write(jsonOutput, jsonOutputSize); + fs.Write(jsonOutput.data(), jsonOutput.size()); } - json_t* FromString(std::string_view raw) + json_t FromString(std::string_view raw) { - json_t* root; - json_error_t error; - root = json_loadb(raw.data(), raw.size(), 0, &error); - if (root == nullptr) + json_t json; + + try { - throw JsonException(&error); + json = json_t::parse(raw, nullptr, true, true); } - return root; + catch (const json_t::exception& e) + { + log_error("Unable to parse JSON string (%s)\n\t%s", raw, e.what()); + } + + return json; + } + + json_t FromVector(const std::vector& vec) + { + json_t json; + + try + { + json = json_t::parse(vec.begin(), vec.end()); + } + catch (const json_t::exception& e) + { + log_error("Unable to parse JSON vector (%s)\n\t%s", vec.data(), e.what()); + } + + return json; + } + + std::string GetString(const json_t& jsonObj, const std::string& defaultValue) + { + return jsonObj.is_string() ? jsonObj.get() : defaultValue; + } + + bool GetBoolean(const json_t& jsonObj, bool defaultValue) + { + return jsonObj.is_boolean() ? jsonObj.get() : defaultValue; + } + + json_t AsObject(const json_t& jsonObj) + { + return jsonObj.is_object() ? jsonObj : json_t::object(); + } + + json_t AsArray(const json_t& jsonObj) + { + if (jsonObj.is_array()) + { + return jsonObj; + } + + json_t retVal = json_t::array(); + + if (jsonObj.is_object()) + { + for (const auto& jItem : jsonObj) + { + retVal.push_back(jItem); + } + } + else if (!jsonObj.is_null()) + { + retVal.push_back(jsonObj); + } + + return retVal; } } // namespace Json diff --git a/src/openrct2/core/Json.hpp b/src/openrct2/core/Json.hpp index 1059590f7c..69899b12a5 100644 --- a/src/openrct2/core/Json.hpp +++ b/src/openrct2/core/Json.hpp @@ -11,36 +11,181 @@ #include "../common.h" -#include -#include +#include #include #include +using json_t = nlohmann::json; + namespace Json { // Don't try to load JSON files that exceed 64 MiB constexpr uint64_t MAX_JSON_SIZE = 64 * 1024 * 1024; - json_t* ReadFromFile(const utf8* path, size_t maxSize = MAX_JSON_SIZE); - void WriteToFile(const utf8* path, const json_t* json, size_t flags = 0); + /** + * Read JSON file and parse contents + * @param path Path to the source file + * @param maxSize Max file size in bytes allowed + * @return A JSON representation of the file + * @note This function will throw an exception if the JSON file cannot be parsed + */ + json_t ReadFromFile(const utf8* path, size_t maxSize = MAX_JSON_SIZE); - json_t* FromString(std::string_view raw); + /** + * Read JSON file and parse the contents + * @param path Path to the destination file + * @param jsonData A JSON object + * @param indentSize The number of spaces in an indent, or removes whitespace on -1 + */ + void WriteToFile(const utf8* path, const json_t& jsonData, int indentSize = 4); + + /** + * Parse JSON from a string + * @param raw JSON string + * @return A JSON representation of the string + * @note This function will throw an exception if the JSON string cannot be parsed + */ + json_t FromString(std::string_view raw); + + /** + * Parse JSON from a vector of characters + * @param vec Vector of characters containing JSON + * @return A JSON representation of the vector + * @note This function will throw an exception if the JSON vector cannot be parsed + */ + json_t FromVector(const std::vector& vec); + + /** + * Explicit type conversion between a JSON object and a compatible number value + * @param T Destination numeric type + * @param jsonObj JSON object holding the value + * @param defaultValue Default value to return if the JSON object is not a number type + * @return Copy of the JSON value converted to the given type + */ + template T GetNumber(const json_t& jsonObj, T defaultValue = 0) + { + static_assert(std::is_arithmetic::value, "GetNumber template parameter must be arithmetic"); + + return jsonObj.is_number() ? jsonObj.get() : defaultValue; + } + + /** + * Explicit type conversion between a JSON object and a compatible enum value + * @param T Destination enum + * @param jsonObj JSON object holding the value + * @param defaultValue Default value to return if the JSON object is not an enum type + * @return Copy of the JSON value converted to the given enum type + */ + template T GetEnum(const json_t& jsonObj, T defaultValue) + { + static_assert(std::is_enum::value, "GetEnum template parameter must be an enum"); + + return jsonObj.is_number_integer() ? jsonObj.get() : defaultValue; + } + + /** + * Explicit type conversion between a JSON object and an std::string + * @param jsonObj JSON object holding the value + * @param defaultValue Default value to return if the JSON object is not a string + * @return Copy of the JSON value converted to std::string + */ + std::string GetString(const json_t& jsonObj, const std::string& defaultValue = std::string()); + + /** + * Explicit type conversion between a JSON object and a boolean + * @param jsonObj JSON object holding the value + * @param defaultValue Default value to return if the JSON object is not a boolean + * @return Copy of the JSON value converted to bool + */ + bool GetBoolean(const json_t& jsonObj, bool defaultValue = false); + + /** + * Ensures a given JSON object is an object type + * @param jsonObj JSON object + * @return The JSON object if it is an object type, or an empty object otherwise + */ + json_t AsObject(const json_t& jsonObj); + + /** + * Ensures a given JSON object is an array type + * @param jsonObj JSON object + * @return The JSON object if it is an array type, or an array containing the JSON object otherwise + */ + json_t AsArray(const json_t& jsonObj); + + /** + * Helper function to convert a json object and an initializer list to binary flags + * @param T Type to return + * @param jsonObj JSON object containing boolean values + * @param list List of pairs of keys and bits to enable if that key in the object is true + * @return Value with relevant bits flipped + */ + template T GetFlags(const json_t& jsonObj, std::initializer_list> list) + { + static_assert(std::is_convertible::value, "GetFlags template parameter must be integral or a weak enum"); + + T flags{}; + for (const auto& item : list) + { + if (jsonObj.contains(item.first) && Json::GetBoolean(jsonObj[item.first])) + { + flags = static_cast(flags | item.second); + } + } + return flags; + } + + /** + * Used by the GetFlags function to allow for inverted values + */ + enum class FlagType : uint8_t + { + // Flag is turned on if the key is true + Normal, + // Flag is turned on if the key is false + Inverted + }; + + /** + * Helper function to convert a json object and an initializer list to binary flags + * @param T Type to return + * @param jsonObj JSON object containing boolean values + * @param list List of tuples of keys, bits to change and flag type + * @return Value with relevant bits flipped + * @note FLAG_NORMAL behaves like the other GetFlags function, but FLAG_INVERTED will turn the flag on when false + */ + template T GetFlags(const json_t& jsonObj, std::initializer_list> list) + { + static_assert(std::is_convertible::value, "GetFlags template parameter must be integral or a weak enum"); + + T flags{}; + for (const auto& item : list) + { + if (std::get<2>(item) == FlagType::Normal) + { + if (jsonObj.contains(std::get<0>(item)) && Json::GetBoolean(jsonObj[std::get<0>(item)])) + { + flags = static_cast(flags | std::get<1>(item)); + } + } + else + { + // if the json flag doesn't exist, assume it's false + if (!jsonObj.contains(std::get<0>(item)) || !Json::GetBoolean(jsonObj[std::get<0>(item)])) + { + flags = static_cast(flags | std::get<1>(item)); + } + } + } + return flags; + } } // namespace Json class JsonException final : public std::runtime_error { -private: - json_error_t _jsonError = {}; - public: explicit JsonException(const std::string& message) : std::runtime_error(message) { } - - explicit JsonException(const json_error_t* jsonError) - : JsonException(std::string(jsonError->text)) - { - _jsonError = *jsonError; - } };