aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2026-06-06 07:14:27 +0200
committerNathan Reiner <nathan@nathanreiner.xyz>2026-06-06 07:14:27 +0200
commit17561c6be1e4fba11e243f2ac842ebe26fae7e60 (patch)
tree5cf16c94d444d772d9f549d0d83158c9a7a133f1
parent8001b02547ba6261682a6d6990157293b23e18d7 (diff)
implement Route and RouteSet
-rw-r--r--src/http/Route.zig60
-rw-r--r--src/http/RouteSet.zig124
2 files changed, 150 insertions, 34 deletions
diff --git a/src/http/Route.zig b/src/http/Route.zig
index bc86544..f979d04 100644
--- a/src/http/Route.zig
+++ b/src/http/Route.zig
@@ -14,14 +14,16 @@ pub const Segment = union(enum) {
segments: []const Segment,
+pub const index: @This() = .{ .segments = &.{} };
+
pub fn fromAny(comptime route: anytype) @This() {
var segments: []const Segment = &.{};
- inline for(route) |r| {
- const segment: []const Segment = &.{ switch (@TypeOf(r)) {
+ inline for (route) |r| {
+ const segment: []const Segment = &.{switch (@TypeOf(r)) {
@EnumLiteral() => .{ .identifier = @tagName(r) },
else => .{ .literal = r },
- } };
+ }};
segments = segments ++ segment;
}
@@ -35,11 +37,55 @@ pub fn format(self: *const @This(), writer: *std.Io.Writer) !void {
}
}
+pub fn isPrefixOf(self: *const @This(), other: *const @This()) bool {
+ if (self.segments.len > other.segments.len) {
+ return false;
+ }
+
+ for (self.segments, other.segments[0..self.segments.len]) |s, o| {
+ if (std.meta.activeTag(s) != std.meta.activeTag(o)) {
+ return false;
+ }
+
+ if (s == .literal) {
+ if (!std.mem.eql(u8, s.literal, o.literal)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+pub fn equals(self: *const @This(), other: *const @This()) bool {
+ if (self.segments.len != other.segments.len) {
+ return false;
+ }
-pub fn shift(self: *const @This()) struct { Segment, @This() } {
- if (self.segments.len == 0) {
- @compileError("shift on empty route");
+ for (self.segments, other.segments) |s, o| {
+ if (std.meta.activeTag(s) != std.meta.activeTag(o)) {
+ return false;
+ }
+
+ if (s == .literal) {
+ if (!std.mem.eql(u8, s.literal, o.literal)) {
+ return false;
+ }
+ }
}
- return .{ self.segments[0], self.segments[1..] };
+ return true;
+}
+
+test "isPrefixOf" {
+ {
+ const parent = comptime fromAny(.{ "api", "images", .id });
+ const child = comptime fromAny(.{ "api", "images", .id, "metadata" });
+ try std.testing.expect(parent.isPrefixOf(&child));
+ }
+ {
+ const parent = comptime fromAny(.{ "api", "videos", .id });
+ const child = comptime fromAny(.{ "api", "images", .id, "metadata" });
+ try std.testing.expect(!parent.isPrefixOf(&child));
+ }
}
diff --git a/src/http/RouteSet.zig b/src/http/RouteSet.zig
index 3b6bfa7..623a2a9 100644
--- a/src/http/RouteSet.zig
+++ b/src/http/RouteSet.zig
@@ -5,45 +5,115 @@ const handler = @import("handler.zig");
const Interface = handler.Interface;
const Context = handler.Context;
-literals: []struct {
- value: []const u8,
- node: *@This(),
-},
-identifiers: []@This(),
+literals: std.StaticStringMap(@This()),
+identifiers: []const @This(),
interface: ?Interface,
-pub fn init(comptime interfaces: []const Interface) @This() {
- _ = interfaces;
- return undefined;
+pub inline fn init(comptime interfaces: []const Interface) @This() {
+ return initPrefix(.index, interfaces);
+}
+
+pub inline fn initPrefix(
+ comptime prefix: Route,
+ comptime interfaces: []const Interface,
+) @This() {
+ return comptime {
+ @setEvalBranchQuota(interfaces.len * interfaces.len * 1000);
+ const KV = struct { []const u8, @This() };
+ var literals: []const KV = &.{};
+ var identifiers: []const @This() = &.{};
+ var current_interface: ?Interface = null;
+
+ for (interfaces) |interface| {
+ if (prefix.equals(&interface.route)) {
+ if (current_interface != null) {
+ @compileError("re-assignment of interface");
+ }
+ current_interface = interface;
+ } else if (prefix.isPrefixOf(&interface.route)) {
+ const inner_prefix: Route = .{
+ .segments = interface.route.segments[0 .. prefix.segments.len + 1],
+ };
+ switch (interface.route.segments[prefix.segments.len]) {
+ .literal => |l| {
+ const lit: []const KV = &.{.{ l, .initPrefix(inner_prefix, interfaces) }};
+ literals = literals ++ lit;
+ },
+ .identifier => {
+ const ident: []const @This() = &.{.initPrefix(inner_prefix, interfaces)};
+ identifiers = identifiers ++ ident;
+ },
+ }
+ }
+ }
+
+ return .{
+ .literals = .initComptime(literals),
+ .identifiers = identifiers,
+ .interface = current_interface,
+ };
+ };
}
pub fn getInterface(self: *const @This(), url: []const u8) ?Interface {
- _ = self;
- _ = url;
+ var current = std.mem.trimStart(u8, url, "/");
+ const index = std.mem.indexOfScalar(u8, current, '/') orelse current.len;
+ const next = current[index..];
+ current = current[0..index];
+
+ if (current.len == 0) {
+ return self.interface;
+ }
+
+ if (self.literals.get(current)) |child| {
+ if (child.getInterface(next)) |interface| {
+ return interface;
+ }
+ }
+
+ for (self.identifiers) |id| {
+ if (id.getInterface(next)) |interface| {
+ return interface;
+ }
+ }
+
return null;
}
test "init" {
- const M = struct {
- const Response = @import("Response.zig");
+ const ctx1: Context(.{ "api", "images", .id, "metadata" }) = .{};
+ const ctx2: Context(.{ "api", "users", "settings", "overview" }) = .{};
+ const ctx3: Context(.{ "api", "users", .id, "settings", .key }) = .{};
+ const ctx4: Context(.{ "api", "users" }) = .{};
- const C = Context(.{ "api", "images", .id, "metadata" });
- const context: C = .{};
+ const routes = init(&.{
+ ctx1.interface(),
+ ctx2.interface(),
+ ctx3.interface(),
+ ctx4.interface(),
+ });
- const Query = struct {
- condition: []const u8,
- start: usize,
- length: usize,
- };
+ {
+ const interface = routes.getInterface("/api/images/123456/metadata") orelse return error.TestFailed;
+ try std.testing.expectEqual(@as(*const anyopaque, @ptrCast(&ctx1)), interface.base);
+ }
- pub fn get(request: C.Request) Response {
- _ = request;
- return .ok(.static("hello"));
- }
- };
+ {
+ const interface = routes.getInterface("/api/users/settings/overview") orelse return error.TestFailed;
+ try std.testing.expectEqual(@as(*const anyopaque, @ptrCast(&ctx2)), interface.base);
+ }
+
+ {
+ const interface = routes.getInterface("/api/users/1234/settings/name") orelse return error.TestFailed;
+ try std.testing.expectEqual(@as(*const anyopaque, @ptrCast(&ctx3)), interface.base);
+ }
- const routes = init(&.{M.context.interface()});
+ {
+ const interface = routes.getInterface("/////api//users///") orelse return error.TestFailed;
+ try std.testing.expectEqual(@as(*const anyopaque, @ptrCast(&ctx4)), interface.base);
+ }
- const interface = routes.getInterface("/api/images/123456/metadata");
- _ = interface;
+ {
+ try std.testing.expectEqual(null, routes.getInterface("/api/images/1234/"));
+ }
}