首次提交
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
#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 {
|
||||
inline void write_http_json(httplib::Response& response, const Json& value) {
|
||||
response.set_content(value.dump(2), "application/json; charset=utf-8");
|
||||
}
|
||||
inline void write_http_error(httplib::Response& response, int status, const Update_Result& result) {
|
||||
response.status = status;
|
||||
write_http_json(response, Json{{"status", status}, {"msg", result.message}, {"data", Json::object()}, {"errors", result.field_errors}});
|
||||
}
|
||||
template <Described_Type T>
|
||||
requires std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
class Http_Resource {
|
||||
public:
|
||||
Http_Resource(T& value, std::string path) : value_(value), path_(std::move(path)) {}
|
||||
void bind(httplib::Server& server) {
|
||||
server.Get(path_ + "/descriptor", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", to_descriptor_json<T>()}});
|
||||
});
|
||||
server.Get(path_ + "/data", [this](const httplib::Request&, httplib::Response& response) {
|
||||
std::scoped_lock lock(mutex_);
|
||||
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", to_frontend_json(value_)}});
|
||||
});
|
||||
server.Get(path_ + "/amis", [this](const httplib::Request&, httplib::Response& response) {
|
||||
std::scoped_lock lock(mutex_);
|
||||
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", to_amis_form_schema(value_, path_ + "/data")}});
|
||||
});
|
||||
server.Post(path_ + "/data", [this](const httplib::Request& request, httplib::Response& response) {
|
||||
try {
|
||||
const Json patch = Json::parse(request.body);
|
||||
std::scoped_lock lock(mutex_);
|
||||
const auto result = apply_frontend_patch(value_, patch);
|
||||
if(!result.success) {
|
||||
write_http_error(response, 422, result);
|
||||
return;
|
||||
}
|
||||
write_http_json(response, Json{{"status", 0}, {"msg", result.message}, {"data", to_frontend_json(value_)}});
|
||||
} catch(const std::exception& error) {
|
||||
response.status = 400;
|
||||
write_http_json(response, Json{{"status", 400}, {"msg", error.what()}, {"data", Json::object()}});
|
||||
}
|
||||
});
|
||||
}
|
||||
private:
|
||||
T& value_;
|
||||
std::string path_;
|
||||
std::mutex mutex_;
|
||||
};
|
||||
template <Described_Type T>
|
||||
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) {
|
||||
static_assert(std::is_member_function_pointer_v<decltype(Function)>);
|
||||
using Traits = Member_Function_Traits<Function>;
|
||||
using Owner = typename Traits::owner_type;
|
||||
using Status = std::remove_cvref_t<typename Traits::result_type>;
|
||||
static_assert(std::same_as<Owner, T>);
|
||||
static_assert(Described_Type<Status>);
|
||||
status_descriptor_ = to_status_descriptor_json<Status>();
|
||||
status_interval_ = interval;
|
||||
status_reader_ = [](T& value) {
|
||||
return to_status_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();
|
||||
for(const auto& entry : entries_) {
|
||||
items.push_back(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<T>(path_, descriptor, status_interval_);
|
||||
}
|
||||
return to_amis_crud_status_schema<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, Json{{"status", 0}, {"msg", ""}, {"data", to_descriptor_json<T>()}});
|
||||
});
|
||||
server.Get(path_ + "/amis", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", amis_schema()}});
|
||||
});
|
||||
if(status_reader_) {
|
||||
server.Get(path_ + "/status/descriptor", [this](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", 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, Json{{"status", 0}, {"msg", ""}, {"data", 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);
|
||||
}
|
||||
if(request.has_param("orderBy")) {
|
||||
const std::string order_by = request.get_param_value("orderBy");
|
||||
if(is_sortable_field(order_by)) {
|
||||
const bool descending = request.has_param("orderDir") && request.get_param_value("orderDir") == "desc";
|
||||
std::stable_sort(ordered_entries.begin(), ordered_entries.end(), [&](const Entry* left, const Entry* right) {
|
||||
const int comparison = compare_json_values(to_frontend_json(left->value).at(order_by), to_frontend_json(right->value).at(order_by));
|
||||
return descending ? comparison > 0 : comparison < 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
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();
|
||||
for(std::size_t index = offset; index < end; ++index) {
|
||||
items.push_back(encode_entry(*ordered_entries[index]));
|
||||
}
|
||||
write_http_json(response, Json{{"status", 0}, {"msg", ""}, {"data", Json{{"items", std::move(items)}, {"total", ordered_entries.size()}}}});
|
||||
});
|
||||
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, Json{{"status", 0}, {"msg", ""}, {"data", encode_entry(*iterator)}});
|
||||
});
|
||||
server.Post(path_, [this](const httplib::Request& request, httplib::Response& response) {
|
||||
try {
|
||||
Json input = Json::parse(request.body);
|
||||
input.erase("id");
|
||||
T value{};
|
||||
const auto result = apply_frontend_create(value, input);
|
||||
if(!result.success) {
|
||||
write_http_error(response, 422, result);
|
||||
return;
|
||||
}
|
||||
std::scoped_lock lock(mutex_);
|
||||
entries_.push_back(Entry{next_id_++, std::move(value)});
|
||||
write_http_json(response, Json{{"status", 0}, {"msg", "created"}, {"data", encode_entry(entries_.back())}});
|
||||
} catch(const std::exception& error) {
|
||||
response.status = 400;
|
||||
write_http_json(response, Json{{"status", 400}, {"msg", error.what()}, {"data", Json::object()}});
|
||||
}
|
||||
});
|
||||
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, Json{{"status", 0}, {"msg", "deleted"}, {"data", Json::object()}});
|
||||
});
|
||||
}
|
||||
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(left.is_number() && right.is_number()) {
|
||||
const long double left_value = left.get<long double>();
|
||||
const long double right_value = right.get<long double>();
|
||||
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
|
||||
}
|
||||
if(left.is_string() && right.is_string()) {
|
||||
const auto& left_value = left.get_ref<const std::string&>();
|
||||
const auto& right_value = right.get_ref<const std::string&>();
|
||||
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
|
||||
}
|
||||
if(left.is_boolean() && right.is_boolean()) {
|
||||
const bool left_value = left.get<bool>();
|
||||
const bool right_value = right.get<bool>();
|
||||
return left_value == right_value ? 0 : left_value ? 1 : -1;
|
||||
}
|
||||
const std::string left_value = left.dump();
|
||||
const std::string right_value = right.dump();
|
||||
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
|
||||
}
|
||||
static bool is_sortable_field(const std::string& name) {
|
||||
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;
|
||||
}
|
||||
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(entry.value);
|
||||
result["id"] = entry.id;
|
||||
return result;
|
||||
}
|
||||
static void write_not_found(httplib::Response& response) {
|
||||
response.status = 404;
|
||||
write_http_json(response, Json{{"status", 404}, {"msg", "record not found"}, {"data", Json::object()}});
|
||||
}
|
||||
void update(const httplib::Request& request, httplib::Response& response) {
|
||||
try {
|
||||
Json patch = Json::parse(request.body);
|
||||
patch.erase("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(iterator->value, patch);
|
||||
if(!result.success) {
|
||||
write_http_error(response, 422, result);
|
||||
return;
|
||||
}
|
||||
write_http_json(response, Json{{"status", 0}, {"msg", result.message}, {"data", encode_entry(*iterator)}});
|
||||
} catch(const std::exception& error) {
|
||||
response.status = 400;
|
||||
write_http_json(response, Json{{"status", 400}, {"msg", error.what()}, {"data", Json::object()}});
|
||||
}
|
||||
}
|
||||
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_;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user