367 lines
14 KiB
C++
367 lines
14 KiB
C++
#pragma once
|
|
#include "adminive/descriptor.hpp"
|
|
#include "magic_enum/magic_enum.hpp"
|
|
#include "nlohmann/json.hpp"
|
|
#include <map>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
namespace adminive {
|
|
using Json = nlohmann::json;
|
|
struct Update_Result {
|
|
bool success{};
|
|
std::string message;
|
|
std::map<std::string, std::string> field_errors;
|
|
Json to_json() const {
|
|
return Json{{"success", success}, {"message", message}, {"field_errors", field_errors}};
|
|
}
|
|
};
|
|
class Json_Assignment_Error : public std::invalid_argument {
|
|
public:
|
|
explicit Json_Assignment_Error(Update_Result result) : std::invalid_argument(result.message), result_(std::move(result)) {}
|
|
const Update_Result& result() const noexcept {
|
|
return result_;
|
|
}
|
|
private:
|
|
Update_Result result_;
|
|
};
|
|
enum class Write_Mode {
|
|
internal,
|
|
create,
|
|
update
|
|
};
|
|
template <Described_Type T>
|
|
Json to_json(const T& value);
|
|
template <Described_Type T>
|
|
Json to_frontend_json(const T& value);
|
|
template <Described_Type T>
|
|
requires std::default_initializable<T>
|
|
Json to_descriptor_json();
|
|
template <Described_Type T>
|
|
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
|
|
Update_Result assign_json(T& target, const Json& value);
|
|
template <class T>
|
|
void assign_json_value(T& target, const Json& value);
|
|
template <class T>
|
|
Json encode_json_value(const T& value) {
|
|
using Value = std::remove_cvref_t<T>;
|
|
if constexpr (Range_Value_Type<Value> || Validated_Value_Type<Value>) {
|
|
return encode_json_value(value.value());
|
|
} else if constexpr (std::is_enum_v<Value>) {
|
|
const auto name = magic_enum::enum_name(value);
|
|
if (!name.empty()) {
|
|
return std::string(name);
|
|
}
|
|
return static_cast<std::underlying_type_t<Value>>(value);
|
|
} else if constexpr (Described_Type<Value>) {
|
|
return to_json(value);
|
|
} else if constexpr (String_Key_Map_Type<Value>) {
|
|
Json result = Json::object();
|
|
for (const auto& [key, item] : value) {
|
|
result[std::string(key)] = encode_json_value(item);
|
|
}
|
|
return result;
|
|
} else if constexpr (Sequence_Type<Value>) {
|
|
Json result = Json::array();
|
|
for (const auto& item : value) {
|
|
result.push_back(encode_json_value(item));
|
|
}
|
|
return result;
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
template <class T>
|
|
void assign_json_value(T& target, const Json& value) {
|
|
using Value = std::remove_cvref_t<T>;
|
|
if constexpr (Range_Value_Type<Value> || Validated_Value_Type<Value>) {
|
|
using Inner = typename Value::value_type;
|
|
target = value.get<Inner>();
|
|
} else if constexpr (std::is_enum_v<Value>) {
|
|
if (value.is_string()) {
|
|
const auto parsed = magic_enum::enum_cast<Value>(value.get<std::string>());
|
|
if (!parsed) {
|
|
throw std::invalid_argument("unknown enum value");
|
|
}
|
|
target = *parsed;
|
|
} else {
|
|
const auto parsed = magic_enum::enum_cast<Value>(value.get<std::underlying_type_t<Value>>());
|
|
if (!parsed) {
|
|
throw std::invalid_argument("unknown enum value");
|
|
}
|
|
target = *parsed;
|
|
}
|
|
} else if constexpr (Described_Type<Value>) {
|
|
const auto result = assign_json(target, value);
|
|
if (!result.success) {
|
|
throw Json_Assignment_Error(result);
|
|
}
|
|
} else if constexpr (String_Key_Map_Type<Value>) {
|
|
if (!value.is_object()) {
|
|
throw std::invalid_argument("value must be a JSON object");
|
|
}
|
|
Value parsed;
|
|
for (auto iterator = value.begin(); iterator != value.end(); ++iterator) {
|
|
typename Value::mapped_type item{};
|
|
assign_json_value(item, iterator.value());
|
|
parsed.emplace(iterator.key(), std::move(item));
|
|
}
|
|
target = std::move(parsed);
|
|
} else if constexpr (Sequence_Type<Value>) {
|
|
if (!value.is_array()) {
|
|
throw std::invalid_argument("value must be a JSON array");
|
|
}
|
|
Value parsed;
|
|
for (const auto& source : value) {
|
|
typename Value::value_type item{};
|
|
assign_json_value(item, source);
|
|
parsed.push_back(std::move(item));
|
|
}
|
|
target = std::move(parsed);
|
|
} else {
|
|
target = value.get<Value>();
|
|
}
|
|
}
|
|
template <class T>
|
|
std::string value_type_name() {
|
|
using Value = Unwrapped_Value_Type<T>;
|
|
if constexpr (std::is_enum_v<Value>) {
|
|
return "enum";
|
|
} else if constexpr (std::same_as<Value, bool>) {
|
|
return "boolean";
|
|
} else if constexpr (std::integral<Value>) {
|
|
return "integer";
|
|
} else if constexpr (std::floating_point<Value>) {
|
|
return "number";
|
|
} else if constexpr (String_Type<Value>) {
|
|
return "string";
|
|
} else if constexpr (Described_Type<Value>) {
|
|
return "object";
|
|
} else if constexpr (String_Key_Map_Type<Value>) {
|
|
return "map";
|
|
} else if constexpr (Sequence_Type<Value>) {
|
|
return "array";
|
|
} else {
|
|
return "unknown";
|
|
}
|
|
}
|
|
template <class T>
|
|
void append_constraints(Json& result) {
|
|
using Value = std::remove_cvref_t<T>;
|
|
if constexpr (Range_Value_Type<Value>) {
|
|
result["minimum"] = Value::minimum;
|
|
result["maximum"] = Value::maximum;
|
|
append_constraints<typename Value::value_type>(result);
|
|
} else if constexpr (Validated_Value_Type<Value>) {
|
|
using Validator = typename Value::validator_type;
|
|
if constexpr (Multiple_Of_Validator<Validator>) {
|
|
result["multiple_of"] = Validator::multiple_of;
|
|
}
|
|
if constexpr (Validator_With_Message<Validator>) {
|
|
result["validation_message"] = std::string(Validator::message);
|
|
}
|
|
append_constraints<typename Value::value_type>(result);
|
|
}
|
|
}
|
|
template <class T>
|
|
Json enum_options() {
|
|
using Value = Unwrapped_Value_Type<T>;
|
|
Json result = Json::array();
|
|
if constexpr (std::is_enum_v<Value>) {
|
|
for (const auto value : magic_enum::enum_values<Value>()) {
|
|
const std::string name(magic_enum::enum_name(value));
|
|
result.push_back(Json{{"label", make_label(name)}, {"value", name}});
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
template <Described_Type T>
|
|
requires std::default_initializable<T>
|
|
Json to_descriptor_json() {
|
|
const auto descriptor = describe<T>();
|
|
T defaults{};
|
|
Json fields = Json::array();
|
|
int default_order{};
|
|
std::apply([&](const auto&... item) {
|
|
([&] {
|
|
using Field = std::remove_cvref_t<decltype(item)>;
|
|
using Member = typename Field::member_type;
|
|
const int order = item.order() < 0 ? default_order : item.order();
|
|
++default_order;
|
|
Json field_json{{"name", item.name()}, {"label", item.label()}, {"list_label", item.list_label()}, {"value_type", value_type_name<Member>()}, {"readable", item.is_readable()}, {"editable", item.is_editable()}, {"creatable", item.is_creatable()}, {"visible", item.is_visible()}, {"required", item.is_required()}, {"list_visible", item.is_list_visible()}, {"order", order}, {"sortable", item.is_sortable()}, {"default", encode_json_value(defaults.*Field::member)}};
|
|
if (!item.description().empty()) {
|
|
field_json["description"] = item.description();
|
|
}
|
|
if(!item.widget().empty()) {
|
|
field_json["widget"] = item.widget();
|
|
}
|
|
if(!item.visible_on().empty()) {
|
|
field_json["visible_on"] = item.visible_on();
|
|
}
|
|
if constexpr(Described_Type<Member>) {
|
|
field_json["children"] = to_descriptor_json<Member>().at("fields");
|
|
}
|
|
append_constraints<Member>(field_json);
|
|
const auto options = enum_options<Member>();
|
|
if (!options.empty()) {
|
|
field_json["options"] = options;
|
|
}
|
|
fields.push_back(std::move(field_json));
|
|
}(), ...);
|
|
}, descriptor.fields());
|
|
return Json{{"protocol", "adminive.resource"}, {"protocol_version", 1}, {"name", descriptor.name()}, {"label", descriptor.label()}, {"value_type", "object"}, {"fields", std::move(fields)}};
|
|
}
|
|
template <Described_Type T>
|
|
Json to_json(const T& value) {
|
|
const auto descriptor = describe<T>();
|
|
Json result = Json::object();
|
|
std::apply([&](const auto&... item) {
|
|
([&] {
|
|
using Field = std::remove_cvref_t<decltype(item)>;
|
|
result[item.name()] = encode_json_value(value.*Field::member);
|
|
}(), ...);
|
|
}, descriptor.fields());
|
|
return result;
|
|
}
|
|
template <Described_Type T>
|
|
Json to_frontend_json(const T& value) {
|
|
const auto descriptor = describe<T>();
|
|
Json result = Json::object();
|
|
std::apply([&](const auto&... item) {
|
|
([&] {
|
|
using Field = std::remove_cvref_t<decltype(item)>;
|
|
if (item.is_readable()) {
|
|
result[item.name()] = encode_json_value(value.*Field::member);
|
|
}
|
|
}(), ...);
|
|
}, descriptor.fields());
|
|
return result;
|
|
}
|
|
template <Described_Type T>
|
|
Json to_data_json(const T& value) {
|
|
return to_frontend_json(value);
|
|
}
|
|
template <class Field>
|
|
bool field_writable(const Field& field, Write_Mode mode) {
|
|
if (mode == Write_Mode::internal) {
|
|
return true;
|
|
}
|
|
if (mode == Write_Mode::create) {
|
|
return field.is_creatable();
|
|
}
|
|
return field.is_editable();
|
|
}
|
|
template <Described_Type T>
|
|
requires std::copy_constructible<T> && std::assignable_from<T&, T>
|
|
Update_Result apply_object_json(T& target, const Json& value, Write_Mode mode, bool complete) {
|
|
Update_Result result;
|
|
if (!value.is_object()) {
|
|
result.message = "value must be a JSON object";
|
|
return result;
|
|
}
|
|
T candidate = target;
|
|
const auto descriptor = describe<T>();
|
|
for (auto iterator = value.begin(); iterator != value.end(); ++iterator) {
|
|
bool known{};
|
|
std::apply([&](const auto&... item) {
|
|
known = ((iterator.key() == item.name()) || ...);
|
|
}, descriptor.fields());
|
|
if (!known) {
|
|
result.field_errors[iterator.key()] = "unknown field";
|
|
}
|
|
}
|
|
std::apply([&](const auto&... item) {
|
|
([&] {
|
|
using Field = std::remove_cvref_t<decltype(item)>;
|
|
const auto iterator = value.find(item.name());
|
|
const bool writable = field_writable(item, mode);
|
|
if (complete && item.is_required() && writable && iterator == value.end()) {
|
|
result.field_errors[item.name()] = "field is required";
|
|
return;
|
|
}
|
|
if (iterator == value.end()) {
|
|
return;
|
|
}
|
|
if (!writable) {
|
|
result.field_errors[item.name()] = mode == Write_Mode::create ? "field is not creatable" : "field is not editable";
|
|
return;
|
|
}
|
|
try {
|
|
assign_json_value(candidate.*Field::member, *iterator);
|
|
} catch (const Json_Assignment_Error& error) {
|
|
result.field_errors[item.name()] = error.what();
|
|
} catch (const std::exception& error) {
|
|
result.field_errors[item.name()] = error.what();
|
|
}
|
|
}(), ...);
|
|
}, descriptor.fields());
|
|
if (!result.field_errors.empty()) {
|
|
result.message = "one or more fields are invalid";
|
|
return result;
|
|
}
|
|
try {
|
|
descriptor.object_validator()(candidate);
|
|
} catch (const std::exception& error) {
|
|
result.message = error.what();
|
|
return result;
|
|
}
|
|
target = std::move(candidate);
|
|
result.success = true;
|
|
result.message = complete ? "assigned" : "updated";
|
|
return result;
|
|
}
|
|
template <Described_Type T>
|
|
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
|
|
Update_Result assign_json(T& target, const Json& value) {
|
|
T candidate{};
|
|
auto result = apply_object_json(candidate, value, Write_Mode::internal, true);
|
|
if (result.success) {
|
|
target = std::move(candidate);
|
|
}
|
|
return result;
|
|
}
|
|
template <Described_Type T>
|
|
requires std::copy_constructible<T> && std::assignable_from<T&, T>
|
|
Update_Result apply_json_patch(T& target, const Json& patch) {
|
|
return apply_object_json(target, patch, Write_Mode::internal, false);
|
|
}
|
|
template <Described_Type T>
|
|
requires std::copy_constructible<T> && std::assignable_from<T&, T>
|
|
Update_Result apply_frontend_create(T& target, const Json& value) {
|
|
return apply_object_json(target, value, Write_Mode::create, true);
|
|
}
|
|
template <Described_Type T>
|
|
requires std::copy_constructible<T> && std::assignable_from<T&, T>
|
|
Update_Result apply_frontend_patch(T& target, const Json& patch) {
|
|
return apply_object_json(target, patch, Write_Mode::update, false);
|
|
}
|
|
template <Described_Type T>
|
|
requires std::copy_constructible<T> && std::assignable_from<T&, T>
|
|
Update_Result apply_patch(T& target, const Json& patch) {
|
|
return apply_frontend_patch(target, patch);
|
|
}
|
|
template <Described_Type T>
|
|
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
|
|
T from_json(const Json& value) {
|
|
T result{};
|
|
auto update = assign_json(result, value);
|
|
if (!update.success) {
|
|
throw Json_Assignment_Error(std::move(update));
|
|
}
|
|
return result;
|
|
}
|
|
template <Described_Type T>
|
|
Update_Result validate(const T& value) {
|
|
Update_Result result;
|
|
try {
|
|
describe<T>().object_validator()(value);
|
|
result.success = true;
|
|
result.message = "valid";
|
|
} catch (const std::exception& error) {
|
|
result.message = error.what();
|
|
}
|
|
return result;
|
|
}
|
|
}
|