From 44a4cab58f74efd97ada06ee71ae379f373aea19 Mon Sep 17 00:00:00 2001 From: oduortoni Date: Thu, 12 Feb 2026 07:57:49 +0300 Subject: [PATCH 01/14] refactor: add explicit route_count to Router struct Track number of routes explicitly instead of relying on NULL-terminated array iteration. Prepares for dynamic array migration. - Add route_count field to Router struct - Initialize to 0 in router creation - Increment when adding routes - Update loops to use route_count instead of NULL checks - Extract route matching into router_match function - Introduce Route struct --- src/lib/http/http_form_parser.c | 2 ++ src/lib/http/http_handle.c | 25 ++++++++++---------- src/lib/http/http_handlerfunc.c | 41 ++++++++++++++++++--------------- src/lib/http/http_req_parser.c | 1 + src/lib/http/http_router.c | 16 +++++++++++++ 5 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 src/lib/http/http_router.c diff --git a/src/lib/http/http_form_parser.c b/src/lib/http/http_form_parser.c index a7b90ae..75abf15 100644 --- a/src/lib/http/http_form_parser.c +++ b/src/lib/http/http_form_parser.c @@ -1,3 +1,5 @@ +#include + #include "header.h" #include "utils/logging/header.h" diff --git a/src/lib/http/http_handle.c b/src/lib/http/http_handle.c index cb0651b..d5cffb2 100644 --- a/src/lib/http/http_handle.c +++ b/src/lib/http/http_handle.c @@ -30,24 +30,25 @@ http_handle(Router* router, const char* request_data) InitResponseWriter(&rw); // Find and call handler - HandlerFunc handler = nullptr; - for (size_t i = 0; - i < ARRAY_LEN(router->patterns) && router->patterns[i]; i++) { - req->path_regex = &router->regex_patterns[i]; - if (!regexec(req->path_regex, req->path.data, - ARRAY_LEN(req->path_matches), req->path_matches, - 0)) { - info("Using '%s' handler", router->patterns[i]); - handler = router->handlers[i]; - break; + HandlerFunc handler = router_match(router, req->path.data, req); + + // If no handler found, try 404 + if (handler == nullptr) { + printf("ROUTE NOT FOUND\n"); + req->path_regex = nullptr; + for (int i = 0; i < router->route_count; i++) { + if (strcmp("^/404$", router->patterns[i]) == 0) { + info("Using '%s' handler", router->patterns[i]); + handler = router->handlers[i]; + break; + } } } // If no handler found, try 404 if (handler == nullptr) { req->path_regex = nullptr; - for (size_t i = 0; - i < ARRAY_LEN(router->patterns) && router->patterns[i]; + for (int i = 0; i < router->route_count && router->patterns[i]; i++) { if (strcmp("^/404$", router->patterns[i]) == 0) { info("Using '%s' handler", router->patterns[i]); diff --git a/src/lib/http/http_handlerfunc.c b/src/lib/http/http_handlerfunc.c index 11693d1..50c1bfe 100644 --- a/src/lib/http/http_handlerfunc.c +++ b/src/lib/http/http_handlerfunc.c @@ -1,5 +1,4 @@ #include "header.h" -#include "utils/macros.h" RouterStatus handleFunc(const char* pattern, HandlerFunc handler) @@ -13,30 +12,34 @@ handleFunc(const char* pattern, HandlerFunc handler) if (!http.router) { return ROUTER_NOMEM; } + http.router->route_count = 0; } - for (size_t i = 0; i < ARRAY_LEN(http.router->patterns); i++) { - if (http.router->patterns[i]) { - // prevent duplicate route registration - if (strcmp(http.router->patterns[i], pattern) == 0) { - return ROUTER_DUPLICATE; - } - continue; + for (int i = 0; i < http.router->route_count; i++) { + // prevent duplicate route registration + if (strcmp(http.router->patterns[i], pattern) == 0) { + return ROUTER_DUPLICATE; } + continue; + } - if (regcomp(&http.router->regex_patterns[i], pattern, - REG_EXTENDED)) { - return ROUTER_INVALID_REGEX; - } + if (http.router->route_count >= + MAX_ROUTES) { // TODO: replace with dynamic ARRAY_LEN equivalent + // for now + return ROUTER_FULL; + } - http.router->patterns[i] = strdup(pattern); - if (!http.router->patterns[i]) { - return ROUTER_NOMEM; - } + int i = http.router->route_count; + if (regcomp(&http.router->regex_patterns[i], pattern, REG_EXTENDED)) { + return ROUTER_INVALID_REGEX; + } - http.router->handlers[i] = handler; - return ROUTER_OK; + http.router->patterns[i] = strdup(pattern); + if (!http.router->patterns[i]) { + return ROUTER_NOMEM; } - return ROUTER_FULL; + http.router->handlers[i] = handler; + http.router->route_count++; + return ROUTER_OK; } diff --git a/src/lib/http/http_req_parser.c b/src/lib/http/http_req_parser.c index c040bc3..dc40e24 100644 --- a/src/lib/http/http_req_parser.c +++ b/src/lib/http/http_req_parser.c @@ -1,4 +1,5 @@ #include +#include #include #include "header.h" diff --git a/src/lib/http/http_router.c b/src/lib/http/http_router.c new file mode 100644 index 0000000..e8a8c15 --- /dev/null +++ b/src/lib/http/http_router.c @@ -0,0 +1,16 @@ +#include "header.h" +#include "utils/macros.h" + +HandlerFunc +router_match(Router* router, const char* path, Request* req) +{ + for (int i = 0; i < router->route_count; i++) { + req->path_regex = &router->regex_patterns[i]; + if (!regexec(req->path_regex, path, + ARRAY_LEN(req->path_matches), req->path_matches, + 0)) { + return router->handlers[i]; + } + } + return nullptr; +} From 3d703cf1b39807b320fbeb3183844daa79908dbd Mon Sep 17 00:00:00 2001 From: oduortoni Date: Thu, 12 Feb 2026 08:22:28 +0300 Subject: [PATCH 02/14] fix: uninitialized global http router --- src/lib/http/header.h | 12 +++++++++++- src/lib/http/http_handlerfunc.c | 3 +-- src/lib/http/http_server.c | 4 ++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/lib/http/header.h b/src/lib/http/header.h index eba24aa..20aa79b 100644 --- a/src/lib/http/header.h +++ b/src/lib/http/header.h @@ -1,7 +1,6 @@ #ifndef _HTTP_HEADER_H #define _HTTP_HEADER_H -#include #include #include "net/header.h" @@ -130,9 +129,17 @@ char* BuildResponse(ResponseWriter* rw); typedef int (*HandlerFunc)(ResponseWriter* w, Request* r); +struct Route { + char* pattern; + regex_t compiled_pattern; + HandlerFunc handler; +}; +typedef struct Route Route; + struct Router { char* patterns[50]; regex_t regex_patterns[50]; + int route_count; HandlerFunc handlers[50]; }; typedef struct Router Router; @@ -146,6 +153,8 @@ typedef enum RouterStatus { ROUTER_INVALID_REGEX = -5, } RouterStatus; +HandlerFunc router_match(Router* router, const char* path, Request* req); + struct HttpServer { int (*ListenAndServe)(char* host, Router* router); RouterStatus (*HandleFunc)(const char* pattern, HandlerFunc handler); @@ -167,5 +176,6 @@ ProtocolResponse http_handle_connection(RequestContext* context, size_t request_len); extern HttpServer http; +extern Router router; #endif // _HTTP_HEADER_H diff --git a/src/lib/http/http_handlerfunc.c b/src/lib/http/http_handlerfunc.c index 50c1bfe..ebee180 100644 --- a/src/lib/http/http_handlerfunc.c +++ b/src/lib/http/http_handlerfunc.c @@ -24,8 +24,7 @@ handleFunc(const char* pattern, HandlerFunc handler) } if (http.router->route_count >= - MAX_ROUTES) { // TODO: replace with dynamic ARRAY_LEN equivalent - // for now + 50) { // TODO: replace with dynamic ARRAY_LEN equivalent return ROUTER_FULL; } diff --git a/src/lib/http/http_server.c b/src/lib/http/http_server.c index 43e8490..5bc6766 100644 --- a/src/lib/http/http_server.c +++ b/src/lib/http/http_server.c @@ -10,7 +10,11 @@ int listenAndServe(char* host, Router* router) { if (router == NULL) { + // if no router is provided, use global http router router = http.router; + } else { + // if pprovided, make global point to it + http.router = router; } puts("http/server.c"); for (size_t i = 0; From 447a3512efc317a4a43e94ec2a655e6a33a5434e Mon Sep 17 00:00:00 2001 From: oduortoni Date: Thu, 12 Feb 2026 13:23:40 +0300 Subject: [PATCH 03/14] feat: implement dispatcher interface with regex dispatcher Add pluggable dispatcher interface for routing strategies: - Define Dispatcher with match, add_route, mount, free operations - Implement RegexRouterData with dynamic arrays - Create regex dispatcher with route flattening on mount - Add router_create_regex factory function - Add wrapper functions: router_add, router_mount, router_free - Migrate main.c to use dispatcher-based routing - Maintain backward compatibility with old router_match for now Enables composable routing and future dispatcher implementations. --- src/lib/http/header.h | 25 +++++- src/lib/http/http_router.c | 49 +++++++++++ src/lib/http/http_server.c | 25 ++++-- src/lib/http/route_dispatchers/header.h | 17 ++++ .../http/route_dispatchers/regex_dispatcher.c | 86 +++++++++++++++++++ src/main.c | 15 ++-- 6 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 src/lib/http/route_dispatchers/header.h create mode 100644 src/lib/http/route_dispatchers/regex_dispatcher.c diff --git a/src/lib/http/header.h b/src/lib/http/header.h index 20aa79b..1a8af46 100644 --- a/src/lib/http/header.h +++ b/src/lib/http/header.h @@ -129,6 +129,18 @@ char* BuildResponse(ResponseWriter* rw); typedef int (*HandlerFunc)(ResponseWriter* w, Request* r); +/*------------------------------------------------ Routing + * -------------------------------------------------------------*/ + +struct Dispatcher { + HandlerFunc (*match)(void* impl_data, const char* path, Request* req); + void (*add_route)(void* impl_data, const char* pattern, + HandlerFunc handler); + void (*mount)(void* parent_data, const char* prefix, void* child_data); + void (*free)(void* impl_data); +}; +typedef struct Dispatcher Dispatcher; + struct Route { char* pattern; regex_t compiled_pattern; @@ -139,8 +151,10 @@ typedef struct Route Route; struct Router { char* patterns[50]; regex_t regex_patterns[50]; - int route_count; HandlerFunc handlers[50]; + int route_count; + const Dispatcher* dispatcher; + void* impl_data; }; typedef struct Router Router; @@ -154,6 +168,15 @@ typedef enum RouterStatus { } RouterStatus; HandlerFunc router_match(Router* router, const char* path, Request* req); +void router_add(Router* router, const char* pattern, HandlerFunc handler); +void router_mount(Router* parent, const char* prefix, Router* child); +HandlerFunc router_match_dispatcher(Router* router, const char* path, + Request* req); +void router_free(Router* router); +Router* router_create_regex(void); + +/*------------------------------------------------ END OF Routing + * ----------------------------------*/ struct HttpServer { int (*ListenAndServe)(char* host, Router* router); diff --git a/src/lib/http/http_router.c b/src/lib/http/http_router.c index e8a8c15..20f312a 100644 --- a/src/lib/http/http_router.c +++ b/src/lib/http/http_router.c @@ -4,6 +4,11 @@ HandlerFunc router_match(Router* router, const char* path, Request* req) { + if (router->dispatcher) { + return router->dispatcher->match(router->impl_data, path, req); + } + + // leave the old logic as a fallback for (int i = 0; i < router->route_count; i++) { req->path_regex = &router->regex_patterns[i]; if (!regexec(req->path_regex, path, @@ -14,3 +19,47 @@ router_match(Router* router, const char* path, Request* req) } return nullptr; } + +void +router_add(Router* router, const char* pattern, HandlerFunc handler) +{ + if (router->dispatcher) { + router->dispatcher->add_route(router->impl_data, pattern, + handler); + } +} + +void +router_mount(Router* parent, const char* prefix, Router* child) +{ + if (parent->dispatcher && child->dispatcher && + parent->dispatcher == child->dispatcher) { + parent->dispatcher->mount(parent->impl_data, prefix, + child->impl_data); + router_free(child); + } +} + +HandlerFunc +router_match_dispatcher(Router* router, const char* path, Request* req) +{ + if (router->dispatcher) { + return router->dispatcher->match(router->impl_data, path, req); + } + return nullptr; +} + +void +router_free(Router* router) +{ + if (router->dispatcher) { + router->dispatcher->free(router->impl_data); + } else { + // if there is no dispatcher, this is a fallback for old arrays + for (int i = 0; i < router->route_count; i++) { + free(router->patterns[i]); + regfree(&router->regex_patterns[i]); + } + } + free(router); +} diff --git a/src/lib/http/http_server.c b/src/lib/http/http_server.c index 5bc6766..3cbe52e 100644 --- a/src/lib/http/http_server.c +++ b/src/lib/http/http_server.c @@ -17,12 +17,27 @@ listenAndServe(char* host, Router* router) http.router = router; } puts("http/server.c"); - for (size_t i = 0; - i < ARRAY_LEN(router->patterns) && router->patterns[i]; i++) { - printf("SERVERT: %s\n", router->patterns[i]); + + if (router == NULL) { + printf("ERROR: Router is NULL\n"); + } + + puts("http/server.c"); + printf("Router address: %p\n", (void*)router); + printf("Dispatcher address: %p\n", (void*)router->dispatcher); + + if (!router->dispatcher) { + puts("NOOOOOOPE"); + for (size_t i = 0; + i < ARRAY_LEN(router->patterns) && router->patterns[i]; + i++) { + printf("SERVERT: %s\n", router->patterns[i]); + } + } else { + printf("Using dispatcher-based router\n"); } - // printf("RouteD %s\n", router->patterns[1]); - RequestContext context = {.router = router}; + printf("RouteD\n"); + RequestContext context = {.router = http.router}; net_serve(host, http_handle_connection, &context); return 0; diff --git a/src/lib/http/route_dispatchers/header.h b/src/lib/http/route_dispatchers/header.h new file mode 100644 index 0000000..57fd1dd --- /dev/null +++ b/src/lib/http/route_dispatchers/header.h @@ -0,0 +1,17 @@ +#ifndef HTTP_ROUTE_DISPATHER_H +#define HTTP_ROUTE_DISPATHER_H + +#include + +#include "http/header.h" + +struct RegexRouterData { + struct Route* items; + size_t len; + size_t capacity; +}; +typedef struct RegexRouterData RegexRouterData; + +Router* router_create_regex(void); + +#endif // HTTP_ROUTE_DISPATHER_H diff --git a/src/lib/http/route_dispatchers/regex_dispatcher.c b/src/lib/http/route_dispatchers/regex_dispatcher.c new file mode 100644 index 0000000..e43d42d --- /dev/null +++ b/src/lib/http/route_dispatchers/regex_dispatcher.c @@ -0,0 +1,86 @@ +#include "header.h" +#include "http/header.h" +#include "utils/macros.h" + +HandlerFunc +regex_match(void* impl_data, const char* path, Request* req) +{ + printf("Search for route '%s'\n", path); + + RegexRouterData* data = (RegexRouterData*)impl_data; + for (size_t i = 0; i < data->len; i++) { + req->path_regex = &data->items[i].compiled_pattern; + if (!regexec(req->path_regex, path, + ARRAY_LEN(req->path_matches), req->path_matches, + 0)) { + return data->items[i].handler; + } + } + return nullptr; +} + +/* FIX ISSUES*/ +static void +regex_add_route(void* impl_data, const char* pattern, HandlerFunc handler) +{ + RegexRouterData* data_ptr = (RegexRouterData*)impl_data; + RegexRouterData data; + + data.items = data_ptr->items; + data.capacity = data_ptr->capacity; + data.len = data_ptr->len; + + struct Route route = {.pattern = strdup(pattern), .handler = handler}; + + regcomp(&route.compiled_pattern, pattern, REG_EXTENDED); + + da_append(data, route); + + data_ptr->items = data.items; + data_ptr->capacity = data.capacity; + data_ptr->len = data.len; +} + +static void +regex_mount(void* parent_data, const char* prefix, void* child_data) +{ + RegexRouterData* child = (RegexRouterData*)child_data; + + for (size_t i = 0; i < child->len; i++) { + char* new_pattern = malloc(strlen(prefix) + + strlen(child->items[i].pattern) + 1); + strcpy(new_pattern, prefix); + strcat(new_pattern, child->items[i].pattern); + regex_add_route(parent_data, new_pattern, + child->items[i].handler); + free(new_pattern); + } +} + +static void +regex_free(void* impl_data) +{ + RegexRouterData* data = (RegexRouterData*)impl_data; + for (size_t i = 0; i < data->len; i++) { + free(data->items[i].pattern); + regfree(&data->items[i].compiled_pattern); + } + free(data->items); + free(data); +} + +// initialize the regex dispatcher with member fields +static const Dispatcher regex_dispatcher = {.match = regex_match, + .add_route = regex_add_route, + .mount = regex_mount, + .free = regex_free}; + +Router* +router_create_regex(void) +{ + Router* router = calloc(1, sizeof(Router)); + RegexRouterData* data = calloc(1, sizeof(RegexRouterData)); + router->dispatcher = ®ex_dispatcher; + router->impl_data = data; + return router; +} diff --git a/src/main.c b/src/main.c index 73a4aea..4b477c7 100644 --- a/src/main.c +++ b/src/main.c @@ -24,16 +24,15 @@ main() sprintf(hostname, "%s:%d", host_url, PORT); } - http.HandleFunc("^/$", Index); - http.HandleFunc("^/about$", About); - http.HandleFunc("^/404$", Error404); - http.HandleFunc("^/static/(.*)$", Static); + Router* router = router_create_regex(); + router_add(router, "^/$", Index); + router_add(router, "^/about$", About); + router_add(router, "^/404$", Error404); + router_add(router, "^/static/(.*)$", Static); - http.ListenAndServe(hostname, NULL); + http.ListenAndServe(hostname, router); - // Router router = {{"/404", "/", "/about", NULL}, {Error404, Index, - // About, NULL}}; printf("Route: %s\n", router.patterns[1]); - // http.ListenAndServe(hostname, &router); + router_free(router); printf("Server listening on %d\n", PORT); return 0; From 23ce81f14dd4f328bbb83f12283c99e4b3be1403 Mon Sep 17 00:00:00 2001 From: oduortoni Date: Thu, 12 Feb 2026 14:07:35 +0300 Subject: [PATCH 04/14] fix: correct regex pattern concatenation in router_mount - Fix regex_mount to properly handle regex patterns when mounting sub-routers. - Previously concatenated "/tests" + "^/test$" = "/tests^/test$" (invalid). - Now transforms "^/test$" with prefix "/tests" to "^/tests/test$". - Strip ^ and $ from child patterns before concatenation - Enables correct route flattening for composable routing --- src/app/header.h | 2 +- src/app/test_handler.c | 31 +++++++++++++++++++ src/lib/http/http_server.c | 2 +- .../http/route_dispatchers/regex_dispatcher.c | 19 +++++++++--- src/main.c | 21 ++++++++++++- 5 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 src/app/test_handler.c diff --git a/src/app/header.h b/src/app/header.h index 548397e..1a4c31c 100644 --- a/src/app/header.h +++ b/src/app/header.h @@ -2,7 +2,6 @@ #define _APP_H #include "http/header.h" -#include "template/header.h" #define TEMPLATE_PATH "src/app/templates/" @@ -10,5 +9,6 @@ int Index(ResponseWriter* w, Request* r); int About(ResponseWriter* w, Request* r); int Error404(ResponseWriter* w, Request* r); int Static(ResponseWriter* w, Request* r); +int TestHandler(ResponseWriter* w, Request* r); #endif diff --git a/src/app/test_handler.c b/src/app/test_handler.c new file mode 100644 index 0000000..75e3353 --- /dev/null +++ b/src/app/test_handler.c @@ -0,0 +1,31 @@ +#include "header.h" + +// clang-format off +static char const html[] = +"" +"" +"" +" About Page" +" " +"" +"" +"

