diff --git a/AppleLibs.xcodeproj/project.pbxproj b/AppleLibs.xcodeproj/project.pbxproj index 5414b27f3b4d1562d8bff58fb645bb3840ea94ca..a91c4580a692b3a7ec27b92256fd3d4588ecd463 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 0000000000000000000000000000000000000000..31e9b53d99053ed938819480f18e7c483feb8922 --- /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 0000000000000000000000000000000000000000..8d2020a3bfdd78638a661c549113584c37dfca40 --- /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 0000000000000000000000000000000000000000..a3f2845682188f6889bce3b880786f3d7a490462 --- /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 0000000000000000000000000000000000000000..00b2e2d8797dedb1555abfffc7d7f93e3525e737 --- /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 0000000000000000000000000000000000000000..62797e628df3802d4a5e913f88e7896b5b6c9bf7 --- /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 0000000000000000000000000000000000000000..b21c3ec8a4044db0f6a512e2144dd6fd43c8e4b1 --- /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 a5be0900456adf8d4568771fc4dc273bb3e8f455..397ea5eb2d501147d9a1f894903138f9c7537552 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",