#pragma once #include "adminive/http.hpp" #include "adminive/status.hpp" #include #include #include #include #include #include #include #include #include #include namespace adminive { struct Collection_Query { std::size_t page{1}; std::size_t per_page{20}; std::string order_by; std::string order_dir; }; template requires std::default_initializable && std::copy_constructible && std::assignable_from class Collection_Service { public: explicit Collection_Service(std::string path, std::vector initial = {}) : path_(std::move(path)) { for(auto& value : initial) { entries_.push_back(Entry{next_id_++, std::move(value)}); } } template 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(); overview_status_interval_ = interval; } template void register_status(std::uint64_t interval = 2000) { using Function_Type = decltype(Function); static_assert(std::is_member_function_pointer_v); static_assert(std::is_invocable_v); using Status = std::remove_cvref_t>; static_assert(Described_Type); status_descriptor_ = to_status_descriptor_json(); status_interval_ = interval; status_reader_ = [](T& value) { return to_status_json(std::invoke(Function, value)); }; } const std::string& path() const noexcept { return path_; } bool has_status() const noexcept { return static_cast(status_reader_); } bool user_reorderable() const { return describe().list_options().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(); 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(path_, descriptor, status_interval_); } return to_amis_crud_status_schema(path_, overview_status_api_, overview_status_descriptor_, overview_status_interval_, descriptor, status_interval_); } Http_Response descriptor_response() const noexcept { return safe_response([&] { return make_http_success(to_descriptor_json()); }); } Http_Response amis_response() const noexcept { return safe_response([&] { return make_http_success(amis_schema()); }); } Http_Response status_descriptor_response() const noexcept { return safe_response([&] { if(!status_reader_) { return make_http_error(404, "item status is not configured"); } return make_http_success(status_descriptor_); }); } Http_Response status_response(std::uint64_t id) noexcept { return safe_response([&] { if(!status_reader_) { return make_http_error(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(404, "record not found"); } return make_http_success(status_reader_(iterator->value)); }); } Http_Response list_response(Collection_Query query = {}) const noexcept { return safe_response([&] { std::scoped_lock lock(mutex_); std::vector ordered_entries; ordered_entries.reserve(entries_.size()); for(const auto& entry : entries_) { ordered_entries.push_back(&entry); } 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(); for(std::size_t index = offset; index < end; ++index) { json_append(items, encode_entry(*ordered_entries[index])); } Json data = json_object(); json_set(data, "items", std::move(items)); json_set(data, "total", ordered_entries.size()); return make_http_success(std::move(data)); }); } Http_Response 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(404, "record not found"); } return make_http_success(encode_entry(*iterator)); }); } Http_Response create_response(std::string_view body) noexcept { return safe_response([&] { Json input = parse_json(body); Json_Adapter::erase(input, "id"); T value{}; const auto result = apply_frontend_create(value, input); if(!result.success) { return make_http_error(422, result); } std::scoped_lock lock(mutex_); entries_.push_back(Entry{next_id_++, std::move(value)}); return make_http_success(encode_entry(entries_.back()), "created"); }); } Http_Response update_response(std::uint64_t id, std::string_view body) noexcept { return safe_response([&] { Json patch = parse_json(body); Json_Adapter::erase(patch, "id"); std::scoped_lock lock(mutex_); const auto iterator = find_entry(id); if(iterator == entries_.end()) { return make_http_error(404, "record not found"); } const auto result = apply_frontend_patch(iterator->value, patch); if(!result.success) { return make_http_error(422, result); } return make_http_success(encode_entry(*iterator), result.message); }); } Http_Response 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(404, "record not found"); } entries_.erase(iterator); return make_http_success(json_object(), "deleted"); }); } Http_Response reorder_response(std::string_view body) noexcept { return safe_response([&] { const Json input = parse_json(body); const auto ids = read_order_ids(Json_Adapter::at(input, "ids")); std::scoped_lock lock(mutex_); reorder(ids); return make_http_success(json_object(), "reordered"); }); } private: struct Entry { std::uint64_t id{}; T value; }; using Iterator = typename std::vector::iterator; using Const_Iterator = typename std::vector::const_iterator; template static Http_Response safe_response(Function&& function) noexcept { try { return std::forward(function)(); } catch(const Json_Assignment_Error& error) { return make_http_error(422, error.result()); } catch(const Field_Validation_Error& error) { return make_http_error(422, object_commit_error(error)); } catch(const std::exception& error) { return make_http_error(400, error.what()); } catch(...) { return make_http_error(500, "unknown server error"); } } static int compare_json_values(const Json& left, const Json& right) { if(Json_Adapter::is_number(left) && Json_Adapter::is_number(right)) { const long double left_value = Json_Adapter::number(left); const long double right_value = Json_Adapter::number(right); return left_value < right_value ? -1 : left_value > right_value ? 1 : 0; } if(Json_Adapter::is_string(left) && Json_Adapter::is_string(right)) { const auto left_value = json_get(left); const auto right_value = json_get(right); return left_value < right_value ? -1 : left_value > right_value ? 1 : 0; } if(Json_Adapter::is_boolean(left) && Json_Adapter::is_boolean(right)) { const bool left_value = json_get(left); const bool right_value = json_get(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().fields()); return sortable; } static std::vector read_order_ids(const Json& value) { std::vector result; if(Json_Adapter::is_array(value)) { for(std::size_t index = 0; index < Json_Adapter::size(value); ++index) { const auto& item = Json_Adapter::at(value, index); result.push_back(Json_Adapter::is_string(item) ? std::stoull(json_get(item)) : json_get(item)); } return result; } const std::string text = json_get(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& entries) const { const auto descriptor = describe(); const auto& options = descriptor.list_options(); const std::string order_by = query.order_by.empty() ? options.default_order_by : query.order_by; const std::string order_dir = query.order_by.empty() ? options.default_order_dir : query.order_dir.empty() ? "asc" : query.order_dir; 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(left->value); const auto right_json = to_frontend_json(right->value); comparison = compare_json_values(Json_Adapter::at(left_json, order_by), Json_Adapter::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(entry.value); json_set(result, "id", entry.id); return result; } void reorder(const std::vector& ids) { std::vector remaining = std::move(entries_); std::vector 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 status_reader_; std::uint64_t status_interval_{2000}; std::vector entries_; std::uint64_t next_id_{1}; mutable Lock mutex_; }; }