Gemini/GeminiProtocol/GeminiResponse.swift

133 lines
4.3 KiB
Swift

//
// 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"
}
}
}