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
30 changes: 21 additions & 9 deletions benchmark/uritemplate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

#include <array> // std::array
#include <cassert> // assert
#include <cstddef> // std::size_t
#include <cstdint> // std::uint16_t, std::int64_t
#include <filesystem> // std::filesystem
#include <string> // std::string, std::to_string
#include <string_view> // std::string_view

static constexpr std::string_view ROUTES[] = {
Expand Down Expand Up @@ -113,11 +115,21 @@ static constexpr std::string_view ROUTES[] = {

static constexpr std::size_t ROUTE_COUNT = sizeof(ROUTES) / sizeof(ROUTES[0]);

static auto make_operation_ids() -> std::array<std::string, ROUTE_COUNT> {
std::array<std::string, ROUTE_COUNT> ids;
for (std::size_t index = 0; index < ROUTE_COUNT; ++index) {
ids[index] = "route_" + std::to_string(index);
}
return ids;
}

static const auto OPERATION_IDS = make_operation_ids();

static void URITemplateRouter_Create(benchmark::State &state) {
for (auto _ : state) {
sourcemeta::core::URITemplateRouter router;
for (std::size_t index = 0; index < ROUTE_COUNT; ++index) {
router.add(ROUTES[index],
router.add(ROUTES[index], OPERATION_IDS[index],
static_cast<sourcemeta::core::URITemplateRouter::Identifier>(
index + 1));
}
Expand All @@ -129,7 +141,7 @@ static void URITemplateRouter_Create(benchmark::State &state) {
static void URITemplateRouter_Match(benchmark::State &state) {
sourcemeta::core::URITemplateRouter router;
for (std::size_t index = 0; index < ROUTE_COUNT; ++index) {
router.add(ROUTES[index],
router.add(ROUTES[index], OPERATION_IDS[index],
static_cast<sourcemeta::core::URITemplateRouter::Identifier>(
index + 1));
}
Expand All @@ -151,7 +163,7 @@ static void URITemplateRouterView_Restore(benchmark::State &state) {
{
sourcemeta::core::URITemplateRouter router;
for (std::size_t index = 0; index < ROUTE_COUNT; ++index) {
router.add(ROUTES[index],
router.add(ROUTES[index], OPERATION_IDS[index],
static_cast<sourcemeta::core::URITemplateRouter::Identifier>(
index + 1));
}
Expand All @@ -174,7 +186,7 @@ static void URITemplateRouterView_Match(benchmark::State &state) {
{
sourcemeta::core::URITemplateRouter router;
for (std::size_t index = 0; index < ROUTE_COUNT; ++index) {
router.add(ROUTES[index],
router.add(ROUTES[index], OPERATION_IDS[index],
static_cast<sourcemeta::core::URITemplateRouter::Identifier>(
index + 1));
}
Expand Down Expand Up @@ -322,9 +334,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, 0, small_args);
router.add("/api/v1/users/{id}", 2, 0, small_args);
router.add("/api/v1/many", 3, 0, many_arguments);
router.add("/api/v1/health", "op_5", 1, 0, small_args);
router.add("/api/v1/users/{id}", "op_6", 2, 0, small_args);
router.add("/api/v1/many", "op_7", 3, 0, many_arguments);
sourcemeta::core::URITemplateRouterView::save(router, path);
}

Expand All @@ -350,7 +362,7 @@ static void URITemplateRouterView_Arguments(benchmark::State &state) {
static void URITemplateRouter_Match_BasePath(benchmark::State &state) {
sourcemeta::core::URITemplateRouter router{"/v1/catalog"};
for (std::size_t index = 0; index < ROUTE_COUNT; ++index) {
router.add(ROUTES[index],
router.add(ROUTES[index], OPERATION_IDS[index],
static_cast<sourcemeta::core::URITemplateRouter::Identifier>(
index + 1));
}
Expand All @@ -372,7 +384,7 @@ static void URITemplateRouterView_Match_BasePath(benchmark::State &state) {
{
sourcemeta::core::URITemplateRouter router{"/v1/catalog"};
for (std::size_t index = 0; index < ROUTE_COUNT; ++index) {
router.add(ROUTES[index],
router.add(ROUTES[index], OPERATION_IDS[index],
static_cast<sourcemeta::core::URITemplateRouter::Identifier>(
index + 1));
}
Expand Down
3 changes: 3 additions & 0 deletions src/core/uritemplate/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME uritemplate
if(SOURCEMETA_CORE_INSTALL)
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME uritemplate)
endif()

target_link_libraries(sourcemeta_core_uritemplate PUBLIC sourcemeta::core::io)
target_link_libraries(sourcemeta_core_uritemplate PRIVATE sourcemeta::core::regex)
45 changes: 45 additions & 0 deletions src/core/uritemplate/include/sourcemeta/core/uritemplate_error.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,51 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouterInvalidSegmentError
std::string segment_;
};

/// @ingroup uritemplate
/// An error that represents an operation identifier that does not match the
/// permitted format
class SOURCEMETA_CORE_URITEMPLATE_EXPORT
URITemplateRouterInvalidOperationIdError : public std::exception {
public:
URITemplateRouterInvalidOperationIdError(const std::string_view operation_id)
: operation_id_{operation_id} {}

[[nodiscard]] auto what() const noexcept -> const char * override {
return "Invalid operation identifier";
}

/// Get the offending operation identifier
[[nodiscard]] auto operation_id() const noexcept -> const std::string & {
return this->operation_id_;
}

private:
std::string operation_id_;
};

/// @ingroup uritemplate
/// An error that represents an operation identifier that conflicts with a
/// previously registered route
class SOURCEMETA_CORE_URITEMPLATE_EXPORT
URITemplateRouterDuplicateOperationIdError : public std::exception {
public:
URITemplateRouterDuplicateOperationIdError(
const std::string_view operation_id)
: operation_id_{operation_id} {}

[[nodiscard]] auto what() const noexcept -> const char * override {
return "Duplicate operation identifier";
}

/// Get the conflicting operation identifier
[[nodiscard]] auto operation_id() const noexcept -> const std::string & {
return this->operation_id_;
}

private:
std::string operation_id_;
};

/// @ingroup uritemplate
/// An error that represents a failure to save the router to disk
class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouterSaveError
Expand Down
67 changes: 40 additions & 27 deletions src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@
#include <sourcemeta/core/uritemplate_export.h>
#endif

#include <sourcemeta/core/io.h>

#include <cstddef> // std::size_t
#include <cstdint> // std::uint16_t, std::uint32_t, std::uint8_t, std::int64_t
#include <filesystem> // std::filesystem::path
#include <functional> // std::function
#include <memory> // std::unique_ptr
#include <span> // std::span
#include <string> // std::string
#include <string_view> // std::string_view
#include <tuple> // std::tuple
#include <utility> // std::pair
#include <variant> // std::variant
#include <vector> // std::vector
#include <filesystem> // std::filesystem::path
#include <functional> // std::function
#include <memory> // std::unique_ptr
#include <span> // std::span
#include <string> // std::string
#include <string_view> // std::string_view
#include <tuple> // std::tuple
#include <unordered_map> // std::unordered_map
#include <utility> // std::pair
#include <variant> // std::variant
#include <vector> // std::vector

namespace sourcemeta::core {

Expand Down Expand Up @@ -84,8 +87,11 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter {
auto operator=(URITemplateRouter &&) -> URITemplateRouter & = delete;

/// 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,
/// router. The operation identifier must match the regular expression
/// `^[a-zA-Z][a-zA-Z0-9_-]{0,63}$` and must be unique across all routes
/// registered on this router
auto add(const std::string_view uri_template,
const std::string_view operation_id, const Identifier identifier,
const Identifier context = 0,
const std::span<const Argument> arguments = {}) -> void;

Expand Down Expand Up @@ -128,12 +134,20 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter {
/// for the given identifier
[[nodiscard]] auto path(const Identifier identifier) const -> std::string;

/// Resolve an operation identifier to its registered route. Returns
/// `(identifier, context)` on hit and `(0, 0)` if no route is registered
/// under the given operation identifier
[[nodiscard]] auto operation(const std::string_view operation_id) const
-> std::pair<Identifier, Identifier>;

private:
Node root_;
Node otherwise_;
std::string base_path_;
std::vector<std::pair<Identifier, std::vector<Argument>>> arguments_;
std::vector<std::tuple<Identifier, Identifier, std::string_view>> entries_;
std::unordered_map<std::string_view, std::pair<Identifier, Identifier>>
operations_;
};

/// @ingroup uritemplate
Expand All @@ -145,8 +159,13 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouterView {
const std::filesystem::path &path) -> void;

URITemplateRouterView(const std::filesystem::path &path);

/// Construct a view over an externally-owned buffer. The buffer must
/// outlive the view
URITemplateRouterView(const std::uint8_t *data, std::size_t size);

~URITemplateRouterView();

// To avoid mistakes
URITemplateRouterView(const URITemplateRouterView &) = delete;
URITemplateRouterView(URITemplateRouterView &&) = delete;
Expand All @@ -172,23 +191,17 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouterView {
/// Get the number of registered routes
[[nodiscard]] auto size() const noexcept -> std::size_t;

/// Get the identifier of the route at the given positional index
[[nodiscard]] auto at(const std::size_t index) const
-> URITemplateRouter::Identifier;

/// Get the context identifier associated with a registered route
/// identifier
[[nodiscard]] auto
context(const URITemplateRouter::Identifier identifier) const
-> URITemplateRouter::Identifier;

/// Reconstruct and return the URI Template path string originally registered
/// for the given identifier
[[nodiscard]] auto path(const URITemplateRouter::Identifier identifier) const
-> std::string;
/// Resolve an operation identifier to its registered route. Returns
/// `(identifier, context)` on hit and `(0, 0)` if no route is registered
/// under the given operation identifier
[[nodiscard]] auto operation(const std::string_view operation_id) const
-> std::pair<URITemplateRouter::Identifier,
URITemplateRouter::Identifier>;

private:
std::vector<std::uint8_t> data_;
const std::uint8_t *data_{nullptr};
std::size_t size_{0};
std::unique_ptr<FileView> owner_;
};

#if defined(_MSC_VER)
Expand Down
36 changes: 35 additions & 1 deletion src/core/uritemplate/uritemplate_router.cc
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#include <sourcemeta/core/regex.h>
#include <sourcemeta/core/uritemplate.h>

#include "helpers.h"

#include <algorithm> // std::ranges::lower_bound
#include <algorithm> // std::ranges::lower_bound, std::ranges::find_if
#include <cassert> // assert
#include <limits> // std::numeric_limits
#include <tuple> // std::get, std::make_tuple
Expand Down Expand Up @@ -151,6 +152,15 @@ auto URITemplateRouter::path(const Identifier identifier) const -> std::string {
return std::string{std::get<2>(*entry)};
}

auto URITemplateRouter::operation(const std::string_view operation_id) const
-> std::pair<Identifier, Identifier> {
const auto iterator = this->operations_.find(operation_id);
if (iterator == this->operations_.end()) {
return {Identifier{0}, Identifier{0}};
}
return iterator->second;
}

auto URITemplateRouter::otherwise(const Identifier context,
const std::span<const Argument> arguments)
-> void {
Expand All @@ -174,11 +184,23 @@ auto URITemplateRouter::otherwise(const Identifier context,
}

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

static const Regex operation_id_regex =
to_regex("^[a-zA-Z][a-zA-Z0-9_-]{0,63}$").value();

if (!matches(operation_id_regex, operation_id)) {
throw URITemplateRouterInvalidOperationIdError{operation_id};
}

if (this->operations_.contains(operation_id)) {
throw URITemplateRouterDuplicateOperationIdError{operation_id};
}

// Walk base path segments to establish the trie prefix
Node *current = nullptr;
if (!this->base_path_.empty()) {
Expand Down Expand Up @@ -216,9 +238,15 @@ auto URITemplateRouter::add(const std::string_view uri_template,
if (existing != this->entries_.end()) {
*existing = std::make_tuple(identifier, context, uri_template);
}
std::erase_if(this->operations_,
[&previous_identifier](const auto &entry) {
return entry.second.first == previous_identifier;
});
}
target.identifier = identifier;
target.context = context;
this->operations_.emplace(
operation_id, std::pair<Identifier, Identifier>{identifier, context});
if (!arguments.empty()) {
assert(std::ranges::none_of(this->arguments_,
[&identifier](const auto &entry) {
Expand Down Expand Up @@ -397,9 +425,15 @@ auto URITemplateRouter::add(const std::string_view uri_template,
if (existing != this->entries_.end()) {
*existing = std::make_tuple(identifier, context, uri_template);
}
std::erase_if(this->operations_,
[&previous_identifier](const auto &entry) {
return entry.second.first == previous_identifier;
});
}
current->identifier = identifier;
current->context = context;
this->operations_.emplace(
operation_id, std::pair<Identifier, Identifier>{identifier, context});
if (!arguments.empty()) {
assert(std::ranges::none_of(this->arguments_,
[&identifier](const auto &entry) {
Expand Down
Loading
Loading