Test Page

" +"

If you got here, it means that the mounting of sub-routes worked.

" +"" +""; +// clang-format on + +int +TestHandler(ResponseWriter* w, [[maybe_unused]] Request* r) +{ + // Set status and headers + SetStatus(w, 200, "OK"); + SetHeader(w, "Content-Type", "text/html"); + + // Write the About page to the response + w->WriteString(w, html); + + return EXIT_SUCCESS; +} diff --git a/src/lib/http/http_server.c b/src/lib/http/http_server.c index 3cbe52e..6d40792 100644 --- a/src/lib/http/http_server.c +++ b/src/lib/http/http_server.c @@ -20,6 +20,7 @@ listenAndServe(char* host, Router* router) if (router == NULL) { printf("ERROR: Router is NULL\n"); + return -1; } puts("http/server.c"); @@ -27,7 +28,6 @@ listenAndServe(char* host, Router* router) printf("Dispatcher address: %p\n", (void*)router->dispatcher); if (!router->dispatcher) { - puts("NOOOOOOPE"); for (size_t i = 0; i < ARRAY_LEN(router->patterns) && router->patterns[i]; i++) { diff --git a/src/lib/http/route_dispatchers/regex_dispatcher.c b/src/lib/http/route_dispatchers/regex_dispatcher.c index e43d42d..b9d00fa 100644 --- a/src/lib/http/route_dispatchers/regex_dispatcher.c +++ b/src/lib/http/route_dispatchers/regex_dispatcher.c @@ -47,10 +47,21 @@ regex_mount(void* parent_data, const char* prefix, void* child_data) RegexRouterData* child = (RegexRouterData*)child_data; for (size_t i = 0; i < child->len; i++) { - char* new_pattern = malloc(strlen(prefix) + - strlen(child->items[i].pattern) + 1); - strcpy(new_pattern, prefix); - strcat(new_pattern, child->items[i].pattern); + char* child_pattern = child->items[i].pattern; + + // Remove ^ from start and $ from end of child pattern + char* pattern_body = child_pattern + 1; // Skip ^ + size_t body_len = strlen(pattern_body); + if (body_len > 0 && pattern_body[body_len - 1] == '$') { + body_len--; // Remove $ + } + + // Create new pattern: ^$ + char* new_pattern = + malloc(strlen(prefix) + body_len + 3); // ^ + $ + \0 + sprintf(new_pattern, "^%s%.*s$", prefix, (int)body_len, + pattern_body); + regex_add_route(parent_data, new_pattern, child->items[i].handler); free(new_pattern); diff --git a/src/main.c b/src/main.c index 4b477c7..d574e93 100644 --- a/src/main.c +++ b/src/main.c @@ -30,10 +30,29 @@ main() router_add(router, "^/404$", Error404); router_add(router, "^/static/(.*)$", Static); + // Test router cleanup + Router* test_router = router_create_regex(); + router_add(test_router, "^/test$", TestHandler); + printf("Created test router with 1 route\n"); + + router_mount(router, "/tests", test_router); + + /* + * Used to check for memory leaks in allocation and deallocation of + * memory + */ + // router_free(router); + // printf("Freed test router\n"); + + printf("Server listening on %d\n", PORT); http.ListenAndServe(hostname, router); + /* + * TODO: This is never reached. + * - Need to add a way to handle graceful shut down + */ + printf("\n\n\t << Graceful Shutdown >>\n\n"); router_free(router); - printf("Server listening on %d\n", PORT); return 0; } From 55272b4321912c35f94a1d845115167800c276fe Mon Sep 17 00:00:00 2001 From: oduortoni Date: Thu, 12 Feb 2026 17:38:21 +0300 Subject: [PATCH 05/14] feat: create our first app as a sub-folder - created app/blog directory and initialized a router - the router was then mounted onto the main app It isnow possible to create many composable applications --- src/app/blog/header.h | 10 ++++++++++ src/app/blog/index.c | 32 ++++++++++++++++++++++++++++++++ src/app/blog/router.c | 10 ++++++++++ src/main.c | 7 ++++++- 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/app/blog/header.h create mode 100644 src/app/blog/index.c create mode 100644 src/app/blog/router.c diff --git a/src/app/blog/header.h b/src/app/blog/header.h new file mode 100644 index 0000000..c537bf4 --- /dev/null +++ b/src/app/blog/header.h @@ -0,0 +1,10 @@ +#ifndef BLOG_H +#define BLOG_H + +#include "http/header.h" + +int BlogIndex(ResponseWriter* rw, Request* r); + +Router* BlogRouter(); + +#endif // BLOG_H diff --git a/src/app/blog/index.c b/src/app/blog/index.c new file mode 100644 index 0000000..b2bf352 --- /dev/null +++ b/src/app/blog/index.c @@ -0,0 +1,32 @@ +#include "header.h" + +// clang-format off +static char const html[] = +"" +"" +"" +" About Page" +" " +"" +"" +"

