Skip to content

Instantly share code, notes, and snippets.

@andreasmpet
Created April 11, 2020 21:52
Show Gist options
  • Save andreasmpet/8afe1c83f8c783bc48864f569fdf694d to your computer and use it in GitHub Desktop.
Save andreasmpet/8afe1c83f8c783bc48864f569fdf694d to your computer and use it in GitHub Desktop.
struct TemplatesRequest: PidgeonRequest {
let relativeUrl: String = "/templates"
let httpMethod: PidgeonHTTPMethod = .get
}
struct UploadRequest: PidgeonRequest {
let relativeUrl: String = "/exerciseRoutines"
let httpMethod: PidgeonHTTPMethod = .post
let routine: ExerciseRoutine
var body: PidgeonBody? {
guard let data = try? JSONEncoder().encode(self.routine) else {
return nil
}
return PidgeonBody.jsonData(data)
}
}
struct ExampleFunctions {
let executor = DefaultRequestExecutor(DefaultPidgeonRequestFactory(baseUrl: URL("https://yourhost.com")!, defaultHeaders: nil))
func fetchTemplates() -> Single<[ExerciseTemplate]> {
return self.requestExecutor.rx_perform(request: TemplatesRequest()).map({ (response) -> [ExerciseTemplate] in
return try response.decode([ExerciseTemplate].self)
}).asSingle()
}
func store(routine: ExerciseRoutine) -> Completable {
return self.requestExecutor.rx_perform(request: UploadRequest(routine: routine))
.asCompletable()
}
}
import UIKit
import KeychainAccess
class KeychainAuthStorage: PidgeonAuthTokenStorage {
let service: String
let authStorageKey: String
private let keychain: Keychain
init(service: String, authStorageKey: String) {
self.service = service
self.authStorageKey = authStorageKey
self.keychain = Keychain(service: self.service)
}
private var hasTriedToReadAuthResultFromKeychain: Bool = false
private var _authResult: PidgeonAuthResult?
var authResult: PidgeonAuthResult? {
set(value) {
self._authResult = authResult
if let value = value, let serialized = try? JSONEncoder().encode(value) {
try? self.keychain.set(serialized, key: self.authStorageKey)
} else {
try? self.keychain.remove(self.authStorageKey)
}
}
get {
if self._authResult != nil || self.hasTriedToReadAuthResultFromKeychain {
return self._authResult
}
guard let data: Data = (try? self.keychain.getData(self.authStorageKey)) ?? nil else {
return nil
}
_authResult = try? JSONDecoder().decode(PidgeonAuthResult.self, from: data)
self.hasTriedToReadAuthResultFromKeychain = true
return _authResult
}
}
func clearStorage() {
self.authResult = nil
}
}
import Foundation
struct PidgeonConfig {
static var defaultDecoder: JSONDecoder = JSONDecoder()
static var defaultEncoder: JSONEncoder = JSONEncoder()
}
struct PidgeonResponse {
let response: URLResponse
let data: Data?
}
enum PidgeonError: Error {
case invalidHttpCode(code: Int)
case invalidRequest(request: PidgeonRequest)
case invalidResponse(response: URLResponse)
case internalInconsistency
case noDataToDecode
}
protocol PidgeonRequestFactory {
var baseUrl: URL { get }
var defaultHeaders: [String: String]? { get }
func createURLRequest(from request: PidgeonRequest) throws -> URLRequest
}
extension PidgeonRequestFactory {
var defaultHeaders: [String: String]? {
return nil
}
func createURLRequest(from request: PidgeonRequest) throws -> URLRequest {
guard let url = URL(string: request.relativeUrl, relativeTo: self.baseUrl),
var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
fatalError("Invalid request URL")
}
urlComponents.queryItems = request.parameters?.map({ (k,v) in
return URLQueryItem(name: k, value: v)
})
let urlWithParameters = try! urlComponents.asURL()
var urlRequest = URLRequest(url: urlWithParameters, cachePolicy: request.cachePolicy, timeoutInterval: request.timeoutInterval)
self.defaultHeaders?.forEach({ (key, value) in
urlRequest.setValue(value, forHTTPHeaderField: key)
})
urlRequest.httpMethod = request.httpMethod.rawValue
// Additional headers take precedence
request.additionalHeaders?.forEach({ (key,value) in
urlRequest.setValue(value, forHTTPHeaderField: key)
})
let body = request.body
urlRequest.httpBody = try body?.data()
if let contentType = body?.contentType {
urlRequest.setValue(contentType, forHTTPHeaderField: "Content-Type")
}
return urlRequest
}
}
struct DefaultPidgeonRequestFactory: PidgeonRequestFactory {
let baseUrl: URL
let defaultHeaders: [String: String]?
}
enum PidgeonHTTPMethod: String {
case options = "OPTIONS"
case get = "GET"
case head = "HEAD"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
case trace = "TRACE"
case connect = "CONNECT"
}
typealias PidgeonRequestErrorTransformer = (_ data: Data?, _ error: Error) -> Error?
enum PidgeonBody {
case jsonData(Data)
case json([String: Any])
case data(Data)
func data() throws -> Data? {
switch self {
case .jsonData(let jsonData):
return jsonData
case .json(let json):
return try JSONSerialization.data(withJSONObject: json, options: [])
case .data(let data):
return data
}
}
var contentType: String? {
switch self {
case .json(_), .jsonData(_):
return "application/json"
case .data(_):
return "application/octet-stream"
}
}
}
protocol PidgeonRequest {
var relativeUrl: String { get }
var httpMethod: PidgeonHTTPMethod { get }
var cachePolicy: URLRequest.CachePolicy { get }
var additionalHeaders: [String: String]? { get }
var parameters: [String:String]? { get }
var body: PidgeonBody? { get }
var timeoutInterval: TimeInterval { get }
var errorTransformer: PidgeonRequestErrorTransformer? { get }
}
extension PidgeonRequest {
var cachePolicy: URLRequest.CachePolicy {
return URLRequest.CachePolicy.useProtocolCachePolicy
}
var additionalHeaders: [String: String]? { return nil }
var parameters: [String:String]? { return nil }
var body: PidgeonBody? { return nil }
var timeoutInterval: TimeInterval { return 30 }
var errorTransformer: PidgeonRequestErrorTransformer? { return nil }
}
protocol PidgeonRequestTask {
func cancelTask()
}
typealias RequestExecutorPerformCallback = (_ result: PidgeonResult) -> Void
protocol PidgeonRequestExecutor {
var requestFactory: PidgeonRequestFactory { get }
@discardableResult
func perform(request: PidgeonRequest, completion: @escaping RequestExecutorPerformCallback) -> PidgeonRequestTask?
}
enum PidgeonResult {
case result(Data?, URLResponse)
case error(Error)
}
extension PidgeonResult {
func decode<T: Decodable>(_ type: T.Type,
injecting injectedJSON: [String: Any]? = nil,
decoder: JSONDecoder = PidgeonConfig.defaultDecoder) throws -> T {
switch self {
case .error(let error):
throw error
case .result(let data, _):
return try self.decodeData(data: data, type: type, injecting: injectedJSON, decoder: decoder)
}
}
private func decodeData<T: Decodable>(data: Data?,
type: T.Type,
injecting injectedJSON: [String: Any]?,
decoder: JSONDecoder) throws -> T {
guard let data = data else {
throw PidgeonError.noDataToDecode
}
guard let injectedJSON = injectedJSON else {
return try decoder.decode(type, from: data)
}
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
let modifiedJSON: Any = {
// Inject injected data into json object
if var json = jsonObject as? [String: Any] {
injectedJSON.forEach { k in
json[k.key] = k.value
}
return json
} else if var jsonArray = jsonObject as? [[String: Any]] {
// Inject injected data into every object in the json array
jsonArray.enumerated().forEach { i, jsonObj in
var mutableJsonObj = jsonObj
injectedJSON.forEach { k in
mutableJsonObj[k.key] = k.value
}
jsonArray[i] = mutableJsonObj
}
return jsonArray
}
return [:]
}()
let modifiedData = try JSONSerialization.data(withJSONObject: modifiedJSON, options: [])
return try decoder.decode(type, from: modifiedData)
}
func toJson() throws -> [String: Any]? {
switch self {
case .error(let error):
throw error
case .result(let data, _):
guard let data = data else { return nil }
return try JSONSerialization.jsonObject(with: data) as? [String: Any]
}
}
}
extension PidgeonResult {
var error: Error? {
switch self {
case .error(let error):
return error
default:
return nil
}
}
var data: Data? {
switch self {
case .result(let data, _):
return data
default:
return nil
}
}
}
typealias PidgeonAuthTokenProviderFetchCallback = (_ result: PidgeonAuthResult?, _ error: Error?) -> Void
protocol PidgeonAuthTokenStorage {
var authResult: PidgeonAuthResult? { get set }
func clearStorage()
}
protocol PidgeonAuthTokenProvider {
func reauthorize(completion: @escaping PidgeonAuthTokenProviderFetchCallback)
func login(withRequest: PidgeonRequest, completion: @escaping PidgeonAuthTokenProviderFetchCallback)
var authStorage: PidgeonAuthTokenStorage { get }
}
typealias PidgeonTokenFetcherBlock = (_ isReauthorizing: Bool, (PidgeonResult) -> Void) -> Void
struct PidgeonAuthResult: Codable, Equatable {
let timestamp: TimeInterval
let accessToken: String
let tokenType: String
let refreshToken: String
let expiresInMsec: Int
}
extension PidgeonAuthResult {
var hasExpired: Bool {
let expiryTime = self.timestamp + Double(self.expiresInMsec) / 1000
return Date().timeIntervalSinceNow >= expiryTime
}
}
protocol PidgeonTokenFetcher {
func fetchToken(isReauthorizing: Bool, completion: @escaping PidgeonAuthTokenProviderFetchCallback)
}
typealias AuthRequestFactory = (_ authStorage: PidgeonAuthTokenStorage) -> PidgeonRequest?
extension URLSessionDataTask: PidgeonRequestTask {
func cancelTask() {
self.cancel()
}
}
struct DefaultRequestExecutor: PidgeonRequestExecutor {
let requestFactory: PidgeonRequestFactory
let urlSession: URLSession
init(requestFactory: PidgeonRequestFactory, urlSession: URLSession = URLSession.shared) {
self.requestFactory = requestFactory
self.urlSession = urlSession
}
func perform(request: PidgeonRequest, completion: @escaping RequestExecutorPerformCallback) -> PidgeonRequestTask? {
let attemptedRequest: URLRequest? = {
do {
return try self.requestFactory.createURLRequest(from: request)
} catch let error {
completion(.error(error))
return nil
}
}()
guard let urlRequest = attemptedRequest else {
return nil
}
let task = self.urlSession.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
let transformedError = request.errorTransformer?(data, error) ?? error
DispatchQueue.main.async {
completion(.error(transformedError))
}
return
}
guard let httpResponse = response as? HTTPURLResponse else {
DispatchQueue.main.async {
completion(.error(PidgeonError.internalInconsistency))
}
return
}
let isValidHttpCode = (200..<300).contains(httpResponse.statusCode)
if !isValidHttpCode {
DispatchQueue.main.async {
completion(.error(PidgeonError.invalidHttpCode(code: httpResponse.statusCode)))
}
return
}
DispatchQueue.main.async {
completion(.result(data, httpResponse))
}
}
task.resume()
return task
}
}
class DefaultPidgeonAuthTokenProvider: PidgeonAuthTokenProvider {
enum ProviderError: Error {
case tokenFetchError(Error)
case couldNotCreateReauthRequest
case parseResponseError(Error)
}
private(set) var authStorage: PidgeonAuthTokenStorage
let reauthRequestFactory: AuthRequestFactory
private let authRequestExecutor: PidgeonRequestExecutor
private var pendingReauthRequests: [PidgeonAuthTokenProviderFetchCallback] = []
init(authRequestExecutor: PidgeonRequestExecutor,
authStorage: PidgeonAuthTokenStorage,
reauthRequestFactory: @escaping AuthRequestFactory) {
self.authRequestExecutor = authRequestExecutor
self.authStorage = authStorage
self.reauthRequestFactory = reauthRequestFactory
}
func login(withRequest request: PidgeonRequest, completion: @escaping PidgeonAuthTokenProviderFetchCallback) {
self.fetchAndStoreAuthToken(withRequest: request, completion: completion)
}
func reauthorize(completion: @escaping PidgeonAuthTokenProviderFetchCallback) {
guard let tokenRequest = self.reauthRequestFactory(self.authStorage) else {
completion(nil, ProviderError.couldNotCreateReauthRequest)
return
}
self.pendingReauthRequests.append(completion)
if self.pendingReauthRequests.count == 1 {
self.fetchAndStoreAuthToken(withRequest: tokenRequest, completion: { [weak self] authResult, error in
guard let self = self else { return }
let pendingRequests = self.pendingReauthRequests
self.pendingReauthRequests.removeAll()
pendingRequests.forEach({ (callback) in
callback(authResult, error)
})
})
}
}
func fetchAndStoreAuthToken(withRequest request: PidgeonRequest, completion: @escaping PidgeonAuthTokenProviderFetchCallback) {
self.authRequestExecutor.perform(request: request) { [weak self] (result) in
guard let self = self else { return }
do {
if let error = result.error {
completion(nil, error)
return
}
let authResult = try result.decode(PidgeonAuthResult.self, injecting: [
"timestamp": Date().timeIntervalSince1970
])
self.authStorage.authResult = authResult
completion(authResult, nil)
} catch let error {
completion(nil, error)
}
}
}
}
import RxSwift
extension PidgeonRequestExecutor {
func rx_perform(request: PidgeonRequest, shouldCancelOnDispose: Bool = false) -> Single<PidgeonResult> {
let factory = {
return Single<PidgeonResult>.create(subscribe: { (single) -> Disposable in
let task = self.perform(request: request, completion: { result in
single(.success(result))
})
return Disposables.create {
if shouldCancelOnDispose {
task?.cancelTask()
}
}
})
}
return Single.deferred(factory)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment