Coverage Report

Created: 2026-02-03 06:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pistache/src/server/router.cc
Line
Count
Source
1
/*
2
 * SPDX-FileCopyrightText: 2016 Mathieu Stefani
3
 *
4
 * SPDX-License-Identifier: Apache-2.0
5
 */
6
7
/* router.cc
8
   Mathieu Stefani, 05 janvier 2016
9
10
   Rest routing implementation
11
*/
12
13
#include <algorithm>
14
15
#include <pistache/description.h>
16
#include <pistache/router.h>
17
18
namespace Pistache::Rest
19
{
20
21
    Request::Request(Http::Request request, std::vector<TypedParam>&& params,
22
                     std::vector<TypedParam>&& splats)
23
0
        : Http::Request(std::move(request))
24
0
        , params_(std::move(params))
25
0
        , splats_(std::move(splats))
26
0
    { }
27
28
    bool Request::hasParam(const std::string& name) const
29
0
    {
30
0
        auto it = std::find_if(
31
0
            params_.begin(), params_.end(),
32
0
            [&](const TypedParam& param) { return param.name() == name; });
33
34
0
        return it != std::end(params_);
35
0
    }
36
37
    TypedParam Request::param(const std::string& name) const
38
0
    {
39
0
        auto it = std::find_if(
40
0
            params_.begin(), params_.end(),
41
0
            [&](const TypedParam& param) { return param.name() == name; });
42
43
0
        if (it == std::end(params_))
44
0
        {
45
0
            throw std::runtime_error("Unknown parameter");
46
0
        }
47
48
0
        return *it;
49
0
    }
50
51
    TypedParam Request::splatAt(size_t index) const
52
0
    {
53
0
        if (index >= splats_.size())
54
0
        {
55
0
            throw std::out_of_range("Request splat index out of range");
56
0
        }
57
0
        return splats_[index];
58
0
    }
59
60
0
    std::vector<TypedParam> Request::splat() const { return splats_; }
61
62
    std::regex SegmentTreeNode::multiple_slash = std::regex("//+", std::regex_constants::optimize);
63
64
    SegmentTreeNode::SegmentTreeNode()
65
2.38k
        : resource_ref_()
66
2.38k
        , fixed_()
67
2.38k
        , param_()
68
2.38k
        , optional_()
69
2.38k
        , splat_(nullptr)
70
2.38k
        , route_(nullptr)
71
2.38k
    {
72
2.38k
        std::shared_ptr<char> ptr(new char[1], std::default_delete<char[]>());
73
2.38k
        resource_ref_.swap(ptr);
74
2.38k
    }
75
76
    SegmentTreeNode::SegmentTreeNode(const std::shared_ptr<char>& resourceReference)
77
388k
        : resource_ref_(resourceReference)
78
388k
        , fixed_()
79
388k
        , param_()
80
388k
        , optional_()
81
388k
        , splat_(nullptr)
82
388k
        , route_(nullptr)
83
388k
    { }
84
85
    SegmentTreeNode::SegmentType
86
    SegmentTreeNode::getSegmentType(const std::string_view& fragment)
87
444k
    {
88
444k
        if (fragment.empty())
89
9.72k
            return SegmentType::Fixed;
90
91
434k
        auto optpos = fragment.find('?');
92
434k
        if (fragment[0] == ':')
93
344k
        {
94
344k
            if (optpos != std::string_view::npos)
95
8.34k
            {
96
8.34k
                if (optpos != fragment.length() - 1)
97
673
                {
98
673
                    throw std::runtime_error("? should be at the end of the string");
99
673
                }
100
7.67k
                return SegmentType::Optional;
101
8.34k
            }
102
336k
            return SegmentType::Param;
103
344k
        }
104
89.6k
        else if (fragment[0] == '*')
105
7.88k
        {
106
7.88k
            if (fragment.length() > 1)
107
600
            {
108
600
                throw std::runtime_error("Invalid splat parameter");
109
600
            }
110
7.28k
            return SegmentType::Splat;
111
7.88k
        }
112
113
81.7k
        if (optpos != std::string_view::npos)
114
2.78k
        {
115
2.78k
            throw std::runtime_error(
116
2.78k
                "Only optional parameters are currently supported");
117
2.78k
        }
118
119
79.0k
        return SegmentType::Fixed;
120
81.7k
    }
121
122
    std::string SegmentTreeNode::sanitizeResource(const std::string& path)
123
72.3k
    {
124
72.3k
        const auto& dup = std::regex_replace(path, SegmentTreeNode::multiple_slash,
125
72.3k
                                             std::string("/"));
126
72.3k
        if (dup[dup.length() - 1] == '/')
127
5.79k
        {
128
5.79k
            return dup.substr(1, dup.length() - 2);
129
5.79k
        }
130
66.5k
        return dup.substr(1);
131
72.3k
    }
132
133
    void SegmentTreeNode::addRoute(
134
        const std::string_view& path, const Route::Handler& handler,
135
        const std::shared_ptr<char>& resource_reference)
136
456k
    {
137
        // recursion to correct path segment
138
456k
        if (!path.empty())
139
420k
        {
140
420k
            const auto segment_delimiter = path.find('/');
141
            // current segment value
142
420k
            auto current_segment = path.substr(0, segment_delimiter);
143
            // complete child path (path without this segment)
144
            // if no '/' was found, it means that it is a leaf resource
145
420k
            const auto lower_path = (segment_delimiter == std::string_view::npos)
146
420k
                ? std::string_view { nullptr, 0 }
147
420k
                : path.substr(segment_delimiter + 1);
148
149
420k
            std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>>* collection = nullptr;
150
420k
            const auto fragmentType                                                            = getSegmentType(current_segment);
151
420k
            switch (fragmentType)
152
420k
            {
153
76.6k
            case SegmentType::Fixed:
154
76.6k
                collection = &fixed_;
155
76.6k
                break;
156
326k
            case SegmentType::Param:
157
326k
                collection = &param_;
158
326k
                break;
159
7.42k
            case SegmentType::Optional:
160
                // remove the trailing question mark
161
7.42k
                current_segment = current_segment.substr(0, current_segment.length() - 1);
162
7.42k
                collection      = &optional_;
163
7.42k
                break;
164
5.72k
            case SegmentType::Splat:
165
5.72k
                if (splat_ == nullptr)
166
4.68k
                {
167
4.68k
                    splat_ = std::make_shared<SegmentTreeNode>(resource_reference);
168
4.68k
                }
169
5.72k
                splat_->addRoute(lower_path, handler, resource_reference);
170
5.72k
                return;
171
420k
            }
172
173
            // if the segment tree nodes for the lower path does not exist
174
410k
            if (collection->count(current_segment) == 0)
175
384k
            {
176
                // first create it
177
384k
                collection->insert(std::make_pair(
178
384k
                    current_segment,
179
384k
                    std::make_shared<SegmentTreeNode>(resource_reference)));
180
384k
            }
181
410k
            collection->at(current_segment)
182
410k
                ->addRoute(lower_path, handler, resource_reference);
183
410k
        }
184
35.8k
        else
185
35.8k
        { // current path segment requested
186
35.8k
            if (route_ != nullptr)
187
12.7k
                throw std::runtime_error("Requested route already exist.");
188
23.1k
            route_ = std::make_shared<Route>(handler);
189
23.1k
        }
190
456k
    }
191
192
    bool Pistache::Rest::SegmentTreeNode::removeRoute(
193
        const std::string_view& path)
194
32.3k
    {
195
        // recursion to correct path segment
196
32.3k
        if (!path.empty())
197
23.8k
        {
198
23.8k
            const auto segment_delimiter = path.find('/');
199
            // current segment value
200
23.8k
            auto current_segment = path.substr(0, segment_delimiter);
201
            // complete child path (path without this segment)
202
            // if no '/' was found, it means that it is a leaf resource
203
23.8k
            const auto lower_path = (segment_delimiter == std::string_view::npos)
204
23.8k
                ? std::string_view { nullptr, 0 }
205
23.8k
                : path.substr(segment_delimiter + 1);
206
207
23.8k
            std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>>* collection = nullptr;
208
23.8k
            auto fragmentType                                                                  = getSegmentType(current_segment);
209
23.8k
            switch (fragmentType)
210
23.8k
            {
211
12.0k
            case SegmentType::Fixed:
212
12.0k
                collection = &fixed_;
213
12.0k
                break;
214
9.70k
            case SegmentType::Param:
215
9.70k
                collection = &param_;
216
9.70k
                break;
217
247
            case SegmentType::Optional:
218
                // remove the trailing question mark
219
247
                current_segment = current_segment.substr(0, current_segment.length() - 1);
220
247
                collection      = &optional_;
221
247
                break;
222
1.56k
            case SegmentType::Splat:
223
1.56k
                if (!splat_)
224
490
                    throw std::runtime_error("Requested route does not exist.");
225
1.07k
                return splat_->removeRoute(lower_path);
226
23.8k
            }
227
228
22.0k
            try
229
22.0k
            {
230
22.0k
                const bool removable = collection->at(current_segment)->removeRoute(lower_path);
231
22.0k
                if (removable)
232
6.15k
                {
233
6.15k
                    collection->erase(current_segment);
234
6.15k
                }
235
22.0k
            }
236
22.0k
            catch (const std::out_of_range&)
237
22.0k
            {
238
5.04k
                throw std::runtime_error("Requested route does not exist.");
239
5.04k
            }
240
22.0k
        }
241
8.50k
        else
242
8.50k
        { // current leaf requested
243
8.50k
            route_.reset();
244
8.50k
        }
245
15.6k
        return fixed_.empty() && param_.empty() && optional_.empty() && splat_ == nullptr && route_ == nullptr;
246
32.3k
    }
247
248
    std::tuple<std::shared_ptr<Route>, std::vector<TypedParam>,
249
               std::vector<TypedParam>>
250
    Pistache::Rest::SegmentTreeNode::findRoute(
251
        const std::string_view& path, std::vector<TypedParam>& params,
252
        std::vector<TypedParam>& splats) const
253
152k
    {
254
        // recursion to correct path segment
255
152k
        if (!path.empty())
256
137k
        {
257
137k
            const auto segment_delimiter = path.find('/');
258
            // current segment value
259
137k
            auto current_segment = path.substr(0, segment_delimiter);
260
            // complete child path (path without this segment)
261
            // if no '/' was found, it means that it is a leaf resource
262
137k
            const auto lower_path = (segment_delimiter == std::string_view::npos)
263
137k
                ? std::string_view { nullptr, 0 }
264
137k
                : path.substr(segment_delimiter + 1);
265
266
            // Check if it is a fixed route
267
137k
            if (fixed_.count(current_segment) != 0)
268
1.60k
            {
269
1.60k
                auto result = fixed_.at(current_segment)->findRoute(lower_path, params, splats);
270
1.60k
                auto route  = std::get<0>(result);
271
1.60k
                if (route != nullptr)
272
338
                    return result;
273
1.60k
            }
274
275
            // Check if it is a path param
276
136k
            for (const auto& param : param_)
277
112k
            {
278
112k
                std::string para_name { param.first.data(), param.first.length() };
279
112k
                std::string para_val { current_segment.data(), current_segment.length() };
280
112k
                params.emplace_back(para_name, para_val);
281
112k
                auto result = param.second->findRoute(lower_path, params, splats);
282
112k
                auto route  = std::get<0>(result);
283
112k
                if (route != nullptr)
284
2.18k
                    return result;
285
110k
                params.pop_back();
286
110k
            }
287
288
            // Check if it is an optional path param
289
134k
            for (const auto& optional : optional_)
290
10.5k
            {
291
10.5k
                std::string opt_name { optional.first.data(), optional.first.length() };
292
10.5k
                std::string opt_val { current_segment.data(), current_segment.length() };
293
10.5k
                params.emplace_back(opt_name, opt_val);
294
10.5k
                auto result = optional.second->findRoute(lower_path, params, splats);
295
296
10.5k
                auto route = std::get<0>(result);
297
10.5k
                if (route != nullptr)
298
528
                    return result;
299
9.99k
                params.pop_back();
300
                // try to find a route for lower path assuming that
301
                // this optional path param is not present
302
9.99k
                result = optional.second->findRoute(lower_path, params, splats);
303
304
9.99k
                route = std::get<0>(result);
305
9.99k
                if (route != nullptr)
306
0
                    return result;
307
9.99k
            }
308
309
            // Check if it is a splat
310
134k
            if (splat_ != nullptr)
311
6.13k
            {
312
6.13k
                std::string splat { current_segment.data(), current_segment.length() };
313
6.13k
                splats.emplace_back(splat, splat);
314
6.13k
                auto result = splat_->findRoute(lower_path, params, splats);
315
316
6.13k
                auto route = std::get<0>(result);
317
6.13k
                if (route != nullptr)
318
2.65k
                    return result;
319
3.47k
                splats.pop_back();
320
3.47k
            }
321
            // Requested route does not exists
322
131k
            return std::make_tuple(nullptr, std::vector<TypedParam>(),
323
131k
                                   std::vector<TypedParam>());
324
134k
        }
325
15.5k
        else
326
15.5k
        { // current leaf requested, or empty final optional
327
15.5k
            if (!optional_.empty())
328
4.58k
            {
329
                // in case of more than one optional at this point, as it is an
330
                // ambiguity, it is resolved by using the first optional
331
4.58k
                auto optional = optional_.begin();
332
                // std::string opt {optional->first.data(), optional->first.length()};
333
4.58k
                return optional->second->findRoute(path, params, splats);
334
4.58k
            }
335
10.9k
            else if (route_ == nullptr)
336
7.22k
            {
337
                // if we are here but route is null, we reached this point
338
                // trying to parse an optional, that was missing
339
7.22k
                return std::make_tuple(nullptr, std::vector<TypedParam>(),
340
7.22k
                                       std::vector<TypedParam>());
341
7.22k
            }
342
3.70k
            else
343
3.70k
            {
344
3.70k
                return std::make_tuple(route_, std::move(params), std::move(splats));
345
3.70k
            }
346
15.5k
        }
347
152k
    }
348
349
    std::tuple<std::shared_ptr<Route>, std::vector<TypedParam>,
350
               std::vector<TypedParam>>
351
    Pistache::Rest::SegmentTreeNode::findRoute(const std::string_view& path) const
352
6.98k
    {
353
6.98k
        std::vector<TypedParam> params;
354
6.98k
        std::vector<TypedParam> splats;
355
6.98k
        return findRoute(path, params, splats);
356
6.98k
    }
357
358
    namespace Private
359
    {
360
361
        RouterHandler::RouterHandler(const Rest::Router& router)
362
0
            : router(std::make_shared<Rest::Router>(router))
363
0
        { }
364
365
        RouterHandler::RouterHandler(std::shared_ptr<Rest::Router> router)
366
0
            : router(std::move(router))
367
0
        { }
368
369
        void RouterHandler::onRequest(const Http::Request& req,
370
                                      Http::ResponseWriter response)
371
0
        {
372
0
            PS_TIMEDBG_START_THIS;
373
0
            router->route(req, std::move(response));
374
0
        }
375
376
        void RouterHandler::onDisconnection(const std::shared_ptr<Tcp::Peer>& peer)
377
0
        {
378
0
            PS_TIMEDBG_START_THIS;
379
0
            router->disconnectPeer(peer);
380
0
        }
381
382
    } // namespace Private
383
384
    Router Router::fromDescription(const Rest::Description& desc)
385
0
    {
386
0
        Router router;
387
0
        router.initFromDescription(desc);
388
0
        return router;
389
0
    }
390
391
    std::shared_ptr<Private::RouterHandler> Router::handler() const
392
0
    {
393
0
        return std::make_shared<Private::RouterHandler>(*this);
394
0
    }
395
396
    std::shared_ptr<Private::RouterHandler>
397
    Router::handler(std::shared_ptr<Rest::Router> router)
398
0
    {
399
0
        return std::make_shared<Private::RouterHandler>(router);
400
0
    }
401
402
    void Router::initFromDescription(const Rest::Description& desc)
403
0
    {
404
0
        const auto& paths = desc.rawPaths();
405
0
        for (auto it = paths.flatBegin(), end = paths.flatEnd(); it != end; ++it)
406
0
        {
407
0
            const auto& paths_ = *it;
408
0
            for (const auto& path : paths_)
409
0
            {
410
0
                if (!path.isBound())
411
0
                {
412
0
                    std::ostringstream oss;
413
0
                    oss << "Path '" << path.value << "' is not bound";
414
0
                    throw std::runtime_error(oss.str());
415
0
                }
416
417
0
                addRoute(path.method, path.value, path.handler);
418
0
            }
419
0
        }
420
0
    }
421
422
    void Router::get(const std::string& resource, Route::Handler handler)
423
0
    {
424
0
        PS_TIMEDBG_START_THIS;
425
0
        addRoute(Http::Method::Get, resource, std::move(handler));
426
0
    }
427
428
    void Router::post(const std::string& resource, Route::Handler handler)
429
0
    {
430
0
        PS_TIMEDBG_START_THIS;
431
0
        addRoute(Http::Method::Post, resource, std::move(handler));
432
0
    }
433
434
    void Router::put(const std::string& resource, Route::Handler handler)
435
0
    {
436
0
        PS_TIMEDBG_START_THIS;
437
0
        addRoute(Http::Method::Put, resource, std::move(handler));
438
0
    }
439
440
    void Router::patch(const std::string& resource, Route::Handler handler)
441
0
    {
442
0
        PS_TIMEDBG_START_THIS;
443
0
        addRoute(Http::Method::Patch, resource, std::move(handler));
444
0
    }
445
446
    void Router::del(const std::string& resource, Route::Handler handler)
447
0
    {
448
0
        PS_TIMEDBG_START_THIS;
449
0
        addRoute(Http::Method::Delete, resource, std::move(handler));
450
0
    }
451
452
    void Router::options(const std::string& resource, Route::Handler handler)
453
0
    {
454
0
        PS_TIMEDBG_START_THIS;
455
0
        addRoute(Http::Method::Options, resource, std::move(handler));
456
0
    }
457
458
    void Router::removeRoute(Http::Method method, const std::string& resource)
459
0
    {
460
0
        if (resource.empty())
461
0
            throw std::runtime_error("Invalid zero-length URL.");
462
0
        auto& r              = routes[method];
463
0
        const auto sanitized = SegmentTreeNode::sanitizeResource(resource);
464
0
        const std::string_view path { sanitized.data(), sanitized.size() };
465
0
        r.removeRoute(path);
466
0
    }
467
468
    void Router::head(const std::string& resource, Route::Handler handler)
469
0
    {
470
0
        addRoute(Http::Method::Head, resource, std::move(handler));
471
0
    }
472
473
    void Router::addCustomHandler(Route::Handler handler)
474
0
    {
475
0
        customHandlers.push_back(std::move(handler));
476
0
    }
477
478
    void Router::addMiddleware(Route::Middleware middleware)
479
0
    {
480
0
        middlewares.push_back(std::move(middleware));
481
0
    }
482
483
    void Router::addDisconnectHandler(Route::DisconnectHandler handler)
484
0
    {
485
0
        disconnectHandlers.push_back(std::move(handler));
486
0
    }
487
488
    void Router::addNotFoundHandler(Route::Handler handler)
489
0
    {
490
0
        notFoundHandler = std::move(handler);
491
0
    }
492
493
    void Router::invokeNotFoundHandler(const Http::Request& req,
494
                                       Http::ResponseWriter resp) const
495
0
    {
496
0
        notFoundHandler(Rest::Request(req, std::vector<TypedParam>(),
497
0
                                      std::vector<TypedParam>()),
498
0
                        std::move(resp));
499
0
    }
500
501
    Route::Status Router::route(const Http::Request& request,
502
                                Http::ResponseWriter response) const
503
0
    {
504
0
        PS_TIMEDBG_START_THIS;
505
506
0
        const auto& resource = request.resource();
507
0
        if (resource.empty())
508
0
            throw std::runtime_error("Invalid zero-length URL.");
509
510
0
        auto req  = request;
511
0
        auto resp = response.clone();
512
513
0
        for (const auto& middleware : middlewares)
514
0
        {
515
0
            auto result = middleware(req, resp);
516
517
            // Handler returns true, go to the next piped handler, otherwise break and return
518
0
            if (!result)
519
0
                return Route::Status::Match;
520
0
        }
521
522
0
        std::tuple<std::shared_ptr<Route>, std::vector<TypedParam>,
523
0
                   std::vector<TypedParam>>
524
0
            result;
525
526
0
        const auto sanitized = SegmentTreeNode::sanitizeResource(resource);
527
0
        const std::string_view path { sanitized.data(), sanitized.size() };
528
529
0
        const auto routesIt = routes.find(req.method());
530
0
        if (routesIt != routes.end())
531
0
            result = routesIt->second.findRoute(path);
532
533
0
        auto route = std::get<0>(result);
534
0
        if (route != nullptr)
535
0
        {
536
0
            auto params = std::get<1>(result);
537
0
            auto splats = std::get<2>(result);
538
0
            route->invokeHandler(Request(std::move(req), std::move(params), std::move(splats)),
539
0
                                 std::move(resp));
540
0
            return Route::Status::Match;
541
0
        }
542
543
0
        for (const auto& handler : customHandlers)
544
0
        {
545
0
            auto cloned_resp     = response.clone();
546
0
            auto handler1 = handler(
547
0
                Request(req, std::vector<TypedParam>(), std::vector<TypedParam>()),
548
0
                std::move(cloned_resp));
549
0
            if (handler1 == Route::Result::Ok)
550
0
                return Route::Status::Match;
551
0
        }
552
553
        // No route or custom handler found. Let's walk through the
554
        // list of other methods and see if any of them support
555
        // this resource.
556
        // This will allow server to send a
557
        // HTTP 405 (method not allowed) response.
558
        // RFC 7231 requires HTTP 405 responses to include a list of
559
        // supported methods for the requested resource.
560
0
        std::vector<Http::Method> supportedMethods;
561
0
        for (auto& methods : routes)
562
0
        {
563
0
            if (methods.first == req.method())
564
0
                continue;
565
566
0
            auto res = methods.second.findRoute(path);
567
0
            auto rte = std::get<0>(res);
568
0
            if (rte != nullptr)
569
0
            {
570
0
                supportedMethods.push_back(methods.first);
571
0
            }
572
0
        }
573
574
0
        if (!supportedMethods.empty())
575
0
        {
576
0
            response.sendMethodNotAllowed(supportedMethods);
577
0
            return Route::Status::NotAllowed;
578
0
        }
579
580
0
        if (hasNotFoundHandler())
581
0
        {
582
0
            invokeNotFoundHandler(req, std::move(response));
583
0
        }
584
0
        else
585
0
        {
586
0
            response.send(Http::Code::Not_Found, "Could not find a matching route");
587
0
        }
588
0
        return Route::Status::NotFound;
589
0
    }
590
591
    void Router::addRoute(Http::Method method, const std::string& resource,
592
                          Route::Handler handler)
593
0
    {
594
0
        PS_TIMEDBG_START_THIS;
595
596
0
        if (resource.empty())
597
0
            throw std::runtime_error("Invalid zero-length URL.");
598
0
        auto& r              = routes[method];
599
0
        const auto sanitized = SegmentTreeNode::sanitizeResource(resource);
600
0
        std::shared_ptr<char> ptr(new char[sanitized.length()],
601
0
                                  std::default_delete<char[]>());
602
0
        std::memcpy(ptr.get(), sanitized.data(), sanitized.length());
603
0
        const std::string_view path { ptr.get(), sanitized.length() };
604
0
        r.addRoute(path, handler, ptr);
605
0
    }
606
607
    void Router::disconnectPeer(const std::shared_ptr<Tcp::Peer>& peer)
608
0
    {
609
0
        PS_TIMEDBG_START_THIS;
610
611
0
        for (const auto& handler : disconnectHandlers)
612
0
        {
613
0
            handler(peer);
614
0
        }
615
0
    }
616
617
    namespace Routes
618
    {
619
620
        void Get(Router& router, const std::string& resource, Route::Handler handler)
621
0
        {
622
0
            router.get(resource, std::move(handler));
623
0
        }
624
625
        void Post(Router& router, const std::string& resource, Route::Handler handler)
626
0
        {
627
0
            router.post(resource, std::move(handler));
628
0
        }
629
630
        void Put(Router& router, const std::string& resource, Route::Handler handler)
631
0
        {
632
0
            router.put(resource, std::move(handler));
633
0
        }
634
635
        void Patch(Router& router, const std::string& resource,
636
                   Route::Handler handler)
637
0
        {
638
0
            router.patch(resource, std::move(handler));
639
0
        }
640
641
        void Delete(Router& router, const std::string& resource,
642
                    Route::Handler handler)
643
0
        {
644
0
            router.del(resource, std::move(handler));
645
0
        }
646
647
        void Options(Router& router, const std::string& resource,
648
                     Route::Handler handler)
649
0
        {
650
0
            router.options(resource, std::move(handler));
651
0
        }
652
653
        void Remove(Router& router, Http::Method method, const std::string& resource)
654
0
        {
655
0
            router.removeRoute(method, resource);
656
0
        }
657
658
        void NotFound(Router& router, Route::Handler handler)
659
0
        {
660
0
            router.addNotFoundHandler(std::move(handler));
661
0
        }
662
663
        void Head(Router& router, const std::string& resource, Route::Handler handler)
664
0
        {
665
0
            router.head(resource, std::move(handler));
666
0
        }
667
668
    } // namespace Routes
669
} // namespace Pistache::Rest