Our Blog

" +"

This is the blog for this site.

" +"" +""; +// clang-format on + +int +BlogIndex(ResponseWriter* w, Request* r) +{ + (void)r; + // Set status and headers + SetStatus(w, 200, "OK"); + SetHeader(w, "Content-Type", "text/html"); + + // Write the About page to the response + w->WriteString(w, html); + + return EXIT_SUCCESS; +} diff --git a/src/app/blog/router.c b/src/app/blog/router.c new file mode 100644 index 0000000..3916275 --- /dev/null +++ b/src/app/blog/router.c @@ -0,0 +1,10 @@ +#include "header.h" +#include "http/header.h" + +Router* +BlogRouter() +{ + Router* router = router_create_regex(); + router_add(router, "^/blog$", BlogIndex); + return router; +} diff --git a/src/main.c b/src/main.c index d574e93..1ba6e51 100644 --- a/src/main.c +++ b/src/main.c @@ -1,3 +1,4 @@ +#include "app/blog/header.h" #include "app/header.h" #include "lib/env/header.h" #include "lib/http/header.h" @@ -37,6 +38,9 @@ main() router_mount(router, "/tests", test_router); + // a router created within the app/blog/ directory + router_mount(router, "/apps", BlogRouter()); + /* * Used to check for memory leaks in allocation and deallocation of * memory @@ -48,7 +52,8 @@ main() http.ListenAndServe(hostname, router); /* - * TODO: This is never reached. + * TODO: This is never reached due to infinite listener that stops on + * CTRL + C * - Need to add a way to handle graceful shut down */ printf("\n\n\t << Graceful Shutdown >>\n\n"); From 1027f2c43f8a269e5250379128b4b43d9b388008 Mon Sep 17 00:00:00 2001 From: oduortoni Date: Fri, 13 Feb 2026 10:17:00 +0300 Subject: [PATCH 06/14] docs: add routing guide and update template system documentation - Add comprehensive routing system guide (docs/ROUTING.md) - Rename and update template system documentation - Remove template compiler (thc.c) in favor of external zc compiler - Clean up unused handlers and blog module - Update main.c to reflect routing changes --- README.md | 2 + docs/ROUTING.md | 486 ++++++++++++++++++ ...em-specification.md => TEMPLATE_SYSTEM.md} | 52 ++ src/app/about.c | 32 -- src/app/blog/header.h | 10 - src/app/blog/index.c | 32 -- src/app/blog/router.c | 10 - src/app/index.c | 44 +- src/app/templates/index.html | 38 -- src/app/test_handler.c | 31 -- src/lib/template/header.h | 4 - src/main.c | 12 - src/tools/thc.c | 253 --------- 13 files changed, 543 insertions(+), 463 deletions(-) create mode 100644 docs/ROUTING.md rename docs/{template-system-specification.md => TEMPLATE_SYSTEM.md} (83%) delete mode 100644 src/app/about.c delete mode 100644 src/app/blog/header.h delete mode 100644 src/app/blog/index.c delete mode 100644 src/app/blog/router.c delete mode 100644 src/app/templates/index.html delete mode 100644 src/app/test_handler.c delete mode 100644 src/lib/template/header.h delete mode 100644 src/tools/thc.c diff --git a/README.md b/README.md index 60b3037..f2bae07 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Comprehensive documentation is available in the [docs](docs) folder. The documen - [Core Documentation](docs/DOCUMENTATION.md) - Beginner-friendly explanation of how the server works - [Visual Guide](docs/VISUAL_GUIDE.md) - Diagrams illustrating the server architecture - [Practical Examples](docs/EXAMPLES.md) - Code examples for extending the server +- [Template System](docs/TEMPLATE_SYSTEM.md) - Instructions of how to use the template system +- [Routing System](docs/ROUTING.md) - Instructions on how to use the routing system See the [docs README](docs/README.md) for more information. diff --git a/docs/ROUTING.md b/docs/ROUTING.md new file mode 100644 index 0000000..0f8e9dd --- /dev/null +++ b/docs/ROUTING.md @@ -0,0 +1,486 @@ +# Routing System Guide + +## Overview + +The routing system maps URL paths to handler functions using regex patterns. It supports dynamic path segments, sub-routers, and automatic 404 handling. + +--- + +## Quick Start + +### 1. Create a Router + +```c +Router* router = router_create_regex(); +``` + +### 2. Register Routes + +```c +router_add(router, "^/$", Index); +router_add(router, "^/about$", About); +router_add(router, "^/users/([0-9]+)$", UserProfile); +``` + +### 3. Start the Server + +```c +http.ListenAndServe("127.0.0.1:9000", router); +``` + +--- + +## Handler Functions + +Handlers process requests and write responses: + +```c +int Index(ResponseWriter* w, Request* r) +{ + SetStatus(w, 200, "OK"); + SetHeader(w, "Content-Type", "text/html"); + w->WriteString(w, "

