Skip to content

Commit 758fcdf

Browse files
committed
[WIP] Prototype a URI Template router
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent f1d58ad commit 758fcdf

18 files changed

+2399
-819
lines changed

benchmark/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ if(SOURCEMETA_CORE_EXTENSION_ALTERSCHEMA)
2020
list(APPEND BENCHMARK_SOURCES alterschema.cc)
2121
endif()
2222

23+
if(SOURCEMETA_CORE_URITEMPLATE)
24+
list(APPEND BENCHMARK_SOURCES uritemplate.cc)
25+
endif()
26+
2327
if(BENCHMARK_SOURCES)
2428
sourcemeta_googlebenchmark(NAMESPACE sourcemeta PROJECT core
2529
SOURCES ${BENCHMARK_SOURCES})
@@ -51,6 +55,11 @@ if(BENCHMARK_SOURCES)
5155
PRIVATE sourcemeta::core::alterschema)
5256
endif()
5357

58+
if(SOURCEMETA_CORE_URITEMPLATE)
59+
target_link_libraries(sourcemeta_core_benchmark
60+
PRIVATE sourcemeta::core::uritemplate)
61+
endif()
62+
5463
add_custom_target(benchmark_all
5564
COMMAND sourcemeta_core_benchmark
5665
DEPENDS sourcemeta_core_benchmark

benchmark/uritemplate.cc

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#include <benchmark/benchmark.h>
2+
3+
#include <sourcemeta/core/uritemplate.h>
4+
5+
#include <cassert> // assert
6+
#include <cstdint> // std::uint16_t
7+
#include <filesystem> // std::filesystem
8+
#include <string_view> // std::string_view
9+
10+
static constexpr std::string_view ROUTES[] = {
11+
"/api/v1",
12+
"/api/v2",
13+
"/api/v3",
14+
"/api/v1/health",
15+
"/api/v2/health",
16+
"/api/v3/health",
17+
"/api/v1/users",
18+
"/api/v1/users/{user_id}",
19+
"/api/v1/users/{user_id}/profile",
20+
"/api/v1/users/{user_id}/settings",
21+
"/api/v1/users/{user_id}/preferences",
22+
"/api/v1/users/{user_id}/avatar",
23+
"/api/v1/users/{user_id}/posts",
24+
"/api/v1/users/{user_id}/posts/{post_id}",
25+
"/api/v1/users/{user_id}/posts/{post_id}/comments",
26+
"/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}",
27+
"/api/v1/users/{user_id}/posts/{post_id}/likes",
28+
"/api/v1/users/{user_id}/followers",
29+
"/api/v1/users/{user_id}/following",
30+
"/api/v1/users/{user_id}/notifications",
31+
"/api/v1/users/{user_id}/notifications/{notification_id}",
32+
"/api/v1/organizations",
33+
"/api/v1/organizations/{org_id}",
34+
"/api/v1/organizations/{org_id}/members",
35+
"/api/v1/organizations/{org_id}/members/{member_id}",
36+
"/api/v1/organizations/{org_id}/teams",
37+
"/api/v1/organizations/{org_id}/teams/{team_id}",
38+
"/api/v1/organizations/{org_id}/teams/{team_id}/members",
39+
"/api/v1/organizations/{org_id}/projects",
40+
"/api/v1/organizations/{org_id}/projects/{project_id}",
41+
"/api/v1/organizations/{org_id}/billing",
42+
"/api/v1/organizations/{org_id}/invoices",
43+
"/api/v1/organizations/{org_id}/invoices/{invoice_id}",
44+
"/api/v1/projects",
45+
"/api/v1/projects/{project_id}",
46+
"/api/v1/projects/{project_id}/issues",
47+
"/api/v1/projects/{project_id}/issues/{issue_id}",
48+
"/api/v1/projects/{project_id}/issues/{issue_id}/comments",
49+
"/api/v1/projects/{project_id}/issues/{issue_id}/comments/{comment_id}",
50+
"/api/v1/projects/{project_id}/issues/{issue_id}/labels",
51+
"/api/v1/projects/{project_id}/issues/{issue_id}/assignees",
52+
"/api/v1/projects/{project_id}/milestones",
53+
"/api/v1/projects/{project_id}/milestones/{milestone_id}",
54+
"/api/v1/projects/{project_id}/releases",
55+
"/api/v1/projects/{project_id}/releases/{release_id}",
56+
"/api/v1/projects/{project_id}/releases/{release_id}/assets",
57+
"/api/v1/projects/{project_id}/branches",
58+
"/api/v1/projects/{project_id}/branches/{branch_name}",
59+
"/api/v1/projects/{project_id}/commits",
60+
"/api/v1/projects/{project_id}/commits/{commit_sha}",
61+
"/api/v1/projects/{project_id}/pulls",
62+
"/api/v1/projects/{project_id}/pulls/{pull_id}",
63+
"/api/v1/projects/{project_id}/pulls/{pull_id}/reviews",
64+
"/api/v1/projects/{project_id}/pulls/{pull_id}/reviews/{review_id}",
65+
"/api/v1/projects/{project_id}/pulls/{pull_id}/commits",
66+
"/api/v1/projects/{project_id}/pulls/{pull_id}/files",
67+
"/api/v1/documents",
68+
"/api/v1/documents/{document_id}",
69+
"/api/v1/documents/{document_id}/versions",
70+
"/api/v1/documents/{document_id}/versions/{version_id}",
71+
"/api/v1/documents/{document_id}/permissions",
72+
"/api/v1/documents/{document_id}/comments",
73+
"/api/v1/documents/{document_id}/comments/{comment_id}",
74+
"/api/v1/documents/{document_id}/shares",
75+
"/api/v1/documents/{document_id}/exports/{format}",
76+
"/api/v1/storage/buckets",
77+
"/api/v1/storage/buckets/{bucket_id}",
78+
"/api/v1/storage/buckets/{bucket_id}/objects",
79+
"/api/v1/storage/buckets/{bucket_id}/objects/{object_key}",
80+
"/api/v1/storage/buckets/{bucket_id}/objects/{object_key}/metadata",
81+
"/api/v1/storage/buckets/{bucket_id}/objects/{object_key}/versions",
82+
"/api/v1/storage/buckets/{bucket_id}/policies",
83+
"/api/v1/analytics/events",
84+
"/api/v1/analytics/events/{event_id}",
85+
"/api/v1/analytics/reports",
86+
"/api/v1/analytics/reports/{report_id}",
87+
"/api/v1/analytics/dashboards",
88+
"/api/v1/analytics/dashboards/{dashboard_id}",
89+
"/api/v1/analytics/dashboards/{dashboard_id}/widgets",
90+
"/api/v1/analytics/dashboards/{dashboard_id}/widgets/{widget_id}",
91+
"/api/v1/webhooks",
92+
"/api/v1/webhooks/{webhook_id}",
93+
"/api/v1/webhooks/{webhook_id}/deliveries",
94+
"/api/v1/webhooks/{webhook_id}/deliveries/{delivery_id}",
95+
"/api/v1/search/users",
96+
"/api/v1/search/projects",
97+
"/api/v1/search/documents",
98+
"/api/v1/search/issues",
99+
"/api/v1/admin/users",
100+
"/api/v1/admin/users/{user_id}",
101+
"/api/v1/admin/organizations",
102+
"/api/v1/admin/organizations/{org_id}",
103+
"/api/v1/admin/logs",
104+
"/api/v1/admin/logs/{log_id}",
105+
"/api/v1/admin/settings",
106+
"/api/v1/admin/settings/{setting_key}",
107+
"/api/v1/organizations/{org_id}/teams/{team_id}/projects/{project_id}/"
108+
"issues/{issue_id}/comments/{comment_id}/reactions/{reaction_id}",
109+
"/api/v1/workspaces/{workspace_id}/folders/{folder_id}/documents/"
110+
"{document_id}/sections/{section_id}/paragraphs/{paragraph_id}/comments/"
111+
"{comment_id}"};
112+
113+
static constexpr std::size_t ROUTE_COUNT = sizeof(ROUTES) / sizeof(ROUTES[0]);
114+
115+
static void URITemplateRouter_Create(benchmark::State &state) {
116+
for (auto _ : state) {
117+
sourcemeta::core::URITemplateRouter router;
118+
for (std::size_t index = 0; index < ROUTE_COUNT; ++index) {
119+
router.add(ROUTES[index],
120+
static_cast<sourcemeta::core::URITemplateRouter::Identifier>(
121+
index + 1));
122+
}
123+
124+
benchmark::DoNotOptimize(router);
125+
}
126+
}
127+
128+
static void URITemplateRouter_Match(benchmark::State &state) {
129+
sourcemeta::core::URITemplateRouter router;
130+
for (std::size_t index = 0; index < ROUTE_COUNT; ++index) {
131+
router.add(ROUTES[index],
132+
static_cast<sourcemeta::core::URITemplateRouter::Identifier>(
133+
index + 1));
134+
}
135+
136+
for (auto _ : state) {
137+
auto result = router.match(
138+
"/api/v1/organizations/12345/teams/67890/projects/abc/issues/999/"
139+
"comments/42/reactions/1",
140+
[](auto, auto) {});
141+
assert(result == ROUTE_COUNT - 1);
142+
benchmark::DoNotOptimize(result);
143+
}
144+
}
145+
146+
static void URITemplateRouterView_Restore(benchmark::State &state) {
147+
sourcemeta::core::URITemplateRouter router;
148+
for (std::size_t index = 0; index < ROUTE_COUNT; ++index) {
149+
router.add(ROUTES[index],
150+
static_cast<sourcemeta::core::URITemplateRouter::Identifier>(
151+
index + 1));
152+
}
153+
154+
const std::filesystem::path path =
155+
std::filesystem::temp_directory_path() / "uritemplate_benchmark.bin";
156+
sourcemeta::core::URITemplateRouterView::save(router, path);
157+
158+
for (auto _ : state) {
159+
sourcemeta::core::URITemplateRouterView view{path};
160+
benchmark::DoNotOptimize(view);
161+
}
162+
163+
std::filesystem::remove(path);
164+
}
165+
166+
static void URITemplateRouterView_Match(benchmark::State &state) {
167+
sourcemeta::core::URITemplateRouter router;
168+
for (std::size_t index = 0; index < ROUTE_COUNT; ++index) {
169+
router.add(ROUTES[index],
170+
static_cast<sourcemeta::core::URITemplateRouter::Identifier>(
171+
index + 1));
172+
}
173+
174+
const std::filesystem::path path =
175+
std::filesystem::temp_directory_path() / "uritemplate_benchmark.bin";
176+
sourcemeta::core::URITemplateRouterView::save(router, path);
177+
sourcemeta::core::URITemplateRouterView view{path};
178+
179+
for (auto _ : state) {
180+
auto result = view.match(
181+
"/api/v1/organizations/12345/teams/67890/projects/abc/issues/999/"
182+
"comments/42/reactions/1",
183+
[](auto, auto) {});
184+
assert(result == ROUTE_COUNT - 1);
185+
benchmark::DoNotOptimize(result);
186+
}
187+
188+
std::filesystem::remove(path);
189+
}
190+
191+
BENCHMARK(URITemplateRouter_Create);
192+
BENCHMARK(URITemplateRouter_Match);
193+
BENCHMARK(URITemplateRouterView_Restore);
194+
BENCHMARK(URITemplateRouterView_Match);

config.cmake.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS})
5454
elseif(component STREQUAL "uri")
5555
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uri.cmake")
5656
elseif(component STREQUAL "uritemplate")
57+
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake")
5758
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uritemplate.cmake")
5859
elseif(component STREQUAL "json")
5960
find_dependency(mpdecimal CONFIG)
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME uritemplate
2-
PRIVATE_HEADERS error.h token.h
3-
SOURCES helpers.h uritemplate.cc)
2+
PRIVATE_HEADERS error.h token.h router.h
3+
SOURCES helpers.h uritemplate.cc uritemplate_router.cc uritemplate_router_view.cc)
44

