diff options
Diffstat (limited to 'Hutch/App/DeepLink.swift')
| -rw-r--r-- | Hutch/App/DeepLink.swift | 210 |
1 files changed, 186 insertions, 24 deletions
diff --git a/Hutch/App/DeepLink.swift b/Hutch/App/DeepLink.swift index 574481d..7467fc8 100644 --- a/Hutch/App/DeepLink.swift +++ b/Hutch/App/DeepLink.swift @@ -3,45 +3,50 @@ import os private let deepLinkParserLogger = Logger(subsystem: "net.cleberg.Hutch", category: "DeepLink") -/// Represents a parsed `hutch://` deep link. -enum DeepLink: Equatable { +enum HutchWorkQueueScope: String, CaseIterable, Sendable { + case all + case unread + case assigned +} + +enum HutchRoute: Equatable, Sendable { case home - case work - /// hutch://git/<owner>/<repo> or hutch://hg/<owner>/<repo> + case workQueue(scope: HutchWorkQueueScope = .all) + case recentActivity case repository(service: SRHTService, owner: String, repo: String) - /// hutch://todo/<owner>/<tracker>/<ticketId> + case tracker(owner: String, tracker: String) case ticket(owner: String, tracker: String, ticketId: Int) - /// hutch://builds/<jobId> or hutch://builds/<owner>/job/<jobId> case build(jobId: Int) - /// hutch://lists/<owner>/<list> case mailingList(owner: String, list: String) - /// hutch://lookup/<owner> case userProfile(owner: String) - /// hutch://builds (tab-level) - case buildsTab - /// hutch://repositories (tab-level) - case repositoriesTab - /// hutch://trackers (tab-level) - case trackersTab - /// hutch://status + case builds + case failedBuilds + case repositories + case trackers case systemStatus - /// hutch://lookup case lookup + case search(query: String) + case projectDashboard(id: String, title: String?) - /// Attempt to parse a URL into a DeepLink. - /// Expected format: hutch://<path> init?(url: URL) { guard url.scheme == "hutch" else { return nil } let components = url.deepLinkPathComponents - deepLinkParserLogger.info("DeepLink parser components for \(url.absoluteString, privacy: .public): \(components.joined(separator: ","), privacy: .public)") + let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? [] + let queryValue: (String) -> String? = { name in + queryItems.first { $0.name == name }?.value?.trimmingCharacters(in: .whitespacesAndNewlines) + } switch components.first { case "home", nil: self = .home + case "recent", "recent-activity", "activity": + self = .recentActivity + case "work", "inbox": - self = .work + let scope = queryValue("scope").flatMap(HutchWorkQueueScope.init(rawValue:)) ?? .all + self = .workQueue(scope: scope) case let .some(serviceName) where ["git", "hg", "todo", "builds", "lists"].contains(serviceName) && components.count == 2 @@ -62,6 +67,9 @@ enum DeepLink: Equatable { guard let ticketId = Int(components[3]) else { return nil } self = .ticket(owner: owner, tracker: tracker, ticketId: ticketId) + case "todo" where components.count >= 3: + self = .tracker(owner: components[1], tracker: components[2]) + case "builds" where components.count >= 2: let rawJobId: String if components.count >= 4, components[2] == "job" { @@ -73,16 +81,19 @@ enum DeepLink: Equatable { self = .build(jobId: jobId) case "builds": - self = .buildsTab + self = queryValue("filter") == "failed" ? .failedBuilds : .builds case "lists" where components.count >= 3: self = .mailingList(owner: components[1], list: components[2]) + case "projects" where components.count >= 2: + self = .projectDashboard(id: components[1], title: queryValue("title")) + case "repositories": - self = .repositoriesTab + self = .repositories case "trackers": - self = .trackersTab + self = .trackers case "status": self = .systemStatus @@ -91,12 +102,163 @@ enum DeepLink: Equatable { self = .userProfile(owner: components[1]) case "lookup": - self = .lookup + if let query = queryValue("q"), !query.isEmpty { + self = .search(query: query) + } else { + self = .lookup + } default: return nil } } + + var url: URL { + switch self { + case .home: + return Self.makeURL(host: "home") + case .workQueue(let scope): + return Self.makeURL( + host: "work", + queryItems: scope == .all ? [] : [URLQueryItem(name: "scope", value: scope.rawValue)] + ) + case .recentActivity: + return Self.makeURL(host: "recent-activity") + case .repository(let service, let owner, let repo): + return Self.makeURL(host: service.rawValue, path: [owner, repo]) + case .tracker(let owner, let tracker): + return Self.makeURL(host: "todo", path: [owner, tracker]) + case .ticket(let owner, let tracker, let ticketId): + return Self.makeURL(host: "todo", path: [owner, tracker, String(ticketId)]) + case .build(let jobId): + return Self.makeURL(host: "builds", path: [String(jobId)]) + case .mailingList(let owner, let list): + return Self.makeURL(host: "lists", path: [owner, list]) + case .userProfile(let owner): + return Self.makeURL(host: "lookup", path: [owner]) + case .builds: + return Self.makeURL(host: "builds") + case .failedBuilds: + return Self.makeURL(host: "builds", queryItems: [URLQueryItem(name: "filter", value: "failed")]) + case .repositories: + return Self.makeURL(host: "repositories") + case .trackers: + return Self.makeURL(host: "trackers") + case .systemStatus: + return Self.makeURL(host: "status") + case .lookup: + return Self.makeURL(host: "lookup") + case .search(let query): + return Self.makeURL(host: "lookup", queryItems: [URLQueryItem(name: "q", value: query)]) + case .projectDashboard(let id, let title): + return Self.makeURL( + host: "projects", + path: [id], + queryItems: title.map { [URLQueryItem(name: "title", value: $0)] } ?? [] + ) + } + } + + private static func makeURL( + host: String, + path: [String] = [], + queryItems: [URLQueryItem] = [] + ) -> URL { + var components = URLComponents() + components.scheme = "hutch" + components.host = host + if !path.isEmpty { + components.path = "/" + path.joined(separator: "/") + } + if !queryItems.isEmpty { + components.queryItems = queryItems + } + return components.url! + } +} + +/// Represents a parsed `hutch://` deep link. +enum DeepLink: Equatable { + case home + case work + case workQueue(scope: HutchWorkQueueScope) + case recentActivity + /// hutch://git/<owner>/<repo> or hutch://hg/<owner>/<repo> + case repository(service: SRHTService, owner: String, repo: String) + /// hutch://todo/<owner>/<tracker> + case tracker(owner: String, tracker: String) + /// hutch://todo/<owner>/<tracker>/<ticketId> + case ticket(owner: String, tracker: String, ticketId: Int) + /// hutch://builds/<jobId> or hutch://builds/<owner>/job/<jobId> + case build(jobId: Int) + /// hutch://lists/<owner>/<list> + case mailingList(owner: String, list: String) + /// hutch://lookup/<owner> + case userProfile(owner: String) + /// hutch://builds (tab-level) + case buildsTab + /// hutch://repositories (tab-level) + case repositoriesTab + /// hutch://trackers (tab-level) + case trackersTab + /// hutch://status + case systemStatus + /// hutch://lookup + case lookup + /// hutch://lookup?q=<query> + case search(query: String) + /// hutch://builds?filter=failed + case failedBuilds + /// hutch://projects/<rid> + case projectDashboard(id: String, title: String?) + + /// Attempt to parse a URL into a DeepLink. + /// Expected format: hutch://<path> + init?(url: URL) { + let components = url.deepLinkPathComponents + deepLinkParserLogger.info("DeepLink parser components for \(url.absoluteString, privacy: .public): \(components.joined(separator: ","), privacy: .public)") + guard let route = HutchRoute(url: url) else { return nil } + self = Self(route: route) + } + + init(route: HutchRoute) { + switch route { + case .home: + self = .home + case .workQueue(let scope): + self = scope == .all ? .work : .workQueue(scope: scope) + case .recentActivity: + self = .recentActivity + case .repository(let service, let owner, let repo): + self = .repository(service: service, owner: owner, repo: repo) + case .tracker(let owner, let tracker): + self = .tracker(owner: owner, tracker: tracker) + case .ticket(let owner, let tracker, let ticketId): + self = .ticket(owner: owner, tracker: tracker, ticketId: ticketId) + case .build(let jobId): + self = .build(jobId: jobId) + case .mailingList(let owner, let list): + self = .mailingList(owner: owner, list: list) + case .userProfile(let owner): + self = .userProfile(owner: owner) + case .builds: + self = .buildsTab + case .failedBuilds: + self = .failedBuilds + case .repositories: + self = .repositoriesTab + case .trackers: + self = .trackersTab + case .systemStatus: + self = .systemStatus + case .lookup: + self = .lookup + case .search(let query): + self = .search(query: query) + case .projectDashboard(let id, let title): + self = .projectDashboard(id: id, title: title) + } + } } private extension URL { |
