分离 http依赖
This commit is contained in:
@@ -10,6 +10,33 @@
|
||||
namespace adminive {
|
||||
template <class T>
|
||||
struct Reflection_Adapter;
|
||||
template <class T>
|
||||
struct Object_Adapter {
|
||||
using model_type = std::remove_cvref_t<T>;
|
||||
static model_type snapshot(const model_type& value) requires std::copy_constructible<model_type> {
|
||||
return value;
|
||||
}
|
||||
static model_type create() requires std::default_initializable<model_type> {
|
||||
return {};
|
||||
}
|
||||
static void commit(model_type& target, model_type value) requires std::assignable_from<model_type&, model_type> {
|
||||
target = std::move(value);
|
||||
}
|
||||
};
|
||||
template <class T>
|
||||
using Object_Model_Type = typename Object_Adapter<std::remove_cvref_t<T>>::model_type;
|
||||
template <class T>
|
||||
concept Object_Adapter_With_Snapshot = requires(const std::remove_cvref_t<T>& value) {
|
||||
{ Object_Adapter<std::remove_cvref_t<T>>::snapshot(value) } -> std::same_as<Object_Model_Type<T>>;
|
||||
};
|
||||
template <class T>
|
||||
concept Object_Adapter_With_Create = requires {
|
||||
{ Object_Adapter<std::remove_cvref_t<T>>::create() } -> std::same_as<Object_Model_Type<T>>;
|
||||
};
|
||||
template <class T>
|
||||
concept Object_Adapter_With_Commit = requires(std::remove_cvref_t<T>& target, Object_Model_Type<T> value) {
|
||||
Object_Adapter<std::remove_cvref_t<T>>::commit(target, std::move(value));
|
||||
};
|
||||
template <class Json>
|
||||
struct Json_Adapter;
|
||||
template <class Json>
|
||||
@@ -127,6 +154,28 @@ template <class T, class Json>
|
||||
concept Value_Adapter_With_Validation_Message = requires {
|
||||
{ Value_Adapter<std::remove_cvref_t<T>, Json>::validation_message } -> std::convertible_to<std::string_view>;
|
||||
};
|
||||
template <class T, class Json>
|
||||
struct Control_Adapter {};
|
||||
struct Control_Context {
|
||||
std::string_view permission;
|
||||
std::string_view prefix;
|
||||
};
|
||||
template <class T, class Json>
|
||||
concept Control_Adapter_With_Control = requires(const Json& field, Control_Context context) {
|
||||
{ Control_Adapter<std::remove_cvref_t<T>, Json>::make_control(field, context) } -> std::same_as<Json>;
|
||||
};
|
||||
template <class T, class Json>
|
||||
concept Control_Adapter_With_Column = requires(const Json& field) {
|
||||
{ Control_Adapter<std::remove_cvref_t<T>, Json>::make_column(field) } -> std::same_as<Json>;
|
||||
};
|
||||
template <class T, class Json>
|
||||
struct Polymorphic_Adapter;
|
||||
template <class T, class Json>
|
||||
concept Polymorphic_Type = requires(const std::remove_cvref_t<T>& value, std::remove_cvref_t<T>& target, const Json& input) {
|
||||
{ Polymorphic_Adapter<std::remove_cvref_t<T>, Json>::descriptor() } -> std::same_as<Json>;
|
||||
{ Polymorphic_Adapter<std::remove_cvref_t<T>, Json>::encode(value) } -> std::same_as<Json>;
|
||||
Polymorphic_Adapter<std::remove_cvref_t<T>, Json>::decode(target, input);
|
||||
};
|
||||
template <class Enum>
|
||||
struct Enum_Adapter;
|
||||
template <class Enum>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
#include "adminive/adapter.hpp"
|
||||
#include <boost/pfr.hpp>
|
||||
#if __has_include(<boost/pfr/core_name.hpp>)
|
||||
#include <boost/pfr/core_name.hpp>
|
||||
#define ADMINIVE_BOOST_PFR_HAS_NAMES 1
|
||||
#else
|
||||
#define ADMINIVE_BOOST_PFR_HAS_NAMES 0
|
||||
#endif
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
namespace adminive {
|
||||
template <class T>
|
||||
struct Boost_Pfr_Name_Adapter;
|
||||
namespace detail {
|
||||
template <class T, std::size_t Index>
|
||||
constexpr std::string_view pfr_name() noexcept {
|
||||
#if ADMINIVE_BOOST_PFR_HAS_NAMES
|
||||
return boost::pfr::get_name<Index, T>();
|
||||
#else
|
||||
return Boost_Pfr_Name_Adapter<T>::template name<Index>();
|
||||
#endif
|
||||
}
|
||||
template <std::size_t Index, class Derived, class Base, class... Bases>
|
||||
decltype(auto) pfr_base_get(Derived& value) noexcept {
|
||||
constexpr std::size_t count = boost::pfr::tuple_size_v<Base>;
|
||||
if constexpr(Index < count) {
|
||||
return boost::pfr::get<Index>(static_cast<Base&>(value));
|
||||
} else {
|
||||
static_assert(sizeof...(Bases) != 0);
|
||||
return pfr_base_get<Index - count, Derived, Bases...>(value);
|
||||
}
|
||||
}
|
||||
template <std::size_t Index, class Derived, class Base, class... Bases>
|
||||
decltype(auto) pfr_base_get(const Derived& value) noexcept {
|
||||
constexpr std::size_t count = boost::pfr::tuple_size_v<Base>;
|
||||
if constexpr(Index < count) {
|
||||
return boost::pfr::get<Index>(static_cast<const Base&>(value));
|
||||
} else {
|
||||
static_assert(sizeof...(Bases) != 0);
|
||||
return pfr_base_get<Index - count, Derived, Bases...>(value);
|
||||
}
|
||||
}
|
||||
template <std::size_t Index, class Base, class... Bases>
|
||||
constexpr std::string_view pfr_base_name() noexcept {
|
||||
constexpr std::size_t count = boost::pfr::tuple_size_v<Base>;
|
||||
if constexpr(Index < count) {
|
||||
return pfr_name<Base, Index>();
|
||||
} else {
|
||||
static_assert(sizeof...(Bases) != 0);
|
||||
return pfr_base_name<Index - count, Bases...>();
|
||||
}
|
||||
}
|
||||
}
|
||||
template <class T>
|
||||
struct Boost_Pfr_Reflection_Adapter {
|
||||
static constexpr std::size_t field_count = boost::pfr::tuple_size_v<T>;
|
||||
template <std::size_t Index>
|
||||
static decltype(auto) get(T& value) noexcept {
|
||||
return boost::pfr::get<Index>(value);
|
||||
}
|
||||
template <std::size_t Index>
|
||||
static decltype(auto) get(const T& value) noexcept {
|
||||
return boost::pfr::get<Index>(value);
|
||||
}
|
||||
template <std::size_t Index>
|
||||
static constexpr std::string_view name() noexcept {
|
||||
return detail::pfr_name<T, Index>();
|
||||
}
|
||||
};
|
||||
template <class Derived, class... Bases>
|
||||
struct Boost_Pfr_Base_Reflection_Adapter {
|
||||
static_assert(sizeof...(Bases) != 0);
|
||||
static_assert((std::is_base_of_v<Bases, Derived> && ...));
|
||||
static constexpr std::size_t field_count = (boost::pfr::tuple_size_v<Bases> + ... + 0);
|
||||
template <std::size_t Index>
|
||||
static decltype(auto) get(Derived& value) noexcept {
|
||||
static_assert(Index < field_count);
|
||||
return detail::pfr_base_get<Index, Derived, Bases...>(value);
|
||||
}
|
||||
template <std::size_t Index>
|
||||
static decltype(auto) get(const Derived& value) noexcept {
|
||||
static_assert(Index < field_count);
|
||||
return detail::pfr_base_get<Index, Derived, Bases...>(value);
|
||||
}
|
||||
template <std::size_t Index>
|
||||
static constexpr std::string_view name() noexcept {
|
||||
static_assert(Index < field_count);
|
||||
return detail::pfr_base_name<Index, Bases...>();
|
||||
}
|
||||
};
|
||||
}
|
||||
#undef ADMINIVE_BOOST_PFR_HAS_NAMES
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
#include "adminive/http.hpp"
|
||||
#include <drogon/drogon.h>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
namespace adminive {
|
||||
template <Json_Type Json>
|
||||
drogon::HttpResponsePtr make_drogon_response(Http_Response<Json> value) {
|
||||
auto response = drogon::HttpResponse::newHttpResponse();
|
||||
response->setStatusCode(static_cast<drogon::HttpStatusCode>(value.status));
|
||||
response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
|
||||
response->setBody(dump_json(value.body, 2));
|
||||
return response;
|
||||
}
|
||||
template <class T, Json_Type Json>
|
||||
requires Adapted_Object_Type<T>
|
||||
class Drogon_Resource {
|
||||
public:
|
||||
using Service = Resource_Service<T, Json>;
|
||||
using Commit_Function = typename Service::Commit_Function;
|
||||
Drogon_Resource(T& value, std::string path, Commit_Function commit = {}, std::mutex* shared_mutex = nullptr) : service_(value, std::move(path), std::move(commit), shared_mutex) {}
|
||||
Json amis_schema() const {
|
||||
return service_.amis_schema();
|
||||
}
|
||||
void bind(drogon::HttpAppFramework& app) {
|
||||
app.registerHandler(service_.path() + "/descriptor", [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
callback(make_drogon_response(service_.descriptor_response()));
|
||||
}, {drogon::Get});
|
||||
app.registerHandler(service_.path() + "/data", [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
callback(make_drogon_response(service_.data_response()));
|
||||
}, {drogon::Get});
|
||||
app.registerHandler(service_.path() + "/amis", [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
callback(make_drogon_response(service_.amis_response()));
|
||||
}, {drogon::Get});
|
||||
app.registerHandler(service_.path() + "/data", [this](const drogon::HttpRequestPtr& request, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
callback(make_drogon_response(service_.update_response(request->getBody())));
|
||||
}, {drogon::Post});
|
||||
}
|
||||
private:
|
||||
Service service_;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
#pragma once
|
||||
#include "adminive/http.hpp"
|
||||
#include "httplib.h"
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
namespace adminive {
|
||||
template <Json_Type Json>
|
||||
void write_http_json(httplib::Response& response, const Json& value) {
|
||||
response.set_content(dump_json(value, 2), "application/json; charset=utf-8");
|
||||
}
|
||||
template <Json_Type Json>
|
||||
void write_http_response(httplib::Response& response, Http_Response<Json> value) {
|
||||
response.status = value.status;
|
||||
write_http_json(response, value.body);
|
||||
}
|
||||
template <Json_Type Json>
|
||||
void write_http_error(httplib::Response& response, int status, const Update_Result& update) {
|
||||
write_http_response(response, make_http_error<Json>(status, update));
|
||||
}
|
||||
template <class T, Json_Type Json>
|
||||
requires Adapted_Object_Type<T>
|
||||
class Http_Resource {
|
||||
public:
|
||||
using Service = Resource_Service<T, Json>;
|
||||
using Commit_Function = typename Service::Commit_Function;
|
||||
Http_Resource(T& value, std::string path, Commit_Function commit = {}, std::mutex* shared_mutex = nullptr) : service_(value, std::move(path), std::move(commit), shared_mutex) {}
|
||||
Json amis_schema() const {
|
||||
return service_.amis_schema();
|
||||
}
|
||||
void bind(httplib::Server& server) {
|
||||
server.Get(service_.path() + "/descriptor", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_response(response, service_.descriptor_response());
|
||||
});
|
||||
server.Get(service_.path() + "/data", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_response(response, service_.data_response());
|
||||
});
|
||||
server.Get(service_.path() + "/amis", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_response(response, service_.amis_response());
|
||||
});
|
||||
server.Post(service_.path() + "/data", [this](const httplib::Request& request, httplib::Response& response) {
|
||||
write_http_response(response, service_.update_response(request.body));
|
||||
});
|
||||
}
|
||||
private:
|
||||
Service service_;
|
||||
};
|
||||
template <Described_Type T, Json_Type Json>
|
||||
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
class Http_Collection_Resource {
|
||||
public:
|
||||
explicit Http_Collection_Resource(std::string path, std::vector<T> initial = {}) : path_(std::move(path)) {
|
||||
for(auto& value : initial) {
|
||||
entries_.push_back(Entry{next_id_++, std::move(value)});
|
||||
}
|
||||
}
|
||||
template <Described_Type Status>
|
||||
void register_overview_status(std::string api, std::uint64_t interval = 2000) {
|
||||
overview_status_api_ = std::move(api);
|
||||
overview_status_descriptor_ = to_status_descriptor_json<Json, Status>();
|
||||
overview_status_interval_ = interval;
|
||||
}
|
||||
template <auto Function>
|
||||
void register_status(std::uint64_t interval = 2000) {
|
||||
using Function_Type = decltype(Function);
|
||||
static_assert(std::is_member_function_pointer_v<Function_Type>);
|
||||
static_assert(std::is_invocable_v<Function_Type, T&>);
|
||||
using Status = std::remove_cvref_t<std::invoke_result_t<Function_Type, T&>>;
|
||||
static_assert(Described_Type<Status>);
|
||||
status_descriptor_ = to_status_descriptor_json<Json, Status>();
|
||||
status_interval_ = interval;
|
||||
status_reader_ = [](T& value) {
|
||||
return to_status_json<Json>(std::invoke(Function, value));
|
||||
};
|
||||
}
|
||||
std::size_t size() const {
|
||||
std::scoped_lock lock(mutex_);
|
||||
return entries_.size();
|
||||
}
|
||||
Json items_json() const {
|
||||
std::scoped_lock lock(mutex_);
|
||||
Json items = json_array<Json>();
|
||||
for(const auto& entry : entries_) {
|
||||
json_append(items, encode_entry(entry));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
Json amis_schema() const {
|
||||
const Json* descriptor = status_reader_ ? &status_descriptor_ : nullptr;
|
||||
if(overview_status_api_.empty()) {
|
||||
return to_amis_crud_schema<Json, T>(path_, descriptor, status_interval_);
|
||||
}
|
||||
return to_amis_crud_status_schema<Json, T>(path_, overview_status_api_, overview_status_descriptor_, overview_status_interval_, descriptor, status_interval_);
|
||||
}
|
||||
void bind(httplib::Server& server) {
|
||||
server.Get(path_ + "/descriptor", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, make_http_result<Json>(0, "", to_descriptor_json<Json, T>()));
|
||||
});
|
||||
server.Get(path_ + "/amis", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, make_http_result<Json>(0, "", amis_schema()));
|
||||
});
|
||||
if(status_reader_) {
|
||||
server.Get(path_ + "/status/descriptor", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, make_http_result<Json>(0, "", status_descriptor_));
|
||||
});
|
||||
server.Get(status_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
|
||||
const auto id = read_route_id(request);
|
||||
std::scoped_lock lock(mutex_);
|
||||
const auto iterator = find_entry(id);
|
||||
if(iterator == entries_.end()) {
|
||||
write_not_found(response);
|
||||
return;
|
||||
}
|
||||
write_http_json(response, make_http_result<Json>(0, "", status_reader_(iterator->value)));
|
||||
});
|
||||
}
|
||||
server.Get(path_, [this](const httplib::Request& request, httplib::Response& response) {
|
||||
const std::size_t page = read_size_parameter(request, "page", 1);
|
||||
const std::size_t per_page = read_size_parameter(request, "perPage", 20);
|
||||
std::scoped_lock lock(mutex_);
|
||||
std::vector<const Entry*> ordered_entries;
|
||||
ordered_entries.reserve(entries_.size());
|
||||
for(const auto& entry : entries_) {
|
||||
ordered_entries.push_back(&entry);
|
||||
}
|
||||
apply_request_sort(request, ordered_entries);
|
||||
const std::size_t offset = std::min((page - 1) * per_page, ordered_entries.size());
|
||||
const std::size_t end = std::min(offset + per_page, ordered_entries.size());
|
||||
Json items = json_array<Json>();
|
||||
for(std::size_t index = offset; index < end; ++index) {
|
||||
json_append(items, encode_entry(*ordered_entries[index]));
|
||||
}
|
||||
Json data = json_object<Json>();
|
||||
json_set(data, "items", std::move(items));
|
||||
json_set(data, "total", ordered_entries.size());
|
||||
write_http_json(response, make_http_result<Json>(0, "", std::move(data)));
|
||||
});
|
||||
server.Get(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
|
||||
const auto id = read_route_id(request);
|
||||
std::scoped_lock lock(mutex_);
|
||||
const auto iterator = find_entry(id);
|
||||
if(iterator == entries_.end()) {
|
||||
write_not_found(response);
|
||||
return;
|
||||
}
|
||||
write_http_json(response, make_http_result<Json>(0, "", encode_entry(*iterator)));
|
||||
});
|
||||
server.Post(path_, [this](const httplib::Request& request, httplib::Response& response) {
|
||||
try {
|
||||
Json input = parse_json<Json>(request.body);
|
||||
Json_Adapter<Json>::erase(input, "id");
|
||||
T value{};
|
||||
const auto result = apply_frontend_create<Json>(value, input);
|
||||
if(!result.success) {
|
||||
write_http_error<Json>(response, 422, result);
|
||||
return;
|
||||
}
|
||||
std::scoped_lock lock(mutex_);
|
||||
entries_.push_back(Entry{next_id_++, std::move(value)});
|
||||
write_http_json(response, make_http_result<Json>(0, "created", encode_entry(entries_.back())));
|
||||
} catch(const std::exception& error) {
|
||||
response.status = 400;
|
||||
write_http_json(response, make_http_result<Json>(400, error.what(), json_object<Json>()));
|
||||
}
|
||||
});
|
||||
if(describe<T>().list_options().user_reorderable_) {
|
||||
server.Post(path_ + "/order", [this](const httplib::Request& request, httplib::Response& response) {
|
||||
try {
|
||||
const Json input = parse_json<Json>(request.body);
|
||||
const auto ids = read_order_ids(Json_Adapter<Json>::at(input, "ids"));
|
||||
std::scoped_lock lock(mutex_);
|
||||
reorder(ids);
|
||||
write_http_json(response, make_http_result<Json>(0, "reordered", json_object<Json>()));
|
||||
} catch(const std::exception& error) {
|
||||
response.status = 400;
|
||||
write_http_json(response, make_http_result<Json>(400, error.what(), json_object<Json>()));
|
||||
}
|
||||
});
|
||||
}
|
||||
server.Put(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
|
||||
update(request, response);
|
||||
});
|
||||
server.Patch(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
|
||||
update(request, response);
|
||||
});
|
||||
server.Delete(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
|
||||
const auto id = read_route_id(request);
|
||||
std::scoped_lock lock(mutex_);
|
||||
const auto iterator = find_entry(id);
|
||||
if(iterator == entries_.end()) {
|
||||
write_not_found(response);
|
||||
return;
|
||||
}
|
||||
entries_.erase(iterator);
|
||||
write_http_json(response, make_http_result<Json>(0, "deleted", json_object<Json>()));
|
||||
});
|
||||
}
|
||||
private:
|
||||
struct Entry {
|
||||
std::uint64_t id{};
|
||||
T value;
|
||||
};
|
||||
using Iterator = typename std::vector<Entry>::iterator;
|
||||
static std::size_t read_size_parameter(const httplib::Request& request, const char* name, std::size_t fallback) {
|
||||
if(!request.has_param(name)) {
|
||||
return fallback;
|
||||
}
|
||||
const auto text = request.get_param_value(name);
|
||||
std::size_t value{};
|
||||
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
if(error != std::errc{} || end != text.data() + text.size() || value == 0) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
static int compare_json_values(const Json& left, const Json& right) {
|
||||
if(Json_Adapter<Json>::is_number(left) && Json_Adapter<Json>::is_number(right)) {
|
||||
const long double left_value = Json_Adapter<Json>::number(left);
|
||||
const long double right_value = Json_Adapter<Json>::number(right);
|
||||
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
|
||||
}
|
||||
if(Json_Adapter<Json>::is_string(left) && Json_Adapter<Json>::is_string(right)) {
|
||||
const auto left_value = json_get<Json, std::string>(left);
|
||||
const auto right_value = json_get<Json, std::string>(right);
|
||||
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
|
||||
}
|
||||
if(Json_Adapter<Json>::is_boolean(left) && Json_Adapter<Json>::is_boolean(right)) {
|
||||
const bool left_value = json_get<Json, bool>(left);
|
||||
const bool right_value = json_get<Json, bool>(right);
|
||||
return left_value == right_value ? 0 : left_value ? 1 : -1;
|
||||
}
|
||||
const std::string left_value = dump_json(left);
|
||||
const std::string right_value = dump_json(right);
|
||||
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
|
||||
}
|
||||
static bool is_sortable_field(const std::string& name) {
|
||||
if(name == "id") {
|
||||
return true;
|
||||
}
|
||||
bool sortable{};
|
||||
std::apply([&](const auto&... field) {
|
||||
((field.name() == name ? sortable = field.is_sortable() : false), ...);
|
||||
}, describe<T>().fields());
|
||||
return sortable;
|
||||
}
|
||||
static std::uint64_t read_route_id(const httplib::Request& request) {
|
||||
const std::string text = request.matches[1].str();
|
||||
std::uint64_t id{};
|
||||
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), id);
|
||||
if(error != std::errc{} || end != text.data() + text.size()) {
|
||||
return 0;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
static std::vector<std::uint64_t> read_order_ids(const Json& value) {
|
||||
std::vector<std::uint64_t> result;
|
||||
if(Json_Adapter<Json>::is_array(value)) {
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(value); ++index) {
|
||||
const auto& item = Json_Adapter<Json>::at(value, index);
|
||||
result.push_back(Json_Adapter<Json>::is_string(item) ? std::stoull(json_get<Json, std::string>(item)) : json_get<Json, std::uint64_t>(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const std::string text = json_get<Json, std::string>(value);
|
||||
std::size_t begin{};
|
||||
while(begin < text.size()) {
|
||||
const auto end = text.find(',', begin);
|
||||
result.push_back(std::stoull(text.substr(begin, end == std::string::npos ? text.size() - begin : end - begin)));
|
||||
if(end == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
begin = end + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
void apply_request_sort(const httplib::Request& request, std::vector<const Entry*>& entries) const {
|
||||
const auto descriptor = describe<T>();
|
||||
const auto& options = descriptor.list_options();
|
||||
std::string order_by = options.default_order_by;
|
||||
std::string order_dir = options.default_order_dir;
|
||||
if(request.has_param("orderBy")) {
|
||||
order_by = request.get_param_value("orderBy");
|
||||
order_dir = request.has_param("orderDir") ? request.get_param_value("orderDir") : "asc";
|
||||
}
|
||||
if(order_by.empty() || !is_sortable_field(order_by)) {
|
||||
return;
|
||||
}
|
||||
const bool descending = order_dir == "desc";
|
||||
std::stable_sort(entries.begin(), entries.end(), [&](const Entry* left, const Entry* right) {
|
||||
int comparison{};
|
||||
if(order_by == "id") {
|
||||
comparison = left->id < right->id ? -1 : left->id > right->id ? 1 : 0;
|
||||
} else {
|
||||
const auto left_json = to_frontend_json<Json>(left->value);
|
||||
const auto right_json = to_frontend_json<Json>(right->value);
|
||||
comparison = compare_json_values(Json_Adapter<Json>::at(left_json, order_by), Json_Adapter<Json>::at(right_json, order_by));
|
||||
}
|
||||
return descending ? comparison > 0 : comparison < 0;
|
||||
});
|
||||
}
|
||||
std::string item_pattern() const {
|
||||
return path_ + R"(/(\d+))";
|
||||
}
|
||||
std::string status_pattern() const {
|
||||
return path_ + R"(/(\d+)/status)";
|
||||
}
|
||||
Iterator find_entry(std::uint64_t id) {
|
||||
return std::find_if(entries_.begin(), entries_.end(), [id](const Entry& entry) {
|
||||
return entry.id == id;
|
||||
});
|
||||
}
|
||||
static Json encode_entry(const Entry& entry) {
|
||||
Json result = to_frontend_json<Json>(entry.value);
|
||||
json_set(result, "id", entry.id);
|
||||
return result;
|
||||
}
|
||||
static void write_not_found(httplib::Response& response) {
|
||||
response.status = 404;
|
||||
write_http_json(response, make_http_result<Json>(404, "record not found", json_object<Json>()));
|
||||
}
|
||||
void reorder(const std::vector<std::uint64_t>& ids) {
|
||||
std::vector<Entry> remaining = std::move(entries_);
|
||||
std::vector<Entry> reordered;
|
||||
reordered.reserve(remaining.size());
|
||||
for(const auto id : ids) {
|
||||
const auto iterator = std::find_if(remaining.begin(), remaining.end(), [id](const Entry& entry) {
|
||||
return entry.id == id;
|
||||
});
|
||||
if(iterator != remaining.end()) {
|
||||
reordered.push_back(std::move(*iterator));
|
||||
remaining.erase(iterator);
|
||||
}
|
||||
}
|
||||
for(auto& entry : remaining) {
|
||||
reordered.push_back(std::move(entry));
|
||||
}
|
||||
entries_ = std::move(reordered);
|
||||
}
|
||||
void update(const httplib::Request& request, httplib::Response& response) {
|
||||
try {
|
||||
Json patch = parse_json<Json>(request.body);
|
||||
Json_Adapter<Json>::erase(patch, "id");
|
||||
const auto id = read_route_id(request);
|
||||
std::scoped_lock lock(mutex_);
|
||||
const auto iterator = find_entry(id);
|
||||
if(iterator == entries_.end()) {
|
||||
write_not_found(response);
|
||||
return;
|
||||
}
|
||||
const auto result = apply_frontend_patch<Json>(iterator->value, patch);
|
||||
if(!result.success) {
|
||||
write_http_error<Json>(response, 422, result);
|
||||
return;
|
||||
}
|
||||
write_http_json(response, make_http_result<Json>(0, result.message, encode_entry(*iterator)));
|
||||
} catch(const std::exception& error) {
|
||||
response.status = 400;
|
||||
write_http_json(response, make_http_result<Json>(400, error.what(), json_object<Json>()));
|
||||
}
|
||||
}
|
||||
std::string path_;
|
||||
std::string overview_status_api_;
|
||||
Json overview_status_descriptor_{};
|
||||
std::uint64_t overview_status_interval_{2000};
|
||||
Json status_descriptor_{};
|
||||
std::function<Json(T&)> status_reader_;
|
||||
std::uint64_t status_interval_{2000};
|
||||
std::vector<Entry> entries_;
|
||||
std::uint64_t next_id_{1};
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "adminive/status.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -28,23 +29,59 @@ Json make_amis_form_actions(std::string submit_label = "Apply") {
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_control(const Json& field, std::string_view permission, std::string_view prefix = {}) {
|
||||
Json make_amis_form_body(const Json& descriptor, std::string_view permission, std::string_view prefix = {});
|
||||
template <Json_Type Json, Described_Type T>
|
||||
Json make_typed_amis_form_body(const Json& descriptor, std::string_view permission, std::string_view prefix = {});
|
||||
template <Json_Type Json>
|
||||
Json make_amis_polymorphic_control(const Json& field, std::string_view permission, std::string_view prefix) {
|
||||
const std::string name = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "name"));
|
||||
const std::string full_name = make_field_name(prefix, name);
|
||||
const auto& metadata = Json_Adapter<Json>::at(field, "polymorphic");
|
||||
const std::string discriminator = json_get<Json, std::string>(Json_Adapter<Json>::at(metadata, "discriminator"));
|
||||
Json selector = json_object<Json>();
|
||||
json_set(selector, "type", "select");
|
||||
json_set(selector, "name", make_field_name(full_name, discriminator));
|
||||
json_set(selector, "label", Json_Adapter<Json>::at(metadata, "discriminator_label"));
|
||||
json_set(selector, "options", Json_Adapter<Json>::at(metadata, "options"));
|
||||
json_set(selector, "required", true);
|
||||
Json body = json_array<Json>();
|
||||
json_append(body, std::move(selector));
|
||||
const auto& variants = Json_Adapter<Json>::at(metadata, "variants");
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(variants); ++index) {
|
||||
const auto& variant = Json_Adapter<Json>::at(variants, index);
|
||||
const std::string value = json_get<Json, std::string>(Json_Adapter<Json>::at(variant, "value"));
|
||||
Json group = json_object<Json>();
|
||||
json_set(group, "type", "container");
|
||||
json_set(group, "visibleOn", "${" + make_field_name(full_name, discriminator) + " == '" + value + "'}");
|
||||
json_set(group, "body", make_amis_form_body<Json>(Json_Adapter<Json>::at(variant, "descriptor"), permission, full_name));
|
||||
json_append(body, std::move(group));
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "fieldset");
|
||||
json_set(result, "title", Json_Adapter<Json>::at(field, "label"));
|
||||
json_set(result, "body", std::move(body));
|
||||
json_set(result, "collapsable", true);
|
||||
json_set(result, "collapsed", false);
|
||||
if(Json_Adapter<Json>::contains(field, "description")) {
|
||||
json_set(result, "description", Json_Adapter<Json>::at(field, "description"));
|
||||
}
|
||||
if(Json_Adapter<Json>::contains(field, "visible_on")) {
|
||||
json_set(result, "visibleOn", Json_Adapter<Json>::at(field, "visible_on"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_default_amis_control(const Json& field, std::string_view permission, std::string_view prefix = {}) {
|
||||
const std::string name = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "name"));
|
||||
const std::string full_name = make_field_name(prefix, name);
|
||||
const std::string value_type = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "value_type"));
|
||||
if(value_type == "object" && Json_Adapter<Json>::contains(field, "children")) {
|
||||
Json body = json_array<Json>();
|
||||
const auto& children = Json_Adapter<Json>::at(field, "children");
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(children); ++index) {
|
||||
const auto& child = Json_Adapter<Json>::at(children, index);
|
||||
if(json_get<Json, bool>(Json_Adapter<Json>::at(child, "visible"))) {
|
||||
json_append(body, make_amis_control<Json>(child, permission, full_name));
|
||||
}
|
||||
}
|
||||
Json descriptor = json_object<Json>();
|
||||
json_set(descriptor, "fields", Json_Adapter<Json>::at(field, "children"));
|
||||
Json control = json_object<Json>();
|
||||
json_set(control, "type", "fieldset");
|
||||
json_set(control, "title", Json_Adapter<Json>::at(field, "label"));
|
||||
json_set(control, "body", std::move(body));
|
||||
json_set(control, "body", make_amis_form_body<Json>(descriptor, permission, full_name));
|
||||
json_set(control, "collapsable", true);
|
||||
json_set(control, "collapsed", false);
|
||||
if(Json_Adapter<Json>::contains(field, "description")) {
|
||||
@@ -55,6 +92,9 @@ Json make_amis_control(const Json& field, std::string_view permission, std::stri
|
||||
}
|
||||
return control;
|
||||
}
|
||||
if(value_type == "polymorphic" && Json_Adapter<Json>::contains(field, "polymorphic")) {
|
||||
return make_amis_polymorphic_control<Json>(field, permission, prefix);
|
||||
}
|
||||
Json control = json_object<Json>();
|
||||
json_set(control, "name", full_name);
|
||||
json_set(control, "label", Json_Adapter<Json>::at(field, "label"));
|
||||
@@ -76,8 +116,10 @@ Json make_amis_control(const Json& field, std::string_view permission, std::stri
|
||||
json_set(control, "type", "input-number");
|
||||
} else if(value_type == "enum") {
|
||||
json_set(control, "type", "select");
|
||||
} else {
|
||||
} else if(value_type == "string") {
|
||||
json_set(control, "type", "input-text");
|
||||
} else {
|
||||
throw std::invalid_argument("field '" + full_name + "' requires an explicit widget or Control_Adapter");
|
||||
}
|
||||
const std::string control_type = json_get<Json, std::string>(Json_Adapter<Json>::at(control, "type"));
|
||||
if(control_type == "input-date") {
|
||||
@@ -110,19 +152,75 @@ Json make_amis_control(const Json& field, std::string_view permission, std::stri
|
||||
return control;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_form_body(const Json& descriptor, std::string_view permission) {
|
||||
Json make_amis_control(const Json& field, std::string_view permission, std::string_view prefix = {}) {
|
||||
return make_default_amis_control<Json>(field, permission, prefix);
|
||||
}
|
||||
template <Json_Type Json, class T>
|
||||
Json make_typed_amis_control(const Json& field, std::string_view permission, std::string_view prefix = {}) {
|
||||
using Storage = std::remove_cvref_t<T>;
|
||||
using Value = Adapted_Value_Type<Storage, Json>;
|
||||
const Control_Context context{permission, prefix};
|
||||
if constexpr(Control_Adapter_With_Control<Storage, Json>) {
|
||||
return Control_Adapter<Storage, Json>::make_control(field, context);
|
||||
} else if constexpr(!std::same_as<Storage, Value> && Control_Adapter_With_Control<Value, Json>) {
|
||||
return Control_Adapter<Value, Json>::make_control(field, context);
|
||||
} else if constexpr(Adapted_Object_Type<Value>) {
|
||||
using Model = Object_Model_Type<Value>;
|
||||
const std::string name = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "name"));
|
||||
const std::string full_name = make_field_name(prefix, name);
|
||||
const Json descriptor = to_descriptor_json<Json, Value>();
|
||||
Json control = json_object<Json>();
|
||||
json_set(control, "type", "fieldset");
|
||||
json_set(control, "title", Json_Adapter<Json>::at(field, "label"));
|
||||
json_set(control, "body", make_typed_amis_form_body<Json, Model>(descriptor, permission, full_name));
|
||||
json_set(control, "collapsable", true);
|
||||
json_set(control, "collapsed", false);
|
||||
if(Json_Adapter<Json>::contains(field, "description")) {
|
||||
json_set(control, "description", Json_Adapter<Json>::at(field, "description"));
|
||||
}
|
||||
if(Json_Adapter<Json>::contains(field, "visible_on")) {
|
||||
json_set(control, "visibleOn", Json_Adapter<Json>::at(field, "visible_on"));
|
||||
}
|
||||
return control;
|
||||
} else {
|
||||
return make_default_amis_control<Json>(field, permission, prefix);
|
||||
}
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_form_body(const Json& descriptor, std::string_view permission, std::string_view prefix) {
|
||||
Json body = json_array<Json>();
|
||||
const auto& fields = Json_Adapter<Json>::at(descriptor, "fields");
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(fields); ++index) {
|
||||
const auto& field = Json_Adapter<Json>::at(fields, index);
|
||||
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "visible"))) {
|
||||
json_append(body, make_amis_control<Json>(field, permission));
|
||||
json_append(body, make_default_amis_control<Json>(field, permission, prefix));
|
||||
}
|
||||
}
|
||||
return body;
|
||||
}
|
||||
template <std::size_t Index, Json_Type Json, class Descriptor>
|
||||
void append_typed_amis_form_fields(Json& body, const Json& fields, const Descriptor& descriptor, std::string_view permission, std::string_view prefix) {
|
||||
using Fields = std::remove_cvref_t<decltype(descriptor.fields())>;
|
||||
if constexpr(Index < std::tuple_size_v<Fields>) {
|
||||
const auto& item = std::get<Index>(descriptor.fields());
|
||||
const auto& field = Json_Adapter<Json>::at(fields, Index);
|
||||
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "visible"))) {
|
||||
using Field = std::remove_cvref_t<decltype(item)>;
|
||||
json_append(body, make_typed_amis_control<Json, typename Field::member_type>(field, permission, prefix));
|
||||
}
|
||||
append_typed_amis_form_fields<Index + 1>(body, fields, descriptor, permission, prefix);
|
||||
}
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
Json make_typed_amis_form_body(const Json& descriptor, std::string_view permission, std::string_view prefix) {
|
||||
Json body = json_array<Json>();
|
||||
const auto& fields = Json_Adapter<Json>::at(descriptor, "fields");
|
||||
const auto object_descriptor = describe<T>();
|
||||
append_typed_amis_form_fields<0>(body, fields, object_descriptor, permission, prefix);
|
||||
return body;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_column(const Json& field) {
|
||||
Json make_default_amis_column(const Json& field) {
|
||||
Json column = json_object<Json>();
|
||||
json_set(column, "name", Json_Adapter<Json>::at(field, "name"));
|
||||
json_set(column, "label", Json_Adapter<Json>::at(field, "list_label"));
|
||||
@@ -147,31 +245,44 @@ Json make_amis_column(const Json& field) {
|
||||
}
|
||||
return column;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_status_service(std::string status_api) {
|
||||
Json view = json_object<Json>();
|
||||
json_set(view, "type", "tpl");
|
||||
json_set(view, "tpl", R"(<div class="adminive-status-grid"><div><strong>Service</strong><span>${service_state}</span></div><div><strong>Server time</strong><span>${server_time}</span></div><div><strong>Uptime</strong><span>${uptime_seconds} s</span></div><div><strong>Polling sequence</strong><span>${poll_sequence}</span></div><div><strong>Radio states</strong><span>${radio_state_count}</span></div><div><strong>Listen port</strong><span>${listen_port}</span></div></div>)");
|
||||
Json panel = json_object<Json>();
|
||||
json_set(panel, "type", "panel");
|
||||
json_set(panel, "title", "Backend Status");
|
||||
json_set(panel, "body", std::move(view));
|
||||
Json api = json_object<Json>();
|
||||
json_set(api, "method", "get");
|
||||
json_set(api, "url", std::move(status_api));
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "service");
|
||||
json_set(result, "name", "backend_status");
|
||||
json_set(result, "api", std::move(api));
|
||||
json_set(result, "initFetch", true);
|
||||
json_set(result, "interval", 2000);
|
||||
json_set(result, "silentPolling", true);
|
||||
json_set(result, "showErrorMsg", true);
|
||||
json_set(result, "body", std::move(panel));
|
||||
return result;
|
||||
template <Json_Type Json, class T>
|
||||
Json make_typed_amis_column(const Json& field) {
|
||||
using Storage = std::remove_cvref_t<T>;
|
||||
using Value = Adapted_Value_Type<Storage, Json>;
|
||||
if constexpr(Control_Adapter_With_Column<Storage, Json>) {
|
||||
return Control_Adapter<Storage, Json>::make_column(field);
|
||||
} else if constexpr(!std::same_as<Storage, Value> && Control_Adapter_With_Column<Value, Json>) {
|
||||
return Control_Adapter<Value, Json>::make_column(field);
|
||||
} else {
|
||||
return make_default_amis_column<Json>(field);
|
||||
}
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_object_status_body(const Json& descriptor) {
|
||||
struct Amis_Ordered_Column {
|
||||
int order{};
|
||||
Json column;
|
||||
};
|
||||
template <std::size_t Index, Json_Type Json, class Descriptor>
|
||||
void append_typed_amis_columns(std::vector<Amis_Ordered_Column<Json>>& columns, const Json& fields, const Descriptor& descriptor) {
|
||||
using Fields = std::remove_cvref_t<decltype(descriptor.fields())>;
|
||||
if constexpr(Index < std::tuple_size_v<Fields>) {
|
||||
const auto& item = std::get<Index>(descriptor.fields());
|
||||
const auto& field = Json_Adapter<Json>::at(fields, Index);
|
||||
using Field = std::remove_cvref_t<decltype(item)>;
|
||||
using Member = typename Field::member_type;
|
||||
using Storage = std::remove_cvref_t<Member>;
|
||||
using Value = Adapted_Value_Type<Storage, Json>;
|
||||
constexpr bool custom_column = Control_Adapter_With_Column<Storage, Json> || (!std::same_as<Storage, Value> && Control_Adapter_With_Column<Value, Json>);
|
||||
const std::string value_type = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "value_type"));
|
||||
const bool structural = value_type == "object" || value_type == "array" || value_type == "map" || value_type == "polymorphic";
|
||||
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "readable")) && json_get<Json, bool>(Json_Adapter<Json>::at(field, "list_visible")) && (!structural || custom_column)) {
|
||||
columns.push_back(Amis_Ordered_Column<Json>{json_get<Json, int>(Json_Adapter<Json>::at(field, "order")), make_typed_amis_column<Json, Member>(field)});
|
||||
}
|
||||
append_typed_amis_columns<Index + 1>(columns, fields, descriptor);
|
||||
}
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_status_body(const Json& descriptor) {
|
||||
Json body = json_array<Json>();
|
||||
const auto& fields = Json_Adapter<Json>::at(descriptor, "fields");
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(fields); ++index) {
|
||||
@@ -196,6 +307,26 @@ Json make_amis_object_status_body(const Json& descriptor) {
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_status_service(const Json& descriptor, std::string status_api, std::uint64_t interval = 2000) {
|
||||
Json panel = json_object<Json>();
|
||||
json_set(panel, "type", "panel");
|
||||
json_set(panel, "title", Json_Adapter<Json>::at(descriptor, "label"));
|
||||
json_set(panel, "body", make_amis_status_body<Json>(descriptor));
|
||||
Json api = json_object<Json>();
|
||||
json_set(api, "method", "get");
|
||||
json_set(api, "url", std::move(status_api));
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "service");
|
||||
json_set(result, "name", json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "name")) + "_status");
|
||||
json_set(result, "api", std::move(api));
|
||||
json_set(result, "initFetch", true);
|
||||
json_set(result, "interval", interval);
|
||||
json_set(result, "silentPolling", true);
|
||||
json_set(result, "showErrorMsg", true);
|
||||
json_set(result, "body", std::move(panel));
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_object_status_service(const Json& descriptor, std::string status_api, std::uint64_t interval) {
|
||||
Json api = json_object<Json>();
|
||||
json_set(api, "method", "get");
|
||||
@@ -207,7 +338,7 @@ Json make_amis_object_status_service(const Json& descriptor, std::string status_
|
||||
json_set(result, "interval", interval);
|
||||
json_set(result, "silentPolling", true);
|
||||
json_set(result, "showErrorMsg", true);
|
||||
json_set(result, "body", make_amis_object_status_body<Json>(descriptor));
|
||||
json_set(result, "body", make_amis_status_body<Json>(descriptor));
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
@@ -224,14 +355,16 @@ Json make_amis_object_status_column(const Json& descriptor, std::string status_a
|
||||
json_set(result, "popOver", std::move(pop_over));
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
template <Json_Type Json, class T>
|
||||
requires Adapted_Object_Type<T>
|
||||
Json to_amis_form_schema(const T& value, std::string submit_api = {}, std::string submit_label = "Apply") {
|
||||
using Model = Object_Model_Type<T>;
|
||||
const Json descriptor = to_descriptor_json<Json, T>();
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "form");
|
||||
json_set(result, "title", Json_Adapter<Json>::at(descriptor, "label"));
|
||||
json_set(result, "data", to_frontend_json<Json>(value));
|
||||
json_set(result, "body", make_amis_form_body<Json>(descriptor, "editable"));
|
||||
json_set(result, "body", make_typed_amis_form_body<Json, Model>(descriptor, "editable"));
|
||||
json_set(result, "actions", make_amis_form_actions<Json>(std::move(submit_label)));
|
||||
json_set(result, "affixFooter", true);
|
||||
if(!submit_api.empty()) {
|
||||
@@ -242,8 +375,10 @@ Json to_amis_form_schema(const T& value, std::string submit_api = {}, std::strin
|
||||
}
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
template <Json_Type Json, class T>
|
||||
requires Adapted_Object_Type<T>
|
||||
Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = nullptr, std::uint64_t status_interval = 2000) {
|
||||
using Model = Object_Model_Type<T>;
|
||||
const Json descriptor = to_descriptor_json<Json, T>();
|
||||
const Json& list = Json_Adapter<Json>::at(descriptor, "list");
|
||||
const std::string crud_id = json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "name")) + "_crud";
|
||||
@@ -253,19 +388,15 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
|
||||
json_set(id_column, "label", Json_Adapter<Json>::at(list, "id_label"));
|
||||
json_set(id_column, "sortable", true);
|
||||
json_append(columns, std::move(id_column));
|
||||
std::vector<Json> list_fields;
|
||||
std::vector<Amis_Ordered_Column<Json>> ordered_columns;
|
||||
const auto& fields = Json_Adapter<Json>::at(descriptor, "fields");
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(fields); ++index) {
|
||||
const auto& field = Json_Adapter<Json>::at(fields, index);
|
||||
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "readable")) && json_get<Json, bool>(Json_Adapter<Json>::at(field, "list_visible")) && json_get<Json, std::string>(Json_Adapter<Json>::at(field, "value_type")) != "object") {
|
||||
list_fields.push_back(field);
|
||||
}
|
||||
}
|
||||
std::stable_sort(list_fields.begin(), list_fields.end(), [](const Json& left, const Json& right) {
|
||||
return json_get<Json, int>(Json_Adapter<Json>::at(left, "order")) < json_get<Json, int>(Json_Adapter<Json>::at(right, "order"));
|
||||
const auto object_descriptor = describe<Model>();
|
||||
append_typed_amis_columns<0>(ordered_columns, fields, object_descriptor);
|
||||
std::stable_sort(ordered_columns.begin(), ordered_columns.end(), [](const Amis_Ordered_Column<Json>& left, const Amis_Ordered_Column<Json>& right) {
|
||||
return left.order < right.order;
|
||||
});
|
||||
for(const auto& field : list_fields) {
|
||||
json_append(columns, make_amis_column<Json>(field));
|
||||
for(auto& column : ordered_columns) {
|
||||
json_append(columns, std::move(column.column));
|
||||
}
|
||||
if(status_descriptor) {
|
||||
json_append(columns, make_amis_object_status_column<Json>(*status_descriptor, base_api + "/${id}/status", status_interval, json_get<Json, std::string>(Json_Adapter<Json>::at(list, "view_status_label"))));
|
||||
@@ -279,7 +410,7 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
|
||||
json_set(create_form, "type", "form");
|
||||
json_set(create_form, "api", std::move(create_api));
|
||||
json_set(create_form, "reload", crud_id);
|
||||
json_set(create_form, "body", make_amis_form_body<Json>(descriptor, "creatable"));
|
||||
json_set(create_form, "body", make_typed_amis_form_body<Json, Model>(descriptor, "creatable"));
|
||||
json_set(create_form, "actions", make_amis_form_actions<Json>(create_label));
|
||||
Json edit_api = json_object<Json>();
|
||||
json_set(edit_api, "method", "put");
|
||||
@@ -288,7 +419,7 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
|
||||
json_set(edit_form, "type", "form");
|
||||
json_set(edit_form, "api", std::move(edit_api));
|
||||
json_set(edit_form, "reload", crud_id);
|
||||
json_set(edit_form, "body", make_amis_form_body<Json>(descriptor, "editable"));
|
||||
json_set(edit_form, "body", make_typed_amis_form_body<Json, Model>(descriptor, "editable"));
|
||||
json_set(edit_form, "actions", make_amis_form_actions<Json>(json_get<Json, std::string>(Json_Adapter<Json>::at(list, "confirm_label"))));
|
||||
Json create_dialog = json_object<Json>();
|
||||
json_set(create_dialog, "title", create_label + " " + json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "label")));
|
||||
@@ -366,12 +497,13 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
|
||||
json_set(result, "body", std::move(crud));
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
Json to_amis_crud_status_schema(std::string base_api, std::string status_api, const Json* object_status_descriptor = nullptr, std::uint64_t object_status_interval = 2000) {
|
||||
template <Json_Type Json, class T>
|
||||
requires Adapted_Object_Type<T>
|
||||
Json to_amis_crud_status_schema(std::string base_api, std::string status_api, const Json& status_descriptor, std::uint64_t status_interval = 2000, const Json* object_status_descriptor = nullptr, std::uint64_t object_status_interval = 2000) {
|
||||
Json result = to_amis_crud_schema<Json, T>(std::move(base_api), object_status_descriptor, object_status_interval);
|
||||
Json crud = std::move(Json_Adapter<Json>::at(result, "body"));
|
||||
Json body = json_array<Json>();
|
||||
json_append(body, make_amis_status_service<Json>(std::move(status_api)));
|
||||
json_append(body, make_amis_status_service<Json>(status_descriptor, std::move(status_api), status_interval));
|
||||
json_append(body, std::move(crud));
|
||||
json_set(result, "body", std::move(body));
|
||||
return result;
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
#pragma once
|
||||
#include "adminive/amis.hpp"
|
||||
#include "httplib.h"
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
namespace adminive {
|
||||
template <Json_Type Json>
|
||||
void write_http_json(httplib::Response& response, const Json& value) {
|
||||
response.set_content(dump_json(value, 2), "application/json; charset=utf-8");
|
||||
}
|
||||
struct Http_Response {
|
||||
int status{200};
|
||||
Json body{};
|
||||
};
|
||||
template <Json_Type Json, class Data>
|
||||
Json make_http_result(int status, std::string message, Data&& data) {
|
||||
Json result = json_object<Json>();
|
||||
@@ -26,57 +19,77 @@ Json make_http_result(int status, std::string message, Data&& data) {
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
void write_http_error(httplib::Response& response, int status, const Update_Result& update) {
|
||||
response.status = status;
|
||||
Http_Response<Json> make_http_success(Json data, std::string message = {}) {
|
||||
return Http_Response<Json>{200, make_http_result<Json>(0, std::move(message), std::move(data))};
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Http_Response<Json> make_http_error(int status, const Update_Result& update) {
|
||||
Json result = make_http_result<Json>(status, update.message, json_object<Json>());
|
||||
Json errors = json_object<Json>();
|
||||
Json field_errors = json_object<Json>();
|
||||
for(const auto& [name, message] : update.field_errors) {
|
||||
json_set(errors, name, message);
|
||||
json_set(field_errors, name, message);
|
||||
}
|
||||
json_set(result, "errors", std::move(errors));
|
||||
write_http_json(response, result);
|
||||
json_set(result, "field_errors", std::move(field_errors));
|
||||
return Http_Response<Json>{status, std::move(result)};
|
||||
}
|
||||
template <Described_Type T, Json_Type Json>
|
||||
requires std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
class Http_Resource {
|
||||
template <Json_Type Json>
|
||||
Http_Response<Json> make_http_error(int status, std::string message) {
|
||||
return Http_Response<Json>{status, make_http_result<Json>(status, std::move(message), json_object<Json>())};
|
||||
}
|
||||
template <class T, Json_Type Json>
|
||||
requires Adapted_Object_Type<T>
|
||||
class Resource_Service {
|
||||
public:
|
||||
using Commit_Function = std::function<void(const T&)>;
|
||||
Http_Resource(T& value, std::string path, Commit_Function commit = {}, std::mutex* shared_mutex = nullptr) : value_(value), path_(std::move(path)), commit_(std::move(commit)), shared_mutex_(shared_mutex) {}
|
||||
using Object = std::remove_cvref_t<T>;
|
||||
using Model = Object_Model_Type<Object>;
|
||||
using Commit_Function = std::function<void(const Model&)>;
|
||||
Resource_Service(T& value, std::string path, Commit_Function commit = {}, std::mutex* shared_mutex = nullptr) : value_(value), path_(std::move(path)), commit_(std::move(commit)), shared_mutex_(shared_mutex) {}
|
||||
const std::string& path() const noexcept {
|
||||
return path_;
|
||||
}
|
||||
Json amis_schema() const {
|
||||
std::scoped_lock lock(resource_mutex());
|
||||
return to_amis_form_schema<Json>(value_, path_ + "/data", describe<T>().list_options().confirm_label);
|
||||
return to_amis_form_schema<Json>(value_, path_ + "/data", describe<Model>().list_options().confirm_label);
|
||||
}
|
||||
void bind(httplib::Server& server) {
|
||||
server.Get(path_ + "/descriptor", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, make_http_result<Json>(0, "", to_descriptor_json<Json, T>()));
|
||||
});
|
||||
server.Get(path_ + "/data", [this](const httplib::Request&, httplib::Response& response) {
|
||||
std::scoped_lock lock(resource_mutex());
|
||||
write_http_json(response, make_http_result<Json>(0, "", to_frontend_json<Json>(value_)));
|
||||
});
|
||||
server.Get(path_ + "/amis", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, make_http_result<Json>(0, "", amis_schema()));
|
||||
});
|
||||
server.Post(path_ + "/data", [this](const httplib::Request& request, httplib::Response& response) {
|
||||
try {
|
||||
const Json patch = parse_json<Json>(request.body);
|
||||
std::scoped_lock lock(resource_mutex());
|
||||
T candidate = value_;
|
||||
const auto result = apply_frontend_patch<Json>(candidate, patch);
|
||||
if(!result.success) {
|
||||
write_http_error<Json>(response, 422, result);
|
||||
return;
|
||||
}
|
||||
if(commit_) {
|
||||
commit_(candidate);
|
||||
}
|
||||
value_ = std::move(candidate);
|
||||
write_http_json(response, make_http_result<Json>(0, result.message, to_frontend_json<Json>(value_)));
|
||||
} catch(const std::exception& error) {
|
||||
response.status = 400;
|
||||
write_http_json(response, make_http_result<Json>(400, error.what(), json_object<Json>()));
|
||||
Http_Response<Json> descriptor_response() const {
|
||||
return make_http_success<Json>(to_descriptor_json<Json, Object>());
|
||||
}
|
||||
Http_Response<Json> data_response() const {
|
||||
std::scoped_lock lock(resource_mutex());
|
||||
return make_http_success<Json>(to_frontend_json<Json>(value_));
|
||||
}
|
||||
Http_Response<Json> amis_response() const {
|
||||
return make_http_success<Json>(amis_schema());
|
||||
}
|
||||
Http_Response<Json> update_response(std::string_view body) {
|
||||
Json patch;
|
||||
try {
|
||||
patch = parse_json<Json>(body);
|
||||
} catch(const std::exception& error) {
|
||||
return make_http_error<Json>(400, error.what());
|
||||
}
|
||||
std::scoped_lock lock(resource_mutex());
|
||||
auto candidate = Object_Adapter<Object>::snapshot(value_);
|
||||
auto result = apply_model_json<Json>(candidate, patch, Write_Mode::update, false);
|
||||
if(!result.success) {
|
||||
return make_http_error<Json>(422, result);
|
||||
}
|
||||
try {
|
||||
if(commit_) {
|
||||
commit_(candidate);
|
||||
}
|
||||
});
|
||||
Object_Adapter<Object>::commit(value_, std::move(candidate));
|
||||
} catch(const Json_Assignment_Error& error) {
|
||||
return make_http_error<Json>(422, error.result());
|
||||
} catch(const Field_Validation_Error& error) {
|
||||
return make_http_error<Json>(422, object_commit_error<Object>(error));
|
||||
} catch(const std::exception& error) {
|
||||
return make_http_error<Json>(500, error.what());
|
||||
}
|
||||
return make_http_success<Json>(to_frontend_json<Json>(value_), result.message);
|
||||
}
|
||||
private:
|
||||
std::mutex& resource_mutex() const noexcept {
|
||||
@@ -88,319 +101,4 @@ private:
|
||||
std::mutex* shared_mutex_{};
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
template <Described_Type T, Json_Type Json>
|
||||
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
class Http_Collection_Resource {
|
||||
public:
|
||||
explicit Http_Collection_Resource(std::string path, std::vector<T> initial = {}, std::string overview_status_api = {}) : path_(std::move(path)), overview_status_api_(std::move(overview_status_api)) {
|
||||
for(auto& value : initial) {
|
||||
entries_.push_back(Entry{next_id_++, std::move(value)});
|
||||
}
|
||||
}
|
||||
template <auto Function>
|
||||
void register_status(std::uint64_t interval = 2000) {
|
||||
using Function_Type = decltype(Function);
|
||||
static_assert(std::is_member_function_pointer_v<Function_Type>);
|
||||
static_assert(std::is_invocable_v<Function_Type, T&>);
|
||||
using Status = std::remove_cvref_t<std::invoke_result_t<Function_Type, T&>>;
|
||||
static_assert(Described_Type<Status>);
|
||||
status_descriptor_ = to_status_descriptor_json<Json, Status>();
|
||||
status_interval_ = interval;
|
||||
status_reader_ = [](T& value) {
|
||||
return to_status_json<Json>(std::invoke(Function, value));
|
||||
};
|
||||
}
|
||||
std::size_t size() const {
|
||||
std::scoped_lock lock(mutex_);
|
||||
return entries_.size();
|
||||
}
|
||||
Json items_json() const {
|
||||
std::scoped_lock lock(mutex_);
|
||||
Json items = json_array<Json>();
|
||||
for(const auto& entry : entries_) {
|
||||
json_append(items, encode_entry(entry));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
Json amis_schema() const {
|
||||
const Json* descriptor = status_reader_ ? &status_descriptor_ : nullptr;
|
||||
if(overview_status_api_.empty()) {
|
||||
return to_amis_crud_schema<Json, T>(path_, descriptor, status_interval_);
|
||||
}
|
||||
return to_amis_crud_status_schema<Json, T>(path_, overview_status_api_, descriptor, status_interval_);
|
||||
}
|
||||
void bind(httplib::Server& server) {
|
||||
server.Get(path_ + "/descriptor", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, make_http_result<Json>(0, "", to_descriptor_json<Json, T>()));
|
||||
});
|
||||
server.Get(path_ + "/amis", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, make_http_result<Json>(0, "", amis_schema()));
|
||||
});
|
||||
if(status_reader_) {
|
||||
server.Get(path_ + "/status/descriptor", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, make_http_result<Json>(0, "", status_descriptor_));
|
||||
});
|
||||
server.Get(status_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
|
||||
const auto id = read_route_id(request);
|
||||
std::scoped_lock lock(mutex_);
|
||||
const auto iterator = find_entry(id);
|
||||
if(iterator == entries_.end()) {
|
||||
write_not_found(response);
|
||||
return;
|
||||
}
|
||||
write_http_json(response, make_http_result<Json>(0, "", status_reader_(iterator->value)));
|
||||
});
|
||||
}
|
||||
server.Get(path_, [this](const httplib::Request& request, httplib::Response& response) {
|
||||
const std::size_t page = read_size_parameter(request, "page", 1);
|
||||
const std::size_t per_page = read_size_parameter(request, "perPage", 20);
|
||||
std::scoped_lock lock(mutex_);
|
||||
std::vector<const Entry*> ordered_entries;
|
||||
ordered_entries.reserve(entries_.size());
|
||||
for(const auto& entry : entries_) {
|
||||
ordered_entries.push_back(&entry);
|
||||
}
|
||||
apply_request_sort(request, ordered_entries);
|
||||
const std::size_t offset = std::min((page - 1) * per_page, ordered_entries.size());
|
||||
const std::size_t end = std::min(offset + per_page, ordered_entries.size());
|
||||
Json items = json_array<Json>();
|
||||
for(std::size_t index = offset; index < end; ++index) {
|
||||
json_append(items, encode_entry(*ordered_entries[index]));
|
||||
}
|
||||
Json data = json_object<Json>();
|
||||
json_set(data, "items", std::move(items));
|
||||
json_set(data, "total", ordered_entries.size());
|
||||
write_http_json(response, make_http_result<Json>(0, "", std::move(data)));
|
||||
});
|
||||
server.Get(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
|
||||
const auto id = read_route_id(request);
|
||||
std::scoped_lock lock(mutex_);
|
||||
const auto iterator = find_entry(id);
|
||||
if(iterator == entries_.end()) {
|
||||
write_not_found(response);
|
||||
return;
|
||||
}
|
||||
write_http_json(response, make_http_result<Json>(0, "", encode_entry(*iterator)));
|
||||
});
|
||||
server.Post(path_, [this](const httplib::Request& request, httplib::Response& response) {
|
||||
try {
|
||||
Json input = parse_json<Json>(request.body);
|
||||
Json_Adapter<Json>::erase(input, "id");
|
||||
T value{};
|
||||
const auto result = apply_frontend_create<Json>(value, input);
|
||||
if(!result.success) {
|
||||
write_http_error<Json>(response, 422, result);
|
||||
return;
|
||||
}
|
||||
std::scoped_lock lock(mutex_);
|
||||
entries_.push_back(Entry{next_id_++, std::move(value)});
|
||||
write_http_json(response, make_http_result<Json>(0, "created", encode_entry(entries_.back())));
|
||||
} catch(const std::exception& error) {
|
||||
response.status = 400;
|
||||
write_http_json(response, make_http_result<Json>(400, error.what(), json_object<Json>()));
|
||||
}
|
||||
});
|
||||
if(describe<T>().list_options().user_reorderable_) {
|
||||
server.Post(path_ + "/order", [this](const httplib::Request& request, httplib::Response& response) {
|
||||
try {
|
||||
const Json input = parse_json<Json>(request.body);
|
||||
const auto ids = read_order_ids(Json_Adapter<Json>::at(input, "ids"));
|
||||
std::scoped_lock lock(mutex_);
|
||||
reorder(ids);
|
||||
write_http_json(response, make_http_result<Json>(0, "reordered", json_object<Json>()));
|
||||
} catch(const std::exception& error) {
|
||||
response.status = 400;
|
||||
write_http_json(response, make_http_result<Json>(400, error.what(), json_object<Json>()));
|
||||
}
|
||||
});
|
||||
}
|
||||
server.Put(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
|
||||
update(request, response);
|
||||
});
|
||||
server.Patch(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
|
||||
update(request, response);
|
||||
});
|
||||
server.Delete(item_pattern(), [this](const httplib::Request& request, httplib::Response& response) {
|
||||
const auto id = read_route_id(request);
|
||||
std::scoped_lock lock(mutex_);
|
||||
const auto iterator = find_entry(id);
|
||||
if(iterator == entries_.end()) {
|
||||
write_not_found(response);
|
||||
return;
|
||||
}
|
||||
entries_.erase(iterator);
|
||||
write_http_json(response, make_http_result<Json>(0, "deleted", json_object<Json>()));
|
||||
});
|
||||
}
|
||||
private:
|
||||
struct Entry {
|
||||
std::uint64_t id{};
|
||||
T value;
|
||||
};
|
||||
using Iterator = typename std::vector<Entry>::iterator;
|
||||
static std::size_t read_size_parameter(const httplib::Request& request, const char* name, std::size_t fallback) {
|
||||
if(!request.has_param(name)) {
|
||||
return fallback;
|
||||
}
|
||||
const auto text = request.get_param_value(name);
|
||||
std::size_t value{};
|
||||
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
if(error != std::errc{} || end != text.data() + text.size() || value == 0) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
static int compare_json_values(const Json& left, const Json& right) {
|
||||
if(Json_Adapter<Json>::is_number(left) && Json_Adapter<Json>::is_number(right)) {
|
||||
const long double left_value = Json_Adapter<Json>::number(left);
|
||||
const long double right_value = Json_Adapter<Json>::number(right);
|
||||
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
|
||||
}
|
||||
if(Json_Adapter<Json>::is_string(left) && Json_Adapter<Json>::is_string(right)) {
|
||||
const auto left_value = json_get<Json, std::string>(left);
|
||||
const auto right_value = json_get<Json, std::string>(right);
|
||||
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
|
||||
}
|
||||
if(Json_Adapter<Json>::is_boolean(left) && Json_Adapter<Json>::is_boolean(right)) {
|
||||
const bool left_value = json_get<Json, bool>(left);
|
||||
const bool right_value = json_get<Json, bool>(right);
|
||||
return left_value == right_value ? 0 : left_value ? 1 : -1;
|
||||
}
|
||||
const std::string left_value = dump_json(left);
|
||||
const std::string right_value = dump_json(right);
|
||||
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
|
||||
}
|
||||
static bool is_sortable_field(const std::string& name) {
|
||||
if(name == "id") {
|
||||
return true;
|
||||
}
|
||||
bool sortable{};
|
||||
std::apply([&](const auto&... field) {
|
||||
((field.name() == name ? sortable = field.is_sortable() : false), ...);
|
||||
}, describe<T>().fields());
|
||||
return sortable;
|
||||
}
|
||||
static std::uint64_t read_route_id(const httplib::Request& request) {
|
||||
const std::string text = request.matches[1].str();
|
||||
std::uint64_t id{};
|
||||
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), id);
|
||||
if(error != std::errc{} || end != text.data() + text.size()) {
|
||||
return 0;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
static std::vector<std::uint64_t> read_order_ids(const Json& value) {
|
||||
std::vector<std::uint64_t> result;
|
||||
if(Json_Adapter<Json>::is_array(value)) {
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(value); ++index) {
|
||||
const auto& item = Json_Adapter<Json>::at(value, index);
|
||||
result.push_back(Json_Adapter<Json>::is_string(item) ? std::stoull(json_get<Json, std::string>(item)) : json_get<Json, std::uint64_t>(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const std::string text = json_get<Json, std::string>(value);
|
||||
std::size_t begin{};
|
||||
while(begin < text.size()) {
|
||||
const auto end = text.find(',', begin);
|
||||
result.push_back(std::stoull(text.substr(begin, end == std::string::npos ? text.size() - begin : end - begin)));
|
||||
if(end == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
begin = end + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
void apply_request_sort(const httplib::Request& request, std::vector<const Entry*>& entries) const {
|
||||
const auto& options = describe<T>().list_options();
|
||||
std::string order_by = options.default_order_by;
|
||||
std::string order_dir = options.default_order_dir;
|
||||
if(request.has_param("orderBy")) {
|
||||
order_by = request.get_param_value("orderBy");
|
||||
order_dir = request.has_param("orderDir") ? request.get_param_value("orderDir") : "asc";
|
||||
}
|
||||
if(order_by.empty() || !is_sortable_field(order_by)) {
|
||||
return;
|
||||
}
|
||||
const bool descending = order_dir == "desc";
|
||||
std::stable_sort(entries.begin(), entries.end(), [&](const Entry* left, const Entry* right) {
|
||||
int comparison{};
|
||||
if(order_by == "id") {
|
||||
comparison = left->id < right->id ? -1 : left->id > right->id ? 1 : 0;
|
||||
} else {
|
||||
const auto left_json = to_frontend_json<Json>(left->value);
|
||||
const auto right_json = to_frontend_json<Json>(right->value);
|
||||
comparison = compare_json_values(Json_Adapter<Json>::at(left_json, order_by), Json_Adapter<Json>::at(right_json, order_by));
|
||||
}
|
||||
return descending ? comparison > 0 : comparison < 0;
|
||||
});
|
||||
}
|
||||
std::string item_pattern() const {
|
||||
return path_ + R"(/(\d+))";
|
||||
}
|
||||
std::string status_pattern() const {
|
||||
return path_ + R"(/(\d+)/status)";
|
||||
}
|
||||
Iterator find_entry(std::uint64_t id) {
|
||||
return std::find_if(entries_.begin(), entries_.end(), [id](const Entry& entry) {
|
||||
return entry.id == id;
|
||||
});
|
||||
}
|
||||
static Json encode_entry(const Entry& entry) {
|
||||
Json result = to_frontend_json<Json>(entry.value);
|
||||
json_set(result, "id", entry.id);
|
||||
return result;
|
||||
}
|
||||
static void write_not_found(httplib::Response& response) {
|
||||
response.status = 404;
|
||||
write_http_json(response, make_http_result<Json>(404, "record not found", json_object<Json>()));
|
||||
}
|
||||
void reorder(const std::vector<std::uint64_t>& ids) {
|
||||
std::vector<Entry> remaining = std::move(entries_);
|
||||
std::vector<Entry> reordered;
|
||||
reordered.reserve(remaining.size());
|
||||
for(const auto id : ids) {
|
||||
const auto iterator = std::find_if(remaining.begin(), remaining.end(), [id](const Entry& entry) {
|
||||
return entry.id == id;
|
||||
});
|
||||
if(iterator != remaining.end()) {
|
||||
reordered.push_back(std::move(*iterator));
|
||||
remaining.erase(iterator);
|
||||
}
|
||||
}
|
||||
for(auto& entry : remaining) {
|
||||
reordered.push_back(std::move(entry));
|
||||
}
|
||||
entries_ = std::move(reordered);
|
||||
}
|
||||
void update(const httplib::Request& request, httplib::Response& response) {
|
||||
try {
|
||||
Json patch = parse_json<Json>(request.body);
|
||||
Json_Adapter<Json>::erase(patch, "id");
|
||||
const auto id = read_route_id(request);
|
||||
std::scoped_lock lock(mutex_);
|
||||
const auto iterator = find_entry(id);
|
||||
if(iterator == entries_.end()) {
|
||||
write_not_found(response);
|
||||
return;
|
||||
}
|
||||
const auto result = apply_frontend_patch<Json>(iterator->value, patch);
|
||||
if(!result.success) {
|
||||
write_http_error<Json>(response, 422, result);
|
||||
return;
|
||||
}
|
||||
write_http_json(response, make_http_result<Json>(0, result.message, encode_entry(*iterator)));
|
||||
} catch(const std::exception& error) {
|
||||
response.status = 400;
|
||||
write_http_json(response, make_http_result<Json>(400, error.what(), json_object<Json>()));
|
||||
}
|
||||
}
|
||||
std::string path_;
|
||||
std::string overview_status_api_;
|
||||
Json status_descriptor_{};
|
||||
std::function<Json(T&)> status_reader_;
|
||||
std::uint64_t status_interval_{2000};
|
||||
std::vector<Entry> entries_;
|
||||
std::uint64_t next_id_{1};
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,20 +34,75 @@ public:
|
||||
private:
|
||||
Update_Result result_;
|
||||
};
|
||||
class Field_Validation_Error : public std::invalid_argument {
|
||||
public:
|
||||
Field_Validation_Error(std::string path, std::string message) : std::invalid_argument(message), path_(std::move(path)) {}
|
||||
const std::string& path() const noexcept {
|
||||
return path_;
|
||||
}
|
||||
private:
|
||||
std::string path_;
|
||||
};
|
||||
enum class Write_Mode {
|
||||
internal,
|
||||
create,
|
||||
update
|
||||
};
|
||||
template <Json_Type Json, Described_Type T>
|
||||
Json to_json(const T& value);
|
||||
template <Json_Type Json, Described_Type T>
|
||||
Json to_frontend_json(const T& value);
|
||||
template <Json_Type Json, Described_Type T>
|
||||
requires std::default_initializable<T>
|
||||
inline std::string join_field_path(std::string_view prefix, std::string_view child) {
|
||||
if(prefix.empty()) {
|
||||
return std::string(child);
|
||||
}
|
||||
if(child.empty()) {
|
||||
return std::string(prefix);
|
||||
}
|
||||
if(child.front() == '[') {
|
||||
return std::string(prefix) + std::string(child);
|
||||
}
|
||||
return std::string(prefix) + "." + std::string(child);
|
||||
}
|
||||
inline std::string map_key_path(std::string_view key) {
|
||||
std::string result{"[\""};
|
||||
for(const char value : key) {
|
||||
if(value == '\\' || value == '"') {
|
||||
result.push_back('\\');
|
||||
}
|
||||
result.push_back(value);
|
||||
}
|
||||
result += "\"]";
|
||||
return result;
|
||||
}
|
||||
inline Update_Result prefix_update_result(const Update_Result& source, std::string_view prefix) {
|
||||
Update_Result result = source;
|
||||
result.field_errors.clear();
|
||||
for(const auto& [path, message] : source.field_errors) {
|
||||
result.field_errors.emplace(join_field_path(prefix, path), message);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
inline void merge_update_errors(Update_Result& target, const Update_Result& source, std::string_view prefix = {}) {
|
||||
if(target.message.empty() && !source.message.empty()) {
|
||||
target.message = source.message;
|
||||
}
|
||||
if(source.field_errors.empty() && !prefix.empty() && !source.message.empty()) {
|
||||
target.field_errors.insert_or_assign(std::string(prefix), source.message);
|
||||
return;
|
||||
}
|
||||
for(const auto& [path, message] : source.field_errors) {
|
||||
target.field_errors.insert_or_assign(join_field_path(prefix, path), message);
|
||||
}
|
||||
}
|
||||
template <class T>
|
||||
concept Adapted_Object_Type = Object_Adapter_With_Snapshot<T> && Object_Adapter_With_Create<T> && Object_Adapter_With_Commit<T> && Described_Type<Object_Model_Type<T>>;
|
||||
template <Json_Type Json, class T>
|
||||
requires Described_Type<Object_Model_Type<T>> && Object_Adapter_With_Create<T>
|
||||
Json to_descriptor_json();
|
||||
template <Json_Type Json, Described_Type T>
|
||||
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
template <Json_Type Json, class T>
|
||||
requires Described_Type<std::remove_cvref_t<T>> || Object_Adapter_With_Snapshot<T>
|
||||
Json to_json(const T& value);
|
||||
template <Json_Type Json, class T>
|
||||
requires Described_Type<std::remove_cvref_t<T>> || Object_Adapter_With_Snapshot<T>
|
||||
Json to_frontend_json(const T& value);
|
||||
template <Json_Type Json, Adapted_Object_Type T>
|
||||
Update_Result assign_json(T& target, const Json& value);
|
||||
template <Json_Type Json, class T>
|
||||
void assign_json_value(T& target, const Json& value);
|
||||
@@ -59,6 +114,8 @@ Json encode_json_value(const T& value) {
|
||||
return Value_Adapter<Storage, Json>::encode(value);
|
||||
} else if constexpr(!std::same_as<Storage, Value>) {
|
||||
return encode_json_value<Json>(read_adapted_value<Storage, Json>(value));
|
||||
} else if constexpr(Polymorphic_Type<Value, Json>) {
|
||||
return Polymorphic_Adapter<Value, Json>::encode(value);
|
||||
} else if constexpr(std::is_enum_v<Value>) {
|
||||
static_assert(Enum_Type<Value>, "enum type requires an adminive::Enum_Adapter specialization");
|
||||
const auto name = enum_name(value);
|
||||
@@ -66,7 +123,7 @@ Json encode_json_value(const T& value) {
|
||||
return json_scalar<Json>(std::string(name));
|
||||
}
|
||||
return json_scalar<Json>(static_cast<std::underlying_type_t<Value>>(value));
|
||||
} else if constexpr(Described_Type<Value>) {
|
||||
} else if constexpr(Described_Type<Value> || (Object_Adapter_With_Snapshot<Value> && Described_Type<Object_Model_Type<Value>>)) {
|
||||
return to_json<Json>(value);
|
||||
} else if constexpr(String_Key_Map_Type<Value>) {
|
||||
Json result = json_object<Json>();
|
||||
@@ -94,6 +151,8 @@ void assign_json_value(T& target, const Json& value) {
|
||||
Value parsed{};
|
||||
assign_json_value<Json>(parsed, value);
|
||||
write_adapted_value<Storage, Json>(target, std::move(parsed));
|
||||
} else if constexpr(Polymorphic_Type<Value, Json>) {
|
||||
Polymorphic_Adapter<Value, Json>::decode(target, value);
|
||||
} else if constexpr(std::is_enum_v<Value>) {
|
||||
static_assert(Enum_Type<Value>, "enum type requires an adminive::Enum_Adapter specialization");
|
||||
std::optional<Value> parsed;
|
||||
@@ -106,7 +165,7 @@ void assign_json_value(T& target, const Json& value) {
|
||||
throw std::invalid_argument("unknown enum value");
|
||||
}
|
||||
target = *parsed;
|
||||
} else if constexpr(Described_Type<Value>) {
|
||||
} else if constexpr(Adapted_Object_Type<Value>) {
|
||||
const auto result = assign_json<Json>(target, value);
|
||||
if(!result.success) {
|
||||
throw Json_Assignment_Error(result);
|
||||
@@ -118,7 +177,21 @@ void assign_json_value(T& target, const Json& value) {
|
||||
Value parsed;
|
||||
for(const auto& key : Json_Adapter<Json>::keys(value)) {
|
||||
typename Value::mapped_type item{};
|
||||
assign_json_value<Json>(item, Json_Adapter<Json>::at(value, key));
|
||||
try {
|
||||
assign_json_value<Json>(item, Json_Adapter<Json>::at(value, key));
|
||||
} catch(const Json_Assignment_Error& error) {
|
||||
throw Json_Assignment_Error(prefix_update_result(error.result(), map_key_path(key)));
|
||||
} catch(const Field_Validation_Error& error) {
|
||||
Update_Result result;
|
||||
result.message = "one or more fields are invalid";
|
||||
result.field_errors[join_field_path(map_key_path(key), error.path())] = error.what();
|
||||
throw Json_Assignment_Error(std::move(result));
|
||||
} catch(const std::exception& error) {
|
||||
Update_Result result;
|
||||
result.message = "one or more fields are invalid";
|
||||
result.field_errors[map_key_path(key)] = error.what();
|
||||
throw Json_Assignment_Error(std::move(result));
|
||||
}
|
||||
parsed.emplace(key, std::move(item));
|
||||
}
|
||||
target = std::move(parsed);
|
||||
@@ -129,7 +202,22 @@ void assign_json_value(T& target, const Json& value) {
|
||||
Value parsed;
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(value); ++index) {
|
||||
typename Value::value_type item{};
|
||||
assign_json_value<Json>(item, Json_Adapter<Json>::at(value, index));
|
||||
const std::string path = "[" + std::to_string(index) + "]";
|
||||
try {
|
||||
assign_json_value<Json>(item, Json_Adapter<Json>::at(value, index));
|
||||
} catch(const Json_Assignment_Error& error) {
|
||||
throw Json_Assignment_Error(prefix_update_result(error.result(), path));
|
||||
} catch(const Field_Validation_Error& error) {
|
||||
Update_Result result;
|
||||
result.message = "one or more fields are invalid";
|
||||
result.field_errors[join_field_path(path, error.path())] = error.what();
|
||||
throw Json_Assignment_Error(std::move(result));
|
||||
} catch(const std::exception& error) {
|
||||
Update_Result result;
|
||||
result.message = "one or more fields are invalid";
|
||||
result.field_errors[path] = error.what();
|
||||
throw Json_Assignment_Error(std::move(result));
|
||||
}
|
||||
parsed.push_back(std::move(item));
|
||||
}
|
||||
target = std::move(parsed);
|
||||
@@ -143,6 +231,8 @@ std::string value_type_name() {
|
||||
using Value = Adapted_Value_Type<Storage, Json>;
|
||||
if constexpr(Value_Adapter_With_Type_Name<Storage, Json>) {
|
||||
return std::string(Value_Adapter<Storage, Json>::type_name);
|
||||
} else if constexpr(Polymorphic_Type<Value, Json>) {
|
||||
return "polymorphic";
|
||||
} else if constexpr(std::is_enum_v<Value>) {
|
||||
return "enum";
|
||||
} else if constexpr(std::same_as<Value, bool>) {
|
||||
@@ -153,7 +243,7 @@ std::string value_type_name() {
|
||||
return "number";
|
||||
} else if constexpr(String_Type<Value>) {
|
||||
return "string";
|
||||
} else if constexpr(Described_Type<Value>) {
|
||||
} else if constexpr(Described_Type<Value> || (Object_Adapter_With_Snapshot<Value> && Described_Type<Object_Model_Type<Value>>)) {
|
||||
return "object";
|
||||
} else if constexpr(String_Key_Map_Type<Value>) {
|
||||
return "map";
|
||||
@@ -182,6 +272,9 @@ void append_constraints(Json& result) {
|
||||
if constexpr(!std::same_as<Storage, Value>) {
|
||||
append_constraints<Json, Value>(result);
|
||||
}
|
||||
if constexpr(Polymorphic_Type<Value, Json>) {
|
||||
json_set(result, "polymorphic", Polymorphic_Adapter<Value, Json>::descriptor());
|
||||
}
|
||||
if constexpr(Value_Adapter_With_Schema<Storage, Json>) {
|
||||
Value_Adapter<Storage, Json>::append_schema(result);
|
||||
}
|
||||
@@ -203,21 +296,45 @@ Json enum_options(const std::map<std::string, std::string>& labels) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
requires std::default_initializable<T>
|
||||
Json to_descriptor_json() {
|
||||
const auto descriptor = describe<T>();
|
||||
T defaults{};
|
||||
template <Json_Type Json>
|
||||
Json make_polymorphic_variant(std::string value, std::string label, Json descriptor) {
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "value", std::move(value));
|
||||
json_set(result, "label", std::move(label));
|
||||
json_set(result, "descriptor", std::move(descriptor));
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_polymorphic_descriptor(std::string discriminator, std::string discriminator_label, Json variants) {
|
||||
Json options = json_array<Json>();
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(variants); ++index) {
|
||||
const auto& variant = Json_Adapter<Json>::at(variants, index);
|
||||
Json option = json_object<Json>();
|
||||
json_set(option, "label", Json_Adapter<Json>::at(variant, "label"));
|
||||
json_set(option, "value", Json_Adapter<Json>::at(variant, "value"));
|
||||
json_append(options, std::move(option));
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "discriminator", std::move(discriminator));
|
||||
json_set(result, "discriminator_label", std::move(discriminator_label));
|
||||
json_set(result, "options", std::move(options));
|
||||
json_set(result, "variants", std::move(variants));
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, class Model>
|
||||
requires Described_Type<Model>
|
||||
Json model_descriptor_json(const Model& defaults) {
|
||||
const auto descriptor = describe<Model>();
|
||||
Json fields = json_array<Json>();
|
||||
int default_order{};
|
||||
int field_index{};
|
||||
std::apply([&](const auto&... item) {
|
||||
([&] {
|
||||
using Field = std::remove_cvref_t<decltype(item)>;
|
||||
using Member = typename Field::member_type;
|
||||
using Value = Adapted_Value_Type<Member, Json>;
|
||||
const int order = item.order() < 0 ? default_order : item.order();
|
||||
++default_order;
|
||||
Json field_json = json_object<Json>();
|
||||
const int order = item.order() < 0 ? field_index : item.order();
|
||||
++field_index;
|
||||
json_set(field_json, "name", item.name());
|
||||
json_set(field_json, "label", item.label());
|
||||
json_set(field_json, "list_label", item.list_label());
|
||||
@@ -240,7 +357,7 @@ Json to_descriptor_json() {
|
||||
if(!item.visible_on().empty()) {
|
||||
json_set(field_json, "visible_on", item.visible_on());
|
||||
}
|
||||
if constexpr(Described_Type<Value>) {
|
||||
if constexpr(Adapted_Object_Type<Value>) {
|
||||
json_set(field_json, "children", Json_Adapter<Json>::at(to_descriptor_json<Json, Value>(), "fields"));
|
||||
}
|
||||
append_constraints<Json, Member>(field_json);
|
||||
@@ -268,7 +385,7 @@ Json to_descriptor_json() {
|
||||
json_set(list, "column_reorderable", options.column_reorderable_);
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "protocol", "adminive.resource");
|
||||
json_set(result, "protocol_version", 1);
|
||||
json_set(result, "protocol_version", 2);
|
||||
json_set(result, "name", descriptor.name());
|
||||
json_set(result, "label", descriptor.label());
|
||||
json_set(result, "value_type", "object");
|
||||
@@ -276,29 +393,46 @@ Json to_descriptor_json() {
|
||||
json_set(result, "fields", std::move(fields));
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
Json to_json(const T& value) {
|
||||
const auto descriptor = describe<T>();
|
||||
Json result = json_object<Json>();
|
||||
std::apply([&](const auto&... item) {
|
||||
(json_set(result, item.name(), encode_json_value<Json>(item.get(value))), ...);
|
||||
}, descriptor.fields());
|
||||
return result;
|
||||
template <Json_Type Json, class T>
|
||||
requires Described_Type<Object_Model_Type<T>> && Object_Adapter_With_Create<T>
|
||||
Json to_descriptor_json() {
|
||||
return model_descriptor_json<Json>(Object_Adapter<std::remove_cvref_t<T>>::create());
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
Json to_frontend_json(const T& value) {
|
||||
Json model_to_json(const T& value, bool frontend) {
|
||||
const auto descriptor = describe<T>();
|
||||
Json result = json_object<Json>();
|
||||
std::apply([&](const auto&... item) {
|
||||
([&] {
|
||||
if(item.is_readable()) {
|
||||
if(!frontend || item.is_readable()) {
|
||||
json_set(result, item.name(), encode_json_value<Json>(item.get(value)));
|
||||
}
|
||||
}(), ...);
|
||||
}, descriptor.fields());
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
template <Json_Type Json, class T>
|
||||
requires Described_Type<std::remove_cvref_t<T>> || Object_Adapter_With_Snapshot<T>
|
||||
Json to_json(const T& value) {
|
||||
using Object = std::remove_cvref_t<T>;
|
||||
if constexpr(std::same_as<Object, Object_Model_Type<Object>>) {
|
||||
return model_to_json<Json>(value, false);
|
||||
} else {
|
||||
return model_to_json<Json>(Object_Adapter<Object>::snapshot(value), false);
|
||||
}
|
||||
}
|
||||
template <Json_Type Json, class T>
|
||||
requires Described_Type<std::remove_cvref_t<T>> || Object_Adapter_With_Snapshot<T>
|
||||
Json to_frontend_json(const T& value) {
|
||||
using Object = std::remove_cvref_t<T>;
|
||||
if constexpr(std::same_as<Object, Object_Model_Type<Object>>) {
|
||||
return model_to_json<Json>(value, true);
|
||||
} else {
|
||||
return model_to_json<Json>(Object_Adapter<Object>::snapshot(value), true);
|
||||
}
|
||||
}
|
||||
template <Json_Type Json, class T>
|
||||
requires Described_Type<std::remove_cvref_t<T>> || Object_Adapter_With_Snapshot<T>
|
||||
Json to_data_json(const T& value) {
|
||||
return to_frontend_json<Json>(value);
|
||||
}
|
||||
@@ -312,16 +446,14 @@ bool field_writable(const Field& field, Write_Mode mode) {
|
||||
}
|
||||
return field.is_editable();
|
||||
}
|
||||
template <Json_Type Json, 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) {
|
||||
template <Json_Type Json, Described_Type Model>
|
||||
Update_Result apply_model_json(Model& candidate, const Json& value, Write_Mode mode, bool complete) {
|
||||
Update_Result result;
|
||||
if(!Json_Adapter<Json>::is_object(value)) {
|
||||
result.message = "value must be a JSON object";
|
||||
return result;
|
||||
}
|
||||
T candidate = target;
|
||||
const auto descriptor = describe<T>();
|
||||
const auto descriptor = describe<Model>();
|
||||
for(const auto& key : Json_Adapter<Json>::keys(value)) {
|
||||
bool known{};
|
||||
std::apply([&](const auto&... item) {
|
||||
@@ -349,7 +481,9 @@ Update_Result apply_object_json(T& target, const Json& value, Write_Mode mode, b
|
||||
try {
|
||||
assign_json_value<Json>(item.get(candidate), Json_Adapter<Json>::at(value, item.name()));
|
||||
} catch(const Json_Assignment_Error& error) {
|
||||
result.field_errors[item.name()] = error.what();
|
||||
merge_update_errors(result, error.result(), item.name());
|
||||
} catch(const Field_Validation_Error& error) {
|
||||
result.field_errors[join_field_path(item.name(), error.path())] = error.what();
|
||||
} catch(const std::exception& error) {
|
||||
result.field_errors[item.name()] = error.what();
|
||||
}
|
||||
@@ -361,47 +495,71 @@ Update_Result apply_object_json(T& target, const Json& value, Write_Mode mode, b
|
||||
}
|
||||
try {
|
||||
descriptor.object_validator()(candidate);
|
||||
} catch(const Json_Assignment_Error& error) {
|
||||
merge_update_errors(result, error.result());
|
||||
} catch(const Field_Validation_Error& error) {
|
||||
result.field_errors[error.path()] = error.what();
|
||||
} catch(const std::exception& error) {
|
||||
result.message = error.what();
|
||||
return result;
|
||||
}
|
||||
target = std::move(candidate);
|
||||
if(!result.field_errors.empty()) {
|
||||
result.message = "one or more fields are invalid";
|
||||
return result;
|
||||
}
|
||||
result.success = true;
|
||||
result.message = complete ? "assigned" : "updated";
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, 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<Json>(candidate, value, Write_Mode::internal, true);
|
||||
if(result.success) {
|
||||
target = std::move(candidate);
|
||||
template <class T>
|
||||
Update_Result object_commit_error(const Field_Validation_Error& error) {
|
||||
Update_Result result;
|
||||
result.message = "one or more fields are invalid";
|
||||
result.field_errors[error.path()] = error.what();
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, Adapted_Object_Type T>
|
||||
Update_Result apply_object_json(T& target, const Json& value, Write_Mode mode, bool complete) {
|
||||
using Object = std::remove_cvref_t<T>;
|
||||
auto candidate = mode == Write_Mode::create ? Object_Adapter<Object>::create() : Object_Adapter<Object>::snapshot(target);
|
||||
auto result = apply_model_json<Json>(candidate, value, mode, complete);
|
||||
if(!result.success) {
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
Object_Adapter<Object>::commit(target, std::move(candidate));
|
||||
} catch(const Json_Assignment_Error& error) {
|
||||
return error.result();
|
||||
} catch(const Field_Validation_Error& error) {
|
||||
return object_commit_error<Object>(error);
|
||||
} catch(const std::exception& error) {
|
||||
result.success = false;
|
||||
result.message = error.what();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
requires std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
template <Json_Type Json, Adapted_Object_Type T>
|
||||
Update_Result assign_json(T& target, const Json& value) {
|
||||
return apply_object_json<Json>(target, value, Write_Mode::internal, true);
|
||||
}
|
||||
template <Json_Type Json, Adapted_Object_Type T>
|
||||
Update_Result apply_json_patch(T& target, const Json& patch) {
|
||||
return apply_object_json<Json>(target, patch, Write_Mode::internal, false);
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
requires std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
template <Json_Type Json, Adapted_Object_Type T>
|
||||
Update_Result apply_frontend_create(T& target, const Json& value) {
|
||||
return apply_object_json<Json>(target, value, Write_Mode::create, true);
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
requires std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
template <Json_Type Json, Adapted_Object_Type T>
|
||||
Update_Result apply_frontend_patch(T& target, const Json& patch) {
|
||||
return apply_object_json<Json>(target, patch, Write_Mode::update, false);
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
requires std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
template <Json_Type Json, Adapted_Object_Type T>
|
||||
Update_Result apply_patch(T& target, const Json& patch) {
|
||||
return apply_frontend_patch<Json>(target, patch);
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
template <Json_Type Json, Adapted_Object_Type T>
|
||||
requires std::default_initializable<T>
|
||||
T from_json(const Json& value) {
|
||||
T result{};
|
||||
auto update = assign_json<Json>(result, value);
|
||||
@@ -410,13 +568,23 @@ T from_json(const Json& value) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
template <Described_Type T>
|
||||
template <class T>
|
||||
requires Described_Type<std::remove_cvref_t<T>> || Object_Adapter_With_Snapshot<T>
|
||||
Update_Result validate(const T& value) {
|
||||
Update_Result result;
|
||||
try {
|
||||
describe<T>().object_validator()(value);
|
||||
using Object = std::remove_cvref_t<T>;
|
||||
if constexpr(std::same_as<Object, Object_Model_Type<Object>>) {
|
||||
describe<Object>().object_validator()(value);
|
||||
} else {
|
||||
const auto model = Object_Adapter<Object>::snapshot(value);
|
||||
describe<Object_Model_Type<Object>>().object_validator()(model);
|
||||
}
|
||||
result.success = true;
|
||||
result.message = "valid";
|
||||
} catch(const Field_Validation_Error& error) {
|
||||
result.message = "one or more fields are invalid";
|
||||
result.field_errors[error.path()] = error.what();
|
||||
} catch(const std::exception& error) {
|
||||
result.message = error.what();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user