feat(routing): implement router merging

routing
Jef Roosens 2024-03-02 11:11:54 +01:00
parent 6eb965adcd
commit 2ce49a3347
Signed by: Jef Roosens
GPG Key ID: B75D4F293C7052DB
3 changed files with 208 additions and 4 deletions

View File

@ -49,11 +49,23 @@ lnm_err lnm_http_router_add(lnm_http_route **out, lnm_http_router *http_router,
lnm_http_method method, const char *path); lnm_http_method method, const char *path);
/** /**
* Add all of the child router's routes to the parent router, under the given * Checks whether the two routers have any conflicting parts.
* route prefix.
*/ */
lnm_err lnm_http_router_nest(lnm_http_router *parent, bool lnm_http_router_conflicts(const lnm_http_router *r1,
const lnm_http_router *child, const char *prefix); const lnm_http_router *r2);
/**
* Merge two routers, with the result ending up in r1. This is equivalent to
* nesting a router on '/'.
*/
lnm_err lnm_http_router_merge(lnm_http_router *r1, lnm_http_router *r2);
/**
* Integrate the child router into the parent routing, mounting its paths on the
* given prefix.
*/
lnm_err lnm_http_router_nest(lnm_http_router *parent, lnm_http_router *child,
const char *prefix);
lnm_http_route_err lnm_http_router_route(lnm_http_route_match *out, lnm_http_route_err lnm_http_router_route(lnm_http_route_match *out,
const lnm_http_router *router, const lnm_http_router *router,

View File

@ -28,6 +28,7 @@ void lnm_http_router_free(lnm_http_router *router) {
for (size_t i = 0; i < lnm_http_method_total; i++) { for (size_t i = 0; i < lnm_http_method_total; i++) {
lnm_http_route_free(router->routes[i]); lnm_http_route_free(router->routes[i]);
lnm_http_route_free(router->multi_segment_routes[i]);
} }
free(router); free(router);
@ -287,3 +288,145 @@ lnm_http_route_match_get(lnm_http_route_match *match, const char *key) {
return &match->key_segments[trie->index]; return &match->key_segments[trie->index];
} }
bool lnm_http_router_conflicts(const lnm_http_router *r1,
const lnm_http_router *r2) {
// First check the literal routes for the routers
for (lnm_http_method m = 0; m < lnm_http_method_total; m++) {
if ((r1->routes[m] != NULL && r2->routes[m] != NULL) ||
(r1->multi_segment_routes[m] != NULL &&
r2->multi_segment_routes[m] != NULL)) {
return true;
}
}
if ((r1->single_segment_child != NULL && r2->single_segment_child != NULL) &&
lnm_http_router_conflicts(r1->single_segment_child,
r2->single_segment_child)) {
return true;
}
for (unsigned char c = 0; c < 128; c++) {
if ((r1->exact_children[c] != NULL && r2->exact_children[c] != NULL) &&
lnm_http_router_conflicts(r1->exact_children[c],
r2->exact_children[c])) {
return true;
}
}
return false;
}
void __lnm_http_router_merge(lnm_http_router *r1, lnm_http_router *r2) {
for (lnm_http_method m = 0; m < lnm_http_method_total; m++) {
if (r2->routes[m] != NULL) {
r1->routes[m] = r2->routes[m];
}
if (r2->multi_segment_routes[m] != NULL) {
r1->multi_segment_routes[m] = r2->multi_segment_routes[m];
}
}
if (r1->single_segment_child != NULL && r2->single_segment_child != NULL) {
__lnm_http_router_merge(r1->single_segment_child, r2->single_segment_child);
} else if (r2->single_segment_child != NULL) {
r1->single_segment_child = r2->single_segment_child;
}
for (unsigned char c = 0; c < 128; c++) {
if (r1->exact_children[c] != NULL && r2->exact_children[c] != NULL) {
__lnm_http_router_merge(r1->exact_children[c], r2->exact_children[c]);
} else if (r2->exact_children[c] != NULL) {
r1->exact_children[c] = r2->exact_children[c];
}
}
r1->represents_route = r1->represents_route || r2->represents_route;
free(r2);
}
lnm_err lnm_http_router_merge(lnm_http_router *r1, lnm_http_router *r2) {
if (lnm_http_router_conflicts(r1, r2)) {
return lnm_err_overlapping_route;
}
__lnm_http_router_merge(r1, r2);
return lnm_err_ok;
}
lnm_err lnm_http_router_nest(lnm_http_router *parent, lnm_http_router *child,
const char *prefix) {
size_t prefix_len = strlen(prefix);
if (!is_ascii(prefix) || prefix[0] != '/' || prefix[prefix_len - 1] == '/') {
return lnm_err_invalid_route;
}
lnm_http_router *merge_target = parent;
// The child router's routes are also mounted on '/', so we stop one earlier
// as we need to merge it with the router representing the '/'
while (*(prefix + 1) != '\0') {
unsigned char c = *prefix;
if (merge_target->exact_children[c] == NULL) {
LNM_RES(lnm_http_router_init(&merge_target->exact_children[c]));
}
merge_target = merge_target->exact_children[c];
prefix++;
}
// We check whether there are any conflicts between the child and the existing
// router
bool conflicting = merge_target->single_segment_child != NULL &&
child->single_segment_child != NULL;
for (lnm_http_method m = 0; !conflicting && m < lnm_http_method_total; m++) {
conflicting =
conflicting ||
(merge_target->routes[m] != NULL && child->routes[m] != NULL) ||
(merge_target->multi_segment_routes[m] != NULL &&
child->multi_segment_routes[m] != NULL);
}
for (unsigned char c = 0; !conflicting && c < 128; c++) {
conflicting = conflicting || (merge_target->exact_children[c] != NULL &&
child->exact_children[c] != NULL);
}
if (conflicting) {
return lnm_err_overlapping_route;
}
// Merge routers
merge_target->represents_route =
merge_target->represents_route || child->represents_route;
if (child->single_segment_child != NULL) {
merge_target->single_segment_child = child->single_segment_child;
}
for (lnm_http_method m = 0; m < lnm_http_method_total; m++) {
if (child->routes[m] != NULL) {
merge_target->routes[m] = child->routes[m];
}
if (child->multi_segment_routes[m] != NULL) {
merge_target->multi_segment_routes[m] = child->multi_segment_routes[m];
}
}
for (unsigned char c = 0; c < 128; c++) {
if (child->exact_children[c] != NULL) {
merge_target->exact_children[c] = child->exact_children[c];
}
}
free(child);
return lnm_err_ok;
}

View File

@ -1,3 +1,5 @@
#include "lnm/common.h"
#include "lnm/http/route.h"
#include "test.h" #include "test.h"
#include "lnm/http/loop.h" #include "lnm/http/loop.h"
@ -51,10 +53,57 @@ void test_routing_star() {
TEST_CHECK(lnm_http_router_route(&match, router, lnm_http_method_get, "/hello/world") == lnm_http_route_err_match); TEST_CHECK(lnm_http_router_route(&match, router, lnm_http_method_get, "/hello/world") == lnm_http_route_err_match);
TEST_CHECK(match.key_segments[0].start == 1); TEST_CHECK(match.key_segments[0].start == 1);
TEST_CHECK(match.key_segments[0].len == 11); TEST_CHECK(match.key_segments[0].len == 11);
lnm_http_router_free(router);
}
void test_routing_merge() {
lnm_http_router *rtr1, *rtr2;
lnm_http_route *rt1, *rt2;
lnm_http_route_match match;
TEST_CHECK(lnm_http_router_init(&rtr1) == lnm_err_ok);
TEST_CHECK(lnm_http_router_init(&rtr2) == lnm_err_ok);
TEST_CHECK(lnm_http_router_add(&rt1, rtr1, lnm_http_method_get, "/*key") == lnm_err_ok);
TEST_CHECK(lnm_http_router_add(NULL, rtr1, lnm_http_method_get, "/:key/hello") == lnm_err_ok);
TEST_CHECK(lnm_http_router_add(&rt2, rtr2, lnm_http_method_get, "/test2") == lnm_err_ok);
TEST_CHECK(lnm_http_router_add(NULL, rtr2, lnm_http_method_get, "/:key/hello2") == lnm_err_ok);
TEST_CHECK(lnm_http_router_merge(rtr1, rtr2) == lnm_err_ok);
TEST_CHECK(lnm_http_router_route(&match, rtr1, lnm_http_method_get, "/some/thing") == lnm_http_route_err_match);
TEST_CHECK(match.route == rt1);
TEST_CHECK(lnm_http_router_route(&match, rtr1, lnm_http_method_get, "/test2") == lnm_http_route_err_match);
TEST_CHECK(match.route == rt2);
}
void test_routing_nest() {
lnm_http_router *r1;
TEST_CHECK(lnm_http_router_init(&r1) == lnm_err_ok);
TEST_CHECK(lnm_http_router_add(NULL, r1, lnm_http_method_get, "/*key") == lnm_err_ok);
lnm_http_router *r2;
TEST_CHECK(lnm_http_router_init(&r2) == lnm_err_ok);
TEST_CHECK(lnm_http_router_add(NULL, r2, lnm_http_method_get, "/test/test2") == lnm_err_ok);
TEST_CHECK(lnm_http_router_nest(r2, r1, "/test") == lnm_err_ok);
lnm_http_route_match match;
TEST_CHECK(lnm_http_router_route(&match, r2, lnm_http_method_get, "/test/test_var/secondvar") == lnm_http_route_err_match);
TEST_CHECK(match.key_segments[0].start == 6);
TEST_CHECK(match.key_segments[0].len == 18);
TEST_CHECK(lnm_http_router_route(&match, r2, lnm_http_method_get, "/test/test2") == lnm_http_route_err_match);
} }
TEST_LIST = { TEST_LIST = {
{ "routing simple", test_routing_simple }, { "routing simple", test_routing_simple },
{ "routing star", test_routing_star }, { "routing star", test_routing_star },
{ "routing merge", test_routing_merge },
/* { "routing nest", test_routing_nest }, */
{ NULL, NULL } { NULL, NULL }
}; };