Welcome!

"); + return EXIT_SUCCESS; +} +``` + +**Handler Signature:** +- `ResponseWriter* w` - Write response data +- `Request* r` - Read request data (method, path, headers, body) +- Returns `int` - `EXIT_SUCCESS` or error code + +--- + +## Route Patterns + +Routes use POSIX Extended Regular Expressions: + +### Static Routes + +```c +router_add(router, "^/$", Index); // Exact match: / +router_add(router, "^/about$", About); // Exact match: /about +router_add(router, "^/contact$", Contact); // Exact match: /contact +``` + +**Important:** Always use `^` (start) and `$` (end) anchors for exact matching. + +### Dynamic Routes (Capture Groups) + +```c +// Match: /users/123, /users/456 +router_add(router, "^/users/([0-9]+)$", UserProfile); + +// Match: /posts/hello-world, /posts/my-post +router_add(router, "^/posts/([a-z-]+)$", BlogPost); + +// Match: /static/style.css, /static/images/logo.png +router_add(router, "^/static/(.*)$", StaticFiles); +``` + +### Accessing Captured Values + +Use `regmatch_t` to extract captured segments: + +```c +int UserProfile(ResponseWriter* w, Request* r) +{ + // Extract user ID from path + regmatch_t match = r->path_matches[1]; // First capture group + int start = match.rm_so; + int end = match.rm_eo; + + char user_id[32]; + snprintf(user_id, sizeof(user_id), "%.*s", end - start, r->path.data + start); + + SetStatus(w, 200, "OK"); + w->WriteString(w, "User ID: "); + w->WriteString(w, user_id); + + return EXIT_SUCCESS; +} +``` + +**Note:** `path_matches[0]` is the full match, `path_matches[1]` is the first capture group, etc. + +--- + +## Request Object + +Access request data through the `Request` struct: + +```c +int Handler(ResponseWriter* w, Request* r) +{ + // HTTP method + if (strcmp(r->method, "POST") == 0) { + // Handle POST request + } + + // Request path + printf("Path: %.*s\n", (int)r->path.size, r->path.data); + + // Request body + if (r->body.size > 0) { + printf("Body: %.*s\n", (int)r->body.size, r->body.data); + } + + // Headers (iterate through headers array) + for (size_t i = 0; i < r->headers.len; i++) { + printf("Header: %.*s = %.*s\n", + (int)r->headers.items[i].name.size, + r->headers.items[i].name.data, + (int)r->headers.items[i].value.size, + r->headers.items[i].value.data); + } + + return EXIT_SUCCESS; +} +``` + +--- + +## Response Writer + +Build responses using the `ResponseWriter` object: + +### Set Status + +```c +SetStatus(w, 200, "OK"); +SetStatus(w, 404, "Not Found"); +SetStatus(w, 500, "Internal Server Error"); +``` + +### Set Headers + +```c +SetHeader(w, "Content-Type", "text/html"); +SetHeader(w, "Content-Type", "application/json"); +SetHeader(w, "Cache-Control", "no-cache"); +``` + +### Write Body + +```c +// Write string +w->WriteString(w, "

Hello World

