Skip to content

Commit a6f738b

Browse files
authored
Make REST router more like a map and allow overwriting routes (#2403)
1 parent 400c479 commit a6f738b

File tree

5 files changed

+171
-24
lines changed

5 files changed

+171
-24
lines changed

docs/networking/rest-registry.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,26 +228,28 @@ private:
228228
229229
### Service Composition
230230
231+
Multiple services can be registered on a single registry. Each service's member endpoints are registered under the mount path, while the root endpoint reflects the last registered service.
232+
231233
```cpp
232234
int main() {
233235
glz::http_server server;
234-
236+
235237
// Multiple service instances
236238
UserService userService;
237239
ProductService productService;
238240
OrderService orderService;
239-
241+
240242
// Single registry for all services
241243
glz::registry<glz::opts{}, glz::REST> registry;
242-
244+
243245
// Register all services
244246
registry.on(userService);
245-
registry.on(productService);
247+
registry.on(productService);
246248
registry.on(orderService);
247-
249+
248250
// All endpoints available under /api
249251
server.mount("/api", registry.endpoints);
250-
252+
251253
server.bind(8080).with_signals();
252254
server.start();
253255
server.wait_for_signal();
@@ -257,6 +259,15 @@ int main() {
257259
Each registered service in this setup (`UserService`, `ProductService`, `OrderService`) needs its own
258260
`glz::meta<...>::value = glz::object(...)` declaration.
259261

262+
> [!NOTE]
263+
>
264+
> When composing multiple services, the root endpoint (e.g. `GET /api`) returns only the last registered service's data. If you need a combined root endpoint that merges all services into a single view, use `glz::merge` instead:
265+
>
266+
> ```cpp
267+
> auto merged = glz::merge{userService, productService, orderService};
268+
> registry.on(merged);
269+
> ```
270+
260271
## Customization
261272
262273
### Custom Serialization Options

docs/rpc/jsonrpc-registry.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,11 @@ Function pointers support:
314314

315315
> **Note:** Function pointers with more than one parameter are not supported. Use a struct parameter for multiple values.
316316
317-
## Using with `glz::merge`
317+
## Service Composition
318318

319-
Multiple objects can be merged into a single registry:
319+
Multiple services can be registered on a single registry by calling `on()` multiple times. Each service's member endpoints are all registered, while the root endpoint reflects the last registered service.
320+
321+
To combine multiple objects into a single merged view at the root path, use `glz::merge`:
320322

321323
```cpp
322324
struct sensors_t {

docs/rpc/repe-rpc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ See [REPE Buffer Handling](repe-buffer.md) for detailed documentation of these t
104104

105105
## Registering Multiple Objects with `glz::merge`
106106

107-
By default, when you register an object with `server.on(obj)`, the root path `""` returns that object's JSON representation. If you call `server.on()` multiple times with different objects, only the last registered object will be returned at the root path `""`.
107+
By default, when you register an object with `server.on(obj)`, the root path `""` returns that object's JSON representation. If you call `server.on()` multiple times with different objects, the last registered object will be returned at the root path `""`. Each service's member endpoints are all registered regardless.
108108

109109
To combine multiple objects into a single merged view at the root path, use `glz::merge`:
110110

include/glaze/net/http_router.hpp

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -770,15 +770,7 @@ namespace glz
770770

771771
// Optimization: for non-parameterized routes, store them directly
772772
if (path_str.find(':') == std::string::npos && path_str.find('*') == std::string::npos) {
773-
// Check for conflicts first
774-
auto& method_handlers = direct_routes[path_str];
775-
if (method_handlers.find(method) != method_handlers.end()) {
776-
throw std::runtime_error("Route conflict: handler already exists for " + std::string(to_string(method)) +
777-
" " + path_str);
778-
}
779-
780-
// Store the route directly
781-
method_handlers[method] = handle;
773+
direct_routes[path_str][method] = handle;
782774
return;
783775
}
784776

@@ -856,12 +848,6 @@ namespace glz
856848
}
857849
}
858850

859-
// Check for route conflict
860-
if (current->is_endpoint && current->handlers.find(method) != current->handlers.end()) {
861-
throw std::runtime_error("Route conflict: handler already exists for " + std::string(to_string(method)) +
862-
" " + path_str);
863-
}
864-
865851
// Mark as endpoint and set handler
866852
current->is_endpoint = true;
867853
current->handlers[method] = handle;

tests/networking_tests/http_router_test/http_router_test.cpp

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// For the license information refer to glaze.hpp
33

44
#include "glaze/net/http_router.hpp"
5+
#include "glaze/rpc/registry.hpp"
56

67
#include <cassert>
78
#include <regex>
@@ -432,4 +433,151 @@ suite path_param_decoding_tests = [] {
432433
};
433434
};
434435

