Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions benchmark/uritemplate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ static void URITemplateRouter_Match(benchmark::State &state) {
"/api/v1/organizations/12345/teams/67890/projects/abc/issues/999/"
"comments/42/reactions/1",
[](auto, auto, auto) {});
assert(result == ROUTE_COUNT - 1);
assert(result.first == ROUTE_COUNT - 1);
benchmark::DoNotOptimize(result);
}
}
Expand Down Expand Up @@ -191,7 +191,7 @@ static void URITemplateRouterView_Match(benchmark::State &state) {
"/api/v1/organizations/12345/teams/67890/projects/abc/issues/999/"
"comments/42/reactions/1",
[](auto, auto, auto) {});
assert(result == ROUTE_COUNT - 1);
assert(result.first == ROUTE_COUNT - 1);
benchmark::DoNotOptimize(result);
}
}
Expand Down Expand Up @@ -322,9 +322,9 @@ static void URITemplateRouterView_Arguments(benchmark::State &state) {
sourcemeta::core::URITemplateRouter router;
const std::array<sourcemeta::core::URITemplateRouter::Argument, 1>
small_args{{{"schema", std::string_view{"schemas/health"}}}};
router.add("/api/v1/health", 1, small_args);
router.add("/api/v1/users/{id}", 2, small_args);
router.add("/api/v1/many", 3, many_arguments);
router.add("/api/v1/health", 1, 0, small_args);
router.add("/api/v1/users/{id}", 2, 0, small_args);
router.add("/api/v1/many", 3, 0, many_arguments);
sourcemeta::core::URITemplateRouterView::save(router, path);
}

