krz/hutch

an ios client for sourcehut

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

v3.8.2: 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            let cached = try await client.executeCached(
113                service: .paste,
114                query: Self.listQuery,
115                variables: variables,
116                responseType: PasteListResponse.self,
117                cacheKey: APICacheKeys.pasteList(cursor: cursor),
118                resourceType: .pasteList,
119                ttl: APICacheTTLs.ticketList,
120                policy: .cacheFirstThenRefresh
121            )
122            result = cached.value
123        } else {
124            result = try await client.execute(
125                service: .paste,
126                query: Self.listQuery,
127                variables: variables,
128                responseType: PasteListResponse.self
129            )
130        }
131        return result.pastes ?? PasteListPage(results: [], cursor: nil)
132    }
133
134    func loadCachedPastes() async -> PasteListPage? {
135        guard let data = await client.cachedPayload(forKey: APICacheKeys.pasteList()) ?? client.responseCache.get(forKey: Self.cacheKey) else {
136            return nil
137        }
138
139        let decoder = JSONDecoder()
140        decoder.dateDecodingStrategy = .srhtFlexible
141        guard let response = try? decoder.decode(GraphQLResponse<PasteListResponse>.self, from: data) else {
142            return nil
143        }
144        return response.data?.pastes
145    }
146
147    func loadPaste(id: String) async throws -> Paste? {
148        let result = try await client.execute(
149            service: .paste,
150            query: Self.detailQuery,
151            variables: ["id": id],
152            responseType: PasteDetailResponse.self
153        )
154        return result.paste
155    }
156
157    func createPaste(files: [PasteUploadDraft], visibility: Visibility) async throws -> Paste {
158        let uploadFiles = normalizedUploadFiles(from: files)
159        let variables: [String: any Sendable] = [
160            "files": [String?](repeating: nil, count: uploadFiles.count),
161            "visibility": visibility.rawValue
162        ]
163
164        let result = try await client.executeMultipartFiles(
165            service: .paste,
166            query: Self.createMutation,
167            variables: variables,
168            files: uploadFiles.enumerated().map { index, file in
169                MultipartUploadFile(
170                    variablePath: "files.\(index)",
171                    fileData: file.data,
172                    fileName: file.fileName,
173                    mimeType: "text/plain"
174                )
175            },
176            responseType: CreatePasteResponse.self
177        )
178        await invalidatePasteCaches()
179        return result.create
180    }
181
182    func updateVisibility(id: String, visibility: Visibility) async throws -> Paste? {
183        let result = try await client.execute(
184            service: .paste,
185            query: Self.updateMutation,
186            variables: ["id": id, "visibility": visibility.rawValue],
187            responseType: UpdatePasteResponse.self
188        )
189        await invalidatePasteCaches()
190        return result.update
191    }
192
193    func deletePaste(id: String) async throws -> Paste? {
194        let result = try await client.execute(
195            service: .paste,
196            query: Self.deleteMutation,
197            variables: ["id": id],
198            responseType: DeletePasteResponse.self
199        )
200        await invalidatePasteCaches()
201        return result.delete
202    }
203
204    func loadContents(from url: URL) async throws -> String {
205        try await client.fetchText(url: url)
206    }
207
208    private func normalizedUploadFiles(from files: [PasteUploadDraft]) -> [(fileName: String, data: Data)] {
209        files.compactMap { draft in
210            let text = draft.contents
211            guard let data = text.data(using: .utf8) else {
212                return nil
213            }
214
215            return (draft.filename, data)
216        }
217    }
218
219    private func invalidatePasteCaches() async {
220        await client.invalidateCache(prefix: APICacheKeys.prefix(SRHTService.paste.rawValue, "pastes"))
221    }
222}
223
224private struct PasteListResponse: Decodable, Sendable {
225    let pastes: PasteListPage?
226}
227
228private struct PasteDetailResponse: Decodable, Sendable {
229    let paste: Paste?
230}
231
232private struct CreatePasteResponse: Decodable, Sendable {
233    let create: Paste
234}
235
236private struct UpdatePasteResponse: Decodable, Sendable {
237    let update: Paste?
238}
239
240private struct DeletePasteResponse: Decodable, Sendable {
241    let delete: Paste?
242}