diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 7f9adcf..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,19 +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 # Action to clone your repository's code
-
- - name: Build C code
- run: |
- make compile_templates
- make bin/server
-
- - name: Run tests
- run: |
- make test # unit tests
- ./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
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 }}
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
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/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/index.c b/src/app/index.c
index ce84359..30c6408 100644
--- a/src/app/index.c
+++ b/src/app/index.c
@@ -1,51 +1,20 @@
#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/lib/http/header.h b/src/lib/http/header.h
index eba24aa..1a8af46 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,10 +129,32 @@ 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;
+ HandlerFunc handler;
+};
+typedef struct Route Route;
+
struct Router {
char* patterns[50];
regex_t regex_patterns[50];
HandlerFunc handlers[50];
+ int route_count;
+ const Dispatcher* dispatcher;
+ void* impl_data;
};
typedef struct Router Router;
@@ -146,6 +167,17 @@ typedef enum RouterStatus {
ROUTER_INVALID_REGEX = -5,
} 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);
RouterStatus (*HandleFunc)(const char* pattern, HandlerFunc handler);
@@ -167,5 +199,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_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..ebee180 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,33 @@ 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 >=
+ 50) { // TODO: replace with dynamic ARRAY_LEN equivalent
+ 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..20f312a
--- /dev/null
+++ b/src/lib/http/http_router.c
@@ -0,0 +1,65 @@
+#include "header.h"
+#include "utils/macros.h"
+
+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,
+ ARRAY_LEN(req->path_matches), req->path_matches,
+ 0)) {
+ return router->handlers[i];
+ }
+ }
+ 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 43e8490..6d40792 100644
--- a/src/lib/http/http_server.c
+++ b/src/lib/http/http_server.c
@@ -10,15 +10,34 @@ 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;
- 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");
+ return -1;
+ }
+
+ puts("http/server.c");
+ printf("Router address: %p\n", (void*)router);
+ printf("Dispatcher address: %p\n", (void*)router->dispatcher);
+
+ if (!router->dispatcher) {
+ 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..b9d00fa
--- /dev/null
+++ b/src/lib/http/route_dispatchers/regex_dispatcher.c
@@ -0,0 +1,97 @@
+#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* 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);
+ }
+}
+
+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/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 73a4aea..3814e6e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -24,17 +24,28 @@ 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, "^/404$", Error404);
+ router_add(router, "^/static/(.*)$", Static);
- http.ListenAndServe(hostname, NULL);
-
- // Router router = {{"/404", "/", "/about", NULL}, {Error404, Index,
- // About, NULL}}; printf("Route: %s\n", router.patterns[1]);
- // http.ListenAndServe(hostname, &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 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");
+ router_free(router);
+
return 0;
}
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;
-}
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