geode/loader/include/Geode/utils/JsonValidation.hpp
2024-11-04 23:24:20 +03:00

284 lines
9.7 KiB
C++

#pragma once
#include <matjson.hpp>
#include "../loader/Log.hpp"
#include <set>
#include <variant>
#include <Geode/Result.hpp>
namespace geode {
struct JsonChecker;
template <typename T, typename = void>
struct is_iterable : std::false_type {};
template <typename T>
struct is_iterable<
T, std::void_t<decltype(std::begin(std::declval<T>())), decltype(std::end(std::declval<T>()))>> :
std::true_type {};
template <typename T>
constexpr bool is_iterable_v = is_iterable<T>::value;
namespace {
using value_t = matjson::Type;
constexpr char const* jsonValueTypeToString(value_t type) {
switch (type) {
default:
case value_t::Null: return "null";
case value_t::Object: return "object";
case value_t::Array: return "array";
case value_t::String: return "string";
case value_t::Bool: return "boolean";
case value_t::Number: return "number";
}
}
template <class T>
constexpr value_t getJsonType() {
if constexpr (std::is_same_v<T, bool>) {
return value_t::Bool;
}
else if constexpr (std::is_floating_point_v<T>) {
return value_t::Number;
}
else if constexpr (std::is_unsigned_v<T>) {
return value_t::Number;
}
else if constexpr (std::is_integral_v<T>) {
return value_t::Number;
}
else if constexpr (std::is_constructible_v<T, std::string>) {
return value_t::String;
}
else if constexpr (is_iterable_v<T>) {
return value_t::Array;
}
return value_t::Null;
}
bool jsonConvertibleTo(value_t value, value_t to) {
// if we don't know the type we're passing into,
// everything's valid
if (to == value_t::Null) return true;
if (value == value_t::Number) {
return to == value_t::Number;
}
return value == to;
}
}
template <class T>
using JsonValueValidator = std::function<bool(T const&)>;
struct JsonMaybeObject;
struct JsonMaybeValue;
class GEODE_DLL JsonExpectedValue final {
protected:
class Impl;
std::unique_ptr<Impl> m_impl;
JsonExpectedValue();
JsonExpectedValue(Impl* from, matjson::Value& scope, std::string_view key);
static const char* matJsonTypeToString(matjson::Type ty);
bool hasError() const;
void setError(std::string_view error);
matjson::Value const& getJSONRef() const;
template <class... Args>
void setError(fmt::format_string<Args...> error, Args&&... args) {
this->setError(fmt::format(error, std::forward<Args>(args)...));
}
template <class T>
std::optional<T> tryGet() {
if (this->hasError()) return std::nullopt;
if constexpr (std::is_same_v<T, matjson::Value>) {
return this->getJSONRef();
}
else {
try {
if (this->getJSONRef().is<T>()) {
return this->getJSONRef().as<T>();
}
else {
this->setError(
"unexpected type {}",
this->matJsonTypeToString(this->getJSONRef().type())
);
}
}
// matjson can throw variant exceptions too so you need to do this
catch(std::exception const& e) {
this->setError("unable to parse json: {}", e);
}
}
return std::nullopt;
}
public:
JsonExpectedValue(matjson::Value const& value, std::string_view rootScopeName);
~JsonExpectedValue();
JsonExpectedValue(JsonExpectedValue&&);
JsonExpectedValue& operator=(JsonExpectedValue&&);
JsonExpectedValue(JsonExpectedValue const&) = delete;
JsonExpectedValue& operator=(JsonExpectedValue const&) = delete;
/**
* Get a copy of the underlying raw JSON value
*/
matjson::Value json() const;
/**
* Get the key name of this JSON value. If this is an array index,
* returns the index as a string. If this is the root object,
* returns the root scope name.
*/
std::string key() const;
/**
* Check the type of this JSON value. Does not set an error. If an
* error is already set, always returns false
*/
bool is(matjson::Type type) const;
bool isNull() const;
bool isBool() const;
bool isNumber() const;
bool isString() const;
bool isArray() const;
bool isObject() const;
/**
* Asserts that this JSON value is of the specified type. If it is
* not, an error is set and all subsequent operations are no-ops
* @returns Itself
*/
JsonExpectedValue& assertIs(matjson::Type type);
JsonExpectedValue& assertIsNull();
JsonExpectedValue& assertIsBool();
JsonExpectedValue& assertIsNumber();
JsonExpectedValue& assertIsString();
JsonExpectedValue& assertIsArray();
JsonExpectedValue& assertIsObject();
/**
* Asserts that this JSON value is one of a list of specified types
* @returns Itself
*/
JsonExpectedValue& assertIs(std::initializer_list<matjson::Type> type);
// -- Dealing with values --
template <class T>
T get(T const& defaultValue = T()) {
if (auto v = this->tryGet<T>()) {
return *std::move(v);
}
return defaultValue;
}
template <class T>
JsonExpectedValue& into(T& value) {
if (auto v = this->tryGet<T>()) {
value = *std::move(v);
}
return *this;
}
template <class T>
JsonExpectedValue& into(std::optional<T>& value) {
if (auto v = this->tryGet<T>()) {
value.emplace(*std::move(v));
}
return *this;
}
template <class T>
JsonExpectedValue& mustBe(std::string_view name, auto predicate) requires requires {
{ predicate(std::declval<T>()) } -> std::convertible_to<bool>;
} {
if (this->hasError()) return *this;
if (auto v = this->tryGet<T>()) {
if (!predicate(*v)) {
this->setError("json value is not {}", name);
}
}
return *this;
}
template <class T>
JsonExpectedValue& mustBe(std::string_view name, auto predicate) requires requires {
{ predicate(std::declval<T>()) } -> std::convertible_to<Result<>>;
} {
if (this->hasError()) return *this;
if (auto v = this->tryGet<T>()) {
auto p = predicate(*v);
if (!p) {
this->setError("json value is not {}: {}", name, p.unwrapErr());
}
}
return *this;
}
// -- Dealing with objects --
/**
* Check if this object has an optional key. Asserts that this JSON
* value is an object. If the key doesn't exist, returns a
* `JsonExpectValue` that does nothing
* @returns The key, which is a no-op value if it didn't exist
*/
JsonExpectedValue has(std::string_view key);
/**
* Check if this object has an optional key. Asserts that this JSON
* value is an object. If the key doesn't exist, sets an error and
* returns a `JsonExpectValue` that does nothing
* @returns The key, which is a no-op value if it didn't exist
*/
JsonExpectedValue needs(std::string_view key);
/**
* Asserts that this JSON value is an object. Get all object
* properties
*/
std::vector<std::pair<std::string, JsonExpectedValue>> properties();
/**
* Asserts that this JSON value is an object. Logs unknown keys to
* the console as warnings
*/
void checkUnknownKeys();
// -- Dealing with arrays --
/**
* Asserts that this JSON value is an array. Returns the length of
* the array, or 0 on error
*/
size_t length();
/**
* Asserts that this JSON value is an array. Returns the value at
* the specified index. If there is no value at that index, sets an
* error
*/
JsonExpectedValue at(size_t index);
/**
* Asserts that this JSON value is an array. Returns the array items
* @warning The old JsonChecker used `items` for iterating object
* properties - on this new API that function is called `properties`!
*/
std::vector<JsonExpectedValue> items();
operator bool() const;
Result<> ok();
template <class T>
Result<T> ok(T value) {
auto ok = this->ok();
if (!ok) {
return Err(ok.unwrapErr());
}
return Ok(std::forward<T>(value));
}
};
GEODE_DLL JsonExpectedValue checkJson(matjson::Value const& json, std::string_view rootScopeName);
}