436+
// Service composition test types (GitHub issue #2401)
437+
438+
struct Task
439+
{
440+
int id{};
441+
std::string title{};
442+
};
443+
444+
class TaskService
445+
{
446+
std::vector<Task> tasks_;
447+
448+
public:
449+
std::vector<Task> getAllTasks() { return tasks_; }
450+
};
451+
452+
template <>
453+
struct glz::meta<TaskService>
454+
{
455+
using T = TaskService;
456+
static constexpr auto value = object(&T::getAllTasks);
457+
};
458+
459+
struct Post
460+
{
461+
int id{};
462+
std::string title{};
463+
};
464+
465+
class PostService
466+
{
467+
std::vector<Post> posts_;
468+
469+
public:
470+
std::vector<Post> getAllPosts() { return posts_; }
471+
};
472+
473+
template <>
474+
struct glz::meta<PostService>
475+
{
476+
using T = PostService;
477+
static constexpr auto value = object(&T::getAllPosts);
478+
};
479+
480+
struct Config
481+
{
482+
int id{};
483+
std::string title{};
484+
};
485+
486+
class ConfigService
487+
{
488+
std::vector<Config> configs_;
489+
490+
public:
491+
std::vector<Config> getAllConfigs() { return configs_; }
492+
};
493+
494+
template <>
495+
struct glz::meta<ConfigService>
496+
{
497+
using T = ConfigService;
498+
static constexpr auto value = object(&T::getAllConfigs);
499+
};
500+
501+
suite route_replacement_tests = [] {
502+
"duplicate_direct_route_replaces_handler"_test = [] {
503+
glz::http_router router;
504+
505+
router.get("", [](const glz::request&, glz::response& res) { res.body("first"); });
506+
router.get("", [](const glz::request&, glz::response& res) { res.body("second"); });
507+
508+
auto [handler, params] = router.match(glz::http_method::GET, "");
509+
expect(handler != nullptr);
510+
511+
glz::request req{.method = glz::http_method::GET, .target = ""};
512+
glz::response res;
513+
handler(req, res);
514+
expect(res.response_body == "second") << "Last registered handler should win";
515+
};
516+
517+
"duplicate_route_different_methods_coexist"_test = [] {
518+
glz::http_router router;
519+
520+
router.get("/path", [](const glz::request&, glz::response& res) { res.body("get"); });
521+
router.put("/path", [](const glz::request&, glz::response& res) { res.body("put"); });
522+
router.get("/path", [](const glz::request&, glz::response& res) { res.body("get2"); });
523+
524+
auto [get_handler, get_params] = router.match(glz::http_method::GET, "/path");
525+
auto [put_handler, put_params] = router.match(glz::http_method::PUT, "/path");
526+
expect(get_handler != nullptr);
527+
expect(put_handler != nullptr);
528+
529+
glz::request req{.method = glz::http_method::GET, .target = "/path"};
530+
glz::response get_res, put_res;
531+
get_handler(req, get_res);
532+
put_handler(req, put_res);
533+
expect(get_res.response_body == "get2") << "Replaced GET should use new handler";
534+
expect(put_res.response_body == "put") << "PUT should be unchanged";
535+
};
536+
};
537+
538+
suite service_composition_tests = [] {
539+
"multiple_services_on_single_registry"_test = [] {
540+
TaskService taskService;
541+
PostService postService;
542+
ConfigService configService;
543+
544+
glz::registry<glz::opts{}, glz::REST> registry;
545+
546+
// Registering multiple services should not produce errors
547+
registry.on(taskService);
548+
registry.on(postService);
549+
registry.on(configService);
550+
551+
auto check = [&](const std::string& path, bool should_exist) {
552+
auto [handler, params] = registry.endpoints.match(glz::http_method::GET, path);
553+
expect((handler != nullptr) == should_exist) << path;
554+
};
555+
556+
// Each service's endpoints should be accessible
557+
check("/getAllTasks", true);
558+
check("/getAllPosts", true);
559+
check("/getAllConfigs", true);
560+
561+
// Root endpoint should exist (last registered service wins)
562+
check("", true);
563+
};
564+
565+
"single_service_root_endpoint_works"_test = [] {
566+
TaskService taskService;
567+
568+
glz::registry<glz::opts{}, glz::REST> registry;
569+
registry.on(taskService);
570+
571+
// Root should have both GET and PUT
572+
auto [get_handler, get_params] = registry.endpoints.match(glz::http_method::GET, "");
573+
auto [put_handler, put_params] = registry.endpoints.match(glz::http_method::PUT, "");
574+
expect(get_handler != nullptr);
575+
expect(put_handler != nullptr);
576+
577+
// Sub-endpoint should work
578+
auto [tasks_handler, tasks_params] = registry.endpoints.match(glz::http_method::GET, "/getAllTasks");
579+
expect(tasks_handler != nullptr);
580+
};
581+
};
582+
435583
int main() {}

0 commit comments

Comments
 (0)