Prepare to use new router
This commit is contained in:
parent
9e3cb40fca
commit
e456a0aa13
3 changed files with 47 additions and 53 deletions
24
src/http.zig
24
src/http.zig
|
@ -1,12 +1,12 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const root = @import("root");
|
const root = @import("root");
|
||||||
|
|
||||||
pub const Router = @import("./http/Router.zig");
|
|
||||||
|
|
||||||
const ciutf8 = @import("./util.zig").ciutf8;
|
const ciutf8 = @import("./util.zig").ciutf8;
|
||||||
const Reader = std.net.Stream.Reader;
|
const Reader = std.net.Stream.Reader;
|
||||||
const Writer = std.net.Stream.Writer;
|
const Writer = std.net.Stream.Writer;
|
||||||
const Route = Router.Route;
|
|
||||||
|
pub const Handler = fn (*Context) HttpError!void;
|
||||||
|
pub const HttpError = error{Http404};
|
||||||
|
|
||||||
const HeaderMap = std.HashMap([]const u8, []const u8, struct {
|
const HeaderMap = std.HashMap([]const u8, []const u8, struct {
|
||||||
pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {
|
pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {
|
||||||
|
@ -127,16 +127,16 @@ fn parseHeaders(allocator: std.mem.Allocator, reader: Reader) !HeaderMap {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handleConnection(conn: std.net.StreamServer.Connection) void {
|
pub fn handleConnection(conn: std.net.StreamServer.Connection, handler: Handler) void {
|
||||||
defer conn.stream.close();
|
defer conn.stream.close();
|
||||||
const reader = conn.stream.reader();
|
const reader = conn.stream.reader();
|
||||||
const writer = conn.stream.writer();
|
const writer = conn.stream.writer();
|
||||||
|
|
||||||
handleRequest(reader, writer) catch |err| std.log.err("unhandled error processing connection: {}", .{err});
|
handleRequest(reader, writer, handler) catch |err| std.log.err("unhandled error processing connection: {}", .{err});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handleRequest(reader: Reader, writer: Writer) !void {
|
fn handleRequest(reader: Reader, writer: Writer, handler: Handler) !void {
|
||||||
handleHttpRequest(reader, writer) catch |err| switch (err) {
|
handleHttpRequest(reader, writer, handler) catch |err| switch (err) {
|
||||||
error.BadRequest, error.UnknownProtocol => try handleBadRequest(writer),
|
error.BadRequest, error.UnknownProtocol => try handleBadRequest(writer),
|
||||||
error.MethodNotImplemented, error.HttpVersionNotSupported => try handleNotImplemented(writer),
|
error.MethodNotImplemented, error.HttpVersionNotSupported => try handleNotImplemented(writer),
|
||||||
else => {
|
else => {
|
||||||
|
@ -146,7 +146,7 @@ fn handleRequest(reader: Reader, writer: Writer) !void {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handleHttpRequest(reader: Reader, writer: Writer) anyerror!void {
|
fn handleHttpRequest(reader: Reader, writer: Writer, handler: Handler) anyerror!void {
|
||||||
const method = try parseHttpMethod(reader);
|
const method = try parseHttpMethod(reader);
|
||||||
|
|
||||||
var header_buf: [1 << 16]u8 = undefined;
|
var header_buf: [1 << 16]u8 = undefined;
|
||||||
|
@ -189,7 +189,7 @@ fn handleHttpRequest(reader: Reader, writer: Writer) anyerror!void {
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
};
|
};
|
||||||
|
|
||||||
try (Router{ .routes = &root.routes }).routeRequest(&context);
|
try handler(&context);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Context = struct {
|
pub const Context = struct {
|
||||||
|
@ -197,15 +197,9 @@ pub const Context = struct {
|
||||||
method: Method,
|
method: Method,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
|
|
||||||
route: ?*const Route = null,
|
|
||||||
|
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
|
||||||
body: ?Reader,
|
body: ?Reader,
|
||||||
|
|
||||||
pub fn arg(self: *Request, name: []const u8) []const u8 {
|
|
||||||
return self.route.?.arg(name, self.path);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Response = struct {
|
const Response = struct {
|
||||||
|
|
47
src/main.zig
47
src/main.zig
|
@ -3,25 +3,24 @@ const std = @import("std");
|
||||||
pub const db = @import("./db.zig");
|
pub const db = @import("./db.zig");
|
||||||
pub const util = @import("./util.zig");
|
pub const util = @import("./util.zig");
|
||||||
pub const http = @import("./http.zig");
|
pub const http = @import("./http.zig");
|
||||||
|
pub const routing = @import("./routing.zig");
|
||||||
|
|
||||||
pub const Uuid = util.Uuid;
|
pub const Uuid = util.Uuid;
|
||||||
pub const ciutf8 = util.ciutf8;
|
pub const ciutf8 = util.ciutf8;
|
||||||
|
|
||||||
pub const io_mode = .evented;
|
pub const io_mode = .evented;
|
||||||
|
|
||||||
const Route = http.Router.Route;
|
pub const router = routing.makeRouter(*http.Context, [_]routing.RouteFn(*http.Context){
|
||||||
|
routing.makeRoute(.GET, "/", staticString("Index Page")),
|
||||||
pub const routes = [_]Route{
|
routing.makeRoute(.GET, "/abc", staticString("abc")),
|
||||||
Route.from(.GET, "/", staticString("Index Page")),
|
routing.makeRoute(.GET, "/user/:id", getUser),
|
||||||
Route.from(.GET, "/abc", staticString("abc")),
|
routing.makeRoute(.POST, "/note/", postNote),
|
||||||
Route.from(.GET, "/user/:id", getUser),
|
});
|
||||||
Route.from(.POST, "/note/", postNote),
|
|
||||||
};
|
|
||||||
|
|
||||||
const this_scheme = "http";
|
const this_scheme = "http";
|
||||||
const this_host = "localhost:8080";
|
const this_host = "localhost:8080";
|
||||||
|
|
||||||
fn postNote(ctx: *http.Context, _: *const Route) anyerror!void {
|
fn postNote(ctx: *http.Context, _: struct {}) anyerror!void {
|
||||||
const id = try db.createNote(.{
|
const id = try db.createNote(.{
|
||||||
.author = Uuid.randV4(util.getRandom()),
|
.author = Uuid.randV4(util.getRandom()),
|
||||||
.content = "test post",
|
.content = "test post",
|
||||||
|
@ -33,20 +32,17 @@ fn postNote(ctx: *http.Context, _: *const Route) anyerror!void {
|
||||||
try writer.writeAll("\"}");
|
try writer.writeAll("\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getUser(ctx: *http.Context, route: *const Route) anyerror!void {
|
fn getUser(ctx: *http.Context, args: struct { id: []const u8 }) anyerror!void {
|
||||||
const id_str = route.arg("id", ctx.request.path);
|
|
||||||
|
|
||||||
const host = ctx.request.headers.get("host") orelse {
|
const host = ctx.request.headers.get("host") orelse {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uuid = Uuid.parse(args.id) catch {
|
||||||
try ctx.response.statusOnly(400);
|
try ctx.response.statusOnly(400);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const id = Uuid.parse(id_str) catch {
|
const actor = try db.getActorById(args.id);
|
||||||
try ctx.response.statusOnly(400);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const actor = try db.getActorById(id);
|
|
||||||
|
|
||||||
if (actor == null or !std.mem.eql(u8, actor.?.host, host)) {
|
if (actor == null or !std.mem.eql(u8, actor.?.host, host)) {
|
||||||
try ctx.response.statusOnly(404);
|
try ctx.response.statusOnly(404);
|
||||||
|
@ -57,20 +53,19 @@ fn getUser(ctx: *http.Context, route: *const Route) anyerror!void {
|
||||||
|
|
||||||
var writer = try ctx.response.open(200);
|
var writer = try ctx.response.open(200);
|
||||||
try writer.writeAll("{\"type\":\"Person\",");
|
try writer.writeAll("{\"type\":\"Person\",");
|
||||||
try writer.print("\"id\":\"{s}://{s}/user/{}\",", .{ this_scheme, this_host, id });
|
try writer.print("\"id\":\"{s}://{s}/user/{}\",", .{ this_scheme, this_host, uuid });
|
||||||
try writer.print("\"preferredUsername\":\"{s}\"", .{actor.?.handle});
|
try writer.print("\"preferredUsername\":\"{s}\"", .{actor.?.handle});
|
||||||
try writer.writeAll("}");
|
try writer.writeAll("}");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn staticString(comptime str: []const u8) Route.Handler {
|
fn staticString(comptime str: []const u8) routing.RouteFn(http.Context) {
|
||||||
return (struct {
|
return (struct {
|
||||||
fn func(ctx: *http.Context, _: *const Route) anyerror!void {
|
fn func(ctx: *http.Context, _: struct {}) http.HttpError!void {
|
||||||
try ctx.response.headers.put("Content-Type", "text/plain");
|
try ctx.response.headers.put("Content-Type", "text/plain");
|
||||||
try ctx.response.write(200, str);
|
try ctx.response.write(200, str);
|
||||||
}
|
}
|
||||||
}).func;
|
}).func;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() anyerror!void {
|
pub fn main() anyerror!void {
|
||||||
var srv = std.net.StreamServer.init(.{ .reuse_address = true });
|
var srv = std.net.StreamServer.init(.{ .reuse_address = true });
|
||||||
defer srv.deinit();
|
defer srv.deinit();
|
||||||
|
@ -84,7 +79,13 @@ pub fn main() anyerror!void {
|
||||||
const conn = try srv.accept();
|
const conn = try srv.accept();
|
||||||
|
|
||||||
// todo: keep track of connections
|
// todo: keep track of connections
|
||||||
_ = async http.handleConnection(conn);
|
_ = async http.handleConnection(conn, struct {
|
||||||
|
fn func(ctx: *http.Context) http.HttpError!void {
|
||||||
|
//try router(ctx, ctx.method, ctx.path);
|
||||||
|
_ = ctx;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}.func);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,22 +70,21 @@ const RouteSegment = union(enum) {
|
||||||
param: []const u8,
|
param: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
// convention: return HttpError IFF a situation you can't finish the request in happens.
|
// convention: return http.HttpError IFF a situation you can't finish the request in happens.
|
||||||
// If status line/headers were written, always return void
|
// If status line/headers were written, always return void
|
||||||
const HttpError = error{Http404};
|
|
||||||
|
|
||||||
fn RouteFn(comptime Context: type) type {
|
pub fn RouteFn(comptime Context: type) type {
|
||||||
return fn (Context, http.Method, []const u8) HttpError!void;
|
return fn (Context, http.Method, []const u8) http.HttpError!void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `makeRoute` takes a route definition and a handler of the form `fn(<Context>, <Params>) HttpError!void`
|
/// `makeRoute` takes a route definition and a handler of the form `fn(<Context>, <Params>) http.HttpError!void`
|
||||||
/// where `Params` is a struct containing one field of type `[]const u8` for each path parameter
|
/// where `Params` is a struct containing one field of type `[]const u8` for each path parameter
|
||||||
///
|
///
|
||||||
/// Arguments:
|
/// Arguments:
|
||||||
/// method: The HTTP method to match
|
/// method: The HTTP method to match
|
||||||
/// path: The path spec to match against. Path segments beginning with a `:` will cause the rest of
|
/// path: The path spec to match against. Path segments beginning with a `:` will cause the rest of
|
||||||
/// the segment to be treated as the name of a path parameter
|
/// the segment to be treated as the name of a path parameter
|
||||||
/// handler: The code to execute on route match. This must be a function of form `fn(<Context>, <Params>) HttpError!void`
|
/// handler: The code to execute on route match. This must be a function of form `fn(<Context>, <Params>) http.HttpError!void`
|
||||||
///
|
///
|
||||||
/// Implicit Arguments:
|
/// Implicit Arguments:
|
||||||
/// Context: the type of a user-supplied Context that is passed through the route. typically `http.Context` but
|
/// Context: the type of a user-supplied Context that is passed through the route. typically `http.Context` but
|
||||||
|
@ -95,17 +94,17 @@ fn RouteFn(comptime Context: type) type {
|
||||||
/// `[]const u8` and it must have the same name as a single path parameter.
|
/// `[]const u8` and it must have the same name as a single path parameter.
|
||||||
///
|
///
|
||||||
/// Returns:
|
/// Returns:
|
||||||
/// A new route function of type `fn(<Context>, http.Method, []const u8) HttpError!void`. When called,
|
/// A new route function of type `fn(<Context>, http.Method, []const u8) http.HttpError!void`. When called,
|
||||||
/// this function will test the provided values against its specification. If they match, then
|
/// this function will test the provided values against its specification. If they match, then
|
||||||
/// this function will parse path parameters and <handler> will be called with the supplied
|
/// this function will parse path parameters and <handler> will be called with the supplied
|
||||||
/// context and params. If they do not match, this function will return error.Http404
|
/// context and params. If they do not match, this function will return error.Http404
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// route(.GET, "/user/:id/followers", struct{
|
/// route(.GET, "/user/:id/followers", struct{
|
||||||
/// fn getFollowers(ctx: http.Context, params: struct{ id: []const u8 } HttpError { ... }
|
/// fn getFollowers(ctx: http.Context, params: struct{ id: []const u8 } http.HttpError { ... }
|
||||||
/// ).getFollowers)
|
/// ).getFollowers)
|
||||||
///
|
///
|
||||||
fn makeRoute(
|
pub fn makeRoute(
|
||||||
comptime method: http.Method,
|
comptime method: http.Method,
|
||||||
comptime path: []const u8,
|
comptime path: []const u8,
|
||||||
comptime handler: anytype,
|
comptime handler: anytype,
|
||||||
|
@ -115,7 +114,7 @@ fn makeRoute(
|
||||||
break :return_type RouteFn(@typeInfo(@TypeOf(handler)).Fn.args[0].arg_type.?);
|
break :return_type RouteFn(@typeInfo(@TypeOf(handler)).Fn.args[0].arg_type.?);
|
||||||
} {
|
} {
|
||||||
const handler_args = @typeInfo(@TypeOf(handler)).Fn.args;
|
const handler_args = @typeInfo(@TypeOf(handler)).Fn.args;
|
||||||
if (handler_args.len != 2) @compileError("handler function must have signature fn(Context, Params) HttpError");
|
if (handler_args.len != 2) @compileError("handler function must have signature fn(Context, Params) http.HttpError");
|
||||||
if (@typeInfo(handler_args[1].arg_type.?) != .Struct) @compileError("Params in handler(Context, Params) must be struct");
|
if (@typeInfo(handler_args[1].arg_type.?) != .Struct) @compileError("Params in handler(Context, Params) must be struct");
|
||||||
|
|
||||||
const Context = handler_args[0].arg_type.?;
|
const Context = handler_args[0].arg_type.?;
|
||||||
|
@ -143,7 +142,7 @@ fn makeRoute(
|
||||||
}
|
}
|
||||||
|
|
||||||
return struct {
|
return struct {
|
||||||
fn func(ctx: Context, req_method: http.Method, req_path: []const u8) HttpError!void {
|
fn func(ctx: Context, req_method: http.Method, req_path: []const u8) http.HttpError!void {
|
||||||
if (req_method != method) return error.Http404;
|
if (req_method != method) return error.Http404;
|
||||||
|
|
||||||
var params: Params = undefined;
|
var params: Params = undefined;
|
||||||
|
@ -168,8 +167,8 @@ fn makeRoute(
|
||||||
}.func;
|
}.func;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn RouterFn(comptime Context: type) type {
|
pub fn RouterFn(comptime Context: type) type {
|
||||||
return fn (http.Method, path: []const u8, Context) HttpError!void;
|
return fn (http.Method, path: []const u8, Context) http.HttpError!void;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn makeRouter(
|
pub fn makeRouter(
|
||||||
|
@ -177,7 +176,7 @@ pub fn makeRouter(
|
||||||
comptime routes: []const RouteFn(Context),
|
comptime routes: []const RouteFn(Context),
|
||||||
) RouterFn(Context) {
|
) RouterFn(Context) {
|
||||||
return struct {
|
return struct {
|
||||||
fn dispatch(method: http.Method, path: []const u8, ctx: Context) HttpError!void {
|
fn dispatch(method: http.Method, path: []const u8, ctx: Context) http.HttpError!void {
|
||||||
for (routes) |r| {
|
for (routes) |r| {
|
||||||
return r(ctx, method, path) catch |err| switch (err) {
|
return r(ctx, method, path) catch |err| switch (err) {
|
||||||
error.Http404 => continue,
|
error.Http404 => continue,
|
||||||
|
@ -303,7 +302,7 @@ const _tests = struct {
|
||||||
fn dummyHandler(comptime Args: type) type {
|
fn dummyHandler(comptime Args: type) type {
|
||||||
comptime {
|
comptime {
|
||||||
return struct {
|
return struct {
|
||||||
fn func(_: TestContext, _: Args) HttpError!void {}
|
fn func(_: TestContext, _: Args) http.HttpError!void {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue