view seobeo/s_router.c @ 91:19cccf6e866a

Added Epi photo reels.
author June Park <parkjune1995@gmail.com>
date Thu, 01 Jan 2026 16:34:51 -0800
parents 5710108c949e
children 655ea0b661fd
line wrap: on
line source

#include "seobeo/seobeo.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

struct Seobeo_Route_Struct {
  char *method; // "GET", "POST", "PUT", "DELETE"
  char *path_pattern; // "/v1/users/:id/posts/:post_id"
  Seobeo_Route_Handler handler;

  // Pre-parsed path segments for efficient matching
  char **path_segments; // ["v1", "users", ":id", "posts", ":post_id"]
  boolean *is_param; // [false, false, true, false, true]
  size_t segment_count;
};

static Seobeo_Route *g_routes = NULL;

void Seobeo_Router_Init()
{
  Dowa_Array_Reserve(g_routes, 20);
}

void Seobeo_Router_Register(const char *method, const char *path_pattern, Seobeo_Route_Handler handler)
{
  Seobeo_Route route = {0};

  route.method = strdup(method);
  route.path_pattern = strdup(path_pattern);
  route.handler = handler;
  route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL);
  route.segment_count = Dowa_Array_Length(route.path_segments);
  route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count);

  for (size_t i = 0; i < route.segment_count; i++)
    route.is_param[i] = (route.path_segments[i][0] == ':');

  Dowa_Array_Push(g_routes, route);
}

// Match route and extract path parameters
static boolean match_route_and_extract(
  Seobeo_Route *route,
  const char *request_path,
  Seobeo_Request_Entry **pp_request_map,
  Dowa_Arena *p_arena)
{
  Dowa_Arena *p_temp_arena = Dowa_Arena_Create(1024);
  char **request_segments = Dowa_String_Split(request_path, "/", strlen(request_path), 1, p_temp_arena);
  size_t request_segment_count = Dowa_Array_Length(request_segments);
  // Check segment count matches
  if (request_segment_count != route->segment_count)
  {
    Dowa_Arena_Free(p_temp_arena);
    return FALSE;
  }

  for (size_t i = 0; i < route->segment_count; i++)
  {
    // parameters
    if (route->is_param[i])
    {
      char *param_name = route->path_segments[i];  // e.g., ":id"
      char *param_value = request_segments[i];    // e.g., "123"

      // Should Copy to arena 
      char *key = Dowa_String_Copy_Arena(param_name, p_arena);
      char *value = Dowa_String_Copy_Arena(param_value, p_arena);
      Dowa_HashMap_Push_Arena(*pp_request_map, key, value, p_arena);
    }
    else
    {
      // Does not match.
      if (strcmp(route->path_segments[i], request_segments[i]) != 0)
      {
        Dowa_Arena_Free(p_temp_arena);
        return FALSE;
      }
    }
  }
  
  Dowa_Arena_Free(p_temp_arena);
  return TRUE;
}

Seobeo_Route_Handler Seobeo_Router_Find_Handler(const char *method,
                         const char *path,
                         Seobeo_Request_Entry **pp_request_map,
                         Dowa_Arena *p_arena) {
  if (g_routes == NULL)
  {
    return NULL;
  }

  size_t route_count = Dowa_Array_Length(g_routes);
  for (size_t i = 0; i < route_count; i++)
  {
    Seobeo_Route *route = &g_routes[i];
    if (strcmp(route->method, method) != 0)
    {
      continue;
    }

    if (match_route_and_extract(route, path, pp_request_map, p_arena))
    {
      return route->handler;
    }
  }

  return NULL;
}

void Seobeo_Router_Send_Response(
    Seobeo_Handle *p_handle,
    Seobeo_Request_Entry *p_response_map,
    Dowa_Arena *p_arena)
{
  if (p_response_map == NULL)
  {
    char *header = Dowa_Arena_Allocate(p_arena, 1024);
    Seobeo_Web_Header_Generate(header, HTTP_INTERNAL_ERROR, "text/plain", 21);
    Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header));
    Seobeo_Handle_Queue(p_handle, (uint8_t*)"Internal Server Error", 21);
    Seobeo_Handle_Flush(p_handle);
    return;
  }

  // Header 
  int status = HTTP_OK;
  void *p_status_kv = Dowa_HashMap_Get_Ptr(p_response_map, "status");
  if (p_status_kv)
  {
    const char *status_str = ((Seobeo_Request_Entry*)p_status_kv)->value;
    status = atoi(status_str);
  }

  // Body 
  const char *body = "";
  void *p_body_kv = Dowa_HashMap_Get_Ptr(p_response_map, "body");
  if (p_body_kv)
  {
    body = ((Seobeo_Request_Entry*)p_body_kv)->value;
  }

  // Default text plain
  const char *content_type = "text/html";
  void *p_content_type_kv = Dowa_HashMap_Get_Ptr(p_response_map, "content-type");
  if (p_content_type_kv)
  {
    content_type = ((Seobeo_Request_Entry*)p_content_type_kv)->value;
  }

  // All other types are default thoguht to be headers.
  char *header = Dowa_Arena_Allocate(p_arena, 4096);
  Seobeo_Web_Header_Generate(header, status, content_type, strlen(body));
  for (int i = 0; i < Dowa_Array_Length(p_response_map); i++)
  {
    if (
      strstr(p_response_map[i].key, "status") ||
      strstr(p_response_map[i].key, "body") ||
      strstr(p_response_map[i].key, "content-type")
    )
      continue;

    int32 current_header_len = strlen(header);
    char *temp = malloc(sizeof(char) * 1024);
    sprintf(temp, "%s: %s\r\n\r\n", p_response_map[i].key, p_response_map[i].value);
    memcpy(&header[current_header_len - 2 /* \r\n */], temp, strlen(temp));
    free(temp);
  }

  Seobeo_Handle_Queue(p_handle, (uint8_t*)header, strlen(header));
  Seobeo_Handle_Queue(p_handle, (uint8_t*)body, strlen(body));
  Seobeo_Handle_Flush(p_handle);
}

void Seobeo_Router_Destroy()
{
  if (g_routes == NULL)
    return;

  size_t route_count = Dowa_Array_Length(g_routes);
  for (size_t i = 0; i < route_count; i++)
  {
    Seobeo_Route *route = &g_routes[i];
    if (route->method) free(route->method);
    if (route->path_pattern) free(route->path_pattern);
    if (route->path_segments) Dowa_Array_Free(route->path_segments);
    if (route->is_param) free(route->is_param);
  }
  Dowa_Array_Free(g_routes);
  g_routes = NULL;
}