Coverage Report

Created: 2025-07-12 06:38

/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