Skip to content

A route handler for Facil.io #100

@epixoip

Description

@epixoip

I saw that a generic HTTP routing helper library for the http_s struct was on your wishlist. I've been developing a collection of higher-level abstractions for Facil.io called Sencillo, which adds (among other things) a route handler to Facil.io.

Sencillo's route handler does quite a bit that is far outside the scope of Facil.io (https://github.com/epixoip/sencillo/blob/main/sencillo.h#L1030-L1166), therefore a modified implementation that is more inline with Facil.io's goals is provided below for your consideration:

bool http_path_matches(http_s *request, char *__route) {
    // make a mutable copy of the route
    char *route = strdup(__route);

    if (!route) {
        FIO_LOG_ERROR("Failed to allocate memory!");
        return false;
    }

    char *path = fiobj_obj2cstr(request->path).data;

    // truncate the path at the query string delimiter,
    // as we only care about the path itself and the 
    // query string is parsed by http_parse_query()
    path = strtok(path, "?");

    // does the route contain any inline path variables?
    if (strchr(route, ':') == null) {
        // no - perform direct string comparison
        if (strcmp(route, path) == 0) {
            free(route);
            return true;
        } else {
            free(route);
            return false;
        }
    }

    int route_part_cnt = 0;
    int path_part_cnt  = 0;

    // count the number of parts in the route and the path
    for (int i = 0; route[i]; route[i] == '/' ? route_part_cnt++, i++ : i++);
    for (int i = 0; path[i];  path[i]  == '/' ? path_part_cnt++,  i++ : i++);

    // do we have an equal number of parts?
    if (route_part_cnt != path_part_cnt) {
        return false;
    }

    char *route_parts[route_part_cnt];
    char *path_parts[path_part_cnt];

    char *route_remaining;
    char *path_remaining;

    int matches = 0;

    // loop through each part

    char *route_next_part = strtok_r(route, "/", &route_remaining);
    char *path_next_part  = strtok_r(path,  "/", &path_remaining);

    while (route_next_part && path_next_part) {
        // if the route part is an inline variable, extract the variable name and its value
        if (route_next_part[0] == ':') {
            route_parts[matches]  = route_next_part + 1;
            path_parts[matches++] = path_next_part;
        } else {
            // the route part is literal, does it match the path part?
            if (strcmp(route_next_part, path_next_part)) {
                free(route);
                return false;
            }
        }

        route_next_part = strtok_r(null, "/", &route_remaining);
        path_next_part  = strtok_r(null, "/", &path_remaining);
    }

    free(route);

    // add the inline path variable names and values to the request params
    for (int i = 0; i < matches; i++) {
        http_add2hash(request->params, route_parts[i], strlen(route_parts[i]), path_parts[i], strlen(path_parts[i]), 1);
    }

    return true;
}

There are a couple ways you could implement http_path_matches depending on the desired experience.

First, you could create macros for simple conditionals:

#define http_route_get(_h, _route) \
    if (strcmp(fiobj_obj2cstr(request->method).data, "GET") == 0 && http_path_matches(_h, _route))

#define http_route_post(_h, _route) \
    if (strcmp(fiobj_obj2cstr(request->method).data, "POST") == 0 && http_path_matches(_h, _route))

#define http_route_put(_h, _route) \
    if (strcmp(fiobj_obj2cstr(request->method).data, "PUT") == 0 && http_path_matches(_h, _route))

#define http_route_delete(_h, _route) \
    if (strcmp(fiobj_obj2cstr(request->method).data, "DELETE") == 0 && http_path_matches(_h, _route))

Use of these macros would be straightforward:

static void on_http_request(http_s *h) {
    http_parse_query(h);
    http_parse_body(h);

    http_route_get(h, "/user/:id") {
        FIOBJ id_str = fiobj_str_new("id", 2);
        FIOBJ id_val = fiobj_hash_get(request->params, id_str);

        char *id = fiobj_obj2cstr(id_val).data;
        
        char greeting[32];
        snprintf(greeting, sizeof greeting, "Welcome user %s!", id);

        fiobj_free(id_str);
        fiobj_free(id_val);

        http_send_body(h, greeting, strlen(greeting));
        return;
    }
}

Another way would be to create macros that map each route to a function:

#define http_route_handler(_h, _method, _route, _func) {                                                \
    if (strcmp(fiobj_obj2cstr(request->method).data, _method) == 0 && http_path_matches(_h, _route)) {  \
        FIO_LOG_DEBUG("Matched route %s %s", _method, _route);                                          \
        _func(_h);                                                                                      \
        return;                                                                                         \
    }                                                                                                   \
}

#define http_route_get(_h, _route, _func) \
    http_route_handler(_h, "GET", _route, _func)

#define http_route_post(_h, _route, _func) \
    http_route_handler(_h, "POST", _route, _func)

#define http_route_put(_h, _route, _func) \
    http_route_handler(_h, "PUT", _route, _func)

#define http_route_delete(_h, _route, _func) \
    http_route_handler(_h, "DELETE", _route, _func)

Which could simply be incorporated as:

static void on_http_request(http_s *h) {
    http_parse_query(h);
    http_parse_body(h);

    http_route_get(h, "/user/:id", getUserById);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions