分离 http依赖

This commit is contained in:
2026-08-06 21:33:46 +08:00
parent f060662ffd
commit 14b0b0e3bc
17 changed files with 1486 additions and 514 deletions
+63 -365
View File
@@ -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_;
};
}