Setup a router system for zap.
This commit is contained in:
commit
f7468ef5cf
11 changed files with 1093 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
# IntelliJ IDEA
|
||||
*.iml
|
||||
.idea/
|
||||
|
||||
# Zig
|
||||
.zig-cache/
|
||||
zig-out/
|
9
LICENSE
Normal file
9
LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Zeptotech
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
83
README.md
Normal file
83
README.md
Normal file
|
@ -0,0 +1,83 @@
|
|||
<h1 align="center">
|
||||
Zouter
|
||||
</h1>
|
||||
|
||||
<h4 align="center">
|
||||
<a href="https://code.zeptotech.net/Zedd/Zouter">Documentation</a>
|
||||
|
|
||||
<a href="https://zedd.zeptotech.net/zouter/api">API</a>
|
||||
</h4>
|
||||
|
||||
<p align="center">
|
||||
Zig HTTP router library
|
||||
</p>
|
||||
|
||||
## Zouter for zap
|
||||
|
||||
_Zouter_ is an HTTP router library for Zig **zap** HTTP server. It's made to ease the use of **zap** to build REST APIs.
|
||||
|
||||
## Versions
|
||||
|
||||
Zouter 0.1.0 is made for zig 0.13.0 and tested with zap 0.8.0.
|
||||
|
||||
## How to use
|
||||
|
||||
### Example
|
||||
|
||||
Here is a quick example of how to set up a router. It is an extract from the full test code at [`example.zig`](https://code.zeptotech.net/zedd/zouter/src/branch/main/tests/example.zig).
|
||||
|
||||
```zig
|
||||
/// GET /foo/:arg/bar request handler.
|
||||
fn get(route: zouter.MatchedRoute, request: zap.Request) !void {
|
||||
var bodyBuffer: [512]u8 = undefined;
|
||||
const body = try std.fmt.bufPrint(&bodyBuffer, "get: {s}", .{route.params.get("arg").?});
|
||||
try request.sendBody(body);
|
||||
}
|
||||
|
||||
/// POST /foo/:arg/bar request handler.
|
||||
fn post(route: zouter.MatchedRoute, request: zap.Request) !void {
|
||||
var bodyBuffer: [512]u8 = undefined;
|
||||
const body = try std.fmt.bufPrint(&bodyBuffer, "post: {s}", .{route.params.get("arg").?});
|
||||
try request.sendBody(body);
|
||||
}
|
||||
|
||||
/// Setup an example router.
|
||||
fn setupExampleRouter(allocator: std.mem.Allocator) !zouter.Router {
|
||||
// Initialize an example router.
|
||||
var exampleRouter = try zouter.Router.init(allocator, .{});
|
||||
|
||||
// Add a route to the example router.
|
||||
try exampleRouter.route(.{
|
||||
.path = "foo",
|
||||
.children = &[_]zouter.RouteDefinition{
|
||||
.{
|
||||
.path = ":arg",
|
||||
.children = &[_]zouter.RouteDefinition{
|
||||
.{
|
||||
.path = "bar",
|
||||
.handle = .{
|
||||
.get = &get,
|
||||
.post = &post,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return exampleRouter;
|
||||
}
|
||||
```
|
||||
|
||||
### Route definition
|
||||
|
||||
A route only has one mandatory field: its path. If any part of a path starts with a `':'`, the value is taken as a dynamic variable, retrievable later with `Route.params` `HashMap`.
|
||||
|
||||
A route can have:
|
||||
|
||||
- **Children**: sub-routes definitions, with a `'/'` between the parent and the child. It's useful to prefix a list of routes with the same path / variable.
|
||||
- **Handle object**: you can define a handle function for each HTTP basic request method. If you don't care about the request method, there is an `any` field which will be used for all undefined request methods.
|
||||
- **Handle not found / error**: you can define a custom functions to handle errors or not found pages inside this path.
|
||||
- **Pre-handle / post-handle**: these functions are started before and after the request handling in this path. It looks like middlewares and can assume the same role as most of them (e.g. a pre-handle function to check for authentication under a specific path).
|
||||
|
||||
Full details about route definition fields can be found in the [API reference](https://zedd.zeptotech.net/zouter/api).
|
57
build.zig
Normal file
57
build.zig
Normal file
|
@ -0,0 +1,57 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Standard target options allows the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
// Standard optimization options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// Load zap dependency.
|
||||
const zap = b.dependency("zap", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.openssl = false,
|
||||
});
|
||||
|
||||
const lib = b.addSharedLibrary(.{
|
||||
.name = "zouter",
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Add zap dependency.
|
||||
lib.root_module.addImport("zap", zap.module("zap"));
|
||||
|
||||
// This declares intent for the library to be installed into the standard
|
||||
// location when the user invokes the "install" step (the default step when
|
||||
// running `zig build`).
|
||||
b.installArtifact(lib);
|
||||
|
||||
// Creates a step for unit testing. This only builds the test executable
|
||||
// but does not run it.
|
||||
const lib_unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("tests/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Add zap dependency.
|
||||
lib_unit_tests.root_module.addImport("zap", zap.module("zap"));
|
||||
// Add zouter dependency.
|
||||
lib_unit_tests.root_module.addImport("zouter", &lib.root_module);
|
||||
|
||||
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||
|
||||
// Similar to creating the run step earlier, this exposes a `test` step to
|
||||
// the `zig build --help` menu, providing a way for the user to request
|
||||
// running the unit tests.
|
||||
const test_step = b.step("test", "Run unit tests.");
|
||||
test_step.dependOn(&run_lib_unit_tests.step);
|
||||
}
|
18
build.zig.zon
Normal file
18
build.zig.zon
Normal file
|
@ -0,0 +1,18 @@
|
|||
.{
|
||||
.name = "zouter",
|
||||
.version = "0.1.0",
|
||||
.minimum_zig_version = "0.13.0",
|
||||
|
||||
.dependencies = .{
|
||||
.zap = .{
|
||||
.url = "https://github.com/zigzap/zap/archive/v0.8.0.tar.gz",
|
||||
.hash = "12209936c3333b53b53edcf453b1670babb9ae8c2197b1ca627c01e72670e20c1a21",
|
||||
},
|
||||
},
|
||||
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
18
src/root.zig
Normal file
18
src/root.zig
Normal file
|
@ -0,0 +1,18 @@
|
|||
const std = @import("std");
|
||||
const router = @import("router.zig");
|
||||
const route = @import("route.zig");
|
||||
|
||||
pub const MatchedRoute = router.MatchedRoute;
|
||||
pub const RouteHandler = router.RouteHandler;
|
||||
pub const RoutePreHandler = router.RoutePreHandler;
|
||||
pub const ErrorRouteHandler = router.ErrorRouteHandler;
|
||||
|
||||
pub const RouteHandlerDefinition = router.RouteHandlerDefinition;
|
||||
pub const RouteDefinition = router.RouteDefinition;
|
||||
|
||||
pub const Router = router.Router;
|
||||
|
||||
|
||||
pub const RouteParamsMap = route.RouteParamsMap;
|
||||
pub const RoutingResult = route.RoutingResult;
|
||||
pub const RouteNode = route.RouteNode;
|
387
src/route.zig
Normal file
387
src/route.zig
Normal file
|
@ -0,0 +1,387 @@
|
|||
const std = @import("std");
|
||||
const zap = @import("zap");
|
||||
const router = @import("router.zig");
|
||||
|
||||
/// Route params map type.
|
||||
pub const RouteParamsMap = std.StringHashMap([]u8);
|
||||
|
||||
/// Routing result.
|
||||
pub const RoutingResult = struct {
|
||||
const Self = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
route: *RouteNode,
|
||||
handler: router.RouteHandler,
|
||||
params: RouteParamsMap,
|
||||
preHandlers: std.ArrayList(router.RoutePreHandler),
|
||||
postHandlers: std.ArrayList(router.RouteHandler),
|
||||
notFoundHandler: router.RouteHandler,
|
||||
errorHandlers: std.ArrayList(router.ErrorRouteHandler),
|
||||
|
||||
/// Add a URL-encoded param to the routing result.
|
||||
pub fn addHttpParam(self: *Self, key: []const u8, value: []const u8) !void
|
||||
{
|
||||
// Decoding URL-encoded value.
|
||||
var buffer = try self.allocator.alloc(u8, value.len);
|
||||
const decodedValue = std.Uri.percentDecodeBackwards(buffer, value);
|
||||
|
||||
// Move bytes from the end to the beginning of the buffer.
|
||||
std.mem.copyForwards(u8, buffer[0..(decodedValue.len)], buffer[(buffer.len - decodedValue.len)..]);
|
||||
// Resize the buffer to free remaining bytes.
|
||||
_ = self.allocator.resize(buffer, decodedValue.len);
|
||||
buffer = buffer[0..decodedValue.len];
|
||||
|
||||
// Add value to params.
|
||||
try self.params.put(try self.allocator.dupe(u8, key), buffer);
|
||||
}
|
||||
|
||||
/// Add a param to the routing result.
|
||||
pub fn addParam(self: *Self, key: []const u8, value: []const u8) !void
|
||||
{
|
||||
// Add value to params.
|
||||
try self.params.put(try self.allocator.dupe(u8, key), try self.allocator.dupe(u8, value));
|
||||
}
|
||||
|
||||
/// Clone the given result to a new result pointer.
|
||||
pub fn clone(self: *Self) !*Self
|
||||
{
|
||||
const cloned = try Self.init(self.allocator, self.route, self.handler);
|
||||
cloned.params = try self.params.clone();
|
||||
cloned.notFoundHandler = self.notFoundHandler;
|
||||
cloned.preHandlers = try self.preHandlers.clone();
|
||||
cloned.postHandlers = try self.postHandlers.clone();
|
||||
cloned.errorHandlers = try self.errorHandlers.clone();
|
||||
return cloned;
|
||||
}
|
||||
|
||||
/// Initialize a routing result.
|
||||
pub fn init(allocator: std.mem.Allocator, route: *RouteNode, handler: router.RouteHandler) !*Self
|
||||
{
|
||||
const obj = try allocator.create(Self);
|
||||
obj.* = .{
|
||||
.allocator = allocator,
|
||||
.route = route,
|
||||
.handler = handler,
|
||||
.params = RouteParamsMap.init(allocator),
|
||||
|
||||
.preHandlers = std.ArrayList(router.RoutePreHandler).init(allocator),
|
||||
.postHandlers = std.ArrayList(router.RouteHandler).init(allocator),
|
||||
.notFoundHandler = handler,
|
||||
.errorHandlers = std.ArrayList(router.ErrorRouteHandler).init(allocator),
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
/// Deinitialize a routing result.
|
||||
pub fn deinit(self: *Self) void
|
||||
{
|
||||
// Free params map values.
|
||||
var paramsIterator = self.params.iterator();
|
||||
while (paramsIterator.next()) |param|
|
||||
{ // Free every param.
|
||||
self.allocator.free(param.key_ptr.*);
|
||||
self.allocator.free(param.value_ptr.*);
|
||||
}
|
||||
// Free params map.
|
||||
self.params.deinit();
|
||||
|
||||
// Free handlers list.
|
||||
self.preHandlers.deinit();
|
||||
self.postHandlers.deinit();
|
||||
self.errorHandlers.deinit();
|
||||
|
||||
// Free current object.
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
/// Route tree node data structure.
|
||||
pub const RouteNode = struct {
|
||||
const Self = @This();
|
||||
|
||||
/// Static children map type.
|
||||
const StaticChildren = std.StringHashMap(*RouteNode);
|
||||
/// Dynamic children array type.
|
||||
const DynamicChildren = std.ArrayList(*RouteNode);
|
||||
|
||||
/// The used allocator.
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
/// Route path part.
|
||||
node: []const u8,
|
||||
|
||||
/// Static children (static nodes).
|
||||
staticChildren: *StaticChildren,
|
||||
|
||||
/// Dynamic children (variable nodes).
|
||||
dynamicChildren: *DynamicChildren,
|
||||
|
||||
/// Handle function, called when this route is reached.
|
||||
handle: ?router.RouteHandlerDefinition = null,
|
||||
|
||||
/// Not found function, called when a child route was not found.
|
||||
handleNotFound: ?router.RouteHandler = null,
|
||||
|
||||
/// Error function, called when a child route encountered an error.
|
||||
handleError: ?router.ErrorRouteHandler = null,
|
||||
|
||||
/// Pre-handler function, called before handling this route or any children.
|
||||
preHandle: ?router.RoutePreHandler = null,
|
||||
|
||||
/// Post-handler function, called after handling this route or any children.
|
||||
postHandle: ?router.RouteHandler = null,
|
||||
|
||||
/// Find out if the route is static or dynamic.
|
||||
pub fn isDynamic(self: Self) bool
|
||||
{
|
||||
return self.node.len > 0 and self.node[0] == ':';
|
||||
}
|
||||
/// Find out if the route is static or dynamic.
|
||||
pub fn isStatic(self: Self) bool
|
||||
{
|
||||
return !self.isDynamic();
|
||||
}
|
||||
|
||||
/// Add a child to the static or dynamic children, depending on its type.
|
||||
pub fn addChild(self: *Self, child: *RouteNode) !void
|
||||
{
|
||||
if (child.isStatic())
|
||||
{ // The child is static, adding it to the static children.
|
||||
try self.staticChildren.put(child.node, child);
|
||||
}
|
||||
else
|
||||
{ // The child is dynamic, adding it to the dynamic children.
|
||||
try self.dynamicChildren.append(child);
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize and add a new child to the current route tree with the given node.
|
||||
pub fn newChild(self: *Self, node: []const u8) !*RouteNode
|
||||
{
|
||||
// Initialize the new child.
|
||||
const child = try self.allocator.create(Self);
|
||||
child.* = try Self.init(self.allocator, node);
|
||||
// Add the new child to the tree.
|
||||
try self.addChild(child);
|
||||
return child; // Return initialized child tree.
|
||||
}
|
||||
|
||||
/// Parse a given route definition and add the required routes to the tree.
|
||||
pub fn parse(self: *Self, definition: router.RouteDefinition) !void
|
||||
{
|
||||
// Get a node candidate from the definition path.
|
||||
const nodeCandidate = std.mem.trim(u8, definition.path, " /");
|
||||
|
||||
// If the path contains a "/", we need to parse recursively.
|
||||
if (std.mem.indexOf(u8, nodeCandidate, "/")) |pos|
|
||||
{
|
||||
// Get current child node.
|
||||
const childNode = nodeCandidate[0..pos];
|
||||
|
||||
// Get child tree from current child node.
|
||||
var childTree: *RouteNode = undefined;
|
||||
if (self.staticChildren.get(childNode)) |existingChildTree|
|
||||
{ // A tree already exists for this node.
|
||||
childTree = existingChildTree;
|
||||
}
|
||||
else
|
||||
{ // There is no tree for this current node, initializing one.
|
||||
childTree = try self.newChild(childNode);
|
||||
}
|
||||
|
||||
// The path is the rest of the node candidate.
|
||||
try childTree.parse(.{
|
||||
.path = nodeCandidate[pos+1..],
|
||||
.children = definition.children,
|
||||
.handle = definition.handle,
|
||||
.handleNotFound = definition.handleNotFound,
|
||||
.handleError = definition.handleError,
|
||||
});
|
||||
}
|
||||
else
|
||||
{ // No '/' in the path, so the path is a valid tree node, setting its value.
|
||||
var childTree = try self.newChild(nodeCandidate);
|
||||
childTree.handle = definition.handle;
|
||||
childTree.handleNotFound = definition.handleNotFound;
|
||||
childTree.handleError = definition.handleError;
|
||||
|
||||
if (definition.children) |children|
|
||||
{ // If there are children, recursively parse them.
|
||||
for (children) |child|
|
||||
{ // For each child, parse it.
|
||||
try childTree.parse(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get request handler depending on the request method.
|
||||
pub fn getMethodHandler(self: Self, requestMethod: zap.Method) ?router.RouteHandler
|
||||
{
|
||||
if (self.handle) |handle|
|
||||
{ // A handle object is defined, getting the right handler from it.
|
||||
return switch (requestMethod)
|
||||
{ // Return the defined request handler from the request method.
|
||||
zap.Method.GET => handle.get orelse handle.any,
|
||||
zap.Method.POST => handle.post orelse handle.any,
|
||||
zap.Method.PATCH => handle.patch orelse handle.any,
|
||||
zap.Method.PUT => handle.put orelse handle.any,
|
||||
zap.Method.DELETE => handle.delete orelse handle.any,
|
||||
else => handle.any,
|
||||
};
|
||||
}
|
||||
else
|
||||
{ // Undefined request handler, no matter the request method.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Add pre, post, error and not found handlers, if defined.
|
||||
pub fn addHandlers(self: *Self, result: *RoutingResult) !void
|
||||
{
|
||||
if (self.handleNotFound) |handleNotFound|
|
||||
// Setting defined not found handler.
|
||||
result.notFoundHandler = handleNotFound;
|
||||
if (self.preHandle) |preHandle|
|
||||
// Appending defined pre-handler.
|
||||
try result.preHandlers.append(preHandle);
|
||||
if (self.postHandle) |postHandle|
|
||||
// Appending defined post-handler.
|
||||
try result.postHandlers.append(postHandle);
|
||||
if (self.handleError) |handleError|
|
||||
// Appending defined error handler.
|
||||
try result.errorHandlers.append(handleError);
|
||||
}
|
||||
|
||||
/// Initialize a new routing result for the given route.
|
||||
/// Should be called on a route with a not found handler.
|
||||
pub fn newRoutingResult(self: *Self) !*RoutingResult
|
||||
{
|
||||
// Initialize a new routing result with the not found handler.
|
||||
const result = try RoutingResult.init(self.allocator, self, self.handleNotFound.?);
|
||||
|
||||
// Add pre, post, error and not found handlers, if defined.
|
||||
try self.addHandlers(result);
|
||||
|
||||
// Return the initialized routing result.
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Try to find a matching handler in the current route for the given path.
|
||||
/// Return true when a route is matching the request correctly.
|
||||
pub fn match(self: *Self, requestMethod: zap.Method, path: *std.mem.SplitIterator(u8, std.mem.DelimiterType.scalar), result: *RoutingResult) !bool
|
||||
{
|
||||
// Add pre, post, error and not found handlers, if defined.
|
||||
try self.addHandlers(result);
|
||||
|
||||
if (path.next()) |nextPath|
|
||||
{ // Trying to follow the path by finding a matching children.
|
||||
if (self.staticChildren.get(nextPath)) |child|
|
||||
{ // There is a matching static child, continue to match the path on it.
|
||||
return try child.match(requestMethod, path, result);
|
||||
}
|
||||
|
||||
const currentIndex = path.index;
|
||||
// No matching static child, trying dynamic children.
|
||||
for (self.dynamicChildren.items) |child|
|
||||
{ // For each dynamic child, trying to match it.
|
||||
// If no path can be found, try the next one.
|
||||
// Initialize a child routing result with the not found handler.
|
||||
const childResult = try RoutingResult.init(self.allocator, self, result.notFoundHandler);
|
||||
defer childResult.deinit();
|
||||
if (try child.match(requestMethod, path, childResult))
|
||||
{ // Handler has been found, final result is found.
|
||||
|
||||
// Add an HTTP param to the result.
|
||||
try result.addHttpParam(child.node[1..], nextPath);
|
||||
|
||||
// Copy the child result in the main result.
|
||||
{
|
||||
// Set child handlers in the main result.
|
||||
result.handler = childResult.handler;
|
||||
result.notFoundHandler = childResult.notFoundHandler;
|
||||
|
||||
// Add child pre-handlers to the main result.
|
||||
for (childResult.preHandlers.items) |preHandler| {
|
||||
try result.preHandlers.append(preHandler);
|
||||
}
|
||||
// Add child post-handlers to the main result.
|
||||
for (childResult.postHandlers.items) |postHandler| {
|
||||
try result.postHandlers.append(postHandler);
|
||||
}
|
||||
// Add child error handlers to the main result.
|
||||
for (childResult.errorHandlers.items) |errorHandler| {
|
||||
try result.errorHandlers.append(errorHandler);
|
||||
}
|
||||
|
||||
{ // Copy child params to the main result.
|
||||
var childResultParams = childResult.params.iterator();
|
||||
while (childResultParams.next()) |param|
|
||||
{ try result.addParam(param.key_ptr.*, param.value_ptr.*); }
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
// Otherwise, we try the next one (-> rollback iterator and result).
|
||||
path.index = currentIndex; // Reset iterator index.
|
||||
}
|
||||
|
||||
// No child match the current path part, set a not found handler.
|
||||
result.route = self;
|
||||
result.handler = self.handleNotFound orelse result.notFoundHandler;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{ // Path has ended, pointing at the current route.
|
||||
result.route = self;
|
||||
if (self.getMethodHandler(requestMethod)) |handler|
|
||||
{ // There is a handler, set it in the result.
|
||||
result.handler = handler;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{ // There is no handler, set a not found handler.
|
||||
result.handler = self.handleNotFound orelse result.notFoundHandler;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Initialize a new route tree and its children.
|
||||
pub fn init(allocator: std.mem.Allocator, node: []const u8) !Self
|
||||
{
|
||||
// Allocating static children.
|
||||
const staticChildren = try allocator.create(Self.StaticChildren);
|
||||
staticChildren.* = Self.StaticChildren.init(allocator);
|
||||
// Allocating dynamic children.
|
||||
const dynamicChildren = try allocator.create(Self.DynamicChildren);
|
||||
dynamicChildren.* = Self.DynamicChildren.init(allocator);
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.node = try allocator.dupe(u8, node),
|
||||
.staticChildren = staticChildren,
|
||||
.dynamicChildren = dynamicChildren,
|
||||
};
|
||||
}
|
||||
/// Deinitialize the route tree and its children.
|
||||
pub fn deinit(self: *Self) void
|
||||
{
|
||||
// Free all children.
|
||||
var staticChildrenIterator = self.staticChildren.valueIterator();
|
||||
while (staticChildrenIterator.next()) |value|
|
||||
{ value.*.deinit(); self.allocator.destroy(value.*); }
|
||||
for (self.dynamicChildren.items) |value|
|
||||
{ value.deinit(); self.allocator.destroy(value); }
|
||||
|
||||
// Free node name.
|
||||
self.allocator.free(self.node);
|
||||
|
||||
// Free children map and array.
|
||||
self.staticChildren.deinit();
|
||||
self.allocator.destroy(self.staticChildren);
|
||||
self.dynamicChildren.deinit();
|
||||
self.allocator.destroy(self.dynamicChildren);
|
||||
}
|
||||
};
|
195
src/router.zig
Normal file
195
src/router.zig
Normal file
|
@ -0,0 +1,195 @@
|
|||
const std = @import("std");
|
||||
const zap = @import("zap");
|
||||
const routeManager = @import("route.zig");
|
||||
|
||||
/// Matched route structure.
|
||||
pub const MatchedRoute = struct {
|
||||
route: ?*routeManager.RouteNode = null,
|
||||
params: routeManager.RouteParamsMap = undefined,
|
||||
};
|
||||
|
||||
/// Route handler function.
|
||||
pub const RouteHandler = *const fn (route: MatchedRoute, request: zap.Request) anyerror!void;
|
||||
|
||||
/// Route pre-handler function.
|
||||
pub const RoutePreHandler = *const fn (route: MatchedRoute, request: zap.Request) anyerror!bool;
|
||||
|
||||
/// Error route handler function.
|
||||
pub const ErrorRouteHandler = *const fn (route: MatchedRoute, request: zap.Request, err: anyerror) anyerror!void;
|
||||
|
||||
/// Route handler definition for each request method.
|
||||
pub const RouteHandlerDefinition = struct {
|
||||
get: ?RouteHandler = null,
|
||||
post: ?RouteHandler = null,
|
||||
patch: ?RouteHandler = null,
|
||||
put: ?RouteHandler = null,
|
||||
delete: ?RouteHandler = null,
|
||||
any: ?RouteHandler = null,
|
||||
};
|
||||
|
||||
/// Router root tree definition.
|
||||
pub const RouterDefinition = struct {
|
||||
/// Not found function, called when a route was not found.
|
||||
handleNotFound: ?RouteHandler = null,
|
||||
|
||||
/// Error function, called when a route encountered an error.
|
||||
handleError: ?ErrorRouteHandler = null,
|
||||
};
|
||||
|
||||
/// Route definition object.
|
||||
pub const RouteDefinition = struct {
|
||||
/// Route path.
|
||||
path: []const u8,
|
||||
|
||||
/// Children routes: full path will be their path appended to the current route path.
|
||||
children: ?([]const RouteDefinition) = null,
|
||||
|
||||
/// Handle function, called when this route is reached.
|
||||
handle: ?RouteHandlerDefinition = null,
|
||||
|
||||
/// Not found function, called when a child route was not found.
|
||||
handleNotFound: ?RouteHandler = null,
|
||||
|
||||
/// Error function, called when a child route encountered an error.
|
||||
handleError: ?ErrorRouteHandler = null,
|
||||
|
||||
/// Pre-handler function, called before handling this route or any children.
|
||||
preHandle: ?RoutePreHandler = null,
|
||||
|
||||
/// Post-handler function, called after handling this route or any children.
|
||||
postHandle: ?RouteHandler = null,
|
||||
};
|
||||
|
||||
/// A router structure.
|
||||
pub const Router = struct {
|
||||
const Self = @This();
|
||||
|
||||
/// Internal static router instance.
|
||||
var routerInstance: Self = undefined;
|
||||
|
||||
/// Root of the route tree.
|
||||
root: routeManager.RouteNode,
|
||||
|
||||
/// Initialize a new router instance.
|
||||
pub fn init(allocator: std.mem.Allocator, definition: RouterDefinition) !Self
|
||||
{
|
||||
routerInstance = Self{
|
||||
.root = try routeManager.RouteNode.init(allocator, ""),
|
||||
};
|
||||
// Handle of the root tree is never used.
|
||||
routerInstance.root.handle = .{
|
||||
.any = &impossible,
|
||||
};
|
||||
routerInstance.root.handleNotFound = definition.handleNotFound orelse &defaultNotFoundHandler;
|
||||
routerInstance.root.handleError = definition.handleError orelse &defaultErrorHandler;
|
||||
return routerInstance;
|
||||
}
|
||||
|
||||
/// Deinitialize the router instance.
|
||||
pub fn deinit(self: *Self) void
|
||||
{
|
||||
self.root.deinit();
|
||||
routerInstance = undefined;
|
||||
}
|
||||
|
||||
/// Handle an error which happen in any handler.
|
||||
fn handleError(request: zap.Request, err: anyerror, routingResult: *routeManager.RoutingResult) void
|
||||
{
|
||||
// Run error handlers from the most specific to the least specific (reverse order of the array).
|
||||
var errorHandlersIterator = std.mem.reverseIterator(routingResult.errorHandlers.items);
|
||||
while (errorHandlersIterator.next()) |errorHandler|
|
||||
{ // For each error handler, try to run it with the given error.
|
||||
errorHandler(.{
|
||||
.route = routingResult.route,
|
||||
.params = routingResult.params,
|
||||
}, request, err) catch {
|
||||
// Error handler failed, we try the next one.
|
||||
continue;
|
||||
};
|
||||
return; // Error handler ran successfully, we can stop there.
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an incoming request and call the right route.
|
||||
fn handle(self: *Self, request: zap.Request) void
|
||||
{
|
||||
// Split path in route parts.
|
||||
var path = std.mem.splitScalar(u8, std.mem.trim(u8, request.path.?, " /"), '/');
|
||||
// Try to match a route from its parts.
|
||||
const routingResult = self.root.newRoutingResult() catch |err| {
|
||||
// Run default error handler if something happens while building routing result.
|
||||
self.root.handleError.?(.{}, request, err) catch {};
|
||||
return;
|
||||
};
|
||||
// Matching the requested route. Put the result in routingResult pointer.
|
||||
_ = self.root.match(request.methodAsEnum(), &path, routingResult) catch |err| {
|
||||
Self.handleError(request, err, routingResult);
|
||||
return;
|
||||
};
|
||||
defer routingResult.deinit();
|
||||
|
||||
// Try to run matched route handling.
|
||||
Self.runMatchedRouteHandling(routingResult, request)
|
||||
// Handle error in request handling.
|
||||
catch |err| Self.handleError(request, err, routingResult);
|
||||
}
|
||||
|
||||
/// Run a matched route.
|
||||
fn runMatchedRouteHandling(routingResult: *routeManager.RoutingResult, request: zap.Request) !void
|
||||
{
|
||||
// Initialized route data passed to handlers from the routing result.
|
||||
const routeData = .{
|
||||
.route = routingResult.route,
|
||||
.params = routingResult.params,
|
||||
};
|
||||
|
||||
for (routingResult.preHandlers.items) |preHandle|
|
||||
{ // Run each pre-handler. If a pre-handler returns false, handling must stop now.
|
||||
if (!try preHandle(routeData, request))
|
||||
return;
|
||||
}
|
||||
|
||||
// Run matched route handler with result params.
|
||||
try routingResult.handler(routeData, request);
|
||||
|
||||
for (routingResult.postHandlers.items) |postHandle|
|
||||
{ // Run each post-handler.
|
||||
try postHandle(routeData, request);
|
||||
}
|
||||
}
|
||||
|
||||
/// Define a root route.
|
||||
pub fn route(self: *Self, definition: RouteDefinition) !void
|
||||
{
|
||||
try self.root.parse(definition);
|
||||
}
|
||||
|
||||
/// The on_request function of the HTTP listener.
|
||||
pub fn onRequest(request: zap.Request) void
|
||||
{
|
||||
// Call handle of the current router instance.
|
||||
routerInstance.handle(request);
|
||||
}
|
||||
};
|
||||
|
||||
/// Impossible function.
|
||||
fn impossible(_: MatchedRoute, _: zap.Request) !void
|
||||
{
|
||||
unreachable;
|
||||
}
|
||||
|
||||
/// Default not found handling.
|
||||
fn defaultNotFoundHandler(_: MatchedRoute, request: zap.Request) !void
|
||||
{
|
||||
try request.setContentType(zap.ContentType.TEXT);
|
||||
request.setStatus(zap.StatusCode.not_found);
|
||||
try request.sendBody("404: Not Found");
|
||||
}
|
||||
|
||||
/// Default error handling.
|
||||
fn defaultErrorHandler(_: MatchedRoute, request: zap.Request, _: anyerror) !void
|
||||
{
|
||||
try request.setContentType(zap.ContentType.TEXT);
|
||||
request.setStatus(zap.StatusCode.internal_server_error);
|
||||
try request.sendBody("500: Internal Server Error");
|
||||
}
|
127
tests/example.zig
Normal file
127
tests/example.zig
Normal file
|
@ -0,0 +1,127 @@
|
|||
const std = @import("std");
|
||||
const zap = @import("zap");
|
||||
const zouter = @import("zouter");
|
||||
|
||||
/// Stop zap in 3s.
|
||||
fn stopAfter3s() !void {
|
||||
std.time.sleep(3 * std.time.ns_per_s);
|
||||
zap.stop();
|
||||
}
|
||||
|
||||
// Spawn a new thread to stop zap in 3s.
|
||||
fn stopAfter3sThread() !std.Thread {
|
||||
return try std.Thread.spawn(.{}, stopAfter3s, .{});
|
||||
}
|
||||
|
||||
// Make an HTTP request to the given URL and put the result in the given pointed variable.
|
||||
fn makeRequest(allocator: std.mem.Allocator, method: std.http.Method, url: []const u8, varPointer: *[]const u8) !void {
|
||||
// Emit HTTP request to the server.
|
||||
var http_client: std.http.Client = .{ .allocator = allocator };
|
||||
defer http_client.deinit();
|
||||
var response = std.ArrayList(u8).init(allocator);
|
||||
defer response.deinit();
|
||||
_ = try http_client.fetch(.{
|
||||
.method = method,
|
||||
.location = .{ .url = url },
|
||||
.response_storage = .{ .dynamic = &response },
|
||||
});
|
||||
|
||||
varPointer.* = try allocator.dupe(u8, response.items);
|
||||
}
|
||||
|
||||
/// Make a request thread.
|
||||
fn makeRequestThread(allocator: std.mem.Allocator, method: std.http.Method, url: []const u8, varPointer: *[]const u8) !std.Thread {
|
||||
return try std.Thread.spawn(.{}, makeRequest, .{ allocator, method, url, varPointer });
|
||||
}
|
||||
|
||||
/// GET /foo/:arg/bar request handler.
|
||||
fn get(route: zouter.MatchedRoute, request: zap.Request) !void {
|
||||
var bodyBuffer: [512]u8 = undefined;
|
||||
const body = try std.fmt.bufPrint(&bodyBuffer, "get: {s}", .{route.params.get("arg").?});
|
||||
try request.sendBody(body);
|
||||
}
|
||||
|
||||
/// POST /foo/:arg/bar request handler.
|
||||
fn post(route: zouter.MatchedRoute, request: zap.Request) !void {
|
||||
var bodyBuffer: [512]u8 = undefined;
|
||||
const body = try std.fmt.bufPrint(&bodyBuffer, "post: {s}", .{route.params.get("arg").?});
|
||||
try request.sendBody(body);
|
||||
}
|
||||
|
||||
/// Setup an example router.
|
||||
fn setupExampleRouter(allocator: std.mem.Allocator) !zouter.Router {
|
||||
// Initialize an example router.
|
||||
var exampleRouter = try zouter.Router.init(allocator, .{});
|
||||
|
||||
// Add a route to the example router.
|
||||
try exampleRouter.route(.{
|
||||
.path = "foo",
|
||||
.children = &[_]zouter.RouteDefinition{
|
||||
.{
|
||||
.path = ":arg",
|
||||
.children = &[_]zouter.RouteDefinition{
|
||||
.{
|
||||
.path = "bar",
|
||||
.handle = .{
|
||||
.get = &get,
|
||||
.post = &post,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return exampleRouter;
|
||||
}
|
||||
|
||||
/// Run HTTP server with test router.
|
||||
fn runHttp(allocator: std.mem.Allocator) !void {
|
||||
// Setup test router.
|
||||
var exampleRouter = try setupExampleRouter(allocator);
|
||||
defer exampleRouter.deinit();
|
||||
|
||||
// Setup HTTP listener.
|
||||
var listener = zap.HttpListener.init(.{
|
||||
.interface = "127.0.0.1",
|
||||
.port = 8112,
|
||||
.log = true,
|
||||
// Add zouter to the listener.
|
||||
.on_request = zouter.Router.onRequest,
|
||||
});
|
||||
try listener.listen();
|
||||
|
||||
// Emit a GET HTTP request.
|
||||
const getThread = try makeRequestThread(allocator, std.http.Method.GET, "http://127.0.0.1:8112/foo/any%20value/bar", &getResponse);
|
||||
defer getThread.join();
|
||||
|
||||
// Emit a POST HTTP request.
|
||||
const postThread = try makeRequestThread(allocator, std.http.Method.POST, "http://127.0.0.1:8112/foo/any%20value/bar", &postResponse);
|
||||
defer postThread.join();
|
||||
|
||||
// Add zap stop in 3s.
|
||||
const stopThread = try stopAfter3sThread();
|
||||
defer stopThread.join();
|
||||
|
||||
// Start HTTP server workers.
|
||||
zap.start(.{
|
||||
.threads = 1,
|
||||
.workers = 1,
|
||||
});
|
||||
}
|
||||
|
||||
var getResponse: []const u8 = undefined;
|
||||
var postResponse: []const u8 = undefined;
|
||||
|
||||
test {
|
||||
const allocator = std.testing.allocator;
|
||||
try runHttp(allocator);
|
||||
|
||||
// Test that responses are correct.
|
||||
try std.testing.expectEqualStrings("get: any value", getResponse);
|
||||
try std.testing.expectEqualStrings("post: any value", postResponse);
|
||||
|
||||
// Free allocated responses.
|
||||
allocator.free(getResponse);
|
||||
allocator.free(postResponse);
|
||||
}
|
11
tests/root.zig
Normal file
11
tests/root.zig
Normal file
|
@ -0,0 +1,11 @@
|
|||
const std = @import("std");
|
||||
const zouter = @import("../src/root.zig");
|
||||
|
||||
test {
|
||||
// try std.testing.refAllDecls(zouter);
|
||||
}
|
||||
|
||||
comptime {
|
||||
_ = @import("example.zig");
|
||||
_ = @import("simple_routes.zig");
|
||||
}
|
181
tests/simple_routes.zig
Normal file
181
tests/simple_routes.zig
Normal file
|
@ -0,0 +1,181 @@
|
|||
const std = @import("std");
|
||||
const zap = @import("zap");
|
||||
const zouter = @import("zouter");
|
||||
|
||||
var notFoundResponse: []const u8 = undefined;
|
||||
var errorResponse: []const u8 = undefined;
|
||||
var customErrorResponse: []const u8 = undefined;
|
||||
var customNotFoundResponse: []const u8 = undefined;
|
||||
var okResponse: []const u8 = undefined;
|
||||
var argTestResponse: []const u8 = undefined;
|
||||
|
||||
fn stopAfter3s() !void {
|
||||
std.time.sleep(3 * std.time.ns_per_s);
|
||||
zap.stop();
|
||||
}
|
||||
|
||||
fn stopAfter3sThread() !std.Thread {
|
||||
return try std.Thread.spawn(.{}, stopAfter3s, .{});
|
||||
}
|
||||
|
||||
fn makeRequest(allocator: std.mem.Allocator, method: std.http.Method, url: []const u8, varPointer: *[]const u8) !void {
|
||||
// Emit HTTP request to the server.
|
||||
var http_client: std.http.Client = .{ .allocator = allocator };
|
||||
defer http_client.deinit();
|
||||
var response = std.ArrayList(u8).init(allocator);
|
||||
defer response.deinit();
|
||||
_ = try http_client.fetch(.{
|
||||
.method = method,
|
||||
.location = .{ .url = url },
|
||||
.response_storage = .{ .dynamic = &response },
|
||||
});
|
||||
|
||||
varPointer.* = try allocator.dupe(u8, response.items);
|
||||
}
|
||||
|
||||
fn makeRequestThread(allocator: std.mem.Allocator, method: std.http.Method, url: []const u8, varPointer: *[]const u8) !std.Thread {
|
||||
return try std.Thread.spawn(.{}, makeRequest, .{ allocator, method, url, varPointer });
|
||||
}
|
||||
|
||||
fn notFoundHandler(_: zouter.MatchedRoute, request: zap.Request) !void
|
||||
{
|
||||
try request.sendBody("not found");
|
||||
}
|
||||
|
||||
fn internalErrorHandler(_: zouter.MatchedRoute, request: zap.Request, _: anyerror) !void
|
||||
{
|
||||
try request.sendBody("error!");
|
||||
}
|
||||
|
||||
fn customNotFound(_: zouter.MatchedRoute, request: zap.Request) !void
|
||||
{
|
||||
try request.sendBody("sorry, this page does not exists...");
|
||||
}
|
||||
|
||||
fn customErrorHandler(_: zouter.MatchedRoute, request: zap.Request, _: anyerror) !void
|
||||
{
|
||||
try request.sendBody("custom error!");
|
||||
}
|
||||
|
||||
fn empty(_: zouter.MatchedRoute, _: zap.Request) !void
|
||||
{
|
||||
unreachable;
|
||||
}
|
||||
|
||||
fn ok(_: zouter.MatchedRoute, request: zap.Request) !void
|
||||
{
|
||||
try request.sendBody("ok");
|
||||
}
|
||||
|
||||
fn sendArgTest(route: zouter.MatchedRoute, request: zap.Request) !void
|
||||
{
|
||||
try request.sendBody(route.params.get("argTest").?);
|
||||
}
|
||||
|
||||
fn badlyMadeHandler(_: zouter.MatchedRoute, _: zap.Request) !void
|
||||
{
|
||||
return error.HttpParseBody;
|
||||
}
|
||||
|
||||
/// Setup test router.
|
||||
fn setupTestRouter(allocator: std.mem.Allocator) !zouter.Router {
|
||||
var testRouter = try zouter.Router.init(allocator, .{
|
||||
.handleNotFound = ¬FoundHandler,
|
||||
.handleError = &internalErrorHandler,
|
||||
});
|
||||
|
||||
try testRouter.route(.{
|
||||
.path = "anything",
|
||||
.children = &[_]zouter.RouteDefinition{
|
||||
.{
|
||||
.path = ":argTest",
|
||||
.children = &[_]zouter.RouteDefinition{
|
||||
.{
|
||||
.path = "test",
|
||||
.handle = .{
|
||||
.get = &badlyMadeHandler,
|
||||
.delete = &ok,
|
||||
.patch = &sendArgTest,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
.handleError = &customErrorHandler,
|
||||
.handleNotFound = &customNotFound,
|
||||
});
|
||||
|
||||
try testRouter.route(.{
|
||||
.path = "error",
|
||||
.handle = .{
|
||||
.any = &badlyMadeHandler,
|
||||
},
|
||||
});
|
||||
|
||||
return testRouter;
|
||||
}
|
||||
|
||||
/// Run HTTP server with test router.
|
||||
fn runHttp() !void {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Setup test router.
|
||||
var testRouter = try setupTestRouter(allocator);
|
||||
defer testRouter.deinit();
|
||||
|
||||
// Setup HTTP listener.
|
||||
var listener = zap.HttpListener.init(.{
|
||||
.interface = "127.0.0.1",
|
||||
.port = 8112,
|
||||
.log = false,
|
||||
// Add zouter to the listener.
|
||||
.on_request = zouter.Router.onRequest,
|
||||
});
|
||||
zap.enableDebugLog();
|
||||
try listener.listen();
|
||||
|
||||
const notFoundThread = try makeRequestThread(allocator, std.http.Method.GET, "http://127.0.0.1:8112/notfound/query", ¬FoundResponse);
|
||||
defer notFoundThread.join();
|
||||
|
||||
const errorThread = try makeRequestThread(allocator, std.http.Method.GET, "http://127.0.0.1:8112/error", &errorResponse);
|
||||
defer errorThread.join();
|
||||
|
||||
const customErrorThread = try makeRequestThread(allocator, std.http.Method.GET, "http://127.0.0.1:8112/anything/test%20val/test", &customErrorResponse);
|
||||
defer customErrorThread.join();
|
||||
|
||||
const customNotFoundThread = try makeRequestThread(allocator, std.http.Method.POST, "http://127.0.0.1:8112/anything/test%20val/test", &customNotFoundResponse);
|
||||
defer customNotFoundThread.join();
|
||||
|
||||
const okThread = try makeRequestThread(allocator, std.http.Method.DELETE, "http://127.0.0.1:8112/anything/test%20val/test", &okResponse);
|
||||
defer okThread.join();
|
||||
|
||||
const argTestThread = try makeRequestThread(allocator, std.http.Method.PATCH, "http://127.0.0.1:8112/anything/test%20val/test", &argTestResponse);
|
||||
defer argTestThread.join();
|
||||
|
||||
const stopThread = try stopAfter3sThread();
|
||||
defer stopThread.join();
|
||||
|
||||
// Start HTTP server workers.
|
||||
zap.start(.{
|
||||
.threads = 1,
|
||||
.workers = 1,
|
||||
});
|
||||
}
|
||||
|
||||
test {
|
||||
try runHttp();
|
||||
|
||||
try std.testing.expectEqualStrings("not found", notFoundResponse);
|
||||
try std.testing.expectEqualStrings("error!", errorResponse);
|
||||
try std.testing.expectEqualStrings("custom error!", customErrorResponse);
|
||||
try std.testing.expectEqualStrings("sorry, this page does not exists...", customNotFoundResponse);
|
||||
try std.testing.expectEqualStrings("ok", okResponse);
|
||||
try std.testing.expectEqualStrings("test val", argTestResponse);
|
||||
|
||||
std.testing.allocator.free(notFoundResponse);
|
||||
std.testing.allocator.free(errorResponse);
|
||||
std.testing.allocator.free(customErrorResponse);
|
||||
std.testing.allocator.free(customNotFoundResponse);
|
||||
std.testing.allocator.free(okResponse);
|
||||
std.testing.allocator.free(argTestResponse);
|
||||
}
|
Loading…
Reference in a new issue