╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────╮ │61 user: Array(evt.User.name_max_len), │ │62 user_settings: Array(evt.User.name_max_len), │ │63 user_auth: Array(evt.User.name_max_len), │ │64 + repo: Array(repo_name_max_len), │ │65 + repo_settings: Array(repo_name_max_len), │ │66 + repo_auth: Array(repo_name_max_len), │ │67 │ │68 pub const default: RoutablePage = .home_users;│ │69 │ ╰──────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │70 const user_segment = "/user/"; │ │71 + const repo_segment = "/repo/"; │ │72 + │ │73 + // a repo is identified by "username/reponame"; the route carries both names │ │74 + // joined by '/', so the inline name must fit both plus the separator. │ │75 + pub const repo_name_max_len = evt.User.name_max_len + 1 + evt.Repo.name_max_len;│ │76 │ │77 // an inline, owned array of data. keeping it in the route (rather than a │ │78 // borrowed slice) makes RoutablePage a plain value: it can be copied, stored │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │101 .home_repos => "/repos", │ │102 .home_settings => "/settings", │ │103 .home_auth => "/auth", │ │ 89 - // user routes carry a runtime name; build them with urlAlloc. │ │104 .user, .user_settings, .user_auth => @compileError("user routes are dynamic; use urlAlloc"),│ │105 + .repo, .repo_settings, .repo_auth => @compileError("repo routes are dynamic; use urlAlloc"),│ │106 }; │ │107 } │ │108 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │111 .user => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}", .{name.slice()}), │ │112 .user_settings => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/settings", .{name.slice()}),│ │113 .user_auth => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/auth", .{name.slice()}), │ │114 + .repo => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{name.slice()}), │ │115 + .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/settings", .{name.slice()}),│ │116 + .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/auth", .{name.slice()}), │ │117 inline else => |_, tag| url(tag), │ │118 }; │ │119 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │122 return switch (self) { │ │123 .home_users, .home_repos, .home_settings, .home_auth => .home,│ │124 .user, .user_settings, .user_auth => .user, │ │125 + .repo, .repo_settings, .repo_auth => .repo, │ │126 }; │ │127 } │ │128 │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │132 if (std.mem.eql(u8, path, "/repos")) return .home_repos; │ │133 if (std.mem.eql(u8, path, "/settings")) return .home_settings;│ │134 if (std.mem.eql(u8, path, "/auth")) return .home_auth; │ │135 + // /user/<name>[/settings|/auth] │ │136 if (std.mem.startsWith(u8, path, user_segment)) { │ │137 const rest = path[user_segment.len..]; │ │138 const slash = std.mem.indexOfScalar(u8, rest, '/'); │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │143 const sub = rest[s + 1 ..]; │ │144 if (std.mem.eql(u8, sub, "settings")) return .{ .user_settings = parsed };│ │145 if (std.mem.eql(u8, sub, "auth")) return .{ .user_auth = parsed }; │ │126 - return null; // unknown user sub-path │ │146 + return null; // unknown sub-path │ │147 } │ │148 return .{ .user = parsed }; │ │149 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │150 + // /repo/<username>/<reponame>[/settings|/auth] — the name carried by the │ │151 + // route is the "username/reponame" pair (both segments, joined). │ │152 + if (std.mem.startsWith(u8, path, repo_segment)) { │ │153 + const rest = path[repo_segment.len..]; │ │154 + const user_slash = std.mem.indexOfScalar(u8, rest, '/') orelse return null; │ │155 + const after_user = rest[user_slash + 1 ..]; │ │156 + const repo_slash = std.mem.indexOfScalar(u8, after_user, '/'); │ │157 + const repo_name = if (repo_slash) |s| after_user[0..s] else after_user; │ │158 + // reject an empty username or reponame │ │159 + if (user_slash == 0 or repo_name.len == 0) return null; │ │160 + const pair = rest[0 .. user_slash + 1 + repo_name.len]; │ │161 + const parsed = Array(repo_name_max_len).from(pair) orelse return null; // name too long│ │162 + if (repo_slash) |s| { │ │163 + const sub = after_user[s + 1 ..]; │ │164 + if (std.mem.eql(u8, sub, "settings")) return .{ .repo_settings = parsed }; │ │165 + if (std.mem.eql(u8, sub, "auth")) return .{ .repo_auth = parsed }; │ │166 + return null; // unknown sub-path │ │167 + } │ │168 + return .{ .repo = parsed }; │ │169 + } │ │170 return null; │ │171 } │ │172 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │176 .user => |a_name| std.mem.eql(u8, a_name.slice(), b.user.slice()), │ │177 .user_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.user_settings.slice()),│ │178 .user_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.user_auth.slice()), │ │179 + .repo => |a_name| std.mem.eql(u8, a_name.slice(), b.repo.slice()), │ │180 + .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()),│ │181 + .repo_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_auth.slice()), │ │182 else => true, │ │183 }; │ │184 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────╮ │193 try jw.write(@tagName(self)); │ │194 switch (self) { │ │195 .user, .user_settings, .user_auth => |name| {│ │196 + try jw.objectField("name"); │ │197 + try jw.write(name.slice()); │ │198 + }, │ │199 + .repo, .repo_settings, .repo_auth => |name| {│ │200 try jw.objectField("name"); │ │201 try jw.write(name.slice()); │ │202 }, │ ╰──────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