55
if(SOURCEMETA_CORE_INSTALL)
66
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME uritemplate)
77
endif()
8+
9+
target_link_libraries(sourcemeta_core_uritemplate PUBLIC sourcemeta::core::io)

src/core/uritemplate/helpers.h

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,36 @@ inline auto append_name(std::string &result, const std::string_view name,
111111
}
112112
}
113113

114-
inline auto is_varchar(const char character) -> bool {
114+
// RFC 6570 Section 2.3: varchar = ALPHA / DIGIT / "_"
115+
inline auto is_varchar(const char character) noexcept -> bool {
115116
return (character >= 'A' && character <= 'Z') ||
116117
(character >= 'a' && character <= 'z') ||
117118
(character >= '0' && character <= '9') || character == '_';
118119
}
119120

120-
inline auto is_operator(const char character) -> bool {
121+
// Variable name character including dot for dotted names like "foo.bar"
122+
inline auto is_varname_char(const char character) noexcept -> bool {
123+
return is_varchar(character) || character == '.';
124+
}
125+
126+
// RFC 6570 Section 2.2: operator = op-level2 / op-level3 / op-reserve
127+
inline auto is_operator(const char character) noexcept -> bool {
121128
return character == '+' || character == '#' || character == '.' ||
122129
character == '/' || character == ';' || character == '?' ||
123130
character == '&';
124131
}
125132

133+
// RFC 6570 Section 2.2: op-reserve = "=" / "," / "!" / "@" / "|"
134+
inline auto is_reserved_operator(const char character) noexcept -> bool {
135+
return character == '=' || character == ',' || character == '!' ||
136+
character == '@' || character == '|';
137+
}
138+
139+
// RFC 6570 Section 2.4: modifier = prefix / explode
140+
inline auto is_modifier(const char character) noexcept -> bool {
141+
return character == ':' || character == '*';
142+
}
143+
126144
inline auto parse_varname(const std::string_view input, std::size_t position)
127145
-> std::size_t {
128146
if (position >= input.size() ||

src/core/uritemplate/include/sourcemeta/core/uritemplate.h

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
// NOLINTBEGIN(misc-include-cleaner)
99
#include <sourcemeta/core/uritemplate_error.h>
10+
#include <sourcemeta/core/uritemplate_router.h>
1011
#include <sourcemeta/core/uritemplate_token.h>
1112
// NOLINTEND(misc-include-cleaner)
1213

@@ -99,16 +100,6 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplate {
99100
});
100101
}
101102

102-
/// Check if this template can be used for matching/extraction
103-
[[nodiscard]] auto is_matchable(char delimiter) const noexcept -> bool;
104-
105-
/// Match a URI against this template, extracting variable values.
106-
/// The delimiter character is used to determine variable boundaries.
107-
[[nodiscard]] auto
108-
match(std::string_view uri, char delimiter,
109-
const std::function<void(std::string_view, std::string_view)> &callback)
110-
const -> bool;
111-
112103
private:
113104
// Exporting symbols that depends on the standard C++ library is considered
114105
// safe.

src/core/uritemplate/include/sourcemeta/core/uritemplate_error.h

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
#include <sourcemeta/core/uritemplate_export.h>
66
#endif
77

8-
#include <cstdint> // std::uint64_t
9-
#include <exception> // std::exception
10-
#include <stdexcept> // std::runtime_error
11-
#include <string> // std::string
8+
#include <cstdint> // std::uint64_t
9+
#include <exception> // std::exception
10+
#include <stdexcept> // std::runtime_error
11+
#include <string> // std::string
12+
#include <string_view> // std::string_view
1213

1314
namespace sourcemeta::core {
1415

@@ -48,6 +49,57 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateExpansionError
4849
: std::runtime_error{message} {}
4950
};
5051

52+
/// @ingroup uritemplate
53+
/// An error that represents a variable name mismatch when adding routes
54+
class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouterVariableMismatchError
55+
: public std::exception {
56+
public:
57+
URITemplateRouterVariableMismatchError(const std::string_view left,
58+
const std::string_view right)
59+
: left_{left}, right_{right} {}
60+
61+
[[nodiscard]] auto what() const noexcept -> const char * override {
62+
return "Variable name mismatch when adding route";
63+
}
64+
65+
/// Get the existing variable name
66+
[[nodiscard]] auto left() const noexcept -> const std::string & {
67+
return this->left_;
68+
}
69+
70+
/// Get the conflicting variable name
71+
[[nodiscard]] auto right() const noexcept -> const std::string & {
72+
return this->right_;
73+
}
74+
75+
private:
76+
std::string left_;
77+
std::string right_;
78+
};
79+
80+
/// @ingroup uritemplate
81+
/// An error for invalid segments when adding routes
82+
class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouterInvalidSegmentError
83+
: public std::exception {
84+
public:
85+
URITemplateRouterInvalidSegmentError(const char *message,
86+
const std::string_view segment)
87+
: message_{message}, segment_{segment} {}
88+
89+
[[nodiscard]] auto what() const noexcept -> const char * override {
90+
return this->message_;
91+
}
92+
93+
/// Get the offending segment
94+
[[nodiscard]] auto segment() const noexcept -> const std::string & {
95+
return this->segment_;
96+
}
97+
98+
private:
99+
const char *message_;
100+
std::string segment_;
101+
};
102+
51103
#if defined(_MSC_VER)
52104
#pragma warning(default : 4251 4275)
53105
#endif

0 commit comments

Comments
 (0)