427 lines
18 KiB
C++
427 lines
18 KiB
C++
#pragma once
|
|
#include "adminive/http.hpp"
|
|
#include "adminive/status.hpp"
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <charconv>
|
|
#include <concepts>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
namespace adminive {
|
|
struct Collection_Query {
|
|
std::size_t page{1};
|
|
std::size_t per_page{20};
|
|
std::string order_by;
|
|
std::string order_dir;
|
|
std::map<std::string, std::string> fields;
|
|
};
|
|
template <Described_Type T, Json_Type Json, Basic_Lock Lock = std::mutex>
|
|
requires Table_View_Described_Type<T> && std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
|
|
class Collection_Service {
|
|
public:
|
|
explicit Collection_Service(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));
|
|
};
|
|
}
|
|
const std::string& path() const noexcept {
|
|
return path_;
|
|
}
|
|
bool has_status() const noexcept {
|
|
return static_cast<bool>(status_reader_);
|
|
}
|
|
bool user_reorderable() const {
|
|
return describe_table_view<T>().user_reorderable;
|
|
}
|
|
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 view_schema() const {
|
|
return to_view_json<Json, T>(describe_table_view<T>());
|
|
}
|
|
Json amis_schema() const {
|
|
const Json* item_status = status_reader_ ? &status_descriptor_ : nullptr;
|
|
const Json* overview_status = overview_status_api_.empty() ? nullptr : &overview_status_descriptor_;
|
|
return to_amis_table_schema<Json, T>(path_, describe_table_view<T>(), item_status, status_interval_, overview_status, overview_status_api_, overview_status_interval_);
|
|
}
|
|
Http_Response<Json> descriptor_response() const noexcept {
|
|
return safe_response([&] {
|
|
return make_http_success<Json>(to_descriptor_json<Json, T>());
|
|
});
|
|
}
|
|
Http_Response<Json> view_response() const noexcept {
|
|
return safe_response([&] {
|
|
return make_http_success<Json>(view_schema());
|
|
});
|
|
}
|
|
Http_Response<Json> amis_response() const noexcept {
|
|
return safe_response([&] {
|
|
return make_http_success<Json>(amis_schema());
|
|
});
|
|
}
|
|
Http_Response<Json> status_descriptor_response() const noexcept {
|
|
return safe_response([&] {
|
|
if(!status_reader_) {
|
|
return make_http_error<Json>(404, "item status is not configured");
|
|
}
|
|
return make_http_success<Json>(status_descriptor_);
|
|
});
|
|
}
|
|
Http_Response<Json> status_response(std::uint64_t id) noexcept {
|
|
return safe_response([&] {
|
|
if(!status_reader_) {
|
|
return make_http_error<Json>(404, "item status is not configured");
|
|
}
|
|
std::scoped_lock lock(mutex_);
|
|
const auto iterator = find_entry(id);
|
|
if(iterator == entries_.end()) {
|
|
return make_http_error<Json>(404, "record not found");
|
|
}
|
|
return make_http_success<Json>(status_reader_(iterator->value));
|
|
});
|
|
}
|
|
Http_Response<Json> list_response(Collection_Query query = {}) const noexcept {
|
|
return safe_response([&] {
|
|
const std::string error = query_error(query);
|
|
if(!error.empty()) {
|
|
return make_http_error<Json>(400, error);
|
|
}
|
|
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_filters(query, ordered_entries);
|
|
apply_sort(query, ordered_entries);
|
|
const std::size_t page = query.page == 0 ? 1 : query.page;
|
|
const std::size_t per_page = query.per_page == 0 ? 20 : query.per_page;
|
|
const std::size_t page_index = page - 1;
|
|
const std::size_t offset = page_index > ordered_entries.size() / per_page ? ordered_entries.size() : page_index * per_page;
|
|
const std::size_t end = offset + std::min(per_page, ordered_entries.size() - offset);
|
|
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());
|
|
return make_http_success<Json>(std::move(data));
|
|
});
|
|
}
|
|
Http_Response<Json> item_response(std::uint64_t id) const noexcept {
|
|
return safe_response([&] {
|
|
std::scoped_lock lock(mutex_);
|
|
const auto iterator = find_entry(id);
|
|
if(iterator == entries_.end()) {
|
|
return make_http_error<Json>(404, "record not found");
|
|
}
|
|
return make_http_success<Json>(encode_entry(*iterator));
|
|
});
|
|
}
|
|
Http_Response<Json> create_response(std::string_view body) noexcept {
|
|
return safe_response([&] {
|
|
Json input;
|
|
try {
|
|
input = parse_json<Json>(body);
|
|
} catch(const std::exception& error) {
|
|
return make_http_error<Json>(400, error.what());
|
|
}
|
|
Json_Adapter<Json>::erase(input, "id");
|
|
T value{};
|
|
const auto result = apply_frontend_create<Json>(value, input);
|
|
if(!result.success) {
|
|
return make_http_error<Json>(422, result);
|
|
}
|
|
std::scoped_lock lock(mutex_);
|
|
entries_.push_back(Entry{next_id_++, std::move(value)});
|
|
return make_http_success<Json>(encode_entry(entries_.back()), "created");
|
|
});
|
|
}
|
|
Http_Response<Json> update_response(std::uint64_t id, std::string_view body) noexcept {
|
|
return safe_response([&] {
|
|
Json patch;
|
|
try {
|
|
patch = parse_json<Json>(body);
|
|
} catch(const std::exception& error) {
|
|
return make_http_error<Json>(400, error.what());
|
|
}
|
|
Json_Adapter<Json>::erase(patch, "id");
|
|
std::scoped_lock lock(mutex_);
|
|
const auto iterator = find_entry(id);
|
|
if(iterator == entries_.end()) {
|
|
return make_http_error<Json>(404, "record not found");
|
|
}
|
|
const auto result = apply_frontend_patch<Json>(iterator->value, patch);
|
|
if(!result.success) {
|
|
return make_http_error<Json>(422, result);
|
|
}
|
|
return make_http_success<Json>(encode_entry(*iterator), result.message);
|
|
});
|
|
}
|
|
Http_Response<Json> delete_response(std::uint64_t id) noexcept {
|
|
return safe_response([&] {
|
|
std::scoped_lock lock(mutex_);
|
|
const auto iterator = find_entry(id);
|
|
if(iterator == entries_.end()) {
|
|
return make_http_error<Json>(404, "record not found");
|
|
}
|
|
entries_.erase(iterator);
|
|
return make_http_success<Json>(json_object<Json>(), "deleted");
|
|
});
|
|
}
|
|
Http_Response<Json> reorder_response(std::string_view body) noexcept {
|
|
return safe_response([&] {
|
|
std::vector<std::uint64_t> ids;
|
|
try {
|
|
const Json input = parse_json<Json>(body);
|
|
ids = read_order_ids(Json_Adapter<Json>::at(input, "ids"));
|
|
} catch(const std::exception& error) {
|
|
return make_http_error<Json>(400, error.what());
|
|
}
|
|
std::scoped_lock lock(mutex_);
|
|
reorder(ids);
|
|
return make_http_success<Json>(json_object<Json>(), "reordered");
|
|
});
|
|
}
|
|
private:
|
|
struct Entry {
|
|
std::uint64_t id{};
|
|
T value;
|
|
};
|
|
using Iterator = typename std::vector<Entry>::iterator;
|
|
using Const_Iterator = typename std::vector<Entry>::const_iterator;
|
|
template <class Function>
|
|
static Http_Response<Json> safe_response(Function&& function) noexcept {
|
|
try {
|
|
return std::forward<Function>(function)();
|
|
} 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<T>(error));
|
|
} catch(const std::exception& error) {
|
|
return make_http_error<Json>(500, error.what());
|
|
} catch(...) {
|
|
return make_http_error<Json>(500, "unknown server error");
|
|
}
|
|
}
|
|
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 const Table_Column* find_table_column(const Table_View& view, std::string_view name) {
|
|
const auto iterator = std::find_if(view.columns.begin(), view.columns.end(), [&](const Table_Column& column) {
|
|
return column.field == name;
|
|
});
|
|
return iterator == view.columns.end() ? nullptr : &*iterator;
|
|
}
|
|
static bool is_sortable_field(const Table_View& view, const std::string& name) {
|
|
if(name == "id") {
|
|
return true;
|
|
}
|
|
const auto* column = find_table_column(view, name);
|
|
return column && column->sortable;
|
|
}
|
|
static std::string query_error(const Collection_Query& query) {
|
|
const auto view = describe_table_view<T>();
|
|
for(const auto& [name, value] : query.fields) {
|
|
static_cast<void>(value);
|
|
const auto* column = find_table_column(view, name);
|
|
if(!column || (!column->searchable && !column->filterable)) {
|
|
return "table query field is not searchable or filterable: " + name;
|
|
}
|
|
}
|
|
if(!query.order_by.empty() && !is_sortable_field(view, query.order_by)) {
|
|
return "table query field is not sortable: " + query.order_by;
|
|
}
|
|
if(!query.order_by.empty() && !query.order_dir.empty() && query.order_dir != "asc" && query.order_dir != "desc") {
|
|
return "table order direction must be asc or desc";
|
|
}
|
|
return {};
|
|
}
|
|
static std::string query_text(const Json& value) {
|
|
if(Json_Adapter<Json>::is_string(value)) {
|
|
return json_get<Json, std::string>(value);
|
|
}
|
|
if(Json_Adapter<Json>::is_boolean(value)) {
|
|
return json_get<Json, bool>(value) ? "true" : "false";
|
|
}
|
|
if(Json_Adapter<Json>::is_number(value)) {
|
|
char buffer[64];
|
|
const auto [end, error] = std::to_chars(buffer, buffer + sizeof(buffer), Json_Adapter<Json>::number(value), std::chars_format::general);
|
|
if(error == std::errc{}) {
|
|
return std::string(buffer, end);
|
|
}
|
|
}
|
|
return dump_json(value);
|
|
}
|
|
static std::string lowercase(std::string value) {
|
|
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) {
|
|
return static_cast<char>(std::tolower(character));
|
|
});
|
|
return value;
|
|
}
|
|
static bool matches_query(const Json& value, std::string_view query, const Table_Column& column) {
|
|
const std::string text = query_text(value);
|
|
if(column.searchable) {
|
|
return lowercase(text).find(lowercase(std::string(query))) != std::string::npos;
|
|
}
|
|
return text == query;
|
|
}
|
|
static void apply_filters(const Collection_Query& query, std::vector<const Entry*>& entries) {
|
|
if(query.fields.empty()) {
|
|
return;
|
|
}
|
|
const auto view = describe_table_view<T>();
|
|
for(const auto& [name, value] : query.fields) {
|
|
const auto* column = find_table_column(view, name);
|
|
entries.erase(std::remove_if(entries.begin(), entries.end(), [&](const Entry* entry) {
|
|
const Json encoded = to_frontend_json<Json>(entry->value);
|
|
if(!Json_Adapter<Json>::contains(encoded, name)) {
|
|
return true;
|
|
}
|
|
return !matches_query(Json_Adapter<Json>::at(encoded, name), value, *column);
|
|
}), entries.end());
|
|
}
|
|
}
|
|
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_sort(const Collection_Query& query, std::vector<const Entry*>& entries) const {
|
|
const auto view = describe_table_view<T>();
|
|
const std::string order_by = query.order_by.empty() ? view.default_order_by : query.order_by;
|
|
const std::string order_dir = query.order_by.empty() ? view.default_order_dir : query.order_dir.empty() ? "asc" : query.order_dir;
|
|
if(order_by.empty()) {
|
|
return;
|
|
}
|
|
if(!is_sortable_field(view, 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;
|
|
});
|
|
}
|
|
Iterator find_entry(std::uint64_t id) {
|
|
return std::find_if(entries_.begin(), entries_.end(), [id](const Entry& entry) {
|
|
return entry.id == id;
|
|
});
|
|
}
|
|
Const_Iterator find_entry(std::uint64_t id) const {
|
|
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;
|
|
}
|
|
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);
|
|
}
|
|
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 Lock mutex_;
|
|
};
|
|
}
|