krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.4.0: Hutch/App/DeepLink.swift · raw
1import Foundation
2import os
3
4private let deepLinkParserLogger = Logger(subsystem: "net.cleberg.Hutch", category: "DeepLink")
5
6/// Represents a parsed `hutch://` deep link.
7enum DeepLink: Equatable {
8 case home
9 case work
10 /// hutch://git/<owner>/<repo> or hutch://hg/<owner>/<repo>
11 case repository(service: SRHTService, owner: String, repo: String)
12 /// hutch://todo/<owner>/<tracker>/<ticketId>
13 case ticket(owner: String, tracker: String, ticketId: Int)
14 /// hutch://builds/<jobId> or hutch://builds/<owner>/job/<jobId>
15 case build(jobId: Int)
16 /// hutch://lists/<owner>/<list>
17 case mailingList(owner: String, list: String)
18 /// hutch://lookup/<owner>
19 case userProfile(owner: String)
20 /// hutch://builds (tab-level)
21 case buildsTab
22 /// hutch://repositories (tab-level)
23 case repositoriesTab
24 /// hutch://trackers (tab-level)
25 case trackersTab
26 /// hutch://status
27 case systemStatus
28 /// hutch://lookup
29 case lookup
30
31 /// Attempt to parse a URL into a DeepLink.
32 /// Expected format: hutch://<path>
33 init?(url: URL) {
34 guard url.scheme == "hutch" else { return nil }
35
36 let components = url.deepLinkPathComponents
37 deepLinkParserLogger.info("DeepLink parser components for \(url.absoluteString, privacy: .public): \(components.joined(separator: ","), privacy: .public)")
38
39 switch components.first {
40 case "home", nil:
41 self = .home
42
43 case "work", "inbox":
44 self = .work
45
46 case let .some(serviceName) where ["git", "hg", "todo", "builds", "lists"].contains(serviceName)
47 && components.count == 2
48 && components[1].hasPrefix("~"):
49 deepLinkParserLogger.info("Treating owner-root service URL as user profile: service=\(serviceName, privacy: .public), owner=\(components[1], privacy: .public)")
50 self = .userProfile(owner: components[1])
51
52 case let .some(serviceName) where ["git", "hg"].contains(serviceName) && components.count >= 3:
53 guard let service = SRHTService(rawValue: serviceName)
54 else { return nil }
55 let owner = components[1]
56 let repo = components[2]
57 self = .repository(service: service, owner: owner, repo: repo)
58
59 case "todo" where components.count >= 4:
60 let owner = components[1]
61 let tracker = components[2]
62 guard let ticketId = Int(components[3]) else { return nil }
63 self = .ticket(owner: owner, tracker: tracker, ticketId: ticketId)
64
65 case "builds" where components.count >= 2:
66 let rawJobId: String
67 if components.count >= 4, components[2] == "job" {
68 rawJobId = components[3]
69 } else {
70 rawJobId = components[1]
71 }
72 guard let jobId = Int(rawJobId) else { return nil }
73 self = .build(jobId: jobId)
74
75 case "builds":
76 self = .buildsTab
77
78 case "lists" where components.count >= 3:
79 self = .mailingList(owner: components[1], list: components[2])
80
81 case "repositories":
82 self = .repositoriesTab
83
84 case "trackers":
85 self = .trackersTab
86
87 case "status":
88 self = .systemStatus
89
90 case "lookup" where components.count >= 2:
91 self = .userProfile(owner: components[1])
92
93 case "lookup":
94 self = .lookup
95
96 default:
97 return nil
98 }
99 }
100}
101
102private extension URL {
103 var deepLinkPathComponents: [String] {
104 guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false) else {
105 return []
106 }
107
108 let pathComponents = components.path
109 .split(separator: "/", omittingEmptySubsequences: true)
110 .map(String.init)
111
112 if let host = components.host, !host.isEmpty {
113 return [host] + pathComponents
114 }
115 return pathComponents
116 }
117}