Skip to content
Snippets Groups Projects
Commit f72fd271 authored by Tobias Ullerich's avatar Tobias Ullerich
Browse files

Add DataManager classes

parent b659da63
Branches
No related tags found
No related merge requests found
......@@ -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 */;
}
//
// 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!))
}
}
//
// 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
}
}
//
// 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)"
}
}
}
//
// 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)
}
}
//
// 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())
}
}
}
//
// 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
}
......@@ -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",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment