a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Orgs/OrgViewModels.swift

main
gitbay-ios/gitbay/Orgs/OrgViewModels.swift history · blame · raw

162 lines · 4712 bytes

  1import Foundation
  2import Observation
  3
  4/// One membership row of `org list`.
  5nonisolated struct OrgMembership: Decodable, Sendable, Hashable, Identifiable {
  6    let org: String
  7    let role: String
  8    var id: String { org }
  9}
 10
 11/// One member of `org show` / `org members list`.
 12nonisolated struct OrgMember: Decodable, Sendable, Hashable, Identifiable {
 13    let user: String
 14    let role: String
 15    var id: String { user }
 16}
 17
 18/// An organization: members and teams, plus the membership writes.
 19/// Reads require membership; the refusal names the rule for outsiders.
 20@Observable
 21@MainActor
 22final class OrgViewModel {
 23
 24    nonisolated struct Loaded: Sendable, Hashable {
 25        let members: [OrgMember]
 26        let teams: [String]
 27    }
 28
 29    private(set) var state: LoadState<Loaded> = .loading
 30    private(set) var actionError: String?
 31    private(set) var working = false
 32
 33    private let client: GitbayClient
 34    let orgName: String
 35
 36    init(client: GitbayClient, orgName: String) {
 37        self.client = client
 38        self.orgName = orgName
 39    }
 40
 41    nonisolated private struct OrgShow: Decodable, Sendable {
 42        let org: String
 43        let members: [OrgMember]
 44    }
 45
 46    func load() async {
 47        do {
 48            async let show = client.read(["org", "show", orgName], as: OrgShow.self)
 49            // Team list is bare names.
 50            async let teams = client.readList(["org", "team", "list", orgName], of: String.self)
 51            state = .loaded(Loaded(members: try await show.members, teams: try await teams))
 52        } catch {
 53            state = .from(error)
 54        }
 55    }
 56
 57    func addMember(_ user: String, role: String) async {
 58        await perform(["org", "members", "add", orgName, user, "--role", role])
 59    }
 60
 61    func removeMember(_ member: OrgMember) async {
 62        await perform(["org", "members", "remove", orgName, member.user])
 63    }
 64
 65    /// Role changes reuse `members add`  it updates an existing member.
 66    func setRole(_ member: OrgMember, role: String) async {
 67        await perform(["org", "members", "add", orgName, member.user, "--role", role])
 68    }
 69
 70    func createTeam(_ team: String) async {
 71        await perform(["org", "team", "create", orgName, team])
 72    }
 73
 74    func deleteTeam(_ team: String) async {
 75        await perform(["org", "team", "delete", orgName, team])
 76    }
 77
 78    private func perform(_ argv: [String]) async {
 79        working = true
 80        actionError = nil
 81        defer { working = false }
 82        do {
 83            try await client.run(argv)
 84            await load()
 85        } catch let error as GitbayError {
 86            actionError = error.userFacingMessage
 87        } catch {
 88            actionError = GitbayError.transport(error).userFacingMessage
 89        }
 90    }
 91}
 92
 93/// One team: members and repo grants.
 94@Observable
 95@MainActor
 96final class TeamViewModel {
 97
 98    nonisolated struct Grant: Decodable, Sendable, Hashable, Identifiable {
 99        let repo: String
100        let role: String
101        var id: String { repo }
102    }
103
104    nonisolated struct Loaded: Decodable, Sendable, Hashable {
105        let team: String
106        let members: [String]?
107        let grants: [Grant]?
108    }
109
110    private(set) var state: LoadState<Loaded> = .loading
111    private(set) var actionError: String?
112    private(set) var working = false
113
114    private let client: GitbayClient
115    let orgName: String
116    let teamName: String
117
118    init(client: GitbayClient, orgName: String, teamName: String) {
119        self.client = client
120        self.orgName = orgName
121        self.teamName = teamName
122    }
123
124    func load() async {
125        do {
126            state = .loaded(try await client.read(
127                ["org", "team", "show", orgName, teamName], as: Loaded.self))
128        } catch {
129            state = .from(error)
130        }
131    }
132
133    func addMember(_ user: String) async {
134        await perform(["org", "team", "add", orgName, teamName, user])
135    }
136
137    func removeMember(_ user: String) async {
138        await perform(["org", "team", "remove", orgName, teamName, user])
139    }
140
141    func grant(repo: String, role: String) async {
142        await perform(["org", "team", "grant", orgName, teamName, repo, role])
143    }
144
145    func revoke(_ grant: Grant) async {
146        await perform(["org", "team", "revoke", orgName, teamName, grant.repo])
147    }
148
149    private func perform(_ argv: [String]) async {
150        working = true
151        actionError = nil
152        defer { working = false }
153        do {
154            try await client.run(argv)
155            await load()
156        } catch let error as GitbayError {
157            actionError = error.userFacingMessage
158        } catch {
159            actionError = GitbayError.transport(error).userFacingMessage
160        }
161    }
162}