Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A route handler for Facil.io #100

Open
epixoip opened this issue Mar 1, 2021 · 0 comments
Open

A route handler for Facil.io #100

epixoip opened this issue Mar 1, 2021 · 0 comments

Comments

@epixoip
Copy link

epixoip commented Mar 1, 2021

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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant