#pragma once #include "adminive/collection.hpp" #include "adminive/http.hpp" #include #include #include #include #include #include #include #include #include namespace adminive { template drogon::HttpResponsePtr make_drogon_response(Http_Response value) { auto response = drogon::HttpResponse::newHttpResponse(); response->setStatusCode(static_cast(value.status)); response->setContentTypeCode(drogon::CT_APPLICATION_JSON); response->setBody(dump_json(value.body, 2)); return response; } using Drogon_Executor = std::function)>; using Drogon_Request_Context_Factory = std::function; struct Drogon_Bind_Options { Drogon_Executor executor; Drogon_Request_Context_Factory context_factory; std::vector filters; }; inline Request_Context make_drogon_request_context(const drogon::HttpRequestPtr& request, const Drogon_Request_Context_Factory& factory) { return factory ? factory(request) : Request_Context{}; } inline std::vector make_drogon_constraints(drogon::HttpMethod method, const std::vector& filters) { std::vector result; result.reserve(filters.size() + 1); result.emplace_back(method); for(const auto& filter : filters) { result.emplace_back(filter); } return result; } inline std::size_t read_drogon_size_parameter(const drogon::HttpRequestPtr& request, const char* name, std::size_t fallback) { const std::string& text = request->getParameter(name); if(text.empty()) { return fallback; } std::size_t value{}; const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); return error == std::errc{} && end == text.data() + text.size() && value != 0 ? value : fallback; } template void complete_drogon_request(std::function& callback, Function&& function) noexcept { try { callback(make_drogon_response(std::forward(function)())); } catch(const std::exception& error) { callback(make_drogon_response(make_http_error(500, error.what()))); } catch(...) { callback(make_drogon_response(make_http_error(500, "unknown server error"))); } } template void schedule_drogon_request(const Drogon_Executor& executor, std::function&& callback, Function&& function) noexcept { auto response_callback = std::make_shared>(std::move(callback)); std::function task = [response_callback, function = std::forward(function)]() mutable { complete_drogon_request(*response_callback, std::move(function)); }; try { if(executor) { executor(task); } else { task(); } } catch(const std::exception& error) { (*response_callback)(make_drogon_response(make_http_error(500, error.what()))); } catch(...) { (*response_callback)(make_drogon_response(make_http_error(500, "failed to schedule request"))); } } template requires Writable_Adapted_Object && std::copy_constructible> class Drogon_Resource { public: using Service = Resource_Service; using Transaction = typename Service::Transaction; Drogon_Resource(T& value, std::string path, Transaction transaction = {}, Lock* shared_lock = nullptr) : service_(std::make_shared(value, std::move(path), std::move(transaction), shared_lock)) {} template explicit Drogon_Resource(Synchronized_Value, Lock, Field_Lock>& value, std::string path, Transaction transaction = {}) : service_(std::make_shared(value, std::move(path), std::move(transaction))) {} template requires (!std::is_const_v) && std::same_as::value_type, std::remove_cvref_t> explicit Drogon_Resource(Synchronized_Field value, std::string path, Transaction transaction = {}, Resource_Lock_Scope lock_scope = Resource_Lock_Scope::local) : service_(std::make_shared(value, std::move(path), std::move(transaction), lock_scope)) {} Json amis_schema() const { return service_->amis_schema(); } void bind(drogon::HttpAppFramework& app, Drogon_Bind_Options options = {}) const { const auto service = service_; const auto get_constraints = make_drogon_constraints(drogon::Get, options.filters); const auto post_constraints = make_drogon_constraints(drogon::Post, options.filters); app.registerHandler(service->path() + "/descriptor", [service](const drogon::HttpRequestPtr&, std::function&& callback) { complete_drogon_request(callback, [service] { return service->descriptor_response(); }); }, get_constraints); app.registerHandler(service->path() + "/data", [service](const drogon::HttpRequestPtr&, std::function&& callback) { complete_drogon_request(callback, [service] { return service->data_response(); }); }, get_constraints); app.registerHandler(service->path() + "/amis", [service](const drogon::HttpRequestPtr&, std::function&& callback) { complete_drogon_request(callback, [service] { return service->amis_response(); }); }, get_constraints); app.registerHandler(service->path() + "/data", [service, options](const drogon::HttpRequestPtr& request, std::function&& callback) { Request_Context context; try { context = make_drogon_request_context(request, options.context_factory); } catch(const std::exception& error) { callback(make_drogon_response(make_http_error(500, error.what()))); return; } schedule_drogon_request(options.executor, std::move(callback), [service, body = std::string(request->getBody()), context] { return service->update_response(body, context); }); }, post_constraints); } private: std::shared_ptr service_; }; template requires std::default_initializable && std::copy_constructible && std::assignable_from class Drogon_Collection_Resource { public: using Service = Collection_Service; explicit Drogon_Collection_Resource(std::string path, std::vector initial = {}) : service_(std::make_shared(std::move(path), std::move(initial))) {} template void register_overview_status(std::string api, std::uint64_t interval = 2000) { service_->template register_overview_status(std::move(api), interval); } template void register_status(std::uint64_t interval = 2000) { service_->template register_status(interval); } std::size_t size() const { return service_->size(); } Json items_json() const { return service_->items_json(); } Json amis_schema() const { return service_->amis_schema(); } void bind(drogon::HttpAppFramework& app, Drogon_Bind_Options options = {}) const { const auto service = service_; const auto get_constraints = make_drogon_constraints(drogon::Get, options.filters); const auto post_constraints = make_drogon_constraints(drogon::Post, options.filters); const auto put_constraints = make_drogon_constraints(drogon::Put, options.filters); const auto patch_constraints = make_drogon_constraints(drogon::Patch, options.filters); const auto delete_constraints = make_drogon_constraints(drogon::Delete, options.filters); app.registerHandler(service->path() + "/descriptor", [service](const drogon::HttpRequestPtr&, std::function&& callback) { complete_drogon_request(callback, [service] { return service->descriptor_response(); }); }, get_constraints); app.registerHandler(service->path() + "/amis", [service](const drogon::HttpRequestPtr&, std::function&& callback) { complete_drogon_request(callback, [service] { return service->amis_response(); }); }, get_constraints); if(service->has_status()) { app.registerHandler(service->path() + "/status/descriptor", [service](const drogon::HttpRequestPtr&, std::function&& callback) { complete_drogon_request(callback, [service] { return service->status_descriptor_response(); }); }, get_constraints); app.registerHandler(service->path() + "/{1:id}/status", [service, options](const drogon::HttpRequestPtr&, std::function&& callback, std::uint64_t id) { schedule_drogon_request(options.executor, std::move(callback), [service, id] { return service->status_response(id); }); }, get_constraints); } app.registerHandler(service->path(), [service](const drogon::HttpRequestPtr& request, std::function&& callback) { Collection_Query query; query.page = read_drogon_size_parameter(request, "page", 1); query.per_page = read_drogon_size_parameter(request, "perPage", 20); query.order_by = request->getParameter("orderBy"); query.order_dir = request->getParameter("orderDir"); complete_drogon_request(callback, [service, query = std::move(query)]() mutable { return service->list_response(std::move(query)); }); }, get_constraints); app.registerHandler(service->path() + "/{1:id}", [service](const drogon::HttpRequestPtr&, std::function&& callback, std::uint64_t id) { complete_drogon_request(callback, [service, id] { return service->item_response(id); }); }, get_constraints); app.registerHandler(service->path(), [service, options](const drogon::HttpRequestPtr& request, std::function&& callback) { schedule_drogon_request(options.executor, std::move(callback), [service, body = std::string(request->getBody())] { return service->create_response(body); }); }, post_constraints); if(service->user_reorderable()) { app.registerHandler(service->path() + "/order", [service, options](const drogon::HttpRequestPtr& request, std::function&& callback) { schedule_drogon_request(options.executor, std::move(callback), [service, body = std::string(request->getBody())] { return service->reorder_response(body); }); }, post_constraints); } const auto bind_update = [service, options](const drogon::HttpRequestPtr& request, std::function&& callback, std::uint64_t id) { schedule_drogon_request(options.executor, std::move(callback), [service, id, body = std::string(request->getBody())] { return service->update_response(id, body); }); }; app.registerHandler(service->path() + "/{1:id}", bind_update, put_constraints); app.registerHandler(service->path() + "/{1:id}", bind_update, patch_constraints); app.registerHandler(service->path() + "/{1:id}", [service, options](const drogon::HttpRequestPtr&, std::function&& callback, std::uint64_t id) { schedule_drogon_request(options.executor, std::move(callback), [service, id] { return service->delete_response(id); }); }, delete_constraints); } private: std::shared_ptr service_; }; template class Drogon_Status_Resource { public: using Reader = std::function; Drogon_Status_Resource(std::string path, Reader reader, std::uint64_t interval = 2000) : state_(std::make_shared(State{std::move(path), std::move(reader), interval, to_status_descriptor_json()})) {} Json amis_schema() const { return make_amis_status_service(state_->descriptor, state_->path + "/data", state_->interval); } void bind(drogon::HttpAppFramework& app, Drogon_Bind_Options options = {}) const { const auto state = state_; const auto constraints = make_drogon_constraints(drogon::Get, options.filters); app.registerHandler(state->path + "/descriptor", [state](const drogon::HttpRequestPtr&, std::function&& callback) { complete_drogon_request(callback, [state] { return make_http_success(state->descriptor); }); }, constraints); app.registerHandler(state->path + "/amis", [state](const drogon::HttpRequestPtr&, std::function&& callback) { complete_drogon_request(callback, [state] { return make_http_success(make_amis_status_service(state->descriptor, state->path + "/data", state->interval)); }); }, constraints); app.registerHandler(state->path + "/data", [state, options](const drogon::HttpRequestPtr& request, std::function&& callback) { Request_Context context; try { context = make_drogon_request_context(request, options.context_factory); } catch(const std::exception& error) { callback(make_drogon_response(make_http_error(500, error.what()))); return; } schedule_drogon_request(options.executor, std::move(callback), [state, context] { return make_http_success(to_status_json(state->reader(context))); }); }, constraints); } private: struct State { std::string path; Reader reader; std::uint64_t interval; Json descriptor; }; std::shared_ptr state_; }; }