Repository navigation
Expand file tree
/
Copy pathroutes.cpp
More file actions
313 lines (286 loc) · 13.3 KB
/
Copy pathroutes.cpp
File metadata and controls
313 lines (286 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
/* Copyright (C) 2026 Sebastiano Merlino; SPDX-License-Identifier: LGPL-2.1-or-later */
#include <httpserver/server/routes.hpp>
#include <string>
#include <utility>
#include <vector>
namespace httpserver::server::detail {
http::outcome parse_route_segment(std::string_view segment, std::vector<std::string_view>& names, std::size_t& parameters) {
if (segment.empty()) {
return http::outcome(http::outcome_code::invalid_argument, "route_pattern: empty segment (repeated slash)");
}
if (segment.front() != '{') {
// Literal segment: the RFC 9110 token grammar, which excludes
// braces, spaces, and separators.
if (!http::detail::is_token(segment)) {
return http::outcome(http::outcome_code::invalid_argument, "route_pattern: segment '" + std::string(segment) + "' is not a valid literal token");
}
return http::outcome::okay();
}
if (segment.size() < 3 || segment.back() != '}') {
return http::outcome(http::outcome_code::invalid_argument,
"route_pattern: a parameter segment must be exactly one"
" '{name}'");
}
const std::string_view name = segment.substr(1, segment.size() - 2);
if (!valid_parameter_name(name)) {
return http::outcome(http::outcome_code::invalid_argument, "route_pattern: parameter name '" + std::string(name) + "' must match [A-Za-z0-9_]+");
}
for (const std::string_view seen : names) {
if (seen == name) {
return http::outcome(http::outcome_code::invalid_argument, "route_pattern: duplicate parameter name '" + std::string(name) + "'");
}
}
names.push_back(name);
++parameters;
return http::outcome::okay();
}
std::vector<std::string_view> split_path_segments(std::string_view path) {
std::vector<std::string_view> segments;
if (path.size() < 2) return segments; // "" and "/"
std::size_t pos = 1; // skip the leading slash
while (pos < path.size()) {
const std::size_t slash = path.find('/', pos);
const std::size_t width = slash == std::string_view::npos ? std::string_view::npos : slash - pos;
segments.push_back(path.substr(pos, width));
if (slash == std::string_view::npos) break;
pos = slash + 1;
}
return segments;
}
std::vector<pattern_segment> index_path_segments(std::string_view path) {
std::vector<pattern_segment> segments;
if (path.size() < 2) return segments; // "" and "/"
std::size_t pos = 1; // skip the leading slash
while (pos < path.size()) {
const std::size_t slash = path.find('/', pos);
const std::size_t length = slash == std::string_view::npos ? path.size() - pos : slash - pos;
segments.push_back(pattern_segment{static_cast<std::uint32_t>(pos), static_cast<std::uint32_t>(length)});
if (slash == std::string_view::npos) break;
pos = slash + 1;
}
return segments;
}
bool match_stored_segments(std::string_view pattern_text, const std::vector<pattern_segment>& pattern_segments, bool prefix, const std::vector<std::string_view>& path_segments,
std::vector<route_captures>& captures) {
const std::size_t count = pattern_segments.size();
if (prefix ? count > path_segments.size() : count != path_segments.size()) {
return false;
}
captures.clear();
for (std::size_t i = 0; i < count; ++i) {
const pattern_segment& stored = pattern_segments[i];
const std::string_view pattern = pattern_text.substr(stored.offset, stored.length);
const std::string_view segment = path_segments[i];
if (pattern.front() == '{') {
if (segment.empty()) return false;
captures.push_back(route_captures{pattern.substr(1, pattern.size() - 2), std::string(segment)});
continue;
}
if (pattern != segment) return false;
}
return true;
}
} // namespace httpserver::server::detail
namespace httpserver::server {
auto route_pattern::parse(std::string_view text, route_pattern& out) -> http::outcome {
if (text.empty()) {
return http::outcome(http::outcome_code::invalid_argument, "route_pattern: pattern is empty");
}
if (text.front() != '/') {
return http::outcome(http::outcome_code::invalid_argument, "route_pattern: pattern must begin with '/'");
}
route_pattern parsed;
if (text != "/") {
if (text.back() == '/') {
return http::outcome(http::outcome_code::invalid_argument, "route_pattern: pattern must not end with '/'");
}
std::vector<std::string_view> names;
std::size_t pos = 1;
while (pos <= text.size()) {
const std::size_t slash = text.find('/', pos);
const std::size_t width = slash == std::string_view::npos ? std::string_view::npos : slash - pos;
if (const http::outcome result = detail::parse_route_segment(text.substr(pos, width), names, parsed.parameters_); !result.ok()) {
return result;
}
++parsed.segments_;
if (slash == std::string_view::npos) break;
pos = slash + 1;
}
}
parsed.text_ = std::string(text);
out = std::move(parsed);
return http::outcome::okay();
}
} // namespace httpserver::server
/* Copyright (C) 2026 Sebastiano Merlino; SPDX-License-Identifier: LGPL-2.1-or-later */
namespace httpserver::server {
auto route_registry::create(const resource_budget& budget, route_registry& out) -> http::outcome {
if (budget.capacity(resource::routes) == 0) {
return http::outcome(http::outcome_code::invalid_argument, "route_registry: the budget carries no routes capacity");
}
out = route_registry(budget);
return http::outcome::okay();
}
auto route_registry::route(const http::method& m, std::string_view pattern, route_handler handler) -> http::outcome {
if (!m.valid()) {
return http::outcome(http::outcome_code::invalid_argument, "route_registry: the unknown_ method is not registrable");
}
if (!handler) {
return http::outcome(http::outcome_code::invalid_argument, "route_registry: handler is empty");
}
http::method_set bits;
http::method single;
if (m.is_extension()) {
single = m;
} else {
bits.set(m.id());
}
return admit(pattern, std::move(handler), false, bits, single);
}
auto route_registry::route(const http::method_set& set, std::string_view pattern, route_handler handler) -> http::outcome {
if (!set.any()) {
return http::outcome(http::outcome_code::invalid_argument,
"route_registry: a method-set route needs at least one"
" method");
}
if (!handler) {
return http::outcome(http::outcome_code::invalid_argument, "route_registry: handler is empty");
}
return admit(pattern, std::move(handler), false, set, http::method{});
}
auto route_registry::route_prefix(const http::method_set& set, std::string_view pattern, route_handler handler) -> http::outcome {
if (!set.any()) {
return http::outcome(http::outcome_code::invalid_argument,
"route_registry: a prefix route needs at least one"
" method");
}
if (!handler) {
return http::outcome(http::outcome_code::invalid_argument, "route_registry: handler is empty");
}
return admit(pattern, std::move(handler), true, set, http::method{});
}
auto route_registry::registered(const http::method& m, const route_pattern& p) const -> bool {
for (const entry& known : entries_) {
if (known.pattern_.text() == p.text() && accepts(known, m)) {
return true;
}
}
return false;
}
auto route_registry::match(const http::method& m, std::string_view route_path) const -> match_result {
match_result found;
const std::vector<std::string_view> path_segments = detail::split_path_segments(route_path);
for (const entry& known : entries_) {
if (known.prefix_ || !accepts(known, m)) continue;
std::vector<route_captures> captures;
if (!detail::match_stored_segments(known.pattern_.text(), known.segments_, false, path_segments, captures)) {
continue;
}
found.handler = &known.handler_;
found.parameters.reserve(captures.size());
for (route_captures& capture : captures) {
found.parameters.push_back(std::move(capture.value));
}
break;
}
return found;
}
auto route_registry::resolve(const http::method& m, std::string_view route_path, bool methods_observable) const -> resolve_result {
const std::vector<std::string_view> path_segments = detail::split_path_segments(route_path);
resolve_result full = resolve_tier(m, path_segments, false, methods_observable);
if (full.kind != resolve_result::resolve_kind::miss) return full;
return resolve_tier(m, path_segments, true, methods_observable);
}
auto route_registry::accepts(const entry& known, const http::method& m) noexcept -> bool {
if (known.single_.valid()) return known.single_ == m;
return known.methods_.contains(m);
}
auto route_registry::admit(std::string_view pattern, route_handler handler, bool prefix, const http::method_set& set, const http::method& single) -> http::outcome {
route_pattern parsed;
if (const http::outcome result = route_pattern::parse(pattern, parsed); !result.ok()) {
return result;
}
if (!budget_.valid()) {
return http::outcome(http::outcome_code::invalid_state, "route_registry: registry has no budget");
}
const std::string& canonical = parsed.text();
for (const entry& known : entries_) {
if (known.prefix_ == prefix && known.pattern_.text() == canonical && overlaps(known, set, single)) {
const std::string label = single.valid() ? std::string(single.name()) : http::to_string(set);
return http::outcome(http::outcome_code::invalid_state, "route_registry: duplicate route for " + label + " " + canonical);
}
}
reservation seat;
if (const http::outcome result = budget_.reserve(resource::routes, 1, seat); !result.ok()) {
return result;
}
// Computed before the push: a braced-init-list evaluates
// left-to-right, so the split must not read the pattern after
// the move into the entry.
std::vector<detail::pattern_segment> segments = detail::index_path_segments(canonical);
entries_.push_back(entry{set, single, std::move(parsed), std::move(segments), std::move(handler), std::move(seat), prefix});
return http::outcome::okay();
}
auto route_registry::overlaps(const entry& known, const http::method_set& set, const http::method& single) noexcept -> bool {
if (single.valid()) {
return known.single_.valid() && known.single_ == single;
}
for (std::size_t id = 0; id < static_cast<std::size_t>(http::method_id::extension); ++id) {
const http::method slot = http::method::known(static_cast<http::method_id>(id));
if (set.contains(slot) && known.methods_.contains(slot)) {
return true;
}
}
return false;
}
auto route_registry::resolve_tier(const http::method& m, const std::vector<std::string_view>& path_segments, bool prefix, bool methods_observable) const -> resolve_result {
resolve_result out;
const entry* best = nullptr;
std::vector<route_captures> best_captures;
for (const entry& known : entries_) {
if (known.prefix_ != prefix) continue;
std::vector<route_captures> captures;
if (!match_tier_entry(known, path_segments, prefix, captures)) {
continue;
}
merge_entry_methods(out, known);
if (!accepts(known, m) || !outranks(best, known, prefix)) {
continue;
}
best = &known;
best_captures = std::move(captures);
// The full tier's winner is always its FIRST hit: once the
// merged methods cannot be observed, the scan stops (the
// pre-task match() semantics; the merge feeds nothing).
if (!prefix && !methods_observable) break;
}
if (best == nullptr) return miss_or_method_miss(out);
out.kind = resolve_result::resolve_kind::hit;
out.handler = &best->handler_;
out.captures = std::move(best_captures);
out.pattern_text = best->pattern_.text();
out.is_prefix = prefix;
return out;
}
auto route_registry::match_tier_entry(const entry& known, const std::vector<std::string_view>& path_segments, bool prefix, std::vector<route_captures>& captures) -> bool {
return detail::match_stored_segments(known.pattern_.text(), known.segments_, prefix, path_segments, captures);
}
auto route_registry::merge_entry_methods(resolve_result& out, const entry& known) noexcept -> void {
if (known.single_.valid()) {
out.extension_names.emplace_back(known.single_.name());
} else {
out.methods.merge(known.methods_);
}
}
auto route_registry::outranks(const entry* best, const entry& candidate, bool prefix) noexcept -> bool {
if (best == nullptr) return true;
if (!prefix) return false;
return candidate.pattern_.segment_count() > best->pattern_.segment_count();
}
auto route_registry::miss_or_method_miss(resolve_result out) noexcept -> resolve_result {
if (out.methods.any() || !out.extension_names.empty()) {
out.kind = resolve_result::resolve_kind::method_miss;
}
return out;
}
} // namespace httpserver::server