krz/hutch

an ios client for sourcehut

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

v2.4.1: Hutch/Networking/PasteService.swift · raw

  1import Foundation
  2
  3struct PasteListPage: Decodable, Sendable {
  4    let results: [Paste]
  5    let cursor: String?
  6}
  7
  8final class PasteService: Sendable {
  9    private let client: SRHTClient
 10
 11    init(client: SRHTClient) {
 12        self.client = client
 13    }
 14
 15    private static let listQuery = """
 16    query pastes($cursor: Cursor) {
 17        pastes(cursor: $cursor) {
 18            results {
 19                id
 20                created
 21                visibility
 22                files {
 23                    filename
 24                    hash
 25                }
 26                user {
 27                    canonicalName
 28                }
 29            }
 30            cursor
 31        }
 32    }
 33    """
 34
 35    private static let detailQuery = """
 36    query paste($id: String!) {
 37        paste(id: $id) {
 38            id
 39            created
 40            visibility
 41            files {
 42                filename
 43                hash
 44                contents
 45            }
 46            user {
 47                canonicalName
 48            }
 49        }
 50    }
 51    """
 52
 53    private static let createMutation = """
 54    mutation createPaste($files: [Upload!]!, $visibility: Visibility!) {
 55        create(files: $files, visibility: $visibility) {
 56            id
 57            created
 58            visibility
 59            files {
 60                filename
 61                hash
 62                contents
 63            }
 64            user {
 65                canonicalName
 66            }
 67        }
 68    }
 69    """
 70
 71    private static let updateMutation = """
 72    mutation updatePaste($id: String!, $visibility: Visibility!) {
 73        update(id: $id, visibility: $visibility) {
 74            id
 75            created
 76            visibility
 77            files {
 78                filename
 79                hash
 80                contents
 81            }
 82            user {
 83                canonicalName
 84            }
 85        }
 86    }
 87    """
 88
 89    private static let deleteMutation = """
 90    mutation deletePaste($id: String!) {
 91        delete(id: $id) {
 92            id
 93            created
 94            visibility
 95            files {
 96                filename
 97                hash
 98            }
 99            user {
100                canonicalName
101            }
102        }
103    }
104    """
105
106    private static let cacheKey = "paste.pastes"
107
108    func listPastes(cursor: String?, useCache: Bool) async throws -> PasteListPage {
109        let variables = cursor.map { ["cursor": $0 as any Sendable] }
110        let result: PasteListResponse
111        if useCache, cursor == nil {
112            result = try await client.executeAndCache(
113                service: .paste,
114                query: Self.listQuery,
115                variables: variables,
116                responseType: PasteListResponse.self,
117                cacheKey: Self.cacheKey
118            )
119        } else {
120            result = try await client.execute(
121                service: .paste,
122                query: Self.listQuery,
123                variables: variables,
124                responseType: PasteListResponse.self
125            )
126        }
127        return result.pastes ?? PasteListPage(results: [], cursor: nil)
128    }
129
130    func loadCachedPastes() -> PasteListPage? {
131        guard let data = client.responseCache.get(forKey: Self.cacheKey) else {
132            return nil
133        }
134
135        let decoder = JSONDecoder()
136        decoder.dateDecodingStrategy = .srhtFlexible
137        guard let response = try? decoder.decode(GraphQLResponse<PasteListResponse>.self, from: data) else {
138            return nil
139        }
140        return response.data?.pastes
141    }
142
143    func loadPaste(id: String) async throws -> Paste? {
144        let result = try await client.execute(
145            service: .paste,
146            query: Self.detailQuery,
147            variables: ["id": id],
148            responseType: PasteDetailResponse.self
149        )
150        return result.paste
151    }
152
153    func createPaste(files: [PasteUploadDraft], visibility: Visibility) async throws -> Paste {
154        let uploadFiles = normalizedUploadFiles(from: files)
155        let variables: [String: any Sendable] = [
156            "files": [String?](repeating: nil, count: uploadFiles.count),
157            "visibility": visibility.rawValue
158        ]
159
160        let result = try await client.executeMultipartFiles(
161            service: .paste,
162            query: Self.createMutation,
163            variables: variables,
164            files: uploadFiles.enumerated().map { index, file in
165                MultipartUploadFile(
166                    variablePath: "files.\(index)",
167                    fileData: file.data,
168                    fileName: file.fileName,
169                    mimeType: "text/plain"
170                )
171            },
172            responseType: CreatePasteResponse.self
173        )
174        return result.create
175    }
176
177    func updateVisibility(id: String, visibility: Visibility) async throws -> Paste? {
178        let result = try await client.execute(
179            service: .paste,
180            query: Self.updateMutation,
181            variables: ["id": id, "visibility": visibility.rawValue],
182            responseType: UpdatePasteResponse.self
183        )
184        return result.update
185    }
186
187    func deletePaste(id: String) async throws -> Paste? {
188        let result = try await client.execute(
189            service: .paste,
190            query: Self.deleteMutation,
191            variables: ["id": id],
192            responseType: DeletePasteResponse.self
193        )
194        return result.delete
195    }
196
197    func loadContents(from url: URL) async throws -> String {
198        try await client.fetchText(url: url)
199    }
200
201    private func normalizedUploadFiles(from files: [PasteUploadDraft]) -> [(fileName: String, data: Data)] {
202        files.compactMap { draft in
203            let text = draft.contents
204            guard let data = text.data(using: .utf8) else {
205                return nil
206            }
207
208            return (draft.filename, data)
209        }
210    }
211}
212
213private struct PasteListResponse: Decodable, Sendable {
214    let pastes: PasteListPage?
215}
216
217private struct PasteDetailResponse: Decodable, Sendable {
218    let paste: Paste?
219}
220
221private struct CreatePasteResponse: Decodable, Sendable {
222    let create: Paste
223}
224
225private struct UpdatePasteResponse: Decodable, Sendable {
226    let update: Paste?
227}
228
229private struct DeletePasteResponse: Decodable, Sendable {
230    let delete: Paste?
231}