"); + +// Write formatted data +char buffer[256]; +snprintf(buffer, sizeof(buffer), "User ID: %d", user_id); +w->WriteString(w, buffer); + +// Write raw data +response_write(w, data, data_length); +``` + +--- + +## Form Data + +Parse form submissions: + +```c +int ContactForm(ResponseWriter* w, Request* r) +{ + if (strcmp(r->method, "POST") == 0) { + FormData form_data = {0}; + parse_form_data(r->body, &form_data); + + const char* name = get_form_value(&form_data, "name"); + const char* email = get_form_value(&form_data, "email"); + const char* message = get_form_value(&form_data, "message"); + + if (!name || !email || !message) { + SetStatus(w, 400, "Bad Request"); + w->WriteString(w, "Missing required fields"); + return -1; + } + + // Process form data... + SetStatus(w, 200, "OK"); + w->WriteString(w, "Form submitted successfully!"); + return EXIT_SUCCESS; + } + + // Show form for GET requests + SetStatus(w, 200, "OK"); + SetHeader(w, "Content-Type", "text/html"); + w->WriteString(w, "
...
"); + return EXIT_SUCCESS; +} +``` + +--- + +## Sub-Routers + +Organize routes into modules using sub-routers: + +### Create a Sub-Router + +**`src/app/blog/router.c`** +```c +#include "header.h" +#include "http/header.h" + +Router* BlogRouter() +{ + Router* router = router_create_regex(); + router_add(router, "^/blog$", BlogIndex); + router_add(router, "^/blog/([0-9]+)$", BlogPost); + router_add(router, "^/blog/new$", BlogNew); + return router; +} +``` + +### Mount Sub-Router + +**`src/main.c`** +```c +Router* router = router_create_regex(); +router_add(router, "^/$", Index); + +// Mount blog router at /apps prefix +// Routes become: /apps/blog, /apps/blog/123, /apps/blog/new +router_mount(router, "/apps", BlogRouter()); + +http.ListenAndServe("127.0.0.1:9000", router); +``` + +**How it works:** +- Child pattern `^/blog$` + prefix `/apps` → `^/apps/blog$` +- Child pattern `^/blog/([0-9]+)$` + prefix `/apps` → `^/apps/blog/([0-9]+)$` + +--- + +## 404 Handling + +### Custom 404 Handler + +```c +int Error404(ResponseWriter* w, Request* r) +{ + SetStatus(w, 404, "Not Found"); + SetHeader(w, "Content-Type", "text/html"); + w->WriteString(w, "

404 - Page Not Found

"); + return EXIT_SUCCESS; +} + +// Register 404 handler +router_add(router, "^/404$", Error404); +``` + +The router automatically uses the `^/404$` route when no other route matches. + +### Default 404 + +If no `^/404$` route is registered, the server returns a plain text 404 response. + +--- + +## Complete Example + +**`src/main.c`** +```c +#include "app/header.h" +#include "http/header.h" + +int main() +{ + // Create main router + Router* router = router_create_regex(); + + // Static routes + router_add(router, "^/$", Index); + router_add(router, "^/about$", About); + router_add(router, "^/contact$", Contact); + + // Dynamic routes + router_add(router, "^/users/([0-9]+)$", UserProfile); + router_add(router, "^/posts/([a-z0-9-]+)$", BlogPost); + + // Static files + router_add(router, "^/static/(.*)$", StaticFiles); + + // 404 handler + router_add(router, "^/404$", Error404); + + // Mount sub-routers + router_mount(router, "/api", ApiRouter()); + router_mount(router, "/admin", AdminRouter()); + + // Start server + printf("Server listening on http://127.0.0.1:9000\n"); + http.ListenAndServe("127.0.0.1:9000", router); + + // Cleanup (never reached due to infinite loop) + router_free(router); + + return 0; +} +``` + +**`src/app/handlers.c`** +```c +#include "header.h" + +int Index(ResponseWriter* w, Request* r) +{ + SetStatus(w, 200, "OK"); + SetHeader(w, "Content-Type", "text/html"); + w->WriteString(w, "

Welcome to C HTTP Server

"); + return EXIT_SUCCESS; +} + +int UserProfile(ResponseWriter* w, Request* r) +{ + // Extract user ID from URL + regmatch_t match = r->path_matches[1]; + char user_id[32]; + snprintf(user_id, sizeof(user_id), "%.*s", + match.rm_eo - match.rm_so, + r->path.data + match.rm_so); + + SetStatus(w, 200, "OK"); + SetHeader(w, "Content-Type", "text/html"); + + char response[256]; + snprintf(response, sizeof(response), + "

User Profile

User ID: %s

", user_id); + w->WriteString(w, response); + + return EXIT_SUCCESS; +} + +int Error404(ResponseWriter* w, Request* r) +{ + SetStatus(w, 404, "Not Found"); + SetHeader(w, "Content-Type", "text/html"); + w->WriteString(w, + "" + "

404 - Page Not Found

" + "

The requested page does not exist.

" + ""); + return EXIT_SUCCESS; +} +``` + +--- + +## Common Patterns + +### REST API Routes + +```c +router_add(router, "^/api/users$", UsersIndex); // GET /api/users +router_add(router, "^/api/users/([0-9]+)$", UsersShow); // GET /api/users/123 +router_add(router, "^/api/posts$", PostsIndex); // GET /api/posts +router_add(router, "^/api/posts/([0-9]+)$", PostsShow); // GET /api/posts/456 +``` + +Handle different HTTP methods in the handler: + +```c +int UsersIndex(ResponseWriter* w, Request* r) +{ + if (strcmp(r->method, "GET") == 0) { + // List users + } else if (strcmp(r->method, "POST") == 0) { + // Create user + } else { + SetStatus(w, 405, "Method Not Allowed"); + return -1; + } + return EXIT_SUCCESS; +} +``` + +### File Extensions + +```c +// Match: /page.html, /about.html +router_add(router, "^/([a-z]+)\\.html$", HtmlPages); + +// Match: /api/v1/users, /api/v2/users +router_add(router, "^/api/(v[0-9]+)/users$", ApiUsers); +``` + +### Optional Segments + +```c +// Match: /search or /search/query +router_add(router, "^/search(/.*)?$", Search); +``` + +--- + +## Best Practices + +1. **Always use anchors:** Start patterns with `^` and end with `$` +2. **Order matters:** More specific routes should come before general ones +3. **Validate input:** Always check captured values before using them +4. **Return proper status codes:** Use appropriate HTTP status codes +5. **Set Content-Type:** Always set the correct content type header +6. **Handle errors:** Return error codes when operations fail +7. **Free resources:** Clean up allocated memory in handlers +8. **Use sub-routers:** Organize related routes into modules + +--- + +## Regex Quick Reference + +| Pattern | Matches | Example | +|---------|---------|---------| +| `^/path$` | Exact path | `/path` | +| `[0-9]+` | One or more digits | `123`, `456` | +| `[a-z]+` | One or more lowercase letters | `hello`, `world` | +| `[a-zA-Z0-9-]+` | Alphanumeric with hyphens | `my-post-123` | +| `.*` | Any characters (greedy) | `anything/here` | +| `[^/]+` | Any characters except `/` | `filename` | +| `(/.*)?` | Optional path segment | `/optional` or empty | +| `\\.` | Literal dot | `.html` | + +--- + +## Troubleshooting + +### Route Not Matching + +- Check that pattern has `^` and `$` anchors +- Test regex pattern with online tools +- Verify route is registered before starting server +- Check route order (specific before general) + +### Capture Groups Not Working + +- Ensure pattern has parentheses: `([0-9]+)` +- Use correct index: `path_matches[1]` for first group +- Check `rm_so` and `rm_eo` are valid before accessing + +### 404 Always Returned + +- Verify handler function signature is correct +- Check that route pattern matches the URL exactly +- Ensure router is passed to `ListenAndServe` + +--- + +## Memory Management + +The router automatically manages memory for routes. Always free the router when done: + +```c +router_free(router); +``` + +**Note:** In the current implementation, the server runs indefinitely, so `router_free` is never reached. For testing or cleanup, you can call it explicitly. + +--- + +## See Also + +- [Server Architecture](SERVER_ARCHITECTURE.md) - Overall system design +- [Examples](EXAMPLES.md) - More code examples +- [Template System](TEMPLATE_SYSTEM.md) - Rendering HTML responses diff --git a/docs/template-system-specification.md b/docs/TEMPLATE_SYSTEM.md similarity index 83% rename from docs/template-system-specification.md rename to docs/TEMPLATE_SYSTEM.md index b05f2db..cbf61b8 100644 --- a/docs/template-system-specification.md +++ b/docs/TEMPLATE_SYSTEM.md @@ -1,3 +1,55 @@ +# The Template System And The ZC Compiler + +## Install The Compiler + +### 1. System-Wide Install (Requires superuser permissions) + +```bash +git clone https://github.com/koutoftimer/zc.git /tmp/zc +cd /tmp/zc && make +sudo cp /tmp/zc/build/zc /usr/local/bin/zc +``` + +Verify installation: +```bash +which zc +# Output: /usr/local/bin/zc +``` + +### 2. Rootless Install (User directory) + +```bash +git clone https://github.com/koutoftimer/zc.git /tmp/zc +cd /tmp/zc && make +mkdir -p ~/bin +cp /tmp/zc/build/zc ~/bin/zc +``` + +Add to your shell profile (`~/.bashrc` or `~/.zshrc`): +```bash +export PATH="$HOME/bin:$PATH" +``` + +Reload your shell: +```bash +source ~/.bashrc # or source ~/.zshrc +``` + +Verify installation: +```bash +which zc +# Output: /home/username/bin/zc +``` + +**Note:** If using rootless install, update the Makefile: +```makefile +ZC_TOOL_PATH ?= $(HOME)/bin/zc +``` + + + +## How It Works + Templates are the files with an extension `*.th` ("template header" files). **`index-template.th`** diff --git a/src/app/about.c b/src/app/about.c deleted file mode 100644 index 1dede08..0000000 --- a/src/app/about.c +++ /dev/null @@ -1,32 +0,0 @@ -#include "header.h" - -// clang-format off -static char const html[] = -"" -"" -"" -" About Page" -" " -"" -"" -"

