From f72fd271890715395ea27d4466af8529b298a7c8 Mon Sep 17 00:00:00 2001
From: tobias <thinkdifferent055@gmail.com>
Date: Sun, 25 Apr 2021 20:51:21 +0200
Subject: [PATCH] Add DataManager classes

---
 AppleLibs.xcodeproj/project.pbxproj           |  61 +++++++-
 AppleLibs/Network/Requests/DataManager.swift  | 142 ++++++++++++++++++
 .../Network/Requests/HttpStatusCode.swift     |  20 +++
 AppleLibs/Network/Requests/Request.swift      | 108 +++++++++++++
 .../Network/Requests/RequestBuilder.swift     |  64 ++++++++
 .../Network/Requests/ResponseError.swift      |  57 +++++++
 .../Network/Requests/ResponseHandler.swift    |  14 ++
 Package.swift                                 |   4 +-
 8 files changed, 468 insertions(+), 2 deletions(-)
 create mode 100644 AppleLibs/Network/Requests/DataManager.swift
 create mode 100644 AppleLibs/Network/Requests/HttpStatusCode.swift
 create mode 100644 AppleLibs/Network/Requests/Request.swift
 create mode 100644 AppleLibs/Network/Requests/RequestBuilder.swift
 create mode 100644 AppleLibs/Network/Requests/ResponseError.swift
 create mode 100644 AppleLibs/Network/Requests/ResponseHandler.swift

diff --git a/AppleLibs.xcodeproj/project.pbxproj b/AppleLibs.xcodeproj/project.pbxproj
index 5414b27..a91c458 100644
--- a/AppleLibs.xcodeproj/project.pbxproj
+++ b/AppleLibs.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 50;
+	objectVersion = 52;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -15,6 +15,13 @@
 		F623A6742635B5BE00F50371 /* Enum+Extended.swift in Sources */ = {isa = PBXBuildFile; fileRef = F623A6732635B5BE00F50371 /* Enum+Extended.swift */; };
 		F623A67B2635B5F900F50371 /* Date+Styled.swift in Sources */ = {isa = PBXBuildFile; fileRef = F623A67A2635B5F900F50371 /* Date+Styled.swift */; };
 		F62B93A7261A492700D7F8E6 /* String+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = F62B93A6261A492700D7F8E6 /* String+Size.swift */; };
+		F673A9942635EF510017AD37 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F673A98E2635EF510017AD37 /* DataManager.swift */; };
+		F673A9952635EF510017AD37 /* HttpStatusCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F673A98F2635EF510017AD37 /* HttpStatusCode.swift */; };
+		F673A9962635EF510017AD37 /* ResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F673A9902635EF510017AD37 /* ResponseError.swift */; };
+		F673A9972635EF510017AD37 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = F673A9912635EF510017AD37 /* Request.swift */; };
+		F673A9982635EF510017AD37 /* ResponseHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F673A9922635EF510017AD37 /* ResponseHandler.swift */; };
+		F673A9992635EF510017AD37 /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F673A9932635EF510017AD37 /* RequestBuilder.swift */; };
+		F673A9A12635EF8D0017AD37 /* Future in Frameworks */ = {isa = PBXBuildFile; productRef = F673A9A02635EF8D0017AD37 /* Future */; };
 		F68C2D422616482A00042967 /* IsoDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F68C2D412616482A00042967 /* IsoDateFormatter.swift */; };
 		F6A251A0260B670000132DEC /* AppleLibs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6A25196260B66FF00132DEC /* AppleLibs.framework */; };
 		F6A251A5260B670000132DEC /* AppleLibsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6A251A4260B670000132DEC /* AppleLibsTests.swift */; };
