Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 122 additions & 1 deletion src/framework/core.zig
Original file line number Diff line number Diff line change
@@ -1,11 +1,132 @@
const std = @import("std");

pub const Method = enum {
GET,
POST,
PUT,
DELETE,
OPTIONS,
HEAD,
PATCH,

pub fn fromString(method_str: []const u8) ?Method {
if (std.mem.eql(u8, method_str, "GET")) return .GET;
if (std.mem.eql(u8, method_str, "POST")) return .POST;
if (std.mem.eql(u8, method_str, "PUT")) return .PUT;
if (std.mem.eql(u8, method_str, "DELETE")) return .DELETE;
if (std.mem.eql(u8, method_str, "OPTIONS")) return .OPTIONS;
if (std.mem.eql(u8, method_str, "HEAD")) return .HEAD;
if (std.mem.eql(u8, method_str, "PATCH")) return .PATCH;
return null;
}

pub fn toString(self: Method) []const u8 {
return switch (self) {
.GET => "GET",
.POST => "POST",
.PUT => "PUT",
.DELETE => "DELETE",
.OPTIONS => "OPTIONS",
.HEAD => "HEAD",
.PATCH => "PATCH",
};
}
};

pub const Request = struct {
method: Method,
path: []const u8,
headers: std.StringHashMap([]const u8),
body: ?[]const u8,

pub fn init(allocator: std.mem.Allocator) Request {
return .{
.method = .GET,
.path = "",
.headers = std.StringHashMap([]const u8).init(allocator),
.body = null,
};
}

pub fn deinit(self: *Request) void {
var allocator = self.headers.allocator;
self.headers.deinit();
if (self.body) |body| {
allocator.free(body);
}
}
};

pub const Response = struct {
status: u16,
headers: std.StringHashMap([]const u8),
body: ?[]const u8,

pub fn init(allocator: std.mem.Allocator) Response {
return .{
.status = 200,
.headers = std.StringHashMap([]const u8).init(allocator),
.body = null,
};
}

pub fn deinit(self: *Response) void {
var allocator = self.headers.allocator;
self.headers.deinit();
if (self.body) |body| {
allocator.free(body);
}
}
};

pub const Context = struct {
allocator: std.mem.Allocator,

request: Request,
response: Response,
params: std.StringHashMap([]const u8),

pub fn init(allocator: std.mem.Allocator) Context {
return .{
.allocator = allocator,
.request = Request.init(allocator),
.response = Response.init(allocator),
.params = std.StringHashMap([]const u8).init(allocator),
};
}

pub fn deinit(self: *Context) void {
self.request.deinit();
self.response.deinit();
self.params.deinit();
}
};

pub const Handler = *const fn (*Context) anyerror!void;

pub const Middleware = struct {
data: ?*anyopaque,
handle_fn: *const fn (*Context, Handler) anyerror!void,
deinit_fn: ?*const fn (*Middleware) void,

pub fn init(
data: ?*anyopaque,
handle_fn: *const fn (*Context, Handler) anyerror!void,
deinit_fn: ?*const fn (*Middleware) void,
) Middleware {
return .{
.data = data,
.handle_fn = handle_fn,
.deinit_fn = deinit_fn,
};
}

pub fn handle(self: *const Middleware, ctx: *Context, next: Handler) !void {
return self.handle_fn(ctx, next);
}

pub fn deinit(self: *Middleware) void {
if (self.deinit_fn) |deinit_fn| {
deinit_fn(self);
}
}
};
108 changes: 75 additions & 33 deletions src/framework/example.zig
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const std = @import("std");
const Server = @import("server.zig").Server;
const Config = @import("server.zig").Config;
const ServerConfig = @import("server.zig").ServerConfig;
const core = @import("core.zig");
const Router = @import("router.zig").Router;

// Example middleware that logs requests
const LoggerMiddleware = struct {
Expand All @@ -27,17 +28,33 @@ const LoggerMiddleware = struct {
}
};

// Helper function to set text response
fn setText(ctx: *core.Context, text: []const u8) !void {
ctx.response.body = try ctx.allocator.dupe(u8, text);
try ctx.response.headers.put("Content-Type", "text/plain");
}

// Helper function to set JSON response
fn setJson(ctx: *core.Context, data: anytype) !void {
var json_string = std.ArrayList(u8).init(ctx.allocator);
defer json_string.deinit();

try std.json.stringify(data, .{}, json_string.writer());
ctx.response.body = try ctx.allocator.dupe(u8, json_string.items);
try ctx.response.headers.put("Content-Type", "application/json");
}

// Example handlers
fn homeHandlerImpl(ctx: *core.Context) !void {
try ctx.text("Welcome to Zup!");
try setText(ctx, "Welcome to Zup!");
}

fn jsonHandlerImpl(ctx: *core.Context) !void {
const data = .{
.message = "Hello, JSON!",
.timestamp = std.time.timestamp(),
};
try ctx.json(data);
try setJson(ctx, data);
}

fn userHandlerImpl(ctx: *core.Context) !void {
Expand All @@ -47,11 +64,15 @@ fn userHandlerImpl(ctx: *core.Context) !void {
.name = "Example User",
.email = "user@example.com",
};
try ctx.json(response);
try setJson(ctx, response);
}

fn echoHandlerImpl(ctx: *core.Context) !void {
try ctx.text(ctx.request.body);
if (ctx.request.body) |body| {
try setText(ctx, body);
} else {
try setText(ctx, "No body provided");
}
}

