krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.0.0: Hutch/Extensions/SRHTWebURL.swift · raw

  1import Foundation
  2
  3enum SRHTWebURL {
  4    static let chat = URL(string: "https://chat.sr.ht")!
  5    static let status = URL(string: "https://status.sr.ht")!
  6
  7    static func repository(_ repository: RepositorySummary) -> URL? {
  8        userScopedURL(
  9            host: "\(repository.service.rawValue).sr.ht",
 10            ownerCanonicalName: repository.owner.canonicalName,
 11            pathComponents: [repository.name]
 12        )
 13    }
 14
 15    static func httpsCloneURL(_ repositorySummary: RepositorySummary) -> String? {
 16        repository(repositorySummary)?.absoluteString
 17    }
 18
 19    static func sshCloneURL(_ repositorySummary: RepositorySummary) -> String {
 20        let host = "\(repositorySummary.service.rawValue).sr.ht"
 21        let user = repositorySummary.service == .hg ? "hg" : "git"
 22        return "\(user)@\(host):\(repositorySummary.owner.canonicalName)/\(repositorySummary.name)"
 23    }
 24
 25    static func commit(repository: RepositorySummary, commitId: String) -> URL? {
 26        userScopedURL(
 27            host: "\(repository.service.rawValue).sr.ht",
 28            ownerCanonicalName: repository.owner.canonicalName,
 29            pathComponents: [repository.name, commitPathComponent(for: repository.service), commitId]
 30        )
 31    }
 32
 33    static func file(repository: RepositorySummary, revspec: String, path: String) -> URL? {
 34        switch repository.service {
 35        case .git:
 36            return userScopedURL(
 37                host: "git.sr.ht",
 38                ownerCanonicalName: repository.owner.canonicalName,
 39                pathComponents: [repository.name, "tree", revspec, "item"] + pathComponents(from: path)
 40            )
 41        case .hg:
 42            var components = URLComponents()
 43            components.scheme = "https"
 44            components.host = "hg.sr.ht"
 45
 46            let ownerUsername = username(from: repository.owner.canonicalName)
 47            let encodedRepository = repository.name.addingPercentEncoding(withAllowedCharacters: pathComponentCharacterSet) ?? repository.name
 48            let encodedPath = path.split(separator: "/").map {
 49                String($0).addingPercentEncoding(withAllowedCharacters: pathComponentCharacterSet) ?? String($0)
 50            }.joined(separator: "/")
 51
 52            components.percentEncodedPath = "/~\(ownerUsername)/\(encodedRepository)/browse/\(encodedPath)"
 53            components.queryItems = [URLQueryItem(name: "rev", value: revspec)]
 54            return components.url
 55        default:
 56            return nil
 57        }
 58    }
 59
 60    static func build(jobId: Int, ownerCanonicalName: String) -> URL? {
 61        userScopedURL(
 62            host: "builds.sr.ht",
 63            ownerCanonicalName: ownerCanonicalName,
 64            pathComponents: ["job", String(jobId)]
 65        )
 66    }
 67
 68    static func tracker(ownerUsername: String, trackerName: String) -> URL? {
 69        userScopedURL(
 70            host: "todo.sr.ht",
 71            ownerUsername: ownerUsername,
 72            pathComponents: [trackerName]
 73        )
 74    }
 75
 76    static func tracker(_ trackerSummary: TrackerSummary) -> URL? {
 77        tracker(ownerUsername: trackerSummary.owner.canonicalName.srhtUsername, trackerName: trackerSummary.name)
 78    }
 79
 80    static func projectSource(_ source: Project.SourceRepo) -> URL? {
 81        userScopedURL(
 82            host: "\(source.repoType.service.rawValue).sr.ht",
 83            ownerUsername: source.ownerUsername,
 84            pathComponents: [source.name]
 85        )
 86    }
 87
 88    static func mailingList(ownerUsername: String, listName: String) -> URL? {
 89        userScopedURL(
 90            host: "lists.sr.ht",
 91            ownerUsername: ownerUsername,
 92            pathComponents: [listName]
 93        )
 94    }
 95
 96    static func ticket(ownerUsername: String, trackerName: String, ticketId: Int) -> URL? {
 97        userScopedURL(
 98            host: "todo.sr.ht",
 99            ownerUsername: ownerUsername,
100            pathComponents: [trackerName, String(ticketId)]
101        )
102    }
103
104    static func profile(canonicalName: String) -> URL? {
105        userScopedURL(
106            host: "sr.ht",
107            ownerCanonicalName: canonicalName,
108            pathComponents: []
109        )
110    }
111
112    static func paste(ownerCanonicalName: String, pasteId: String) -> URL? {
113        userScopedURL(
114            host: "paste.sr.ht",
115            ownerCanonicalName: ownerCanonicalName,
116            pathComponents: [pasteId]
117        )
118    }
119
120    private static func userScopedURL(
121        host: String,
122        ownerCanonicalName: String,
123        pathComponents: [String]
124    ) -> URL? {
125        userScopedURL(
126            host: host,
127            ownerUsername: username(from: ownerCanonicalName),
128            pathComponents: pathComponents
129        )
130    }
131
132    private static func userScopedURL(
133        host: String,
134        ownerUsername: String,
135        pathComponents: [String]
136    ) -> URL? {
137        var components = URLComponents()
138        components.scheme = "https"
139        components.host = host
140
141        let encodedComponents = (["~\(ownerUsername)"] + pathComponents).map { pathComponent in
142            pathComponent.addingPercentEncoding(withAllowedCharacters: pathComponentCharacterSet) ?? pathComponent
143        }
144        components.percentEncodedPath = "/" + encodedComponents.joined(separator: "/")
145        return components.url
146    }
147
148    private static func username(from canonicalName: String) -> String {
149        if canonicalName.hasPrefix("~") {
150            return String(canonicalName.dropFirst())
151        }
152        return canonicalName
153    }
154
155    private static func commitPathComponent(for service: SRHTService) -> String {
156        switch service {
157        case .hg:
158            return "rev"
159        default:
160            return "commit"
161        }
162    }
163
164    private static let pathComponentCharacterSet: CharacterSet = {
165        var characterSet = CharacterSet.urlPathAllowed
166        characterSet.remove(charactersIn: "/")
167        return characterSet
168    }()
169
170    private static func pathComponents(from path: String) -> [String] {
171        path.split(separator: "/").map(String.init)
172    }
173}