diff --git a/src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h b/src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h index 255c46618..65c3970cc 100644 --- a/src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h +++ b/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/src/core/uritemplate/uritemplate_router.cc b/src/core/uritemplate/uritemplate_router.cc index e5020b0f3..9f74e1e11 100644 --- a/src/core/uritemplate/uritemplate_router.cc +++ b/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/src/core/uritemplate/uritemplate_router_view.cc b/src/core/uritemplate/uritemplate_router_view.cc index 80bfa1ea1..a8a2d5e62 100644 --- a/src/core/uritemplate/uritemplate_router_view.cc +++ b/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( diff --git a/test/uritemplate/uritemplate_router_test.cc b/test/uritemplate/uritemplate_router_test.cc index b5d85b98e..eab007977 100644 --- a/test/uritemplate/uritemplate_router_test.cc +++ b/test/uritemplate/uritemplate_router_test.cc @@ -1339,3 +1339,206 @@ TEST(URITemplateRouter, size_with_base_path) { router.add("/posts", 2); EXPECT_EQ(router.size(), 2); } + +TEST(URITemplateRouter, otherwise_default_is_zero_context) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1); + EXPECT_ROUTER_MATCH(router, "/unknown", 0, 0, captures); +} + +TEST(URITemplateRouter, otherwise_sets_context) { + sourcemeta::core::URITemplateRouter router; + router.otherwise(42); + EXPECT_ROUTER_MATCH(router, "/anything", 0, 42, captures); +} + +TEST(URITemplateRouter, otherwise_returned_from_match_on_unknown_path) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1, 5); + router.otherwise(99); + EXPECT_ROUTER_MATCH(router, "/unknown", 0, 99, captures); + EXPECT_EQ(captures.size(), 0); +} + +TEST(URITemplateRouter, otherwise_not_returned_from_matching_route) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1, 5); + router.otherwise(99); + EXPECT_ROUTER_MATCH(router, "/users", 1, 5, captures); + EXPECT_EQ(captures.size(), 0); +} + +TEST(URITemplateRouter, otherwise_returned_for_empty_segment) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1); + router.otherwise(77); + EXPECT_ROUTER_MATCH(router, "/users//", 0, 77, captures); +} + +TEST(URITemplateRouter, otherwise_returned_for_root_slash_no_match) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1); + router.otherwise(88); + EXPECT_ROUTER_MATCH(router, "/", 0, 88, captures); +} + +TEST(URITemplateRouter, otherwise_without_registration_returns_zero_context) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1); + EXPECT_ROUTER_MATCH(router, "/unknown", 0, 0, captures); +} + +TEST(URITemplateRouter, otherwise_arguments_lookup) { + sourcemeta::core::URITemplateRouter router; + const std::array arguments{ + {{"status", std::int64_t{404}}, + {"message", std::string_view{"Not Found"}}}}; + router.otherwise(55, arguments); + + std::vector> + collected; + router.arguments( + 0, [&collected]( + const std::string_view name, + const sourcemeta::core::URITemplateRouter::ArgumentValue &value) { + collected.emplace_back(name, value); + }); + + EXPECT_EQ(collected.size(), 2); + EXPECT_EQ(collected[0].first, "status"); + EXPECT_TRUE(std::holds_alternative(collected[0].second)); + EXPECT_EQ(std::get(collected[0].second), 404); + EXPECT_EQ(collected[1].first, "message"); + EXPECT_TRUE(std::holds_alternative(collected[1].second)); + EXPECT_EQ(std::get(collected[1].second), "Not Found"); +} + +TEST(URITemplateRouter, otherwise_overwrite_context) { + sourcemeta::core::URITemplateRouter router; + router.otherwise(10); + router.otherwise(20); + EXPECT_ROUTER_MATCH(router, "/nope", 0, 20, captures); +} + +TEST(URITemplateRouter, otherwise_overwrite_arguments) { + sourcemeta::core::URITemplateRouter router; + const std::array first{ + {{"version", std::int64_t{1}}}}; + const std::array second{ + {{"version", std::int64_t{2}}}}; + router.otherwise(10, first); + router.otherwise(20, second); + + std::vector> + collected; + router.arguments( + 0, [&collected]( + const std::string_view name, + const sourcemeta::core::URITemplateRouter::ArgumentValue &value) { + collected.emplace_back(name, value); + }); + + EXPECT_EQ(collected.size(), 1); + EXPECT_EQ(collected[0].first, "version"); + EXPECT_EQ(std::get(collected[0].second), 2); +} + +TEST(URITemplateRouter, otherwise_overwrite_with_empty_clears_arguments) { + sourcemeta::core::URITemplateRouter router; + const std::array arguments{ + {{"key", std::string_view{"value"}}}}; + router.otherwise(1, arguments); + router.otherwise(2); + + std::vector> + collected; + router.arguments( + 0, [&collected]( + const std::string_view name, + const sourcemeta::core::URITemplateRouter::ArgumentValue &value) { + collected.emplace_back(name, value); + }); + + EXPECT_TRUE(collected.empty()); + EXPECT_ROUTER_MATCH(router, "/nope", 0, 2, captures); +} + +TEST(URITemplateRouter, otherwise_boolean_argument) { + sourcemeta::core::URITemplateRouter router; + const std::array arguments{ + {{"cached", true}}}; + router.otherwise(3, arguments); + + std::vector> + collected; + router.arguments( + 0, [&collected]( + const std::string_view name, + const sourcemeta::core::URITemplateRouter::ArgumentValue &value) { + collected.emplace_back(name, value); + }); + + EXPECT_EQ(collected.size(), 1); + EXPECT_TRUE(std::holds_alternative(collected[0].second)); + EXPECT_EQ(std::get(collected[0].second), true); +} + +TEST(URITemplateRouter, otherwise_does_not_affect_other_arguments) { + sourcemeta::core::URITemplateRouter router; + const std::array route_args{ + {{"schema", std::string_view{"user.json"}}}}; + router.add("/users", 1, 0, route_args); + + const std::array + default_args{{{"message", std::string_view{"not found"}}}}; + router.otherwise(99, default_args); + + std::vector> + route_collected; + router.arguments( + 1, [&route_collected]( + const std::string_view name, + const sourcemeta::core::URITemplateRouter::ArgumentValue &value) { + route_collected.emplace_back(name, value); + }); + EXPECT_EQ(route_collected.size(), 1); + EXPECT_EQ(route_collected[0].first, "schema"); + + std::vector> + default_collected; + router.arguments( + 0, [&default_collected]( + const std::string_view name, + const sourcemeta::core::URITemplateRouter::ArgumentValue &value) { + default_collected.emplace_back(name, value); + }); + EXPECT_EQ(default_collected.size(), 1); + EXPECT_EQ(default_collected[0].first, "message"); +} + +TEST(URITemplateRouter, otherwise_does_not_count_as_route_in_size) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1); + router.otherwise(99); + EXPECT_EQ(router.size(), 1); +} + +TEST(URITemplateRouter, otherwise_with_base_path_and_unmatched) { + sourcemeta::core::URITemplateRouter router{"/v1"}; + router.add("/users", 1); + router.otherwise(42); + EXPECT_ROUTER_MATCH(router, "/v1/other", 0, 42, captures); +} + +TEST(URITemplateRouter, otherwise_with_partial_trie_walk) { + sourcemeta::core::URITemplateRouter router; + router.add("/users/{id}/posts", 1); + router.otherwise(42); + EXPECT_ROUTER_MATCH(router, "/users/123", 0, 42, captures); +} diff --git a/test/uritemplate/uritemplate_router_view_test.cc b/test/uritemplate/uritemplate_router_view_test.cc index da9d101d5..851ee80ff 100644 --- a/test/uritemplate/uritemplate_router_view_test.cc +++ b/test/uritemplate/uritemplate_router_view_test.cc @@ -724,7 +724,7 @@ TEST(URITemplateRouterView, corrupt_too_small_for_header) { } TEST(URITemplateRouterView, corrupt_wrong_magic) { - const std::uint32_t data[] = {0xDEADBEEF, 4, 1, 60, 60, 0, 0}; + const std::uint32_t data[] = {0xDEADBEEF, 5, 1, 64, 64, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -732,7 +732,7 @@ TEST(URITemplateRouterView, corrupt_wrong_magic) { } TEST(URITemplateRouterView, corrupt_wrong_version) { - const std::uint32_t data[] = {0x52544552, 99, 1, 60, 60, 0, 0}; + const std::uint32_t data[] = {0x52544552, 99, 1, 64, 64, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -740,7 +740,7 @@ TEST(URITemplateRouterView, corrupt_wrong_version) { } TEST(URITemplateRouterView, corrupt_node_count_exceeds_file) { - const std::uint32_t data[] = {0x52544552, 4, 10, 28, 28, 0, 0}; + const std::uint32_t data[] = {0x52544552, 5, 10, 32, 32, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -748,8 +748,8 @@ TEST(URITemplateRouterView, corrupt_node_count_exceeds_file) { } TEST(URITemplateRouterView, corrupt_literal_child_out_of_bounds) { - const std::uint32_t data[] = {0x52544552, 4, 1, 60, 60, 0, 0, 0, - 0, 999, 1, 0xFFFFFFFF, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 5, 1, 64, 64, 0, 0, 0, + 0, 0, 999, 1, 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -757,8 +757,8 @@ TEST(URITemplateRouterView, corrupt_literal_child_out_of_bounds) { } TEST(URITemplateRouterView, corrupt_variable_child_out_of_bounds) { - const std::uint32_t data[] = {0x52544552, 4, 1, 60, 60, 0, 0, 0, - 0, 0xFFFFFFFF, 0, 500, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 5, 1, 64, 64, 0, 0, 0, + 0, 0, 0xFFFFFFFF, 0, 500, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -766,10 +766,9 @@ TEST(URITemplateRouterView, corrupt_variable_child_out_of_bounds) { } TEST(URITemplateRouterView, corrupt_string_offset_out_of_bounds) { - const std::uint32_t data[] = {0x52544552, 4, 2, 92, 92, 0, - 0, 0, 0, 1, 1, 0xFFFFFFFF, - 0, 0, 0, 9999, 5, 0xFFFFFFFF, - 0, 0xFFFFFFFF, 0, 0, 0}; + const std::uint32_t data[] = { + 0x52544552, 5, 2, 96, 96, 0, 0, 0, 0, 0, 0, 1, 1, + 0xFFFFFFFF, 0, 0, 0, 9999, 5, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -778,9 +777,9 @@ TEST(URITemplateRouterView, corrupt_string_offset_out_of_bounds) { TEST(URITemplateRouterView, corrupt_variable_string_offset_out_of_bounds) { const std::uint32_t data[] = { - 0x52544552, 4, 2, 92, 92, 0, 0, 0, - 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, 9999, - 100, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000002, 0, 0}; + 0x52544552, 5, 2, 96, 96, 0, 0, 0, + 0, 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, + 9999, 100, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000002, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -803,7 +802,7 @@ TEST(URITemplateRouterView, corrupt_all_ones) { } TEST(URITemplateRouterView, corrupt_string_table_offset_overlaps_header) { - const std::uint32_t data[] = {0x52544552, 4, 1, 4, 60, 0, 0, 0, 0, + const std::uint32_t data[] = {0x52544552, 5, 1, 4, 64, 0, 0, 0, 0, 0, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; @@ -812,9 +811,9 @@ TEST(URITemplateRouterView, corrupt_string_table_offset_overlaps_header) { } TEST(URITemplateRouterView, corrupt_string_table_offset_past_end) { - const std::uint32_t data[] = {0x52544552, 4, 1, 99999, 99999, - 0, 0, 0, 0, 0xFFFFFFFF, - 0, 0xFFFFFFFF, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 5, 1, 99999, 99999, 0, + 0, 0, 0, 0, 0xFFFFFFFF, 0, + 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -822,7 +821,7 @@ TEST(URITemplateRouterView, corrupt_string_table_offset_past_end) { } TEST(URITemplateRouterView, corrupt_zero_node_count) { - const std::uint32_t data[] = {0x52544552, 4, 0, 28, 28, 0, 0}; + const std::uint32_t data[] = {0x52544552, 5, 0, 32, 32, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -843,9 +842,8 @@ TEST(URITemplateRouterView, corrupt_empty_data_match_root) { TEST(URITemplateRouterView, corrupt_literal_child_count_overflow) { const std::uint32_t data[] = { - 0x52544552, 4, 2, 92, 92, 0, 0, 0, - 0, 1, 0xFFFFFFFF, 0xFFFFFFFF, 0, 0, 0, 0, - 0, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; + 0x52544552, 5, 2, 96, 96, 0, 0, 0, 0, 0, 1, 0xFFFFFFFF, + 0xFFFFFFFF, 0, 0, 0, 0, 0, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -853,8 +851,8 @@ TEST(URITemplateRouterView, corrupt_literal_child_count_overflow) { } TEST(URITemplateRouterView, corrupt_root_literal_child_oob_match_root) { - const std::uint32_t data[] = {0x52544552, 4, 1, 60, 60, 0, 0, 0, - 0, 999, 1, 0xFFFFFFFF, 0, 0, 0}; + const std::uint32_t data[] = {0x52544552, 5, 1, 64, 64, 0, 0, 0, + 0, 0, 999, 1, 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/", 0, 0, captures); @@ -863,7 +861,7 @@ TEST(URITemplateRouterView, corrupt_root_literal_child_oob_match_root) { TEST(URITemplateRouterView, corrupt_deep_node_variable_child_oob) { std::vector data; - const std::uint32_t header[] = {0x52544552, 4, 2, 92, 97, 0, 0}; + const std::uint32_t header[] = {0x52544552, 5, 2, 96, 101, 0, 0, 0}; const std::uint32_t root[] = {0, 0, 1, 1, 0xFFFFFFFF, 0, 0, 0}; const std::uint32_t child[] = {0, 5, 0xFFFFFFFF, 0, 999, 0x00000001, 0, 0}; data.insert(data.end(), reinterpret_cast(header), @@ -881,9 +879,9 @@ TEST(URITemplateRouterView, corrupt_deep_node_variable_child_oob) { TEST(URITemplateRouterView, corrupt_expansion_string_oob) { const std::uint32_t data[] = { - 0x52544552, 4, 2, 92, 92, 0, 0, 0, - 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, 5000, - 200, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000003, 0, 0}; + 0x52544552, 5, 2, 96, 96, 0, 0, 0, + 0, 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, + 5000, 200, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000003, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/files/foo/bar", 0, 0, captures); @@ -892,8 +890,8 @@ TEST(URITemplateRouterView, corrupt_expansion_string_oob) { TEST(URITemplateRouterView, corrupt_empty_string_table_with_string_ref) { const std::uint32_t data[] = { - 0x52544552, 4, 2, 92, 92, 0, 0, 0, 0, 1, 1, 0xFFFFFFFF, - 0, 0, 0, 0, 10, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; + 0x52544552, 5, 2, 96, 96, 0, 0, 0, 0, 0, 1, 1, + 0xFFFFFFFF, 0, 0, 0, 0, 10, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -901,7 +899,7 @@ TEST(URITemplateRouterView, corrupt_empty_string_table_with_string_ref) { } TEST(URITemplateRouterView, corrupt_node_count_max_uint32) { - const std::uint32_t data[] = {0x52544552, 4, 0xFFFFFFFF, 40, 40, + const std::uint32_t data[] = {0x52544552, 5, 0xFFFFFFFF, 44, 44, 0, 0, 0, 0, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; @@ -911,9 +909,9 @@ TEST(URITemplateRouterView, corrupt_node_count_max_uint32) { TEST(URITemplateRouterView, corrupt_string_offset_plus_length_overflow) { const std::uint32_t data[] = { - 0x52544552, 4, 2, 92, 92, 0, 0, 0, - 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, 0x80000000, - 0x80000001, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000002, 0, 0}; + 0x52544552, 5, 2, 96, 96, 0, 0, 0, + 0, 0, 0xFFFFFFFF, 0, 1, 0, 0, 0, + 0x80000000, 0x80000001, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000002, 0, 0}; const sourcemeta::core::URITemplateRouterView view{ reinterpret_cast(data), sizeof(data)}; EXPECT_ROUTER_MATCH(view, "/users", 0, 0, captures); @@ -923,7 +921,7 @@ TEST(URITemplateRouterView, corrupt_string_offset_plus_length_overflow) { TEST(URITemplateRouterView, corrupt_string_offset_plus_length_overflow_with_data) { std::vector data; - const std::uint32_t header[] = {0x52544552, 4, 2, 92, 93, 0, 0}; + const std::uint32_t header[] = {0x52544552, 5, 2, 96, 97, 0, 0, 0}; const std::uint32_t root[] = {0, 0, 0xFFFFFFFF, 0, 1, 0, 0, 0}; const std::uint32_t variable[] = {0xFFFFFFFF, 2, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0x00000002, 0, 0}; @@ -944,7 +942,7 @@ TEST(URITemplateRouterView, TEST(URITemplateRouterView, corrupt_literal_string_offset_plus_length_overflow) { std::vector data; - const std::uint32_t header[] = {0x52544552, 4, 2, 92, 93, 0, 0}; + const std::uint32_t header[] = {0x52544552, 5, 2, 96, 97, 0, 0, 0}; const std::uint32_t root[] = {0, 0, 1, 1, 0xFFFFFFFF, 0, 0, 0}; const std::uint32_t child[] = {0xFFFFFFFF, 2, 0xFFFFFFFF, 0, 0xFFFFFFFF, 0, 0, 0}; @@ -2377,3 +2375,213 @@ TEST_F(URITemplateRouterViewTest, size_with_base_path) { const sourcemeta::core::URITemplateRouterView restored{this->path}; EXPECT_EQ(restored.size(), 2); } + +TEST_F(URITemplateRouterViewTest, otherwise_returned_from_unknown_path) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1, 5); + router.otherwise(99); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_ROUTER_MATCH(restored, "/unknown", 0, 99, captures); + EXPECT_EQ(captures.size(), 0); +} + +TEST_F(URITemplateRouterViewTest, otherwise_not_returned_from_matching_route) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1, 5); + router.otherwise(99); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_ROUTER_MATCH(restored, "/users", 1, 5, captures); + EXPECT_EQ(captures.size(), 0); +} + +TEST_F(URITemplateRouterViewTest, otherwise_returned_for_empty_segment) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1); + router.otherwise(77); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_ROUTER_MATCH(restored, "/users//", 0, 77, captures); +} + +TEST_F(URITemplateRouterViewTest, otherwise_returned_for_root_slash_no_match) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1); + router.otherwise(88); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_ROUTER_MATCH(restored, "/", 0, 88, captures); +} + +TEST_F(URITemplateRouterViewTest, + otherwise_without_registration_returns_zero_context) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_ROUTER_MATCH(restored, "/unknown", 0, 0, captures); +} + +TEST_F(URITemplateRouterViewTest, otherwise_arguments_lookup) { + { + sourcemeta::core::URITemplateRouter router; + const std::string message_value{"Not Found"}; + const std::array + arguments{{{"status", std::int64_t{404}}, + {"message", std::string_view{message_value}}}}; + router.otherwise(55, arguments); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + + std::vector> + collected; + restored.arguments( + 0, [&collected]( + const std::string_view name, + const sourcemeta::core::URITemplateRouter::ArgumentValue &value) { + collected.emplace_back(name, value); + }); + + EXPECT_EQ(collected.size(), 2); + EXPECT_EQ(collected[0].first, "status"); + EXPECT_TRUE(std::holds_alternative(collected[0].second)); + EXPECT_EQ(std::get(collected[0].second), 404); + EXPECT_EQ(collected[1].first, "message"); + EXPECT_TRUE(std::holds_alternative(collected[1].second)); + EXPECT_EQ(std::get(collected[1].second), "Not Found"); +} + +TEST_F(URITemplateRouterViewTest, otherwise_boolean_argument) { + { + sourcemeta::core::URITemplateRouter router; + const std::array + arguments{{{"cached", true}}}; + router.otherwise(3, arguments); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + + std::vector> + collected; + restored.arguments( + 0, [&collected]( + const std::string_view name, + const sourcemeta::core::URITemplateRouter::ArgumentValue &value) { + collected.emplace_back(name, value); + }); + + EXPECT_EQ(collected.size(), 1); + EXPECT_TRUE(std::holds_alternative(collected[0].second)); + EXPECT_EQ(std::get(collected[0].second), true); +} + +TEST_F(URITemplateRouterViewTest, otherwise_does_not_affect_other_arguments) { + { + sourcemeta::core::URITemplateRouter router; + const std::string schema_value{"user.json"}; + const std::array + route_args{{{"schema", std::string_view{schema_value}}}}; + router.add("/users", 1, 0, route_args); + + const std::string message_value{"not found"}; + const std::array + default_args{{{"message", std::string_view{message_value}}}}; + router.otherwise(99, default_args); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + + std::vector> + route_collected; + restored.arguments( + 1, [&route_collected]( + const std::string_view name, + const sourcemeta::core::URITemplateRouter::ArgumentValue &value) { + route_collected.emplace_back(name, value); + }); + EXPECT_EQ(route_collected.size(), 1); + EXPECT_EQ(route_collected[0].first, "schema"); + + std::vector> + default_collected; + restored.arguments( + 0, [&default_collected]( + const std::string_view name, + const sourcemeta::core::URITemplateRouter::ArgumentValue &value) { + default_collected.emplace_back(name, value); + }); + EXPECT_EQ(default_collected.size(), 1); + EXPECT_EQ(default_collected[0].first, "message"); +} + +TEST_F(URITemplateRouterViewTest, otherwise_does_not_count_as_route_in_size) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", 1); + router.otherwise(99); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.size(), 1); +} + +TEST_F(URITemplateRouterViewTest, otherwise_with_base_path_and_unmatched) { + { + sourcemeta::core::URITemplateRouter router{"/v1"}; + router.add("/users", 1); + router.otherwise(42); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_ROUTER_MATCH(restored, "/v1/other", 0, 42, captures); +} + +TEST_F(URITemplateRouterViewTest, otherwise_with_partial_trie_walk) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users/{id}/posts", 1); + router.otherwise(42); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_ROUTER_MATCH(restored, "/users/123", 0, 42, captures); +} + +TEST_F(URITemplateRouterViewTest, otherwise_overwrite_context) { + { + sourcemeta::core::URITemplateRouter router; + router.otherwise(10); + router.otherwise(20); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_ROUTER_MATCH(restored, "/nope", 0, 20, captures); +}