aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/api/root.zig2
-rw-r--r--src/api/sketch.zig29
-rw-r--r--src/http/Parameter.zig71
-rw-r--r--src/http/Response.zig28
-rw-r--r--src/http/Route.zig45
-rw-r--r--src/http/RouteSet.zig49
-rw-r--r--src/http/Url.zig39
-rw-r--r--src/http/handler.zig149
-rw-r--r--src/http/mime.zig0
-rw-r--r--src/http/root.zig5
10 files changed, 346 insertions, 71 deletions
diff --git a/src/api/root.zig b/src/api/root.zig
index c28991c..5128aa1 100644
--- a/src/api/root.zig
+++ b/src/api/root.zig
@@ -3,8 +3,6 @@
const std = @import("std");
-pub const _ = @import("sketch.zig");
-
test {
_ = std.testing.refAllDecls(@This());
}
diff --git a/src/api/sketch.zig b/src/api/sketch.zig
deleted file mode 100644
index 409606f..0000000
--- a/src/api/sketch.zig
+++ /dev/null
@@ -1,29 +0,0 @@
-const std = @import("std");
-const http = @import("http");
-
-pub const Url = http.Url.Template(
- "/api/images/{id}/metadata/{name}",
- .{
- .guard = Auth(.admin),
- }
-);
-
-const Query = struct {
- condition: []const u8,
- start: usize,
- end: usize,
-}
-
-pub fn post(request: http.Request(Url)) !http.Response {
- const query = try request.body.json(Query, request.arena);
-
- return .string(request.params.id);
-}
-
-pub fn get(request: http.Request(Url)) !http.Response {
- return .file("./some/path.txt");
-}
-
-test {
- _ = std.testing.refAllDecls(@This());
-}
diff --git a/src/http/Parameter.zig b/src/http/Parameter.zig
new file mode 100644
index 0000000..03260b2
--- /dev/null
+++ b/src/http/Parameter.zig
@@ -0,0 +1,71 @@
+const std = @import("std");
+const Route = @import("Route.zig");
+
+repr: []const u8,
+
+pub fn as(self: @This(), T: type) (if (T == []const u8) T else anyerror!T) {
+ if (T == []const u8) {
+ return self.repr;
+ } else if (@typeInfo(T) == .int) {
+ return std.fmt.parseInt(T, self.repr, 10);
+ }
+
+ @compileError(std.fmt.comptimePrint("unsupported default type '{}'", .{T}));
+}
+
+pub fn convert(self: @This(), func: anytype) (@typeInfo(@TypeOf(func)).@"fn".return_type orelse void) {
+ return func(self.repr);
+}
+
+pub fn Struct(comptime route: Route) type {
+ var names: []const []const u8 = &.{};
+
+ inline for (route.segments) |segment| {
+ switch (segment) {
+ .literal => {},
+ .identifier => |id| {
+ const name: []const []const u8 = &.{id};
+ names = names ++ name;
+ },
+ }
+ }
+
+ var types: [names.len]type = undefined;
+ var attrs: [names.len]std.builtin.Type.StructField.Attributes = undefined;
+
+ @memset(&types, @This());
+ @memset(&attrs, std.builtin.Type.StructField.Attributes{});
+
+ return @Struct(.auto, null, names, &types, &attrs);
+}
+
+pub fn parse(P: type, comptime route: *const Route, url: []const u8) P {
+ var params: P = undefined;
+ var buffer = url;
+
+ inline for (route.segments) |segment| {
+ buffer = std.mem.trimStart(u8, buffer, "/");
+ const index = std.mem.indexOfScalar(u8, buffer, '/') orelse buffer.len;
+ const value = buffer[0..index];
+ buffer = buffer[index..];
+
+ switch (segment) {
+ .literal => {},
+ .identifier => |id| {
+ @field(params, id) = .{ .repr = value };
+ },
+ }
+ }
+
+ return params;
+}
+
+test "parse" {
+ const route: Route = comptime .fromAny(.{ "api", "users", .name, "settings", .id });
+ const Params = Struct(route);
+ const url = "/api/users/nathan.reiner/settings/123456789";
+
+ const params = parse(Params, &route, url);
+ try std.testing.expectEqualStrings("nathan.reiner", params.name.as([]const u8));
+ try std.testing.expectEqual(123456789, try params.id.as(usize));
+}
diff --git a/src/http/Response.zig b/src/http/Response.zig
new file mode 100644
index 0000000..a5f7727
--- /dev/null
+++ b/src/http/Response.zig
@@ -0,0 +1,28 @@
+const std = @import("std");
+
+pub const Message = union(enum) {
+ void: void,
+ static: []const u8,
+ file_content: std.Io.File,
+
+ pub fn string(content: []const u8) Message {
+ return .{ .static = content };
+ }
+
+ pub fn file(io: std.Io, path: []const u8) !Message {
+ return .{ .file_content = try std.Io.Dir.cwd().openFile(io, path, .{
+ .allow_directory = false,
+ }) };
+ }
+};
+
+status: std.http.Status,
+message: Message,
+
+pub fn with(status: std.http.Status, message: Message) @This() {
+ return .{ .status = status, .message = message };
+}
+
+pub fn ok(message: Message) @This() {
+ return with(.ok, message);
+}
diff --git a/src/http/Route.zig b/src/http/Route.zig
new file mode 100644
index 0000000..bc86544
--- /dev/null
+++ b/src/http/Route.zig
@@ -0,0 +1,45 @@
+const std = @import("std");
+
+pub const Segment = union(enum) {
+ literal: []const u8,
+ identifier: []const u8,
+
+ pub fn format(self: *const @This(), writer: *std.Io.Writer) !void {
+ switch (self.*) {
+ .literal => |l| try writer.print("{s}", .{l}),
+ .identifier => |l| try writer.print("{{{s}}}", .{l}),
+ }
+ }
+};
+
+segments: []const Segment,
+
+pub fn fromAny(comptime route: anytype) @This() {
+ var segments: []const Segment = &.{};
+
+ inline for(route) |r| {
+ const segment: []const Segment = &.{ switch (@TypeOf(r)) {
+ @EnumLiteral() => .{ .identifier = @tagName(r) },
+ else => .{ .literal = r },
+ } };
+
+ segments = segments ++ segment;
+ }
+
+ return .{ .segments = segments };
+}
+
+pub fn format(self: *const @This(), writer: *std.Io.Writer) !void {
+ for (self.segments) |*seg| {
+ try writer.print("/{f}", .{seg});
+ }
+}
+
+
+pub fn shift(self: *const @This()) struct { Segment, @This() } {
+ if (self.segments.len == 0) {
+ @compileError("shift on empty route");
+ }
+
+ return .{ self.segments[0], self.segments[1..] };
+}
diff --git a/src/http/RouteSet.zig b/src/http/RouteSet.zig
new file mode 100644
index 0000000..3b6bfa7
--- /dev/null
+++ b/src/http/RouteSet.zig
@@ -0,0 +1,49 @@
+const std = @import("std");
+
+const Route = @import("Route.zig");
+const handler = @import("handler.zig");
+const Interface = handler.Interface;
+const Context = handler.Context;
+
+literals: []struct {
+ value: []const u8,
+ node: *@This(),
+},
+identifiers: []@This(),
+interface: ?Interface,
+
+pub fn init(comptime interfaces: []const Interface) @This() {
+ _ = interfaces;
+ return undefined;
+}
+
+pub fn getInterface(self: *const @This(), url: []const u8) ?Interface {
+ _ = self;
+ _ = url;
+ return null;
+}
+
+test "init" {
+ const M = struct {
+ const Response = @import("Response.zig");
+
+ const C = Context(.{ "api", "images", .id, "metadata" });
+ const context: C = .{};
+
+ const Query = struct {
+ condition: []const u8,
+ start: usize,
+ length: usize,
+ };
+
+ pub fn get(request: C.Request) Response {
+ _ = request;
+ return .ok(.static("hello"));
+ }
+ };
+
+ const routes = init(&.{M.context.interface()});
+
+ const interface = routes.getInterface("/api/images/123456/metadata");
+ _ = interface;
+}
diff --git a/src/http/Url.zig b/src/http/Url.zig
deleted file mode 100644
index 4a0a2b0..0000000
--- a/src/http/Url.zig
+++ /dev/null
@@ -1,39 +0,0 @@
-const std = @import("std");
-
-const Segment = union(enum) {
- literal: []const u8,
- parameter: []const u8,
-
- pub fn format(self: @This(), writer: *std.Io.Writer) !void {
- try writer.print("{s}{{ {s} }}", .{ @tagName(self), switch (self) {
- inline else => |s| s,
- } });
- }
-};
-
-pub fn Template(comptime template: []const u8) []const Segment {
- var segments: []const Segment = &.{};
- var t = template;
-
- if (template.len == 0 or template[0] != '/') {
- @compileError("template needs to be absolute");
- }
-
- while (t.len > 0) {
- const index = std.mem.indexOfScalar(u8, t, '/');
- const segment = t[0 .. index orelse t.len];
- t = t[(index orelse t.len - 1) + 1 ..];
-
- if (segment.len > 0) {
- const s: []const Segment = &.{seg: {
- if (segment[0] == '{' and segment[segment.len - 1] == '}') {
- break :seg .{ .parameter = segment[1 .. segment.len - 1] };
- } else {
- break :seg .{ .literal = segment };
- }
- }};
- segments = segments ++ s;
- }
- }
- return segments;
-}
diff --git a/src/http/handler.zig b/src/http/handler.zig
new file mode 100644
index 0000000..2220fd0
--- /dev/null
+++ b/src/http/handler.zig
@@ -0,0 +1,149 @@
+const std = @import("std");
+
+const Response = @import("Response.zig");
+const Route = @import("Route.zig");
+const Parameter = @import("Parameter.zig");
+
+pub fn Context(comptime r: anytype) type {
+ return struct {
+ const route = Route.fromAny(r);
+ const Params = Parameter.Struct(route);
+
+ const Request = struct {
+ base_request: *std.http.Server.Request,
+ params: Params,
+ arena: std.mem.Allocator,
+ gpa: std.mem.Allocator,
+ io: std.Io,
+
+ /// Parses the body of the request and returns type `T`.
+ /// The arena allocator will be used to do the parsing.
+ pub fn json(self: @This(), T: type) !T {
+ var buffer: [1024]u8 = undefined;
+ var reader = std.json.Reader.init(self.arena, self.base_request.readerExpectNone(&buffer));
+ return std.json.parseFromTokenSourceLeaky(T, self.arena, &reader, .{});
+ }
+ };
+
+ const badRequest = struct {
+ pub fn badRequest(_: Request) Response {
+ return .with(.bad_request, .string("method does not exist"));
+ }
+ }.badRequest;
+
+ get: *const fn (Request) Response = badRequest,
+ post: *const fn (Request) Response = badRequest,
+ delete: *const fn (Request) Response = badRequest,
+ head: *const fn (Request) Response = badRequest,
+ put: *const fn (Request) Response = badRequest,
+ connect: *const fn (Request) Response = badRequest,
+ options: *const fn (Request) Response = badRequest,
+ trace: *const fn (Request) Response = badRequest,
+ patch: *const fn (Request) Response = badRequest,
+
+ pub fn handle(
+ self: *const @This(),
+ gpa: std.mem.Allocator,
+ arena: std.mem.Allocator,
+ io: std.Io,
+ request: *std.http.Server.Request,
+ ) Response {
+ const req: Request = .{
+ .base_request = request,
+ .arena = arena,
+ .gpa = gpa,
+ .io = io,
+ .params = Parameter.parse(Params, &route, request.head.target),
+ };
+
+ return (switch (request.head.method) {
+ .GET => self.get,
+ .POST => self.post,
+ .DELETE => self.delete,
+ .HEAD => self.head,
+ .PUT => self.put,
+ .CONNECT => self.connect,
+ .OPTIONS => self.options,
+ .TRACE => self.trace,
+ .PATCH => self.patch,
+ })(req);
+ }
+
+ pub fn interface(self: *const @This()) Interface {
+ return .from(self, route);
+ }
+ };
+}
+
+pub const Interface = struct {
+ route: Route,
+ base: *const anyopaque,
+ handler: *const fn (
+ *const anyopaque,
+ std.mem.Allocator,
+ std.mem.Allocator,
+ std.Io,
+ *std.http.Server.Request,
+ ) Response,
+
+ pub fn from(instance: anytype, route: Route) @This() {
+ const PtrT = @TypeOf(instance);
+ const func = struct {
+ pub fn handle(
+ base: *const anyopaque,
+ gpa: std.mem.Allocator,
+ arena: std.mem.Allocator,
+ io: std.Io,
+ request: *std.http.Server.Request,
+ ) Response {
+ return @as(PtrT, @ptrCast(@alignCast(base))).handle(gpa, arena, io, request);
+ }
+ }.handle;
+
+ return .{
+ .base = @ptrCast(instance),
+ .handler = func,
+ .route = route,
+ };
+ }
+
+ pub fn handle(
+ self: *const @This(),
+ gpa: std.mem.Allocator,
+ arena: std.mem.Allocator,
+ io: std.Io,
+ request: std.http.Server.Request,
+ ) Response {
+ return self.handler(self.base, gpa, arena, io, request);
+ }
+};
+
+test "interface" {
+ const M = struct {
+ const C = Context(.{ "api", "images", .id, "metadata" });
+ const context: C = .{
+ .post = post,
+ .get = get,
+ };
+
+ const Query = struct {
+ condition: []const u8,
+ start: usize,
+ length: usize,
+ };
+
+ fn post(request: C.Request) Response {
+ const query = request.json(Query) catch return .with(.bad_request, .void);
+ _ = query;
+ _ = request.params.id;
+
+ return .ok(.string(request.params.id.as([]const u8)));
+ }
+
+ fn get(request: C.Request) Response {
+ return .ok(Response.Message.file(request.io, "./some/path.txt") catch return .with(.not_found, .void));
+ }
+ };
+
+ _ = M.context.interface();
+}
diff --git a/src/http/mime.zig b/src/http/mime.zig
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/http/mime.zig
diff --git a/src/http/root.zig b/src/http/root.zig
index 317dacd..2fa4042 100644
--- a/src/http/root.zig
+++ b/src/http/root.zig
@@ -3,7 +3,10 @@
const std = @import("std");
pub const Server = @import("Server.zig");
-pub const Url = @import("Url.zig");
+pub const handler = @import("handler.zig");
+pub const Response = @import("Response.zig");
+pub const Route = @import("Route.zig");
+pub const RouteSet = @import("RouteSet.zig");
test {
_ = std.testing.refAllDecls(@This());