gitbay/Builds/BuildDetailViewModel.swift

v1.2.0
gitbay-ios/gitbay/Builds/BuildDetailViewModel.swift history · blame · raw

72 lines · 2298 bytes

 1import Foundation
 2import Observation
 3
 4nonisolated struct BuildDetail: Sendable {
 5    let build: Build
 6    let log: String
 7}
 8
 9/// One build: `build show` for what it was, `build log` for what it did.
10/// The web's build page shows both; the log alone leaves the screen
11/// unable to say whether the build passed.
12@Observable
13@MainActor
14final class BuildDetailViewModel {
15
16    private(set) var state: LoadState<BuildDetail> = .loading
17    private(set) var actionError: String?
18    private(set) var working = false
19
20    private let client: GitbayClient
21    let repoPath: String
22    let number: Int64
23
24    init(client: GitbayClient, repoPath: String, number: Int64) {
25        self.client = client
26        self.repoPath = repoPath
27        self.number = number
28    }
29
30    func load() async {
31        let ref = [repoPath, String(number)]
32        do {
33            async let build = client.read(["build", "show"] + ref, as: Build.self)
34            // A queued build has no log yet; that is not a failure.
35            async let log = client.readText(["build", "log"] + ref)
36            state = .loaded(BuildDetail(build: try await build, log: (try? await log) ?? ""))
37        } catch {
38            state = .from(error)
39        }
40    }
41
42    /// Only `pending` or `running` builds can be cancelled server-side
43    /// (`internal/control/build.go:750`). The refusal message there says
44    /// "queued", but the wire status is always "pending" — matching on
45    /// "queued" would hide this action forever with no error.
46    static func isCancellable(status: String) -> Bool {
47        status == "pending" || status == "running"
48    }
49
50    var isCancellable: Bool {
51        guard let build = state.value?.build else { return false }
52        return Self.isCancellable(status: build.status)
53    }
54
55    func cancel() async {
56        await perform(["build", "cancel", repoPath, String(number)])
57    }
58
59    private func perform(_ argv: [String]) async {
60        working = true
61        actionError = nil
62        defer { working = false }
63        do {
64            try await client.run(argv)
65            await load()
66        } catch let error as GitbayError {
67            actionError = error.userFacingMessage
68        } catch {
69            actionError = GitbayError.transport(error).userFacingMessage
70        }
71    }
72}