diff options
| author | Nathan Reiner <nathan@nathanreiner.xyz> | 2026-06-06 07:14:27 +0200 |
|---|---|---|
| committer | Nathan Reiner <nathan@nathanreiner.xyz> | 2026-06-06 07:14:27 +0200 |
| commit | 17561c6be1e4fba11e243f2ac842ebe26fae7e60 (patch) | |
| tree | 5cf16c94d444d772d9f549d0d83158c9a7a133f1 | |
| parent | 8001b02547ba6261682a6d6990157293b23e18d7 (diff) | |
implement Route and RouteSet
| -rw-r--r-- | src/http/Route.zig | 60 | ||||
| -rw-r--r-- | src/http/RouteSet.zig | 124 |
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/")); + } } |