Expand All @@ -333,7 +333,7 @@ static void URITemplateRouterView_Arguments(benchmark::State &state) {

for (auto _ : state) {
auto result = view.match("/api/v1/many", [](auto, auto, auto) {});
assert(result == 3);
assert(result.first == 3);
benchmark::DoNotOptimize(result);

std::size_t argument_count = 0;
Expand All @@ -360,7 +360,7 @@ static void URITemplateRouter_Match_BasePath(benchmark::State &state) {
"/v1/catalog/api/v1/organizations/12345/teams/67890/projects/abc/"
"issues/999/comments/42/reactions/1",
[](auto, auto, auto) {});
assert(result == ROUTE_COUNT - 1);
assert(result.first == ROUTE_COUNT - 1);
benchmark::DoNotOptimize(result);
}
}
Expand All @@ -387,7 +387,7 @@ static void URITemplateRouterView_Match_BasePath(benchmark::State &state) {
"/v1/catalog/api/v1/organizations/12345/teams/67890/projects/abc/"
"issues/999/comments/42/reactions/1",
[](auto, auto, auto) {});
assert(result == ROUTE_COUNT - 1);
assert(result.first == ROUTE_COUNT - 1);
benchmark::DoNotOptimize(result);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter {
/// A node in the router trie
struct Node {
Identifier identifier{0};
Identifier context{0};
NodeType type{NodeType::Root};
std::string_view value;

Expand All @@ -82,12 +83,14 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter {
/// Add a route to the router. Make sure the string lifetime survives the
/// router
auto add(const std::string_view uri_template, const Identifier identifier,
const Identifier context = 0,
const std::span<const Argument> 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,
const Callback &callback) const -> Identifier;
const Callback &callback) const
-> std::pair<Identifier, Identifier>;

/// Access the root node of the trie
[[nodiscard]] auto root() const noexcept -> const Node &;
Expand Down Expand Up @@ -131,7 +134,8 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouterView {
/// initial matches even though the entire match might still fail
[[nodiscard]] auto match(const std::string_view path,
const URITemplateRouter::Callback &callback) const
-> URITemplateRouter::Identifier;
-> std::pair<URITemplateRouter::Identifier,
URITemplateRouter::Identifier>;

/// Access the stored arguments for a given route identifier
auto arguments(const URITemplateRouter::Identifier identifier,
Expand Down
21 changes: 13 additions & 8 deletions src/core/uritemplate/uritemplate_router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ auto URITemplateRouter::base_path() const noexcept -> std::string_view {

auto URITemplateRouter::add(const std::string_view uri_template,
const Identifier identifier,
const Identifier context,
const std::span<const Argument> arguments) -> void {
assert(identifier > 0);

Expand Down Expand Up @@ -141,6 +142,7 @@ auto URITemplateRouter::add(const std::string_view uri_template,
if (uri_template.empty()) {
auto &target = current ? *current : this->root_;
target.identifier = identifier;
target.context = context;
if (!arguments.empty()) {
assert(std::ranges::none_of(this->arguments_,
[&identifier](const auto &entry) {
Expand Down Expand Up @@ -309,6 +311,7 @@ auto URITemplateRouter::add(const std::string_view uri_template,

if (!absorbed && current != nullptr) {
current->identifier = identifier;
current->context = context;
if (!arguments.empty()) {
assert(std::ranges::none_of(this->arguments_,
[&identifier](const auto &entry) {
Expand Down Expand Up @@ -345,16 +348,17 @@ auto URITemplateRouter::arguments() const noexcept
}

auto URITemplateRouter::match(const std::string_view path,
const Callback &callback) const -> Identifier {
const Callback &callback) const
-> std::pair<Identifier, Identifier> {
if (path.empty()) {
return this->root_.identifier;
return {this->root_.identifier, this->root_.context};
}

if (path.size() == 1 && path[0] == '/') {
if (auto *child = find_literal_child(this->root_.literals, "")) {
return child->identifier;
return {child->identifier, child->context};
}
return 0;
return {};
}

const Node *current = nullptr;
Expand Down Expand Up @@ -382,7 +386,7 @@ auto URITemplateRouter::match(const std::string_view path,

// Empty segment (from double slash or trailing slash) doesn't match
if (segment.empty()) {
return 0;
return {};
}

if (auto *literal_match = find_literal_child(*literal_children, segment)) {
Expand All @@ -395,14 +399,14 @@ auto URITemplateRouter::match(const std::string_view path,
segment_start, static_cast<std::size_t>(path_end - segment_start)};
callback(static_cast<URITemplateRouter::Index>(variable_index),
(*variable_child)->value, remaining);
return (*variable_child)->identifier;
return {(*variable_child)->identifier, (*variable_child)->context};
}
callback(static_cast<URITemplateRouter::Index>(variable_index),
(*variable_child)->value, segment);
++variable_index;
current = variable_child->get();
} else {
return 0;
return {};
}

literal_children = &current->literals;
Expand All @@ -417,7 +421,8 @@ auto URITemplateRouter::match(const std::string_view path,
++position;
}

return current ? current->identifier : this->root_.identifier;
return current ? std::pair{current->identifier, current->context}
: std::pair{this->root_.identifier, this->root_.context};
}

} // namespace sourcemeta::core
49 changes: 29 additions & 20 deletions src/core/uritemplate/uritemplate_router_view.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <sourcemeta/core/uritemplate.h>

#include <array> // std::array
#include <cassert> // assert
#include <cstring> // std::memcmp, std::memcpy
#include <fstream> // std::ofstream, std::ifstream
Expand All @@ -14,7 +15,7 @@ namespace sourcemeta::core {
namespace {

constexpr std::uint32_t ROUTER_MAGIC = 0x52544552; // "RTER"
constexpr std::uint32_t ROUTER_VERSION = 3;
constexpr std::uint32_t ROUTER_VERSION = 4;
constexpr std::uint32_t NO_CHILD = std::numeric_limits<std::uint32_t>::max();

// Type tags for argument value serialization
Expand Down Expand Up @@ -47,6 +48,8 @@ struct alignas(8) SerializedNode {
URITemplateRouter::NodeType type;
std::uint8_t padding;
URITemplateRouter::Identifier identifier;
URITemplateRouter::Identifier context;
std::array<std::uint8_t, 6> padding2;
};

// Binary search for a literal child matching the given segment
Expand Down Expand Up @@ -109,6 +112,7 @@ auto URITemplateRouterView::save(const URITemplateRouter &router,
root_serialized.type = URITemplateRouter::NodeType::Root;
root_serialized.padding = 0;
root_serialized.identifier = root.identifier;
root_serialized.context = root.context;

if (root.literals.empty()) {
root_serialized.first_literal_child = NO_CHILD;
Expand Down Expand Up @@ -146,6 +150,7 @@ auto URITemplateRouterView::save(const URITemplateRouter &router,

serialized.padding = 0;
serialized.identifier = node->identifier;
serialized.context = node->context;

const auto first_child_index =
static_cast<std::uint32_t>(nodes.size() + queue.size() + 1);
Expand Down Expand Up @@ -314,23 +319,24 @@ URITemplateRouterView::URITemplateRouterView(const std::uint8_t *data,
const std::size_t size)
: data_{data, data + size} {}

auto URITemplateRouterView::match(const std::string_view path,
const URITemplateRouter::Callback &callback)
const -> URITemplateRouter::Identifier {
auto URITemplateRouterView::match(
const std::string_view path,
const URITemplateRouter::Callback &callback) const
-> std::pair<URITemplateRouter::Identifier, URITemplateRouter::Identifier> {
if (this->data_.size() < sizeof(RouterHeader)) {
return 0;
return {};
}

const auto *header =
reinterpret_cast<const RouterHeader *>(this->data_.data());
if (header->magic != ROUTER_MAGIC || header->version != ROUTER_VERSION) {
return 0;
return {};
}

if (header->node_count == 0 ||
header->node_count > (this->data_.size() - sizeof(RouterHeader)) /
sizeof(SerializedNode)) {
return 0;
return {};
}

const auto *nodes = reinterpret_cast<const SerializedNode *>(
Expand All @@ -340,12 +346,12 @@ auto URITemplateRouterView::match(const std::string_view path,
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 0;
return {};
}

if (header->arguments_offset < header->string_table_offset ||
header->arguments_offset > this->data_.size()) {
return 0;
return {};
}

const auto *string_table = reinterpret_cast<const char *>(
Expand All @@ -355,26 +361,29 @@ auto URITemplateRouterView::match(const std::string_view path,

// Empty path matches empty template
if (path.empty()) {
return nodes[0].identifier;
return {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 0;
return {};
}

if (root.first_literal_child >= header->node_count ||
root.literal_child_count >
header->node_count - root.first_literal_child) {
return 0;
return {};
}

const auto match = binary_search_literal_children(
nodes, string_table, string_table_size, root.first_literal_child,
root.literal_child_count, "", 0);
return match != NO_CHILD ? nodes[match].identifier : 0;
if (match == NO_CHILD) {
return {};
}
return {nodes[match].identifier, nodes[match].context};
}

// Walk the trie, matching each path segment
Expand All @@ -401,7 +410,7 @@ auto URITemplateRouterView::match(const std::string_view path,

// Empty segment (from double slash or trailing slash) doesn't match
if (segment_length == 0) {
return 0;
return {};
}

const auto &node = nodes[current_node];
Expand All @@ -411,7 +420,7 @@ auto URITemplateRouterView::match(const std::string_view path,
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 0;
return {};
}

const auto literal_match = binary_search_literal_children(
Expand All @@ -432,15 +441,15 @@ auto URITemplateRouterView::match(const std::string_view path,
if (node.variable_child >= node_count ||
variable_index >
std::numeric_limits<URITemplateRouter::Index>::max()) {
return 0;
return {};
}

const auto &variable_node = nodes[node.variable_child];

if (variable_node.string_offset > string_table_size ||
variable_node.string_length >
string_table_size - variable_node.string_offset) {
return 0;
return {};
}

// Check if this is an expansion (catch-all)
Expand All @@ -451,7 +460,7 @@ auto URITemplateRouterView::match(const std::string_view path,
{string_table + variable_node.string_offset,
variable_node.string_length},
{segment_start, remaining_length});
return variable_node.identifier;
return {variable_node.identifier, variable_node.context};
}

// Regular variable - match single segment
Expand All @@ -469,10 +478,10 @@ auto URITemplateRouterView::match(const std::string_view path,
}

// No match
return 0;
return {};
}

return nodes[current_node].identifier;
return {nodes[current_node].identifier, nodes[current_node].context};
}

auto URITemplateRouterView::arguments(
Expand Down
20 changes: 12 additions & 8 deletions test/uritemplate/uritemplate_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,20 @@
.explode, \
expected_explode)

#define EXPECT_ROUTER_MATCH(router, path, expected_handler, captures_name) \
#define EXPECT_ROUTER_MATCH(router, path, expected_handler, expected_context, \
captures_name) \
std::vector<std::tuple<std::uint8_t, std::string, std::string>> \
captures_name; \
EXPECT_EQ((router).match((path), \
[&captures_name](const std::uint8_t index, \
const std::string_view name, \
const std::string_view value) { \
captures_name.emplace_back(index, name, value); \
}), \
expected_handler)
{ \
const auto sourcemeta_router_match_result = (router).match( \
(path), [&captures_name](const std::uint8_t index, \
const std::string_view name, \
const std::string_view value) { \
captures_name.emplace_back(index, name, value); \
}); \
EXPECT_EQ(sourcemeta_router_match_result.first, (expected_handler)); \
EXPECT_EQ(sourcemeta_router_match_result.second, (expected_context)); \
}

#define EXPECT_ROUTER_CAPTURE(captures, index, expected_name, expected_value) \
EXPECT_EQ(std::get<0>((captures).at(index)), (index)); \
Expand Down
Loading
Loading