1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-26 16:24:35 +01:00

Fix #9088: refactor JSON code to use a more modern C++ library

- Removed dependency on jansson
- Implemented nlohmann JSON for Modern C++
- Moved generic GetString, etc., helper functions out of ObjectJsonHelpers.hpp to Json.hpp
- Added GetEnum helper function
- Added AsObject and AsArray helper functions
- Removed GetStringArray, etc., helper functions as they're not needed with the better language features
- Added second GetFlags definition that allows specifying inverted flags - this is a bit cleaner than previous hacks
- Comments!
This commit is contained in:
Simon Jarrett
2020-08-12 00:56:28 +01:00
parent 3acbb92074
commit 324e94e6ce
2 changed files with 238 additions and 35 deletions

View File

@@ -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<size_t>(fs.GetLength());
@@ -26,42 +25,101 @@ namespace Json
throw IOException("Json file too large.");
}
utf8* fileData = Memory::Allocate<utf8>(fileLength + 1);
fs.Read(fileData, fileLength);
fileData[fileLength] = '\0';
auto fileData = std::string(static_cast<size_t>(fileLength) + 1, '\0');
fs.Read(static_cast<void*>(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<uint8_t>& 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<std::string>() : defaultValue;
}
bool GetBoolean(const json_t& jsonObj, bool defaultValue)
{
return jsonObj.is_boolean() ? jsonObj.get<bool>() : 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

View File

@@ -11,36 +11,181 @@
#include "../common.h"
#include <jansson.h>
#include <stdexcept>
#include <nlohmann/json.hpp>
#include <string>
#include <string_view>
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<uint8_t>& 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<typename T> T GetNumber(const json_t& jsonObj, T defaultValue = 0)
{
static_assert(std::is_arithmetic<T>::value, "GetNumber template parameter must be arithmetic");
return jsonObj.is_number() ? jsonObj.get<T>() : 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<typename T> T GetEnum(const json_t& jsonObj, T defaultValue)
{
static_assert(std::is_enum<T>::value, "GetEnum template parameter must be an enum");
return jsonObj.is_number_integer() ? jsonObj.get<T>() : 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<typename T> T GetFlags(const json_t& jsonObj, std::initializer_list<std::pair<std::string, T>> list)
{
static_assert(std::is_convertible<T, int>::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<T>(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<typename T> T GetFlags(const json_t& jsonObj, std::initializer_list<std::tuple<std::string, T, FlagType>> list)
{
static_assert(std::is_convertible<T, int>::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<T>(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<T>(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;
}
};