// // GeminiResponse.swift // Gemini // // Created by Shadowfacts on 7/12/20. // import Foundation import UniformTypeIdentifiers struct GeminiResponse { let status: StatusCode let meta: String let body: Data? // Helpers var rawMimeType: String? { guard status.isSuccess else { return nil } return meta.trimmingCharacters(in: .whitespacesAndNewlines) } var mimeType: String? { guard let rawMimeType = rawMimeType else { return nil } return rawMimeType.split(separator: ";").first?.trimmingCharacters(in: .whitespaces) } var mimeTypeParameters: [String: String]? { guard let rawMimeType = rawMimeType else { return nil } return rawMimeType.split(separator: ";").dropFirst().reduce(into: [String: String]()) { (parameters, parameter) in let parts = parameter.split(separator: "=").map { $0.trimmingCharacters(in: .whitespaces) } precondition(parts.count == 2) parameters[parts[0].lowercased()] = parts[1] } } @available(macOS 10.16, *) var utiType: UTType? { guard let mimeType = mimeType else { return nil } return UTType.types(tag: mimeType, tagClass: .mimeType, conformingTo: nil).first } var bodyText: String? { guard let body = body, let parameters = mimeTypeParameters else { return nil } let encoding: String.Encoding switch parameters["charset"]?.lowercased() { case nil, "utf-8": // The Gemini spec defines UTF-8 to be the default charset. encoding = .utf8 case "us-ascii": encoding = .ascii default: // todo: log warning encoding = .utf8 } return String(data: body, encoding: encoding) } } extension GeminiResponse { enum StatusCode: Int { // All statuses and subtypes case input = 10 case sensitiveInput = 11 case success = 20 case temporaryRedirect = 30 case permanentRedirect = 31 case temporaryFailure = 40 case serverUnavailable = 41 case cgiError = 42 case proxyError = 43 case slowDown = 44 case permanentFailure = 50 case notFound = 51 case gone = 52 case proxyRequestRefused = 53 case badRequest = 59 case clientCertificateRequested = 60 case certificateNotAuthorised = 61 case certificateNotValid = 62 // Status type helpers var isInput: Bool { rawValue / 10 == 1 } var isSuccess: Bool { rawValue / 10 == 2 } var isRedirect: Bool { rawValue / 10 == 3 } var isTemporaryFailure: Bool { rawValue / 10 == 4 } var isPermanentFailure: Bool { rawValue / 10 == 5 } var isClientCertificateRequired: Bool { rawValue / 10 == 6 } // Other helpers var isFailure: Bool { isTemporaryFailure || isPermanentFailure } } } extension GeminiResponse.StatusCode: CustomStringConvertible { var description: String { switch self { case .input: return "input" case .sensitiveInput: return "sensitiveInput" case .success: return "success" case .temporaryRedirect: return "temporaryRedirect" case .permanentRedirect: return "permanentRedirect" case .temporaryFailure: return "temporaryFailure" case .serverUnavailable: return "serverUnavailable" case .cgiError: return "cgiError" case .proxyError: return "proxyError" case .slowDown: return "slowDown" case .permanentFailure: return "permanentFailure" case .notFound: return "notFound" case .gone: return "gone" case .proxyRequestRefused: return "proxyRequestRefused" case .badRequest: return "badRequest" case .clientCertificateRequested: return "clientCertificateRequested" case .certificateNotAuthorised: return "certificateNotAuthorised" case .certificateNotValid: return "certificateNotValid" } } }