/src/pistache/include/pistache/router.h
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * SPDX-FileCopyrightText: 2016 Mathieu Stefani |
3 | | * |
4 | | * SPDX-License-Identifier: Apache-2.0 |
5 | | */ |
6 | | |
7 | | /* router.h |
8 | | Mathieu Stefani, 05 janvier 2016 |
9 | | |
10 | | Simple HTTP Rest Router |
11 | | */ |
12 | | |
13 | | #pragma once |
14 | | |
15 | | #include <memory> |
16 | | #include <regex> |
17 | | #include <string> |
18 | | #include <tuple> |
19 | | #include <unordered_map> |
20 | | #include <vector> |
21 | | |
22 | | #include <pistache/flags.h> |
23 | | #include <pistache/http.h> |
24 | | #include <pistache/http_defs.h> |
25 | | |
26 | | namespace Pistache::Rest |
27 | | { |
28 | | |
29 | | class Description; |
30 | | |
31 | | namespace details |
32 | | { |
33 | | template <typename T> |
34 | | struct LexicalCast |
35 | | { |
36 | | static T cast(const std::string& value) |
37 | | { |
38 | | std::istringstream iss(value); |
39 | | T out; |
40 | | if (!(iss >> out)) |
41 | | throw std::runtime_error("Bad lexical cast"); |
42 | | return out; |
43 | | } |
44 | | }; |
45 | | |
46 | | template <> |
47 | | struct LexicalCast<std::string> |
48 | | { |
49 | 0 | static std::string cast(const std::string& value) { return value; } |
50 | | }; |
51 | | } // namespace details |
52 | | |
53 | | class TypedParam |
54 | | { |
55 | | public: |
56 | | TypedParam(std::string name, std::string value) |
57 | 122k | : name_(std::move(name)) |
58 | 122k | , value_(std::move(value)) |
59 | 122k | { } |
60 | | |
61 | | template <typename T> |
62 | | T as() const |
63 | | { |
64 | | return details::LexicalCast<T>::cast(value_); |
65 | | } |
66 | | |
67 | 0 | const std::string& name() const { return name_; } |
68 | | |
69 | | private: |
70 | | const std::string name_; |
71 | | const std::string value_; |
72 | | }; |
73 | | |
74 | | class Request; |
75 | | |
76 | | struct Route |
77 | | { |
78 | | enum class Result { Ok, |
79 | | Failure }; |
80 | | |
81 | | enum class Status { Match, |
82 | | NotFound, |
83 | | NotAllowed }; |
84 | | |
85 | | typedef std::function<Result(const Request, Http::ResponseWriter)> Handler; |
86 | | |
87 | | typedef std::function<bool(Http::Request& req, Http::ResponseWriter& resp)> Middleware; |
88 | | |
89 | | typedef std::function<void(const std::shared_ptr<Tcp::Peer>& peer)> |
90 | | DisconnectHandler; |
91 | | |
92 | | explicit Route(Route::Handler handler) |
93 | 23.3k | : handler_(std::move(handler)) |
94 | 23.3k | { } |
95 | | |
96 | | template <typename... Args> |
97 | | void invokeHandler(Args&&... args) const |
98 | 0 | { |
99 | 0 | handler_(std::forward<Args>(args)...); |
100 | 0 | } |
101 | | |
102 | | Handler handler_; |
103 | | }; |
104 | | |
105 | | namespace Private |
106 | | { |
107 | | class RouterHandler; |
108 | | } |
109 | | |
110 | | /** |
111 | | * A request URI is made of various path segments. |
112 | | * Since all routes handled by a router are naturally |
113 | | * represented as a tree, this class provides support for it. |
114 | | * It is possible to perform tree-based routing search instead |
115 | | * of linear one. |
116 | | * This class holds all data for a given path segment, meaning |
117 | | * that it holds the associated route handler (if any) and all |
118 | | * next child routes (by means of fixed routes, parametric, |
119 | | * optional parametric and splats). |
120 | | * Each child is in turn a SegmentTreeNode. |
121 | | */ |
122 | | class SegmentTreeNode |
123 | | { |
124 | | private: |
125 | | enum class SegmentType { Fixed, |
126 | | Param, |
127 | | Optional, |
128 | | Splat }; |
129 | | |
130 | | /** |
131 | | * string_view are very efficient when working with the |
132 | | * substring function (massively used for routing) but are |
133 | | * non-owning. To let the content survive after it is firstly |
134 | | * created, their content is stored in this reference pointer |
135 | | * that is in charge of managing their lifecycle (create on add, |
136 | | * reset on remove). |
137 | | */ |
138 | | std::shared_ptr<char> resource_ref_; |
139 | | |
140 | | std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>> fixed_; |
141 | | std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>> param_; |
142 | | std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>> |
143 | | optional_; |
144 | | std::shared_ptr<SegmentTreeNode> splat_; |
145 | | std::shared_ptr<Route> route_; |
146 | | |
147 | | /** |
148 | | * Common web servers (nginx, httpd, IIS) collapse multiple |
149 | | * forward slashes to a single one. This regex is used to |
150 | | * obtain the same result. |
151 | | */ |
152 | | static std::regex multiple_slash; |
153 | | |
154 | | static SegmentType getSegmentType(const std::string_view& fragment); |
155 | | |
156 | | /** |
157 | | * Fetches the route associated to a given path. |
158 | | * \param[in] path Requested resource path. Must have no leading slash |
159 | | * and no multiple slashes: |
160 | | * eg: |
161 | | * - auth/login is valid |
162 | | * - /auth/login is invalid |
163 | | * - auth//login is invalid |
164 | | * \param[in,out] params Contains all the parameters parsed so far. |
165 | | * \param[in,out] splats Contains all the splats parsed so far. |
166 | | * \returns Tuple containing the route, the list of all parsed parameters |
167 | | * and the list of all parsed splats. |
168 | | * \throws std::runtime_error An empty path was given |
169 | | */ |
170 | | std::tuple<std::shared_ptr<Route>, std::vector<TypedParam>, |
171 | | std::vector<TypedParam>> |
172 | | findRoute(const std::string_view& path, std::vector<TypedParam>& params, |
173 | | std::vector<TypedParam>& splats) const; |
174 | | |
175 | | public: |
176 | | SegmentTreeNode(); |
177 | | explicit SegmentTreeNode(const std::shared_ptr<char>& resourceReference); |
178 | | |
179 | | /** |
180 | | * Sanitizes a resource URL by removing any duplicate slash, leading |
181 | | * slash and trailing slash. |
182 | | * @param path URL to sanitize. |
183 | | * @return Sanitized URL. |
184 | | */ |
185 | | static std::string sanitizeResource(const std::string& path); |
186 | | |
187 | | /** |
188 | | * Associates a route handler to a given path. |
189 | | * \param[in] path Requested resource path. Must have no leading and trailing |
190 | | * slashes and no multiple slashes: |
191 | | * eg: |
192 | | * - auth/login is valid |
193 | | * - /auth/login is invalid |
194 | | * - auth/login/ is invalid |
195 | | * - auth//login is invalid |
196 | | * \param[in] handler Handler to associate to path. |
197 | | * \param[in] resource_reference See SegmentTreeNode::resource_ref_ (private) |
198 | | * \throws std::runtime_error An empty path was given |
199 | | */ |
200 | | void addRoute(const std::string_view& path, const Route::Handler& handler, |
201 | | const std::shared_ptr<char>& resource_reference); |
202 | | |
203 | | /** |
204 | | * Removes the route handler associated to a given path. |
205 | | * \param[in] path Requested resource path. Must have no leading slash |
206 | | * and no multiple slashes: |
207 | | * eg: |
208 | | * - auth/login is valid |
209 | | * - /auth/login is invalid |
210 | | * - auth//login is invalid |
211 | | * \throws std::runtime_error An empty path was given |
212 | | */ |
213 | | bool removeRoute(const std::string_view& path); |
214 | | |
215 | | /** |
216 | | * Finds the correct route for the given path. |
217 | | * \param[in] path Requested resource path. Must have no leading slash |
218 | | * and no multiple slashes: |
219 | | * eg: |
220 | | * - auth/login is valid |
221 | | * - /auth/login is invalid |
222 | | * - auth//login is invalid |
223 | | * \throws std::runtime_error An empty path was given |
224 | | * \return Found route with its resolved parameters and splats (if no route |
225 | | * is found, first element of the tuple is a null pointer). |
226 | | */ |
227 | | std::tuple<std::shared_ptr<Route>, std::vector<TypedParam>, |
228 | | std::vector<TypedParam>> |
229 | | findRoute(const std::string_view& path) const; |
230 | | }; |
231 | | |
232 | | class Router |
233 | | { |
234 | | public: |
235 | | static Router fromDescription(const Rest::Description& desc); |
236 | | |
237 | | std::shared_ptr<Private::RouterHandler> handler() const; |
238 | | static std::shared_ptr<Private::RouterHandler> |
239 | | handler(std::shared_ptr<Rest::Router> router); |
240 | | |
241 | | void initFromDescription(const Rest::Description& desc); |
242 | | |
243 | | void get(const std::string& resource, Route::Handler handler); |
244 | | void post(const std::string& resource, Route::Handler handler); |
245 | | void put(const std::string& resource, Route::Handler handler); |
246 | | void patch(const std::string& resource, Route::Handler handler); |
247 | | void del(const std::string& resource, Route::Handler handler); |
248 | | void options(const std::string& resource, Route::Handler handler); |
249 | | void addRoute(Http::Method method, const std::string& resource, |
250 | | Route::Handler handler); |
251 | | void removeRoute(Http::Method method, const std::string& resource); |
252 | | void head(const std::string& resource, Route::Handler handler); |
253 | | |
254 | | void addCustomHandler(Route::Handler handler); |
255 | | void addMiddleware(Route::Middleware middleware); |
256 | | |
257 | | void addNotFoundHandler(Route::Handler handler); |
258 | | void addDisconnectHandler(Route::DisconnectHandler handler); |
259 | 0 | inline bool hasNotFoundHandler() const { return notFoundHandler != nullptr; } |
260 | | void invokeNotFoundHandler(const Http::Request& req, |
261 | | Http::ResponseWriter resp) const; |
262 | | |
263 | | void disconnectPeer(const std::shared_ptr<Tcp::Peer>& peer); |
264 | | |
265 | | Route::Status route(const Http::Request& request, |
266 | | Http::ResponseWriter response) const; |
267 | | |
268 | | Router() |
269 | 0 | : routes() |
270 | 0 | , customHandlers() |
271 | 0 | , middlewares() |
272 | 0 | , notFoundHandler() |
273 | 0 | { } |
274 | | |
275 | | private: |
276 | | std::unordered_map<Http::Method, SegmentTreeNode> routes; |
277 | | |
278 | | std::vector<Route::Handler> customHandlers; |
279 | | |
280 | | std::vector<Route::Middleware> middlewares; |
281 | | |
282 | | std::vector<Route::DisconnectHandler> disconnectHandlers; |
283 | | |
284 | | Route::Handler notFoundHandler; |
285 | | }; |
286 | | |
287 | | namespace Private |
288 | | { |
289 | | |
290 | | class RouterHandler : public Http::Handler |
291 | | { |
292 | | public: |
293 | | HTTP_PROTOTYPE(RouterHandler) |
294 | | |
295 | | /** |
296 | | * Used for immutable router. Useful if all the routes are |
297 | | * defined at compile time (and for backward compatibility) |
298 | | * \param[in] router Immutable router. |
299 | | */ |
300 | | explicit RouterHandler(const Rest::Router& router); |
301 | | |
302 | | /** |
303 | | * Used for mutable router. Useful if it is required to |
304 | | * add/remove routes at runtime. |
305 | | * \param[in] router Pointer to a (mutable) router. |
306 | | */ |
307 | | explicit RouterHandler(std::shared_ptr<Rest::Router> router); |
308 | | |
309 | | void onRequest(const Http::Request& req, |
310 | | Http::ResponseWriter response) override; |
311 | | |
312 | | void onDisconnection(const std::shared_ptr<Tcp::Peer>& peer) override; |
313 | | |
314 | | private: |
315 | | std::shared_ptr<Rest::Router> router; |
316 | | }; |
317 | | } // namespace Private |
318 | | |
319 | | class Request : public Http::Request |
320 | | { |
321 | | public: |
322 | | friend class Router; |
323 | | |
324 | | bool hasParam(const std::string& name) const; |
325 | | TypedParam param(const std::string& name) const; |
326 | | |
327 | | TypedParam splatAt(size_t index) const; |
328 | | std::vector<TypedParam> splat() const; |
329 | | |
330 | | private: |
331 | | explicit Request(Http::Request request, |
332 | | std::vector<TypedParam>&& params, |
333 | | std::vector<TypedParam>&& splats); |
334 | | |
335 | | std::vector<TypedParam> params_; |
336 | | std::vector<TypedParam> splats_; |
337 | | }; |
338 | | |
339 | | namespace Routes |
340 | | { |
341 | | |
342 | | void Get(Router& router, const std::string& resource, Route::Handler handler); |
343 | | void Post(Router& router, const std::string& resource, Route::Handler handler); |
344 | | void Put(Router& router, const std::string& resource, Route::Handler handler); |
345 | | void Patch(Router& router, const std::string& resource, Route::Handler handler); |
346 | | void Delete(Router& router, const std::string& resource, |
347 | | Route::Handler handler); |
348 | | void Options(Router& router, const std::string& resource, |
349 | | Route::Handler handler); |
350 | | void Remove(Router& router, Http::Method method, const std::string& resource); |
351 | | void Head(Router& router, const std::string& resource, Route::Handler handler); |
352 | | |
353 | | void NotFound(Router& router, Route::Handler handler); |
354 | | |
355 | | namespace details |
356 | | { |
357 | | template <typename... Args> |
358 | | struct TypeList |
359 | | { |
360 | | template <size_t N> |
361 | | struct At |
362 | | { |
363 | | static_assert(N < sizeof...(Args), "Invalid index"); |
364 | | |
365 | | using Type = typename std::tuple_element<N, std::tuple<Args...>>::type; |
366 | | }; |
367 | | }; |
368 | | |
369 | | template <typename Request, typename Response> |
370 | | struct BindChecks |
371 | | { |
372 | | constexpr static bool request_check = std::is_const<typename std::remove_reference<Request>::type>::value && std::is_lvalue_reference<typename std::remove_cv<Request>::type>::value && std::is_same<typename std::decay<Request>::type, Rest::Request>::value; |
373 | | |
374 | | constexpr static bool response_check = !std::is_const<typename std::remove_reference<Response>::type>::value && std::is_same<typename std::remove_reference<Response>::type, Response>::value && std::is_same<typename std::decay<Response>::type, Http::ResponseWriter>::value; |
375 | | |
376 | | static_assert( |
377 | | request_check && response_check, |
378 | | "Function should accept (const Rest::Request&, HttpResponseWriter)"); |
379 | | }; |
380 | | |
381 | | template <typename Request, typename Response> |
382 | | struct MiddlewareChecks |
383 | | { |
384 | | constexpr static bool request_check = !std::is_const<typename std::remove_reference<Request>::type>::value && std::is_lvalue_reference<typename std::remove_cv<Request>::type>::value && std::is_same<typename std::decay<Request>::type, Http::Request>::value; |
385 | | |
386 | | constexpr static bool response_check = !std::is_const<typename std::remove_reference<Response>::type>::value && std::is_lvalue_reference<typename std::remove_cv<Response>::type>::value && std::is_same<typename std::decay<Response>::type, Http::ResponseWriter>::value; |
387 | | |
388 | | static_assert(request_check && response_check, |
389 | | "Function should accept (Http::Request&, HttpResponseWriter&)"); |
390 | | }; |
391 | | |
392 | | template <template <typename, typename> class Checks, typename... Args> |
393 | | constexpr void static_checks() |
394 | | { |
395 | | static_assert(sizeof...(Args) == 2, "Function should take 2 parameters"); |
396 | | |
397 | | using Arguments = details::TypeList<Args...>; |
398 | | |
399 | | using Request = typename Arguments::template At<0>::Type; |
400 | | using Response = typename Arguments::template At<1>::Type; |
401 | | |
402 | | // instantiate template this way |
403 | | [[maybe_unused]] constexpr Checks<Request, Response> checks; |
404 | | } |
405 | | } // namespace details |
406 | | |
407 | | template <typename Result, typename Cls, typename... Args, typename Obj> |
408 | | Route::Handler bind(Result (Cls::*func)(Args...), Obj obj) |
409 | | { |
410 | | details::static_checks<details::BindChecks, Args...>(); |
411 | | |
412 | | return [=](const Rest::Request& request, Http::ResponseWriter response) { |
413 | | (obj->*func)(request, std::move(response)); |
414 | | |
415 | | return Route::Result::Ok; |
416 | | }; |
417 | | } |
418 | | |
419 | | template <typename Result, typename Cls, typename... Args, typename Obj> |
420 | | Route::Handler bind(Result (Cls::*func)(Args...) const, Obj obj) |
421 | | { |
422 | | details::static_checks<details::BindChecks, Args...>(); |
423 | | |
424 | | return [=](const Rest::Request& request, Http::ResponseWriter response) { |
425 | | (obj->*func)(request, std::move(response)); |
426 | | |
427 | | return Route::Result::Ok; |
428 | | }; |
429 | | } |
430 | | |
431 | | template <typename Result, typename Cls, typename... Args, typename Obj> |
432 | | Route::Handler bind(Result (Cls::*func)(Args...), std::shared_ptr<Obj> objPtr) |
433 | | { |
434 | | details::static_checks<details::BindChecks, Args...>(); |
435 | | |
436 | | return [=](const Rest::Request& request, Http::ResponseWriter response) { |
437 | | (objPtr.get()->*func)(request, std::move(response)); |
438 | | |
439 | | return Route::Result::Ok; |
440 | | }; |
441 | | } |
442 | | |
443 | | template <typename Result, typename Cls, typename... Args, typename Obj> |
444 | | Route::Handler bind(Result (Cls::*func)(Args...) const, std::shared_ptr<Obj> objPtr) |
445 | | { |
446 | | details::static_checks<details::BindChecks, Args...>(); |
447 | | |
448 | | return [=](const Rest::Request& request, Http::ResponseWriter response) { |
449 | | (objPtr.get()->*func)(request, std::move(response)); |
450 | | |
451 | | return Route::Result::Ok; |
452 | | }; |
453 | | } |
454 | | |
455 | | template <typename Result, typename... Args> |
456 | | Route::Handler bind(Result (*func)(Args...)) |
457 | | { |
458 | | details::static_checks<details::BindChecks, Args...>(); |
459 | | |
460 | | return [=](const Rest::Request& request, Http::ResponseWriter response) { |
461 | | func(request, std::move(response)); |
462 | | |
463 | | return Route::Result::Ok; |
464 | | }; |
465 | | } |
466 | | |
467 | | template <typename Cls, typename... Args, typename Obj> |
468 | | Route::Middleware middleware(bool (Cls::*func)(Args...), Obj obj) |
469 | | { |
470 | | details::static_checks<details::MiddlewareChecks, Args...>(); |
471 | | |
472 | | return [=](Http::Request& request, Http::ResponseWriter& response) { |
473 | | return (obj->*func)(request, response); |
474 | | }; |
475 | | } |
476 | | |
477 | | template <typename Cls, typename... Args, typename Obj> |
478 | | Route::Middleware middleware(bool (Cls::*func)(Args...), |
479 | | std::shared_ptr<Obj> objPtr) |
480 | | { |
481 | | details::static_checks<details::MiddlewareChecks, Args...>(); |
482 | | |
483 | | return [=](Http::Request& request, Http::ResponseWriter& response) { |
484 | | return (objPtr.get()->*func)(request, response); |
485 | | }; |
486 | | } |
487 | | |
488 | | template <typename... Args> |
489 | | Route::Middleware middleware(bool (*func)(Args...)) |
490 | | { |
491 | | details::static_checks<details::MiddlewareChecks, Args...>(); |
492 | | |
493 | | return [=](Http::Request& request, Http::ResponseWriter& response) { |
494 | | return func(request, response); |
495 | | }; |
496 | | } |
497 | | |
498 | | } // namespace Routes |
499 | | } // namespace Pistache::Rest |