@@ -42,6 +49,12 @@
 		F623A6732635B5BE00F50371 /* Enum+Extended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Enum+Extended.swift"; sourceTree = "<group>"; };
 		F623A67A2635B5F900F50371 /* Date+Styled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Styled.swift"; sourceTree = "<group>"; };
 		F62B93A6261A492700D7F8E6 /* String+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Size.swift"; sourceTree = "<group>"; };
+		F673A98E2635EF510017AD37 /* DataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = "<group>"; };
+		F673A98F2635EF510017AD37 /* HttpStatusCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpStatusCode.swift; sourceTree = "<group>"; };
+		F673A9902635EF510017AD37 /* ResponseError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseError.swift; sourceTree = "<group>"; };
+		F673A9912635EF510017AD37 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
+		F673A9922635EF510017AD37 /* ResponseHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseHandler.swift; sourceTree = "<group>"; };
+		F673A9932635EF510017AD37 /* RequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestBuilder.swift; sourceTree = "<group>"; };
 		F68C2D412616482A00042967 /* IsoDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsoDateFormatter.swift; sourceTree = "<group>"; };
 		F6A25196260B66FF00132DEC /* AppleLibs.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppleLibs.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		F6A25199260B66FF00132DEC /* AppleLibs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppleLibs.h; sourceTree = "<group>"; };
@@ -59,6 +72,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				F673A9A12635EF8D0017AD37 /* Future in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -86,6 +100,7 @@
 		F623A65C2635B36900F50371 /* Network */ = {
 			isa = PBXGroup;
 			children = (
+				F673A99C2635EF5A0017AD37 /* Requests */,
 				F623A65D2635B38200F50371 /* CharacterSet+Url.swift */,
 				F623A6642635B3A800F50371 /* Dictornary+Url.swift */,
 			);
@@ -112,6 +127,19 @@
 			path = String;
 			sourceTree = "<group>";
 		};
+		F673A99C2635EF5A0017AD37 /* Requests */ = {
+			isa = PBXGroup;
+			children = (
+				F673A98E2635EF510017AD37 /* DataManager.swift */,
+				F673A98F2635EF510017AD37 /* HttpStatusCode.swift */,
+				F673A9912635EF510017AD37 /* Request.swift */,
+				F673A9932635EF510017AD37 /* RequestBuilder.swift */,
+				F673A9902635EF510017AD37 /* ResponseError.swift */,
+				F673A9922635EF510017AD37 /* ResponseHandler.swift */,
+			);
+			path = Requests;
+			sourceTree = "<group>";
+		};
 		F68C2D402616482000042967 /* Date */ = {
 			isa = PBXGroup;
 			children = (
@@ -197,6 +225,9 @@
 			dependencies = (
 			);
 			name = AppleLibs;
+			packageProductDependencies = (
+				F673A9A02635EF8D0017AD37 /* Future */,
+			);
 			productName = AppleLibs;
 			productReference = F6A25196260B66FF00132DEC /* AppleLibs.framework */;
 			productType = "com.apple.product-type.framework";
@@ -246,6 +277,9 @@
 				Base,
 			);
 			mainGroup = F6A2518C260B66FF00132DEC;
+			packageReferences = (
+				F673A99F2635EF8D0017AD37 /* XCRemoteSwiftPackageReference "Future" */,
+			);
 			productRefGroup = F6A25197260B66FF00132DEC /* Products */;
 			projectDirPath = "";
 			projectRoot = "";
@@ -279,16 +313,22 @@
 			buildActionMask = 2147483647;
 			files = (
 				F623A65E2635B38200F50371 /* CharacterSet+Url.swift in Sources */,
+				F673A9942635EF510017AD37 /* DataManager.swift in Sources */,
 				F6A251BA260B697300132DEC /* Double+Rounded.swift in Sources */,
 				F623A64D2635B2B100F50371 /* Array+Extended.swift in Sources */,
 				F62B93A7261A492700D7F8E6 /* String+Size.swift in Sources */,
 				F623A67B2635B5F900F50371 /* Date+Styled.swift in Sources */,
 				F623A6652635B3A800F50371 /* Dictornary+Url.swift in Sources */,
+				F673A9972635EF510017AD37 /* Request.swift in Sources */,
 				F623A6562635B34100F50371 /* Sequence+Extended.swift in Sources */,
 				F68C2D422616482A00042967 /* IsoDateFormatter.swift in Sources */,
 				F623A66C2635B57500F50371 /* NumberFormatter.swift in Sources */,
 				F6A251BE260B699100132DEC /* String+Html.swift in Sources */,
+				F673A9952635EF510017AD37 /* HttpStatusCode.swift in Sources */,
+				F673A9982635EF510017AD37 /* ResponseHandler.swift in Sources */,
 				F623A6742635B5BE00F50371 /* Enum+Extended.swift in Sources */,
+				F673A9962635EF510017AD37 /* ResponseError.swift in Sources */,
+				F673A9992635EF510017AD37 /* RequestBuilder.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -555,6 +595,25 @@
 			defaultConfigurationName = Release;
 		};
 /* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+		F673A99F2635EF8D0017AD37 /* XCRemoteSwiftPackageReference "Future" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/kean/Future.git";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 1.4.0;
+			};
+		};
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+		F673A9A02635EF8D0017AD37 /* Future */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = F673A99F2635EF8D0017AD37 /* XCRemoteSwiftPackageReference "Future" */;
+			productName = Future;
+		};
+/* End XCSwiftPackageProductDependency section */
 	};
 	rootObject = F6A2518D260B66FF00132DEC /* Project object */;
 }
