Skip to main content

c_src/libwrouter/src/builder.c

#include "token.h"
#include "graph.h"
#include "wrouter.h"
#include "router.h"
#include "builder.h"
#include "prelexer.h"
#include "symbol.h"
#include "segment.h"
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <limits.h>

static inline void builder_retain(const wrouter_builder_t *builder, const wrouter_route_t *route)
{
    if (builder->retain != NULL)
        builder->retain(route->ctx);
}

static wrouter_route_t *builder_terminate(wrouter_builder_t *builder, wrouter_route_t route)
{
    wrouter_route_t *terminal = calloc(1, sizeof(*terminal));
    if (terminal == NULL)
        return NULL;

    *terminal = route;

    builder_retain(builder, terminal);

    return terminal;
}

/**
 * Add a route to the route tree.
 */
static wrouter_error_t builder_add_route(wrouter_builder_t *builder, const char *pattern,
                                         wrouter_route_t route)
{
    wrouter_error_t err = 0;
    token_t tok;
    prelexer_t lx = { 0 };
    segment_t *cur = builder->root;

    if (builder->corrupted)
        return WROUTER_ERR_BUILDER_CORRUPTED;

    prelexer_init(&lx, builder->param_syntax);
    prelexer_load(&lx, pattern);

    for (;;) {
        tok = prelexer_next(&lx);

        switch (tok.type) {
            case TOKEN_END:
                // Check for duplicate routes.
                if (cur->terminal != NULL)
                    return WROUTER_ERR_DUPLICATE_ROUTE;

                // TERMINATE!
                // Append terminal route to the end segment.
                cur->terminal = builder_terminate(builder, route);
                if (cur->terminal == NULL)
                    return WROUTER_ERR_NO_MEMORY;

                return WROUTER_OK;

            case TOKEN_LITERAL: {
                // Literals are incompatible with parameters.
                if (cur->spec_type == SPEC_PARAM)
                    return WROUTER_ERR_LITERAL_CONFLICTS_WITH_PARAM;

                // Check for an existing child.
                segment_t *child = segment_find_child_by_token(cur, tok);

                // If there is not an equivalent literal child of this segment, create one.
                if (child == NULL) {

                    child = segment_create(&builder->literals, &tok, &err);
                    if (err)
                        return err;

                    err = segment_append_child(cur, child);
                    if (err) {
                        free(child);
                        return err;
                    }
                }

                cur = child;
                break;
            }

            case TOKEN_PARAM: {
                // Parameters are incompatible with wildcards.
                if (cur->spec_type == SPEC_WILDCARD)
                    return WROUTER_ERR_PARAM_CONFLICTS_WITH_WILDCARD;

                // Parameters are incompatible with literals.
                if (cur->child_count)
                    return WROUTER_ERR_PARAM_CONFLICTS_WITH_LITERAL;

                // Create a parameter on this segment if one does not already exist.
                if (cur->spec_type == SPEC_NONE) {

                    // Create segment.
                    cur->spec_type = SPEC_PARAM;
                    cur->special.param = segment_create(&builder->params, &tok, &err);
                    if (cur->special.param == NULL)
                        return err;

                } else if (!token_matches_segment(tok, cur->special.param)) {

                    // If a parameter is already assigned, it should have the same name.
                    return WROUTER_ERR_PARAM_NAME_MISMATCH;
                }

                cur = cur->special.param;
                break;
            }

            case TOKEN_WILDCARD: {
                // Check that a wildcard is not already assigned.
                if (cur->spec_type == SPEC_WILDCARD)
                    return WROUTER_ERR_DUPLICATE_ROUTE;

                // Wildcards are incompatible with parameters.
                if (cur->spec_type == SPEC_PARAM)
                    return WROUTER_ERR_WILDCARD_CONFLICTS_WITH_PARAM;

                // Wildcards must be at the end of the pattern string.
                tok = prelexer_next(&lx);
                if (tok.type != TOKEN_END)
                    return WROUTER_ERR_WILDCARD_NOT_FINAL;

                // TERMINATE!
                // Append wildcard terminal route.
                cur->spec_type = SPEC_WILDCARD;
                cur->special.wildcard = builder_terminate(builder, route);
                if (cur->special.wildcard == NULL)
                    return WROUTER_ERR_NO_MEMORY;

                return WROUTER_OK;
            }

            case TOKEN_TRAILING: {
                // Check that a trailing-slash is not already assigned.
                if (cur->trailing != NULL)
                    return WROUTER_ERR_DUPLICATE_ROUTE;

                // TERMINATE!
                // Append trailing-slash terminal route.
                cur->trailing = builder_terminate(builder, route);
                if (cur->trailing == NULL)
                    return WROUTER_ERR_NO_MEMORY;

                return WROUTER_OK;
            }

            case TOKEN_ILLEGAL:
            default:
                return WROUTER_ERR_ILLEGAL_PATTERN;
        }
    }
}