About This Server

" -"

This is a simple HTTP server written in C.

" -"

It supports basic HTTP requests and serves static content.

" -"" -""; -// clang-format on - -int -About(ResponseWriter* w, [[maybe_unused]] Request* r) -{ - // Set status and headers - SetStatus(w, 200, "OK"); - SetHeader(w, "Content-Type", "text/html"); - - // Write the About page to the response - w->WriteString(w, html); - - return EXIT_SUCCESS; -} diff --git a/src/app/blog/header.h b/src/app/blog/header.h deleted file mode 100644 index c537bf4..0000000 --- a/src/app/blog/header.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef BLOG_H -#define BLOG_H - -#include "http/header.h" - -int BlogIndex(ResponseWriter* rw, Request* r); - -Router* BlogRouter(); - -#endif // BLOG_H diff --git a/src/app/blog/index.c b/src/app/blog/index.c deleted file mode 100644 index b2bf352..0000000 --- a/src/app/blog/index.c +++ /dev/null @@ -1,32 +0,0 @@ -#include "header.h" - -// clang-format off -static char const html[] = -"" -"" -"" -" About Page" -" " -"" -"" -"

Our Blog

" -"

This is the blog for this site.

" -"" -""; -// clang-format on - -int -BlogIndex(ResponseWriter* w, Request* r) -{ - (void)r; - // Set status and headers - SetStatus(w, 200, "OK"); - SetHeader(w, "Content-Type", "text/html"); - - // Write the About page to the response - w->WriteString(w, html); - - return EXIT_SUCCESS; -} diff --git a/src/app/blog/router.c b/src/app/blog/router.c deleted file mode 100644 index 3916275..0000000 --- a/src/app/blog/router.c +++ /dev/null @@ -1,10 +0,0 @@ -#include "header.h" -#include "http/header.h" - -Router* -BlogRouter() -{ - Router* router = router_create_regex(); - router_add(router, "^/blog$", BlogIndex); - return router; -} diff --git a/src/app/index.c b/src/app/index.c index ce84359..531883f 100644 --- a/src/app/index.c +++ b/src/app/index.c @@ -1,51 +1,13 @@ #include "header.h" -#include "utils/logging/header.h" -#include "utils/string-builder/header.h" - -struct Context { - char const* name; - char const* email; - char const* message; - bool is_method_post; -}; - -#define IMPLEMENTATION -#include "templates/index.h" int Index(ResponseWriter* w, Request* r) { - struct Context ctx = {0}; - FormData form_data = {0}; - - if (strcmp(r->method, "POST") == 0) { - parse_form_data(r->body, &form_data); - - ctx.name = get_form_value(&form_data, "name"); - ctx.email = get_form_value(&form_data, "email"); - ctx.message = get_form_value(&form_data, "message"); - ctx.is_method_post = true; - - debug("name: %s\nemail: %s\nmessage: %s", ctx.name, ctx.email, - ctx.message); - - if (!ctx.name || !ctx.email || !ctx.message) { - SetStatus(w, 400, "Missing form fields"); - return -1; - } - } - - struct StringBuilder sb = {0}; - bool ok = render_template(&ctx, &sb); - if (!ok) { - SetStatus(w, 500, "Failed to render the page"); - return -1; - } - SetStatus(w, 200, "OK"); SetHeader(w, "Content-Type", "text/html"); - w->WriteString(w, sb.ascii.data); - free(sb.ascii.data); + char buffer[300]; + snprintf(buffer, 300, "

A Minimalistic C Server

Request path: %s

", r->path.data); + w->WriteString(w, buffer); return EXIT_SUCCESS; } diff --git a/src/app/templates/index.html b/src/app/templates/index.html deleted file mode 100644 index de84ce3..0000000 --- a/src/app/templates/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - C HTTP Server Form - - - - -

Sample Form

-
-
- - -
-
- - -
-
- - -
- -
-
- - {{ if ($.is_method_post) { -}} -
-

Thank you for your submission!

-

Name: {{ $.name }}

-

Email: {{ $.email }}

-

Message: {{ $.message }}

-
- {{ } -}} - - - diff --git a/src/app/test_handler.c b/src/app/test_handler.c deleted file mode 100644 index 75e3353..0000000 --- a/src/app/test_handler.c +++ /dev/null @@ -1,31 +0,0 @@ -#include "header.h" - -// clang-format off -static char const html[] = -"" -"" -"" -" About Page" -" " -"" -"" -"

Test Page

" -"

If you got here, it means that the mounting of sub-routes worked.

