diff --git a/DEPENDENCIES b/DEPENDENCIES index cbb52e14..b1382d26 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,6 +1,6 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 uwebsockets https://github.com/uNetworking/uWebSockets v20.76.0 -core https://github.com/sourcemeta/core bd4eb912ea0970c99a1caa2b3a75f44799ec1c38 +core https://github.com/sourcemeta/core 53a18883063506c8b70cc2c137dd8a07b89ef647 blaze https://github.com/sourcemeta/blaze 83e2458856b934aa7995c44da36ab09152bdfc8d jsonbinpack https://github.com/sourcemeta/jsonbinpack 2fe6502cad30e539de271d2e58ee7f8432b95a4d hydra https://github.com/sourcemeta/hydra 8cbbe5739c31d282a822dece8ed36ef0120cccbe diff --git a/src/actions/action_default_v1.h b/src/actions/action_default_v1.h index 6331d49f..bfc2b745 100644 --- a/src/actions/action_default_v1.h +++ b/src/actions/action_default_v1.h @@ -21,10 +21,6 @@ class ActionDefault_v1 : public sourcemeta::one::Action { const sourcemeta::core::URITemplateRouterView &router, const sourcemeta::core::URITemplateRouter::Identifier identifier) : sourcemeta::one::Action{base, router.base_path()} { - // TODO: This implies the API is mounted - this->error_schema_ = - std::string{this->base_path()} + "/self/v1/schemas/api/error"; - router.arguments(identifier, [this](const auto &key, const auto &value) { if (key == "errorSchema") { this->error_schema_ = std::get(value); @@ -115,8 +111,7 @@ class ActionDefault_v1 : public sourcemeta::one::Action { } private: - // TODO: This should be a string view - std::string error_schema_; + std::string_view error_schema_; }; #endif diff --git a/src/actions/dispatch.cc b/src/actions/dispatch.cc index c9f69222..17a0dc50 100644 --- a/src/actions/dispatch.cc +++ b/src/actions/dispatch.cc @@ -9,11 +9,10 @@ #include "action_serve_schema_artifact_v1.h" #include "action_serve_static_v1.h" -#include // std::array -#include // std::size_t -#include // std::unique_ptr, std::make_unique -#include // std::once_flag, std::call_once -#include // std::string +#include // std::array +#include // std::unique_ptr, std::make_unique +#include // std::once_flag, std::call_once +#include // std::string template static auto @@ -44,45 +43,47 @@ static constexpr std::array instance; - std::once_flag flag; -}; - -// Heap array because Slot contains a non-movable std::once_flag -// NOLINTNEXTLINE(modernize-avoid-c-arrays) -static std::unique_ptr SLOTS; -static std::size_t SLOTS_SIZE{0}; +sourcemeta::one::ActionDispatcher::ActionDispatcher( + const std::filesystem::path &base, + const sourcemeta::core::URITemplateRouterView &router) + : base_{base}, router_{router}, + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + slots_{std::make_unique(router.size() + 1)}, + slots_size_{router.size() + 1} { + router.arguments(0, [this](const auto &key, const auto &value) { + if (key == "errorSchema") { + this->default_error_schema_ = std::get(value); + } + }); +} -auto sourcemeta::one::actions_initialize( - const sourcemeta::core::URITemplateRouterView &router) -> void { - SLOTS_SIZE = router.size() + 1; - // NOLINTNEXTLINE(modernize-avoid-c-arrays) - SLOTS = std::make_unique(SLOTS_SIZE); +auto sourcemeta::one::ActionDispatcher::error( + const sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response, const char *const code, + std::string &&identifier, std::string &&message) const -> void { + sourcemeta::one::json_error(request, response, code, std::move(identifier), + std::move(message), this->default_error_schema_); } -auto sourcemeta::one::actions_dispatch( +auto sourcemeta::one::ActionDispatcher::dispatch( const sourcemeta::core::URITemplateRouter::Identifier identifier, const sourcemeta::core::URITemplateRouter::Identifier context, - const sourcemeta::core::URITemplateRouterView &router, - const std::filesystem::path &base, const std::span matches, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response) -> void { - if (identifier >= SLOTS_SIZE || context >= CONSTRUCTORS.size()) [[unlikely]] { - sourcemeta::one::json_error( - request, response, sourcemeta::one::STATUS_NOT_IMPLEMENTED, - "unknown-handler-code", - "This server version does not implement the handler for " - "this URL", - // TODO: This implies the API is mounted - std::string{router.base_path()} + "/self/v1/schemas/api/error"); + if (identifier >= this->slots_size_ || context >= CONSTRUCTORS.size()) + [[unlikely]] { + this->error(request, response, sourcemeta::one::STATUS_NOT_IMPLEMENTED, + "unknown-handler-code", + "This server version does not implement the handler for " + "this URL"); return; } - auto &slot{SLOTS[identifier]}; - std::call_once(slot.flag, [&] { - slot.instance = CONSTRUCTORS[context](base, router, identifier); + auto &slot{this->slots_[identifier]}; + std::call_once(slot.flag, [this, &slot, context, identifier] { + slot.instance = + CONSTRUCTORS[context](this->base_, this->router_, identifier); }); slot.instance->run(matches, request, response); diff --git a/src/actions/include/sourcemeta/one/actions.h b/src/actions/include/sourcemeta/one/actions.h index 08dda14a..7235ed50 100644 --- a/src/actions/include/sourcemeta/one/actions.h +++ b/src/actions/include/sourcemeta/one/actions.h @@ -5,8 +5,11 @@ #include +#include // std::size_t #include // std::uint8_t #include // std::filesystem::path +#include // std::unique_ptr +#include // std::once_flag #include // std::optional #include // std::span #include // std::string_view @@ -64,14 +67,42 @@ class Action { std::string_view base_path_; }; -auto actions_initialize(const core::URITemplateRouterView &router) -> void; +class ActionDispatcher { +public: + ActionDispatcher(const std::filesystem::path &base, + const core::URITemplateRouterView &router); + ~ActionDispatcher() = default; + + // To avoid mistakes + ActionDispatcher(const ActionDispatcher &) = delete; + ActionDispatcher(ActionDispatcher &&) = delete; + auto operator=(const ActionDispatcher &) -> ActionDispatcher & = delete; + auto operator=(ActionDispatcher &&) -> ActionDispatcher & = delete; -auto actions_dispatch(const core::URITemplateRouter::Identifier identifier, - const core::URITemplateRouter::Identifier context, - const core::URITemplateRouterView &router, - const std::filesystem::path &base, - const std::span matches, - HTTPRequest &request, HTTPResponse &response) -> void; + auto dispatch(const core::URITemplateRouter::Identifier identifier, + const core::URITemplateRouter::Identifier context, + const std::span matches, HTTPRequest &request, + HTTPResponse &response) -> void; + + auto error(const HTTPRequest &request, HTTPResponse &response, + const char *const code, std::string &&identifier, + std::string &&message) const -> void; + +private: + struct Slot { + std::unique_ptr instance; + std::once_flag flag; + }; + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + const std::filesystem::path &base_; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + const core::URITemplateRouterView &router_; + // NOLINTNEXTLINE(modernize-avoid-c-arrays) + std::unique_ptr slots_; + std::size_t slots_size_; + std::string_view default_error_schema_; +}; } // namespace sourcemeta::one diff --git a/src/index/generators.h b/src/index/generators.h index d9040d12..8eaf228b 100644 --- a/src/index/generators.h +++ b/src/index/generators.h @@ -689,6 +689,11 @@ struct GENERATE_URITEMPLATE_ROUTES { const auto error_schema{configuration.base_path + "/self/v1/schemas/api/error"}; + const sourcemeta::core::URITemplateRouter::Argument otherwise_arguments[] = + {{"errorSchema", std::string_view{error_schema}}}; + router.otherwise(sourcemeta::one::ACTION_TYPE_DEFAULT_V1, + otherwise_arguments); + sourcemeta::core::URITemplateRouter::Identifier next_id{1}; const sourcemeta::core::URITemplateRouter::Argument list_arguments[] = { diff --git a/src/server/server.cc b/src/server/server.cc index 5bb31eaa..b697edae 100644 --- a/src/server/server.cc +++ b/src/server/server.cc @@ -14,8 +14,9 @@ #include // std::string, std::to_string #include // std::string_view -static auto dispatch(const sourcemeta::core::URITemplateRouterView &router, - const std::filesystem::path &base, +// TODO: Maybe we should merge this entire function into `ActionDispatcher`? +static auto dispatch(sourcemeta::one::ActionDispatcher &actions, + const sourcemeta::core::URITemplateRouterView &router, sourcemeta::one::HTTPRequest &request, sourcemeta::one::HTTPResponse &response) noexcept -> void { try { @@ -34,29 +35,22 @@ static auto dispatch(const sourcemeta::core::URITemplateRouterView &router, matches_size = static_cast(index) + 1; })}; - sourcemeta::one::actions_dispatch( - match_result.first, match_result.second, router, base, - std::span{matches.data(), matches_size}, request, response); + actions.dispatch(match_result.first, match_result.second, + std::span{matches.data(), matches_size}, request, + response); } else { - sourcemeta::one::json_error( - request, response, sourcemeta::one::STATUS_NOT_ACCEPTABLE, - "cannot-satisfy-content-encoding", - "The server cannot satisfy the request content encoding", - // TODO: This implies the API is mounted - "/self/v1/schemas/api/error"); + actions.error(request, response, sourcemeta::one::STATUS_NOT_ACCEPTABLE, + "cannot-satisfy-content-encoding", + "The server cannot satisfy the request content encoding"); } } catch (const std::exception &error) { - sourcemeta::one::json_error(request, response, - sourcemeta::one::STATUS_INTERNAL_SERVER_ERROR, - "uncaught-error", error.what(), - // TODO: This implies the API is mounted - "/self/v1/schemas/api/error"); + actions.error(request, response, + sourcemeta::one::STATUS_INTERNAL_SERVER_ERROR, + "uncaught-error", error.what()); } catch (...) { - sourcemeta::one::json_error( - request, response, sourcemeta::one::STATUS_INTERNAL_SERVER_ERROR, - "uncaught-error", "An unknown unexpected error occurred", - // TODO: This implies the API is mounted - "/self/v1/schemas/api/error"); + actions.error(request, response, + sourcemeta::one::STATUS_INTERNAL_SERVER_ERROR, + "uncaught-error", "An unknown unexpected error occurred"); } } @@ -100,13 +94,13 @@ auto main(int argc, char *argv[]) noexcept -> int { } const sourcemeta::core::URITemplateRouterView router{base / "routes.bin"}; - sourcemeta::one::actions_initialize(router); + sourcemeta::one::ActionDispatcher actions{base, router}; sourcemeta::one::HTTPServer( port, - [&router, &base](sourcemeta::one::HTTPRequest &request, - sourcemeta::one::HTTPResponse &response) { - dispatch(router, base, request, response); + [&actions, &router](sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) { + dispatch(actions, router, request, response); }, [timestamp_start](const std::uint16_t bound_port) { const auto duration{ diff --git a/vendor/core/src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h b/vendor/core/src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h index 255c4661..65c3970c 100644 --- a/vendor/core/src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h +++ b/vendor/core/src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h @@ -29,6 +29,8 @@ namespace sourcemeta::core { /// DOES NOT define expansion. So this is an opinionated non-standard adaptation /// of URI Template for path routing purposes class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter { + friend class URITemplateRouterView; + public: /// A handler identifier 0 means "no handler" using Identifier = std::uint16_t; @@ -86,6 +88,11 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter { const Identifier context = 0, const std::span arguments = {}) -> void; + /// Register a fallback context and arguments to be returned when matching + /// a path that does not correspond to any registered route + auto otherwise(const Identifier context, + const std::span arguments = {}) -> void; + /// Match a path against the router. Note the callback might fire for /// initial matches even though the entire match might still fail [[nodiscard]] auto match(const std::string_view path, @@ -111,6 +118,7 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter { private: Node root_; + Node otherwise_; std::string base_path_; std::vector>> arguments_; std::size_t size_{0}; diff --git a/vendor/core/src/core/uritemplate/uritemplate_router.cc b/vendor/core/src/core/uritemplate/uritemplate_router.cc index e5020b0f..9f74e1e1 100644 --- a/vendor/core/src/core/uritemplate/uritemplate_router.cc +++ b/vendor/core/src/core/uritemplate/uritemplate_router.cc @@ -92,6 +92,17 @@ inline auto extract_segment(const char *start, const char *end) return {start, static_cast(position - start)}; } +inline auto finalize_match(const Node &otherwise, + const URITemplateRouter::Identifier identifier, + const URITemplateRouter::Identifier context) + -> std::pair { + if (identifier == 0) { + return {URITemplateRouter::Identifier{0}, otherwise.context}; + } + + return {identifier, context}; +} + } // namespace URITemplateRouter::URITemplateRouter(const std::string_view base_path) @@ -113,6 +124,28 @@ auto URITemplateRouter::size() const noexcept -> std::size_t { return this->size_; } +auto URITemplateRouter::otherwise(const Identifier context, + const std::span arguments) + -> void { + this->otherwise_.context = context; + + const auto existing = std::ranges::find_if( + this->arguments_, [](const auto &entry) { return entry.first == 0; }); + if (existing == this->arguments_.end()) { + if (!arguments.empty()) { + this->arguments_.emplace_back( + Identifier{0}, + std::vector{arguments.begin(), arguments.end()}); + } + } else { + if (arguments.empty()) { + this->arguments_.erase(existing); + } else { + existing->second.assign(arguments.begin(), arguments.end()); + } + } +} + auto URITemplateRouter::add(const std::string_view uri_template, const Identifier identifier, const Identifier context, @@ -361,14 +394,16 @@ auto URITemplateRouter::match(const std::string_view path, const Callback &callback) const -> std::pair { if (path.empty()) { - return {this->root_.identifier, this->root_.context}; + return finalize_match(this->otherwise_, this->root_.identifier, + this->root_.context); } if (path.size() == 1 && path[0] == '/') { if (auto *child = find_literal_child(this->root_.literals, "")) { - return {child->identifier, child->context}; + return finalize_match(this->otherwise_, child->identifier, + child->context); } - return {}; + return finalize_match(this->otherwise_, 0, 0); } const Node *current = nullptr; @@ -396,7 +431,7 @@ auto URITemplateRouter::match(const std::string_view path, // Empty segment (from double slash or trailing slash) doesn't match if (segment.empty()) { - return {}; + return finalize_match(this->otherwise_, 0, 0); } if (auto *literal_match = find_literal_child(*literal_children, segment)) { @@ -409,14 +444,15 @@ auto URITemplateRouter::match(const std::string_view path, segment_start, static_cast(path_end - segment_start)}; callback(static_cast(variable_index), (*variable_child)->value, remaining); - return {(*variable_child)->identifier, (*variable_child)->context}; + return finalize_match(this->otherwise_, (*variable_child)->identifier, + (*variable_child)->context); } callback(static_cast(variable_index), (*variable_child)->value, segment); ++variable_index; current = variable_child->get(); } else { - return {}; + return finalize_match(this->otherwise_, 0, 0); } literal_children = ¤t->literals; @@ -431,8 +467,10 @@ auto URITemplateRouter::match(const std::string_view path, ++position; } - return current ? std::pair{current->identifier, current->context} - : std::pair{this->root_.identifier, this->root_.context}; + return current ? finalize_match(this->otherwise_, current->identifier, + current->context) + : finalize_match(this->otherwise_, this->root_.identifier, + this->root_.context); } } // namespace sourcemeta::core diff --git a/vendor/core/src/core/uritemplate/uritemplate_router_view.cc b/vendor/core/src/core/uritemplate/uritemplate_router_view.cc index 80bfa1ea..a8a2d5e6 100644 --- a/vendor/core/src/core/uritemplate/uritemplate_router_view.cc +++ b/vendor/core/src/core/uritemplate/uritemplate_router_view.cc @@ -8,6 +8,7 @@ #include // std::queue #include // std::string #include // std::unordered_map +#include // std::pair #include // std::vector namespace sourcemeta::core { @@ -15,7 +16,7 @@ namespace sourcemeta::core { namespace { constexpr std::uint32_t ROUTER_MAGIC = 0x52544552; // "RTER" -constexpr std::uint32_t ROUTER_VERSION = 4; +constexpr std::uint32_t ROUTER_VERSION = 5; constexpr std::uint32_t NO_CHILD = std::numeric_limits::max(); // Type tags for argument value serialization @@ -31,6 +32,7 @@ struct RouterHeader { std::uint32_t arguments_offset; std::uint32_t base_path_offset; std::uint32_t base_path_length; + std::uint32_t otherwise_context; }; struct ArgumentEntryHeader { @@ -52,6 +54,18 @@ struct alignas(8) SerializedNode { std::array padding2; }; +inline auto +finalize_match(const URITemplateRouter::Identifier otherwise_context, + const URITemplateRouter::Identifier identifier, + const URITemplateRouter::Identifier context) + -> std::pair { + if (identifier == 0) { + return {URITemplateRouter::Identifier{0}, otherwise_context}; + } + + return {identifier, context}; +} + // Binary search for a literal child matching the given segment inline auto binary_search_literal_children( const SerializedNode *nodes, const char *string_table, @@ -258,6 +272,7 @@ auto URITemplateRouterView::save(const URITemplateRouter &router, header.string_table_offset + string_table.size()); header.base_path_offset = base_path_string_offset; header.base_path_length = static_cast(base_path_value.size()); + header.otherwise_context = router.otherwise_.context; std::ofstream file(path, std::ios::binary); if (!file) { @@ -333,10 +348,13 @@ auto URITemplateRouterView::match( return {}; } + const auto otherwise_context = + static_cast(header->otherwise_context); + if (header->node_count == 0 || header->node_count > (this->data_.size() - sizeof(RouterHeader)) / sizeof(SerializedNode)) { - return {}; + return finalize_match(otherwise_context, 0, 0); } const auto *nodes = reinterpret_cast( @@ -346,12 +364,12 @@ auto URITemplateRouterView::match( const auto expected_string_table_offset = sizeof(RouterHeader) + nodes_size; if (header->string_table_offset < expected_string_table_offset || header->string_table_offset > this->data_.size()) { - return {}; + return finalize_match(otherwise_context, 0, 0); } if (header->arguments_offset < header->string_table_offset || header->arguments_offset > this->data_.size()) { - return {}; + return finalize_match(otherwise_context, 0, 0); } const auto *string_table = reinterpret_cast( @@ -361,29 +379,31 @@ auto URITemplateRouterView::match( // Empty path matches empty template if (path.empty()) { - return {nodes[0].identifier, nodes[0].context}; + return finalize_match(otherwise_context, nodes[0].identifier, + nodes[0].context); } // Root path "/" is stored as an empty literal segment if (path.size() == 1 && path[0] == '/') { const auto &root = nodes[0]; if (root.first_literal_child == NO_CHILD) { - return {}; + return finalize_match(otherwise_context, 0, 0); } if (root.first_literal_child >= header->node_count || root.literal_child_count > header->node_count - root.first_literal_child) { - return {}; + return finalize_match(otherwise_context, 0, 0); } const auto match = binary_search_literal_children( nodes, string_table, string_table_size, root.first_literal_child, root.literal_child_count, "", 0); if (match == NO_CHILD) { - return {}; + return finalize_match(otherwise_context, 0, 0); } - return {nodes[match].identifier, nodes[match].context}; + return finalize_match(otherwise_context, nodes[match].identifier, + nodes[match].context); } // Walk the trie, matching each path segment @@ -410,7 +430,7 @@ auto URITemplateRouterView::match( // Empty segment (from double slash or trailing slash) doesn't match if (segment_length == 0) { - return {}; + return finalize_match(otherwise_context, 0, 0); } const auto &node = nodes[current_node]; @@ -420,7 +440,7 @@ auto URITemplateRouterView::match( if (node.first_literal_child != NO_CHILD) { if (node.first_literal_child >= node_count || node.literal_child_count > node_count - node.first_literal_child) { - return {}; + return finalize_match(otherwise_context, 0, 0); } const auto literal_match = binary_search_literal_children( @@ -441,7 +461,7 @@ auto URITemplateRouterView::match( if (node.variable_child >= node_count || variable_index > std::numeric_limits::max()) { - return {}; + return finalize_match(otherwise_context, 0, 0); } const auto &variable_node = nodes[node.variable_child]; @@ -449,7 +469,7 @@ auto URITemplateRouterView::match( if (variable_node.string_offset > string_table_size || variable_node.string_length > string_table_size - variable_node.string_offset) { - return {}; + return finalize_match(otherwise_context, 0, 0); } // Check if this is an expansion (catch-all) @@ -460,7 +480,8 @@ auto URITemplateRouterView::match( {string_table + variable_node.string_offset, variable_node.string_length}, {segment_start, remaining_length}); - return {variable_node.identifier, variable_node.context}; + return finalize_match(otherwise_context, variable_node.identifier, + variable_node.context); } // Regular variable - match single segment @@ -478,10 +499,11 @@ auto URITemplateRouterView::match( } // No match - return {}; + return finalize_match(otherwise_context, 0, 0); } - return {nodes[current_node].identifier, nodes[current_node].context}; + return finalize_match(otherwise_context, nodes[current_node].identifier, + nodes[current_node].context); } auto URITemplateRouterView::arguments(