static void builder_release(wrouter_builder_t *builder)
{
    if (builder->release == NULL)
        return;

    // Release all route contexts in the tree.
    segment_release(builder->root, builder->release);

    // Release fallback route context.
    builder->release(builder->fallback.ctx);
}

/**
 * Create a route builder.
 *
 * Use the builder to create a router tree by adding routes to it. Then compile
 * the tree into a router graph. After this, free the builder.
 *
 * The builder is not thread-safe.
 */
wrouter_builder_t *wrouter_builder_create(const wrouter_options_t options)
{
    wrouter_builder_t *builder;

    // Allocate builder.
    builder = calloc(1, sizeof(*builder));
    if (builder == NULL)
        return NULL;

    // Copy options.
    builder->param_syntax = options.param_syntax;
    builder->fallback.handler = options.fallback_handler;
    builder->fallback.ctx = options.fallback_ctx;
    builder->retain = options.retain;
    builder->release = options.release;

    // Retain fallback context.
    builder_retain(builder, &builder->fallback);

    // Create root node.
    builder->root = calloc(1, sizeof(segment_t));
    if (builder->root == NULL)
        goto failure;

    return builder;

failure:
    // Use the normal free path to release any retained fallback context.
    wrouter_builder_free(builder);
    return NULL;
}

/**
 * Add a route handler to the route tree.
 */
wrouter_error_t wrouter_add_handler(wrouter_builder_t *builder, const char *pattern,
                                    wrouter_handler_fn handler)
{
    wrouter_route_t route = {
        .handler = handler,
        .ctx = NULL,
    };

    return wrouter_add_route(builder, pattern, route);
}

/**
 * Add a route handler and context to the route tree.
 */
wrouter_error_t wrouter_add_handler_ctx(wrouter_builder_t *builder, const char *pattern,
                                        wrouter_handler_fn handler, const void *ctx)
{
    wrouter_route_t route = {
        .handler = handler,
        .ctx = ctx,
    };

    return wrouter_add_route(builder, pattern, route);
}

wrouter_error_t wrouter_add_context(wrouter_builder_t *builder, const char *pattern,
                                    const void *ctx)
{
    wrouter_route_t route = {
        .handler = NULL,
        .ctx = ctx,
    };

    return wrouter_add_route(builder, pattern, route);
}

wrouter_error_t wrouter_add_route(wrouter_builder_t *builder, const char *pattern,
                                  wrouter_route_t route)
{
    wrouter_error_t err = builder_add_route(builder, pattern, route);

    if (err != WROUTER_OK) {
        builder->corrupted = true;
    }

    return err;
}

/**
 * Free the route tree builder.
 *
 * After compiling the router, there is no need for the builder, and it should
 * therefore be freed to save memory.
 */
void wrouter_builder_free(wrouter_builder_t *builder)
{
    if (builder == NULL)
        return;

    // Release all route contexts.
    builder_release(builder);

    // Free literal and parameter symbol tables.
    symbol_table_free(&builder->literals);
    symbol_table_free(&builder->params);

    // Free all segments.
    segment_free(builder->root);

    // Free the builder.
    free(builder);
}

void wrouter_builder_destroy(wrouter_builder_t **bpp)
{
    if (bpp == NULL)
        return;

    if (*bpp == NULL)
        return;

    // Release all route contexts.
    wrouter_builder_free(*bpp);

    *bpp = NULL;
}