" -"" -""; -// clang-format on - -int -TestHandler(ResponseWriter* w, [[maybe_unused]] Request* r) -{ - // Set status and headers - SetStatus(w, 200, "OK"); - SetHeader(w, "Content-Type", "text/html"); - - // Write the About page to the response - w->WriteString(w, html); - - return EXIT_SUCCESS; -} diff --git a/src/lib/template/header.h b/src/lib/template/header.h deleted file mode 100644 index 762f203..0000000 --- a/src/lib/template/header.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef _TEMPLATE_HEADER_H -#define _TEMPLATE_HEADER_H - -#endif // _TEMPLATE_HEADER_H diff --git a/src/main.c b/src/main.c index 1ba6e51..3814e6e 100644 --- a/src/main.c +++ b/src/main.c @@ -1,4 +1,3 @@ -#include "app/blog/header.h" #include "app/header.h" #include "lib/env/header.h" #include "lib/http/header.h" @@ -27,20 +26,9 @@ main() Router* router = router_create_regex(); router_add(router, "^/$", Index); - router_add(router, "^/about$", About); router_add(router, "^/404$", Error404); router_add(router, "^/static/(.*)$", Static); - // Test router cleanup - Router* test_router = router_create_regex(); - router_add(test_router, "^/test$", TestHandler); - printf("Created test router with 1 route\n"); - - router_mount(router, "/tests", test_router); - - // a router created within the app/blog/ directory - router_mount(router, "/apps", BlogRouter()); - /* * Used to check for memory leaks in allocation and deallocation of * memory diff --git a/src/tools/thc.c b/src/tools/thc.c deleted file mode 100644 index bf9be2f..0000000 --- a/src/tools/thc.c +++ /dev/null @@ -1,253 +0,0 @@ -#include -#include -#include -#include -#include - -#include "utils/header.h" - -int indentation_level = 1; -constexpr int indentation = 4; - -bool -print_escaped(char const* start, char const* end, struct StringBuilder* sb) -{ -#define output(...) \ - do { \ - bool ok = sb_appendf(sb, __VA_ARGS__); \ - if (!ok) return false; \ - } while (0) - - for (auto p = start; p < end; ++p) { - switch (*p) { - case '\"': - output("%s", "\\\""); - break; - case '\\': - output("%s", "\\\\"); - break; - case '\n': { - // combine newlines on single output line - for (; p < end && *p == '\n'; ++p) { - output("%s", "\\n"); - } - if (p >= end || *p == '\0') break; - --p; - - // split multi-line string - constexpr int quotes_index = sizeof("sb_appendf(sb,"); - output("\"\n%*s\"", - indentation_level * indentation + quotes_index, - ""); - } break; - case '\r': - output("%s", "\\r"); - break; - default: - output("%c", *p); - } - } - - return true; - -#undef output -} - -char* -trim(char* str) -{ - while (isspace(*str)) str++; - if (*str == 0) return str; - char* end = str + strlen(str) - 1; - while (end > str && isspace(*end)) end--; - end[1] = '\0'; - return str; -} - -int -translate(const char* const name, char const* const src) -{ - auto len = strlen(name); - char guard[len + 1]; - guard[len] = '\0'; - for (size_t i = 0; i < len; i++) { - guard[i] = toupper((unsigned char)name[i]); - if (guard[i] == '/') guard[i] = '_'; - } - - struct StringBuilder sb = {0}; - -#define outputf(...) \ - do { \ - bool ok = sb_appendf(&sb, __VA_ARGS__); \ - if (!ok) { \ - free(sb.ascii.data); \ - return EXIT_FAILURE; \ - } \ - } while (0) -#define output(...) outputf("%s", __VA_ARGS__) - -#define print_escaped(...) \ - do { \ - bool ok = print_escaped(__VA_ARGS__); \ - if (!ok) { \ - free(sb.ascii.data); \ - return EXIT_FAILURE; \ - } \ - } while (0) - - // Header Guard - outputf("#ifndef %s_H\n#define %s_H\n\n", guard, guard); - output( - "bool render_template(" - "struct Context* ctx, struct StringBuilder* sb);\n\n"); - output("#endif\n\n#ifdef IMPLEMENTATION\n\n"); - output("#define $ (*ctx)\n"); - output( - "#define sb_appendf(...) do { " - "bool res = sb_appendf(__VA_ARGS__); if (!res) return false; " - "} while(0)\n\n"); - output( - "bool\nrender_template(" - "struct Context* ctx, struct StringBuilder* sb)\n{\n"); - - char const* cursor = src; - while (*cursor) { - char* tag_open = strstr(cursor, "{{"); - - if (!tag_open) { - // Remainder of file - outputf("%*ssb_appendf(sb, \"", - indentation_level * indentation, ""); - print_escaped(cursor, cursor + strlen(cursor), &sb); - output("\");\n"); - break; - } - - // 1. Literal text before tag - struct String text_before_tag = {0}; - if (tag_open > cursor) { - text_before_tag.data = (char*)cursor; - text_before_tag.size = tag_open - cursor; - } - - // 2. Parse Tag - char* tag_contents = tag_open + 2; - char* tag_close = strstr(tag_contents, "}}"); - - if (!tag_close) { - int starting_line = 1; - for (auto it = src; it < tag_open; ++it) { - starting_line += *it == '\n'; - } - fprintf(stderr, - "ERROR: Template statement has unmatched " - "parentecies\n%s:%d:\n", - name, starting_line); - auto tail_len = strlen(tag_open); - if (tail_len > 40) tail_len = 40; - tag_open[tail_len] = '\0'; - fprintf(stderr, "%s", tag_open); - free(sb.ascii.data); - return EXIT_FAILURE; - } - - bool line_elimination = - (tag_close > tag_contents && *(tag_close - 1) == '-'); - - // Extract inner content - size_t content_len = tag_close - tag_contents; - if (line_elimination) content_len--; - - char* raw_content = strndup(tag_contents, content_len); - char* code = trim(raw_content); - - cursor = tag_close + 2; - - // 3. Handle Line Elimination - if (line_elimination) { - // Skip trailing white-spaces and exactly one newline - while (isspace(*cursor) && *cursor != '\n') { - cursor++; - } - if (*cursor == '\n') ++cursor; - - // Skip prepended white-spaces - while (text_before_tag.size) { - char last = text_before_tag - .data[text_before_tag.size - 1]; - if (isspace(last) && last != '\n') { - text_before_tag.size--; - } else { - break; - } - } - } - - // Output text before tag - if (text_before_tag.size) { - outputf("%*ssb_appendf(sb, \"", - indentation_level * indentation, ""); - print_escaped( - text_before_tag.data, - text_before_tag.data + text_before_tag.size, &sb); - output("\");\n"); - } - - // Output tag body - size_t const code_len = strlen(code); - if (strncmp(code, "$.", 2) == 0) { - // String Mode shortcut - outputf("%*ssb_appendf(sb, \"%%s\", %s);\n", - indentation_level * indentation, "", code); - } else if (code[0] == '\"') { - // Format Mode shortcut - outputf("%*ssb_appendf(sb, %s);\n", - indentation_level * indentation, "", code); - } else if (code_len) { - // Raw Mode (C code) - // count brackets balance for indentation - int brackets = 0; - for (size_t i = 0; i < code_len; ++i) { - brackets += code[i] == '{'; - brackets -= code[i] == '}'; - } - // decrease indentation before tag's body - if (brackets < 0) indentation_level += brackets; - outputf("%*s%s\n", indentation_level * indentation, "", - code); - // increase indentation after tag's body - if (brackets > 0) indentation_level += brackets; - } - free(raw_content); - } - - output(" return true;\n"); - output("}\n\n#undef sb_appendf\n#undef $\n#endif\n"); - - printf("%s", sb.ascii.data); - free(sb.ascii.data); - - return EXIT_SUCCESS; -} - -int -main(int argc, char** argv) -{ - if (argc < 2) return fprintf(stderr, "Usage: thc \n"), 1; - - auto content = read_entire_file(argv[1]); - if (!content.data) return perror("file"), 1; - - // Get filename without extension for the guard - char* filename = strdup(argv[1]); - char* ext = strrchr(filename, '.'); - if (ext) *ext = '\0'; - - auto exit_code = translate(filename, content.data); - - free(filename); - free(content.data); - - return exit_code; -} From b0a44297b83f4901d74f53dfb99388a8ecbf4321 Mon Sep 17 00:00:00 2001 From: oduortoni Date: Fri, 13 Feb 2026 10:22:46 +0300 Subject: [PATCH 07/14] fix: failing tests --- tests/app/index.hpp | 37 ------------------------------------ tests/gtest.cpp | 1 - tests/integration-tests.hurl | 4 ---- 3 files changed, 42 deletions(-) delete mode 100644 tests/app/index.hpp diff --git a/tests/app/index.hpp b/tests/app/index.hpp deleted file mode 100644 index 47323de..0000000 --- a/tests/app/index.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#include - -extern "C" { -#include "src/app/header.h" -} - -TEST(app_index, wrong_form_data) -{ - struct ResponseWriter rw = {}; - const char* body = ""; - struct Request r = {}; - r.body.size = strlen(body) + 1; - r.body.data = (char*)malloc(r.body.size); - strcpy(r.method, "POST"); - strcpy(r.body.data, body); - int result = Index(&rw, &r); - ASSERT_EQ(result, -1); - free(r.body.data); -} - -TEST(app_index, correct_form_data) -{ - struct ResponseWriter rw = {}; - InitResponseWriter(&rw); - const char* body = - "name=John Doe" - "&email=jd@example.com" - "&message=hello world"; - struct Request r = {}; - r.body.size = strlen(body) + 1; - r.body.data = (char*)malloc(r.body.size); - strcpy(r.method, "POST"); - strcpy(r.body.data, body); - int result = Index(&rw, &r); - ASSERT_EQ(result, 0); - free(r.body.data); -} diff --git a/tests/gtest.cpp b/tests/gtest.cpp index de5cd37..12f4a16 100644 --- a/tests/gtest.cpp +++ b/tests/gtest.cpp @@ -1,6 +1,5 @@ #include -#include "./app/index.hpp" #include "./lib/utils/string-builder/tests.hpp" int diff --git a/tests/integration-tests.hurl b/tests/integration-tests.hurl index 280d38d..056643e 100644 --- a/tests/integration-tests.hurl +++ b/tests/integration-tests.hurl @@ -4,10 +4,6 @@ GET http://localhost:{{PORT}}/ HTTP 200 -# Test: About page -GET http://localhost:{{PORT}}/about -HTTP 200 - # Test: Explicit 404 page GET http://localhost:{{PORT}}/404 HTTP 404 From fb24e08c06ee263f5eaf9317533b7746a9dd62f9 Mon Sep 17 00:00:00 2001 From: oduortoni Date: Fri, 13 Feb 2026 10:50:15 +0300 Subject: [PATCH 08/14] chore: add auto-formatting workflow with clang-format - Adds GitHub Actions workflow to auto-format C files on push/PR - Uses pre-commit with clang-format configured for Google style + custom rules - Removes need for developers to install LLVM or clang-format locally - Ensures consistent code style across the repository automatically --- .github/workflows/clang-format.yml | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/clang-format.yml diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml new file mode 100644 index 0000000..1b3e3a7 --- /dev/null +++ b/.github/workflows/clang-format.yml @@ -0,0 +1,36 @@ +name: Auto-format C code + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + clang-format: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Python (needed for pre-commit) + uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run clang-format via pre-commit + run: pre-commit run --all-files --show-diff-on-failure --color=always + + - name: Commit formatted files + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -u + git commit -m "chore: auto-format with clang-format" || echo "No changes to commit" + git push + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From e6df46163a35a42d7181fe0da063271597bfa2c0 Mon Sep 17 00:00:00 2001 From: oduortoni Date: Fri, 13 Feb 2026 10:59:08 +0300 Subject: [PATCH 09/14] feat(cicd): auto commit formatting styles --- .github/workflows/auto-format.yml | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/auto-format.yml diff --git a/.github/workflows/auto-format.yml b/.github/workflows/auto-format.yml new file mode 100644 index 0000000..601fe7e --- /dev/null +++ b/.github/workflows/auto-format.yml @@ -0,0 +1,39 @@ +name: Auto-format C code + +on: + push: + branches: [ main, "feat/**" ] + pull_request: + branches: [ main, "feat/**" ] + +jobs: + clang-format: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run clang-format and auto-commit + run: | + # Run pre-commit, apply formatting + pre-commit run --all-files + + # Check if there are changes after formatting + if [[ -n "$(git status --porcelain)" ]]; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add . + git commit -m "chore: auto-format with clang-format" + git push + else + echo "No formatting changes needed" + fi From f14d85ef9b6d55994666a60246023c8e4c7d8600 Mon Sep 17 00:00:00 2001 From: oduortoni Date: Fri, 13 Feb 2026 11:06:36 +0300 Subject: [PATCH 10/14] fix(cicd): order of github actions --- .github/workflows/build-and-test.yml | 13 +++++++++++-- .github/workflows/pre-commit.yml | 19 ------------------- 2 files changed, 11 insertions(+), 21 deletions(-) delete mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 7f9adcf..2e55dbb 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -17,7 +17,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 # Action to clone your repository's code + uses: actions/checkout@v4 + + - name: Auto-format code + run: | + git config --global --add safe.directory $PWD + pre-commit run --all-files || true + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + git diff --staged --quiet || git commit -m "style: auto-format code with clang-format" - name: Build C code run: | @@ -26,5 +35,5 @@ jobs: - name: Run tests run: | - make test # unit tests + make test ./tests/integration-tests.sh diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index 12ebb2e..0000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Enforce C Code Style - -on: - pull_request: - push: - branch: main - -jobs: - pre-commit: - runs-on: ubuntu-latest - container: - image: koutoftimer/oduortoni-c-http-server-actions:latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Ceck all files - run: | - git config --global --add safe.directory $PWD - pre-commit run --all-files From d65068451cc537cfa05e012751f0d7c3e8bc0444 Mon Sep 17 00:00:00 2001 From: oduortoni Date: Fri, 13 Feb 2026 11:09:10 +0300 Subject: [PATCH 11/14] fix(cicd): order errors --- .github/workflows/build-and-test.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 2e55dbb..ea5a373 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -18,15 +18,21 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 - name: Auto-format code run: | git config --global --add safe.directory $PWD - pre-commit run --all-files || true git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add -A - git diff --staged --quiet || git commit -m "style: auto-format code with clang-format" + pre-commit run --all-files || true + if ! git diff --quiet; then + git add -A + git commit -m "style: auto-format code with clang-format [skip ci]" + git push + fi - name: Build C code run: | From 6bf1eec64c4121a2c220ca7055db52b0c9a86afd Mon Sep 17 00:00:00 2001 From: oduortoni Date: Fri, 13 Feb 2026 11:12:10 +0300 Subject: [PATCH 12/14] fix: capture precommit exit code --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index ea5a373..750c52b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -27,7 +27,7 @@ jobs: git config --global --add safe.directory $PWD git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - pre-commit run --all-files || true + pre-commit run --all-files && echo "No formatting needed" || echo "Files formatted" if ! git diff --quiet; then git add -A git commit -m "style: auto-format code with clang-format [skip ci]" From bb156c1fb610b1df0c619e73d18329c09ee61588 Mon Sep 17 00:00:00 2001 From: oduortoni Date: Fri, 13 Feb 2026 11:24:31 +0300 Subject: [PATCH 13/14] fix(cicd): consolidate workflows --- .github/workflows/auto-format.yml | 39 ----------------- .github/workflows/build-and-test.yml | 65 +++++++++++++++------------- 2 files changed, 35 insertions(+), 69 deletions(-) delete mode 100644 .github/workflows/auto-format.yml diff --git a/.github/workflows/auto-format.yml b/.github/workflows/auto-format.yml deleted file mode 100644 index 601fe7e..0000000 --- a/.github/workflows/auto-format.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Auto-format C code - -on: - push: - branches: [ main, "feat/**" ] - pull_request: - branches: [ main, "feat/**" ] - -jobs: - clang-format: - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.x" - - - name: Install pre-commit - run: pip install pre-commit - - - name: Run clang-format and auto-commit - run: | - # Run pre-commit, apply formatting - pre-commit run --all-files - - # Check if there are changes after formatting - if [[ -n "$(git status --porcelain)" ]]; then - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add . - git commit -m "chore: auto-format with clang-format" - git push - else - echo "No formatting changes needed" - fi diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 750c52b..fa9e0e9 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,9 +1,9 @@ name: C Build and Test - on: pull_request: push: - branch: main + branches: + - main env: BUILD_TYPE: debug @@ -12,34 +12,39 @@ env: jobs: build-and-test: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + container: image: koutoftimer/oduortoni-c-http-server-actions:latest - + steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - - - name: Auto-format code - run: | - git config --global --add safe.directory $PWD - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - pre-commit run --all-files && echo "No formatting needed" || echo "Files formatted" - if ! git diff --quiet; then - git add -A - git commit -m "style: auto-format code with clang-format [skip ci]" - git push - fi - - - name: Build C code - run: | - make compile_templates - make bin/server - - - name: Run tests - run: | - make test - ./tests/integration-tests.sh + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT_TOKEN }} # Use PAT instead of GITHUB_TOKEN + fetch-depth: 0 + ref: ${{ github.head_ref || github.ref }} + + - name: Auto-format code + run: | + git config --global --add safe.directory $PWD + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + pre-commit run --all-files || true + if ! git diff --quiet; then + git add -A + git commit -m "style: auto-format code with clang-format [skip ci]" + git push + fi + + - name: Build C code + run: | + make compile_templates + make bin/server + + - name: Run tests + run: | + make test + ./tests/integration-tests.sh \ No newline at end of file From a21aaf6cf04fe7cd0fa0183eb545d0d087ca6fa4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Feb 2026 08:25:36 +0000 Subject: [PATCH 14/14] style: auto-format code with clang-format [skip ci] --- src/app/index.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/index.c b/src/app/index.c index 531883f..30c6408 100644 --- a/src/app/index.c +++ b/src/app/index.c @@ -6,7 +6,14 @@ Index(ResponseWriter* w, Request* r) SetStatus(w, 200, "OK"); SetHeader(w, "Content-Type", "text/html"); char buffer[300]; - snprintf(buffer, 300, "

A Minimalistic C Server

Request path: %s

", r->path.data); + snprintf(buffer, 300, + "

A " + "Minimalistic C Server

Request path: " + "%s

", + r->path.data); w->WriteString(w, buffer); return EXIT_SUCCESS;