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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -86,6 +88,11 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter {
const Identifier context = 0,
const std::span<const Argument> 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<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,
Expand All @@ -111,6 +118,7 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter {

private:
Node root_;
Node otherwise_;
std::string base_path_;
std::vector<std::pair<Identifier, std::vector<Argument>>> arguments_;
std::size_t size_{0};
Expand Down
54 changes: 46 additions & 8 deletions src/core/uritemplate/uritemplate_router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ inline auto extract_segment(const char *start, const char *end)
return {start, static_cast<std::size_t>(position - start)};
}

inline auto finalize_match(const Node &otherwise,
const URITemplateRouter::Identifier identifier,
const URITemplateRouter::Identifier context)
-> std::pair<URITemplateRouter::Identifier, URITemplateRouter::Identifier> {
if (identifier == 0) {
return {URITemplateRouter::Identifier{0}, otherwise.context};
}

return {identifier, context};
}

} // namespace

URITemplateRouter::URITemplateRouter(const std::string_view base_path)
Expand All @@ -113,6 +124,28 @@ auto URITemplateRouter::size() const noexcept -> std::size_t {
return this->size_;
}

auto URITemplateRouter::otherwise(const Identifier context,
const std::span<const Argument> 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<Argument>{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,
Expand Down Expand Up @@ -361,14 +394,16 @@ auto URITemplateRouter::match(const std::string_view path,
const Callback &callback) const
-> std::pair<Identifier, Identifier> {
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;
Expand Down Expand Up @@ -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)) {
Expand All @@ -409,14 +444,15 @@ 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, (*variable_child)->context};
return finalize_match(this->otherwise_, (*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 {};
return finalize_match(this->otherwise_, 0, 0);
}

literal_children = &current->literals;
Expand All @@ -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
54 changes: 38 additions & 16 deletions src/core/uritemplate/uritemplate_router_view.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
#include <queue> // std::queue
#include <string> // std::string
#include <unordered_map> // std::unordered_map
#include <utility> // std::pair
#include <vector> // std::vector

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<std::uint32_t>::max();

// Type tags for argument value serialization
Expand All @@ -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 {
Expand All @@ -52,6 +54,18 @@ struct alignas(8) SerializedNode {
std::array<std::uint8_t, 6> padding2;
};

inline auto
finalize_match(const URITemplateRouter::Identifier otherwise_context,
const URITemplateRouter::Identifier identifier,
const URITemplateRouter::Identifier context)
-> std::pair<URITemplateRouter::Identifier, URITemplateRouter::Identifier> {
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,
Expand Down Expand Up @@ -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<std::uint32_t>(base_path_value.size());
header.otherwise_context = router.otherwise_.context;

std::ofstream file(path, std::ios::binary);
if (!file) {
Expand Down Expand Up @@ -333,10 +348,13 @@ auto URITemplateRouterView::match(
return {};
}

const auto otherwise_context =
static_cast<URITemplateRouter::Identifier>(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<const SerializedNode *>(
Expand All @@ -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<const char *>(
Expand All @@ -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
Expand All @@ -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];
Expand All @@ -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(
Expand All @@ -441,15 +461,15 @@ auto URITemplateRouterView::match(
if (node.variable_child >= node_count ||
variable_index >
std::numeric_limits<URITemplateRouter::Index>::max()) {
return {};
return finalize_match(otherwise_context, 0, 0);
}

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 {};
return finalize_match(otherwise_context, 0, 0);
}

// Check if this is an expansion (catch-all)
Expand 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
Expand All @@ -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(
Expand Down
Loading
Loading