diff --git a/AppleLibs/Network/Requests/DataManager.swift b/AppleLibs/Network/Requests/DataManager.swift
new file mode 100644
index 0000000..31e9b53
--- /dev/null
+++ b/AppleLibs/Network/Requests/DataManager.swift
@@ -0,0 +1,142 @@
+//
+//  DataManager.swift
+//  DocumentRepositoryMobile
+//
+//  Created by Tobias on 23.10.18.
+//  Copyright © 2018 Tobias. All rights reserved.
+//
+
+import Foundation
+import Future
+import os.log
+
+public protocol HttpResponseFilter
+{
+    func handleResponse(request: Request, data: Data?, responseHandler: ResponseHandler?, sender: Any?, promise: Promise<Data?, ResponseError>)
+}
+
+public protocol HttpSuccessFilter
+{
+    func handleResponse(data: Data?, response: HTTPURLResponse, responseHandler: ResponseHandler?, sender: Any?, promise: Promise<Data?, ResponseError>)
+}
+
+public class DataManager: NSObject, URLSessionDelegate
+{
+    public enum DataManagerError: Error
+    {
+        case authError
+    }
+
+    public struct Host: Equatable
+    {
+        public let `protocol`: String
+        public let host: String
+        public let port: Int
+        public let context: String
+    }
+
+    private let timeout: TimeInterval
+    public private(set) var host: Host
+
+    private lazy var urlSession: URLSession = {
+        URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue.main)
+    }()
+
+    private var responseOkFilter: HttpSuccessFilter
+    private var responseFilters: [Int: HttpResponseFilter] = [:]
+
+    public init(host: Host, responseOkFilter: HttpSuccessFilter, responseFilters: [Int: HttpResponseFilter] = [:], timeout: TimeInterval = 10.0) {
+        self.host = host
+        self.responseOkFilter = responseOkFilter
+        self.responseFilters = responseFilters
+        self.timeout = timeout
+        super.init()
+    }
+
+    public func url(username: String = "", password: String = "", path: String) -> String {
+        var url: String = host.protocol
+
+        if !username.isEmpty || !password.isEmpty {
+            url += username + ":" + password + "@"
+        }
+
+        url += "\(host.host):\(host.port)\(host.context)"
+        url += path
+
+        return url
+    }
+
+    @discardableResult
+    public func request(request r: Request,
+                 responseHandler: ResponseHandler? = nil,
+                 sender: Any? = nil,
+                 promise: Promise<Data?, ResponseError> = Promise<Data?, ResponseError>())
+                    -> Future<Data?, ResponseError> {
+        if #available(iOS 14.0, *) {
+            os_log("Request: \(r)")
+        } else {
+            print("Request: \(r)")
+        }
+
+        if var req: URLRequest = createUrlRequest(request: r) {
+            if let authentication = r.authentication {
+                if let headerKey = authentication.headerKey, let headerValue = authentication.headerValue {
+                    req.setValue(headerValue, forHTTPHeaderField: headerKey)
+                }
+            }
+
+            let task = urlSession.dataTask(with: req) { data, response, error in
+                if let error: NSError = error as NSError? {
+                    promise.fail(error: self.determineResponseError(error))
+                    return
+                }
+
+                guard let response: HTTPURLResponse = response as? HTTPURLResponse else {
+                    promise.fail(error: ResponseError.networkError(error: .unknown()))
+                    return
+                }
+
+                if response.is2xx {
+                    self.responseOkFilter.handleResponse(data: data, response: response, responseHandler: responseHandler, sender: sender, promise: promise)
+                } else if let filter = self.responseFilters[response.statusCode] {
+                    filter.handleResponse(request: r, data: data, responseHandler: responseHandler, sender: sender, promise: promise)
+                } else {
+                    promise.fail(error: ResponseError.getErrorForStatusCode(code: response.statusCode))
+                }
+            }
+            task.resume()
+        }
+        return promise.future
+    }
+
+    private func createUrlRequest(request: Request) -> URLRequest? {
+        if let url: URL = request.constructUrl() {
+            var req: URLRequest = URLRequest(url: url)
+            req.httpMethod = request.method.rawValue
+
+            if let payload = request.payload {
+                req.httpBody = payload
+                req.setValue(request.contentType, forHTTPHeaderField: "Content-Type")
+                req.setValue("application/json", forHTTPHeaderField: "Accept")
+            }
+
+            req.timeoutInterval = self.timeout
+            return req
+        }
+        return nil
+    }
+
+    fileprivate func determineResponseError(_ error: NSError) -> ResponseError {
+        if error.code == NSURLErrorNotConnectedToInternet {
+            return ResponseError.networkError(error: .noInternet)
+        } else if error.code == NSURLErrorTimedOut {
+            return ResponseError.networkError(error: .timeout)
+        } else {
+            return ResponseError.networkError(error: .unknown(error: error))
+        }
+    }
+
+    public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+        completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
+    }
+}
diff --git a/AppleLibs/Network/Requests/HttpStatusCode.swift b/AppleLibs/Network/Requests/HttpStatusCode.swift
new file mode 100644
index 0000000..8d2020a
--- /dev/null
+++ b/AppleLibs/Network/Requests/HttpStatusCode.swift
@@ -0,0 +1,20 @@
+//
+//  HttpStatusCode.swift
+//  WolfManagementMobile
+//
+//  Created by Tobias on 25.04.21.
+//  Copyright © 2021 Tobias. All rights reserved.
+//
+
+import Foundation
+
+public extension HTTPURLResponse
+{
+    var isOk: Bool {
+        return statusCode == 200
+    }
+
+    var is2xx: Bool {
+        return statusCode >= 200 && statusCode < 300
+    }
+}
diff --git a/AppleLibs/Network/Requests/Request.swift b/AppleLibs/Network/Requests/Request.swift
new file mode 100644
index 0000000..a3f2845
--- /dev/null
+++ b/AppleLibs/Network/Requests/Request.swift
@@ -0,0 +1,108 @@
+//
+// Created by Tobias on 14.07.20.
+// Copyright (c) 2020 Tobias. All rights reserved.
+//
+
+import Foundation
+
+public protocol Authentication
+{
+    var headerKey: String? { get }
+
+    var headerValue: String? { get }
+}
+
+public struct BasicAuthentication: Authentication
+{
+    private let username: String
+    private let password: String
+
+    public init(username: String, password: String) {
+        self.username = username
+        self.password = password
+    }
+
+    public var headerKey: String? {
+        "Authorization"
+    }
+    public var headerValue: String? {
+        let credentials = "\(username):\(password)"
+        guard let data = credentials.data(using: .utf8) else {
+            return nil
+        }
+        return "Basic \(data.base64EncodedString())"
+    }
+}
+
+public struct BearerToken: Authentication
+{
+    private let token: String
+    
+    init(token: String) {
+        self.token = token
+    }
+
+    public var headerKey: String? {
+        "Authorization"
+    }
+    public var headerValue: String? {
+        return "Bearer \(token)"
+    }
+}
+
+public class Request: CustomStringConvertible
+{
+    public enum RequestMethod: String
+    {
+        case get = "GET"
+        case post = "POST"
+        case put = "PUT"
+        case delete = "DELETE"
+    }
+
+    let url: String
+    let method: RequestMethod
+    let authentication: Authentication?
+    let parameters: [String: String]
+    let payload: Data?
+
+    let contentType: String?
+
+    public init(url: String, method: RequestMethod, authentication: Authentication?, parameters: [String: String] = [:], payload: Data? = nil, contentType: String?) {
+        self.url = url
+        self.method = method
+        self.authentication = authentication
+        self.parameters = parameters
+        self.payload = payload
+        if payload != nil {
+            self.contentType = contentType ?? "application/json"
+        } else {
+            self.contentType = nil
+        }
+    }
+
+    public func constructUrl() -> URL? {
+        if var components = URLComponents(string: url) {
+            components.queryItems = parameters.map { (key, value) in
+                URLQueryItem(name: key, value: value)
+            }
+            components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
+            return components.url
+        }
+        return nil
+    }
+
+    public var description: String {
+        let filteredParameters: [String: String] = Dictionary(uniqueKeysWithValues: self.parameters.map { (key, value) in
+            if key == "password" {
+                return (key, "********")
+            }
+            return (key, value)
+        })
+        if filteredParameters.isEmpty {
+            return "\(method): \(url)"
+        } else {
+            return "\(method): \(url) \(filteredParameters)"
+        }
+    }
+}
diff --git a/AppleLibs/Network/Requests/RequestBuilder.swift b/AppleLibs/Network/Requests/RequestBuilder.swift
new file mode 100644
index 0000000..00b2e2d
--- /dev/null
+++ b/AppleLibs/Network/Requests/RequestBuilder.swift
@@ -0,0 +1,64 @@
+//
+// Created by Tobias on 14.07.20.
+// Copyright (c) 2020 Tobias. All rights reserved.
+//
+
+import Foundation
+
+public class RequestBuilder
+{
+    private init() {
+    }
+
+    public static func create() -> RequestBuilder {
+        RequestBuilder()
+    }
+
+    private var url: String = ""
+    private var method: Request.RequestMethod = .get
+    private var authentication: Authentication?
+    private var parameters: [String: String] = [:]
+    private var payload: Data?
+    private var contentType: String?
+
+    public func url(_ url: String) -> RequestBuilder {
+        self.url = url
+        return self
+    }
+
+    public func method(_ method: Request.RequestMethod) -> RequestBuilder {
+        self.method = method
+        return self
+    }
+
+    public func authentication(_ authentication: Authentication) -> RequestBuilder {
+        self.authentication = authentication
+        return self
+    }
+
+    public func parameters(_ parameters: [String: String]) -> RequestBuilder {
+        self.parameters = parameters
+        return self
+    }
+
+    public func addParameter(key: String, value: String) -> RequestBuilder {
+        self.parameters[key] = value
+        return self
+    }
+
+    public func payload(_ payload: Data?) -> RequestBuilder {
+        self.payload = payload
+        self.contentType = "application/json"
+        return self
+    }
+
+    public func formPayload(_ payload: [String: Any]) -> RequestBuilder {
+        self.payload = payload.percentEncoded()
+        self.contentType = "application/x-www-form-urlencoded"
+        return self
+    }
+
+    public func build() -> Request {
+        Request(url: url, method: method, authentication: authentication, parameters: parameters, payload: payload, contentType: contentType)
+    }
+}
diff --git a/AppleLibs/Network/Requests/ResponseError.swift b/AppleLibs/Network/Requests/ResponseError.swift
new file mode 100644
index 0000000..62797e6
--- /dev/null
+++ b/AppleLibs/Network/Requests/ResponseError.swift
@@ -0,0 +1,57 @@
+//
+//  ResponseError.swift
+//  WolfManagementMobile
+//
+//  Created by Tobias on 14.12.19.
+//  Copyright © 2019 Tobias. All rights reserved.
+//
+
+import Foundation
+
+public enum ResponseError: Error
+{
+    public enum AuthError: Error
+    {
+        case unauthorized
+        case forbidden
+    }
+
+    public enum NetworkError: Error
+    {
+        case noInternet
+        case timeout
+        case serverNotFound
+        case unknown(error: Error? = nil)
+    }
+
+    public enum HttpError: Error
+    {
+        case badRequest
+        case notFound
+        case internalError
+    }
+
+    case authError(error: AuthError)
+    case httpError(error: HttpError)
+    case networkError(error: NetworkError)
+    case clientError(error: Error)
+
+    public static func getErrorForStatusCode(code: Int) -> ResponseError {
+        switch code {
+        case 400:
+            return ResponseError.httpError(error: .badRequest)
+
+        case 403:
+            return ResponseError.authError(error: .forbidden)
+
+        case 404:
+            return ResponseError.httpError(error: .notFound)
+
+        case 500:
+            return ResponseError.httpError(error: .internalError)
+
+        default:
+            return ResponseError.networkError(error: .unknown())
+        }
+    }
+}
diff --git a/AppleLibs/Network/Requests/ResponseHandler.swift b/AppleLibs/Network/Requests/ResponseHandler.swift
new file mode 100644
index 0000000..b21c3ec
--- /dev/null
+++ b/AppleLibs/Network/Requests/ResponseHandler.swift
@@ -0,0 +1,14 @@
+//
+//  ResponseHandler.swift
+//  DocumentRepositoryMobile
+//
+//  Created by Tobias on 08.11.18.
+//  Copyright © 2018 Tobias. All rights reserved.
+//
+
+import Foundation
+
+public protocol ResponseHandler
+{
+    func handleResponse(data: Data?, response: URLResponse?, sender: Any?) throws
+}
diff --git a/Package.swift b/Package.swift
index a5be090..397ea5e 100644
--- a/Package.swift
+++ b/Package.swift
@@ -12,7 +12,9 @@ let package = Package(
             targets: ["AppleLibs"]
         ),
     ],
-    dependencies: [],
+    dependencies: [
+        .package(name: "Future", url: "https://github.com/kean/Future.git", .upToNextMajor(from: "1.4.0"))
+    ],
     targets: [
         .target(
             name: "AppleLibs",
-- 
GitLab