pub fn main() !void {
Expand All @@ -60,24 +81,28 @@ pub fn main() !void {
defer _ = gpa.deinit();
const allocator = gpa.allocator();

// Create server with custom config
var server = try Server.init(allocator, .{
.address = "127.0.0.1",
.port = 8080,
.thread_count = 4,
});
defer server.deinit();
// Create router
var router = Router.init(allocator);
defer router.deinit();

// Add global middleware
var logger = LoggerMiddleware.init();
defer logger.deinit();
try server.use(core.Middleware.init(logger, LoggerMiddleware.handle));
try router.use(core.Middleware.init(logger, LoggerMiddleware.handle));

// Define routes
try server.get("/", homeHandlerImpl);
try server.get("/json", jsonHandlerImpl);
try server.get("/users/:id", userHandlerImpl);
try server.post("/echo", echoHandlerImpl);
try router.get("/", homeHandlerImpl);
try router.get("/json", jsonHandlerImpl);
try router.get("/users/:id", userHandlerImpl);
try router.post("/echo", echoHandlerImpl);

// Create server with custom config
var server = try Server.init(allocator, .{
.host = "127.0.0.1",
.port = 8080,
.thread_count = 4,
}, &router);
defer server.deinit();

// Start server
std.log.info("Server running at http://127.0.0.1:8080", .{});
Expand All @@ -88,33 +113,46 @@ test "basic routes" {
const testing = std.testing;
const allocator = testing.allocator;

var server = try Server.init(allocator, .{
.port = 0, // Random port for testing
});
defer server.deinit();
// Create router
var router = Router.init(allocator);
defer router.deinit();

// Add test routes
try server.get("/test", &struct {
try router.get("/test", &struct {
fn handler(ctx: *core.Context) !void {
try ctx.text("test ok");
try setText(ctx, "test ok");
}
}.handler);

try server.post("/echo", echoHandlerImpl);
try router.post("/echo", echoHandlerImpl);

// Create server
var server = try Server.init(allocator, .{
.port = 0, // Random port for testing
.thread_count = 1,
}, &router);
defer server.deinit();

// Start server in background
const thread = try std.Thread.spawn(.{}, Server.start, .{&server});
defer {
server.running.store(false, .release);
thread.join();
}
var running = true;
const thread = try std.Thread.spawn(.{}, struct {
fn run(srv: *Server, is_running: *bool) void {
srv.start() catch |err| {
std.debug.print("Server error: {}\n", .{err});
};
is_running.* = false;
}
}.run, .{&server, &running});

// Wait a bit for server to start
std.time.sleep(10 * std.time.ns_per_ms);
std.time.sleep(100 * std.time.ns_per_ms);

// Get server address
const server_address = server.address;

// Test GET request
{
const client = try std.net.tcpConnectToAddress(server.listener.listen_address);
const client = try std.net.tcpConnectToAddress(server_address);
defer client.close();

try client.writer().writeAll(
Expand All @@ -134,7 +172,7 @@ test "basic routes" {

// Test POST request
{
const client = try std.net.tcpConnectToAddress(server.listener.listen_address);
const client = try std.net.tcpConnectToAddress(server_address);
defer client.close();

const body = "Hello, Echo!";
Expand All @@ -156,4 +194,8 @@ test "basic routes" {

try testing.expect(std.mem.indexOf(u8, response, body) != null);
}
}

// Stop server
server.stop();
thread.join();
}
53 changes: 48 additions & 5 deletions src/framework/router.zig
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ pub const Router = struct {
// Find matching route
const route = self.findRoute(ctx.request.method, ctx.request.path) orelse return error.RouteNotFound;

// Extract path parameters
try extractParams(ctx, route.pattern, ctx.request.path);

// If no middleware, just call the handler
if (self.global_middleware.items.len == 0) {
return route.handler(ctx);
Expand All @@ -103,22 +106,62 @@ pub const Router = struct {

fn matchPattern(self: *Router, pattern: []const u8, path: []const u8) bool {
_ = self;

// Handle root path
if (std.mem.eql(u8, pattern, "/") and std.mem.eql(u8, path, "/")) {
return true;
}

var pattern_parts = std.mem.split(u8, pattern, "/");
var path_parts = std.mem.split(u8, path, "/");


// Skip empty first part if path starts with "/"
if (pattern.len > 0 and pattern[0] == '/') _ = pattern_parts.next();
if (path.len > 0 and path[0] == '/') _ = path_parts.next();

while (true) {
const pattern_part = pattern_parts.next() orelse {
// If we've reached the end of the pattern, the match is successful
// only if we've also reached the end of the path
return path_parts.next() == null;
};
const path_part = path_parts.next() orelse return false;


const path_part = path_parts.next() orelse {
// If we've reached the end of the path but not the pattern,
// the match fails
return false;
};

// Handle path parameters (starting with ":")
if (std.mem.startsWith(u8, pattern_part, ":")) {
// This is a path parameter, it matches any path part
continue;
}


// For regular path parts, they must match exactly
if (!std.mem.eql(u8, pattern_part, path_part)) {
return false;
}
}
}
};

fn extractParams(ctx: *core.Context, pattern: []const u8, path: []const u8) !void {
var pattern_parts = std.mem.split(u8, pattern, "/");
var path_parts = std.mem.split(u8, path, "/");

// Skip empty first part if path starts with "/"
if (pattern.len > 0 and pattern[0] == '/') _ = pattern_parts.next();
if (path.len > 0 and path[0] == '/') _ = path_parts.next();

while (true) {
const pattern_part = pattern_parts.next() orelse break;
const path_part = path_parts.next() orelse break;

// Extract parameter if pattern part starts with ":"
if (std.mem.startsWith(u8, pattern_part, ":")) {
const param_name = pattern_part[1..]; // Skip the ":" prefix
try ctx.params.put(param_name, path_part);
}
}
}
};
Loading
Loading