From c84e15965e1994dd5cc0271c324bc3817a3a9706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ka=C5=A1par=20Jir=C3=A1sek?= Date: Mon, 25 Feb 2019 15:12:54 +0100 Subject: [PATCH 01/36] Add experiments --- FTAPIKit.xcodeproj/project.pbxproj | 50 +++++++++++++++---- Sources/APIConfiguration.swift | 37 ++++++++++++++ Sources/APIEndpoint+URLRequest.swift | 20 ++++++++ ...dapter.swift => URLRequest+Endpoint.swift} | 16 +++--- Sources/URLSessionAPIAdapter.swift | 6 ++- Sources/URSession+Endpoint.swift | 45 +++++++++++++++++ 6 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 Sources/APIConfiguration.swift create mode 100644 Sources/APIEndpoint+URLRequest.swift rename Sources/{URLRequest+APIAdapter.swift => URLRequest+Endpoint.swift} (87%) create mode 100644 Sources/URSession+Endpoint.swift diff --git a/FTAPIKit.xcodeproj/project.pbxproj b/FTAPIKit.xcodeproj/project.pbxproj index ff4db5c..cc1779f 100644 --- a/FTAPIKit.xcodeproj/project.pbxproj +++ b/FTAPIKit.xcodeproj/project.pbxproj @@ -28,6 +28,18 @@ E792629E219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */; }; E792629F219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */; }; E79262A0219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */; }; + E7B74A16220DC7690055EC66 /* APIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A15220DC7690055EC66 /* APIConfiguration.swift */; }; + E7B74A17220DC76B0055EC66 /* APIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A15220DC7690055EC66 /* APIConfiguration.swift */; }; + E7B74A18220DC76C0055EC66 /* APIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A15220DC7690055EC66 /* APIConfiguration.swift */; }; + E7B74A19220DC76C0055EC66 /* APIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A15220DC7690055EC66 /* APIConfiguration.swift */; }; + E7B74A1B220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1A220DD7E60055EC66 /* APIEndpoint+URLRequest.swift */; }; + E7B74A1C220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1A220DD7E60055EC66 /* APIEndpoint+URLRequest.swift */; }; + E7B74A1D220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1A220DD7E60055EC66 /* APIEndpoint+URLRequest.swift */; }; + E7B74A1E220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1A220DD7E60055EC66 /* APIEndpoint+URLRequest.swift */; }; + E7B74A20220DDB300055EC66 /* URSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */; }; + E7B74A21220DDB300055EC66 /* URSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */; }; + E7B74A22220DDB300055EC66 /* URSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */; }; + E7B74A23220DDB300055EC66 /* URSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */; }; E7B7741B2152752A006E7585 /* APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774142152752A006E7585 /* APIAdapter.swift */; }; E7B7741C2152752A006E7585 /* APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774142152752A006E7585 /* APIAdapter.swift */; }; E7B7741D2152752A006E7585 /* APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774142152752A006E7585 /* APIAdapter.swift */; }; @@ -40,10 +52,10 @@ E7B774242152752A006E7585 /* Data+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774162152752A006E7585 /* Data+APIAdapter.swift */; }; E7B774252152752A006E7585 /* Data+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774162152752A006E7585 /* Data+APIAdapter.swift */; }; E7B774262152752A006E7585 /* Data+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774162152752A006E7585 /* Data+APIAdapter.swift */; }; - E7B774272152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774172152752A006E7585 /* URLRequest+APIAdapter.swift */; }; - E7B774282152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774172152752A006E7585 /* URLRequest+APIAdapter.swift */; }; - E7B774292152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774172152752A006E7585 /* URLRequest+APIAdapter.swift */; }; - E7B7742A2152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774172152752A006E7585 /* URLRequest+APIAdapter.swift */; }; + E7B774272152752A006E7585 /* URLRequest+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774172152752A006E7585 /* URLRequest+Endpoint.swift */; }; + E7B774282152752A006E7585 /* URLRequest+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774172152752A006E7585 /* URLRequest+Endpoint.swift */; }; + E7B774292152752A006E7585 /* URLRequest+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774172152752A006E7585 /* URLRequest+Endpoint.swift */; }; + E7B7742A2152752A006E7585 /* URLRequest+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774172152752A006E7585 /* URLRequest+Endpoint.swift */; }; E7B7742B2152752A006E7585 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774182152752A006E7585 /* AnyEncodable.swift */; }; E7B7742C2152752A006E7585 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774182152752A006E7585 /* AnyEncodable.swift */; }; E7B7742D2152752A006E7585 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774182152752A006E7585 /* AnyEncodable.swift */; }; @@ -99,10 +111,13 @@ DD75027A1C68FCFC006590AF /* FTAPIKit-macOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "FTAPIKit-macOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; DD75028D1C690C7A006590AF /* FTAPIKit-tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "FTAPIKit-tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionAPIAdapter.swift; sourceTree = ""; }; + E7B74A15220DC7690055EC66 /* APIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIConfiguration.swift; sourceTree = ""; }; + E7B74A1A220DD7E60055EC66 /* APIEndpoint+URLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIEndpoint+URLRequest.swift"; sourceTree = ""; }; + E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URSession+Endpoint.swift"; sourceTree = ""; }; E7B774142152752A006E7585 /* APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIAdapter.swift; sourceTree = ""; }; E7B774152152752A006E7585 /* URL+APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+APIAdapter.swift"; sourceTree = ""; }; E7B774162152752A006E7585 /* Data+APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+APIAdapter.swift"; sourceTree = ""; }; - E7B774172152752A006E7585 /* URLRequest+APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+APIAdapter.swift"; sourceTree = ""; }; + E7B774172152752A006E7585 /* URLRequest+Endpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+Endpoint.swift"; sourceTree = ""; }; E7B774182152752A006E7585 /* AnyEncodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = ""; }; E7B774192152752A006E7585 /* APIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIEndpoint.swift; sourceTree = ""; }; E7B7741A2152752A006E7585 /* APIAdapter+Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIAdapter+Types.swift"; sourceTree = ""; }; @@ -203,13 +218,16 @@ isa = PBXGroup; children = ( E7B774192152752A006E7585 /* APIEndpoint.swift */, + E7B74A15220DC7690055EC66 /* APIConfiguration.swift */, E7B774142152752A006E7585 /* APIAdapter.swift */, + E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */, E7B7741A2152752A006E7585 /* APIAdapter+Types.swift */, E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */, E7B774182152752A006E7585 /* AnyEncodable.swift */, E7B774162152752A006E7585 /* Data+APIAdapter.swift */, E7B774152152752A006E7585 /* URL+APIAdapter.swift */, - E7B774172152752A006E7585 /* URLRequest+APIAdapter.swift */, + E7B774172152752A006E7585 /* URLRequest+Endpoint.swift */, + E7B74A1A220DD7E60055EC66 /* APIEndpoint+URLRequest.swift */, ); path = Sources; sourceTree = ""; @@ -556,9 +574,12 @@ E7B7742F2152752A006E7585 /* APIEndpoint.swift in Sources */, E7B774232152752A006E7585 /* Data+APIAdapter.swift in Sources */, E7B7741F2152752A006E7585 /* URL+APIAdapter.swift in Sources */, - E7B774272152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */, + E7B774272152752A006E7585 /* URLRequest+Endpoint.swift in Sources */, + E7B74A16220DC7690055EC66 /* APIConfiguration.swift in Sources */, E7B774332152752A006E7585 /* APIAdapter+Types.swift in Sources */, E7B7741B2152752A006E7585 /* APIAdapter.swift in Sources */, + E7B74A20220DDB300055EC66 /* URSession+Endpoint.swift in Sources */, + E7B74A1B220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742B2152752A006E7585 /* AnyEncodable.swift in Sources */, E792629D219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */, ); @@ -580,9 +601,12 @@ E7B774312152752A006E7585 /* APIEndpoint.swift in Sources */, E7B774252152752A006E7585 /* Data+APIAdapter.swift in Sources */, E7B774212152752A006E7585 /* URL+APIAdapter.swift in Sources */, - E7B774292152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */, + E7B774292152752A006E7585 /* URLRequest+Endpoint.swift in Sources */, + E7B74A18220DC76C0055EC66 /* APIConfiguration.swift in Sources */, E7B774352152752A006E7585 /* APIAdapter+Types.swift in Sources */, E7B7741D2152752A006E7585 /* APIAdapter.swift in Sources */, + E7B74A22220DDB300055EC66 /* URSession+Endpoint.swift in Sources */, + E7B74A1D220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742D2152752A006E7585 /* AnyEncodable.swift in Sources */, E792629F219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */, ); @@ -595,9 +619,12 @@ E7B774322152752A006E7585 /* APIEndpoint.swift in Sources */, E7B774262152752A006E7585 /* Data+APIAdapter.swift in Sources */, E7B774222152752A006E7585 /* URL+APIAdapter.swift in Sources */, - E7B7742A2152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */, + E7B7742A2152752A006E7585 /* URLRequest+Endpoint.swift in Sources */, + E7B74A19220DC76C0055EC66 /* APIConfiguration.swift in Sources */, E7B774362152752A006E7585 /* APIAdapter+Types.swift in Sources */, E7B7741E2152752A006E7585 /* APIAdapter.swift in Sources */, + E7B74A23220DDB300055EC66 /* URSession+Endpoint.swift in Sources */, + E7B74A1E220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742E2152752A006E7585 /* AnyEncodable.swift in Sources */, E79262A0219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */, ); @@ -610,9 +637,12 @@ E7B774302152752A006E7585 /* APIEndpoint.swift in Sources */, E7B774242152752A006E7585 /* Data+APIAdapter.swift in Sources */, E7B774202152752A006E7585 /* URL+APIAdapter.swift in Sources */, - E7B774282152752A006E7585 /* URLRequest+APIAdapter.swift in Sources */, + E7B774282152752A006E7585 /* URLRequest+Endpoint.swift in Sources */, + E7B74A17220DC76B0055EC66 /* APIConfiguration.swift in Sources */, E7B774342152752A006E7585 /* APIAdapter+Types.swift in Sources */, E7B7741C2152752A006E7585 /* APIAdapter.swift in Sources */, + E7B74A21220DDB300055EC66 /* URSession+Endpoint.swift in Sources */, + E7B74A1C220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742C2152752A006E7585 /* AnyEncodable.swift in Sources */, E792629E219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */, ); diff --git a/Sources/APIConfiguration.swift b/Sources/APIConfiguration.swift new file mode 100644 index 0000000..232f2ef --- /dev/null +++ b/Sources/APIConfiguration.swift @@ -0,0 +1,37 @@ +// +// APIConfiguration.swift +// FTAPIKit-iOS +// +// Created by Matěj Kašpar Jirásek on 08/02/2019. +// Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved. +// + +import Foundation + +public protocol APIConfiguration { + var baseUrl: URL { get } + + func encode(_ value: Encodable) throws -> Data + func decode(from data: Data) throws -> T + + func error(from data: Data?, response: URLResponse?, error: Error?) -> Error? +} + +public protocol APIJSONConfiguration: APIConfiguration { + var jsonDecoder: JSONDecoder { get } + var jsonEncoder: JSONEncoder { get } +} + +public extension APIJSONConfiguration { + public func encode(_ value: Encodable) throws -> Data { + return try jsonEncoder.encode(AnyEncodable(value)) + } + + public func decode(from data: Data) throws -> T { + return try jsonDecoder.decode(T.self, from: data) + } + + public func error(from data: Data?, response: URLResponse?, error: Error?) -> Error? { + return error + } +} diff --git a/Sources/APIEndpoint+URLRequest.swift b/Sources/APIEndpoint+URLRequest.swift new file mode 100644 index 0000000..c487be1 --- /dev/null +++ b/Sources/APIEndpoint+URLRequest.swift @@ -0,0 +1,20 @@ +// +// APIEndpoint+URLRequest.swift +// FTAPIKit +// +// Created by Matěj Kašpar Jirásek on 08/02/2019. +// Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved. +// + +import Foundation + +extension APIEndpoint { + func request(with configuration: APIConfiguration) throws -> URLRequest { + let url = configuration.baseUrl.appendingPathComponent(path) + var request = URLRequest(url: url) + request.httpMethod = method.description + + try request.setRequestType(type, parameters: parameters, using: configuration.encode) + return request + } +} diff --git a/Sources/URLRequest+APIAdapter.swift b/Sources/URLRequest+Endpoint.swift similarity index 87% rename from Sources/URLRequest+APIAdapter.swift rename to Sources/URLRequest+Endpoint.swift index d3406b9..d7b5360 100644 --- a/Sources/URLRequest+APIAdapter.swift +++ b/Sources/URLRequest+Endpoint.swift @@ -1,5 +1,5 @@ // -// URLRequest+APIAdapter.swift +// URLRequest+Endpoint.swift // FTAPIKit // // Created by Matěj Kašpar Jirásek on 02/09/2018. @@ -9,14 +9,14 @@ import Foundation extension URLRequest { - mutating func setRequestType(_ requestType: RequestType, parameters: HTTPParameters, using jsonEncoder: JSONEncoder) throws { + mutating func setRequestType(_ requestType: RequestType, parameters: HTTPParameters, using encode: (Encodable) throws -> Data) throws { switch requestType { case .jsonBody(let encodable): - try setJSONBody(encodable: encodable, parameters: parameters, using: jsonEncoder) + try setJSONBody(encodable: encodable, parameters: parameters, using: encode) case .urlEncoded: setURLEncoded(parameters: parameters) case .jsonParams: - setJSON(parameters: parameters, using: jsonEncoder) + setJSON(parameters: parameters, using: encode) case let .multipart(files): setMultipart(parameters: parameters, files: files) case .base64Upload: @@ -44,7 +44,7 @@ extension URLRequest { httpBody?.appendRow("--\(boundary)") } - private mutating func setJSON(parameters: HTTPParameters, body: Data? = nil, using jsonEncoder: JSONEncoder) { + private mutating func setJSON(parameters: HTTPParameters, body: Data? = nil, using encode: (Encodable) throws -> Data) { setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") httpBody = body url?.appendQuery(parameters: parameters) @@ -57,9 +57,9 @@ extension URLRequest { setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") } - private mutating func setJSONBody(encodable: Encodable, parameters: HTTPParameters, using jsonEncoder: JSONEncoder) throws { - let body = try jsonEncoder.encode(AnyEncodable(encodable)) - setJSON(parameters: parameters, body: body, using: jsonEncoder) + private mutating func setJSONBody(encodable: Encodable, parameters: HTTPParameters, using encode: (Encodable) throws -> Data) throws { + let body = try encode(encodable) + setJSON(parameters: parameters, body: body, using: encode) } private mutating func appendForm(data: Data, name: String, boundary: String, mimeType: String? = nil, filename: String? = nil) { diff --git a/Sources/URLSessionAPIAdapter.swift b/Sources/URLSessionAPIAdapter.swift index b5243c6..d94ba4c 100644 --- a/Sources/URLSessionAPIAdapter.swift +++ b/Sources/URLSessionAPIAdapter.swift @@ -81,8 +81,12 @@ public final class URLSessionAPIAdapter: APIAdapter { var request = URLRequest(url: url) request.httpMethod = endpoint.method.description + let encode: (Encodable) throws -> Data = { encodable in + try self.jsonEncoder.encode(AnyEncodable(encodable)) + } + do { - try request.setRequestType(endpoint.type, parameters: endpoint.parameters, using: jsonEncoder) + try request.setRequestType(endpoint.type, parameters: endpoint.parameters, using: encode) } catch { completion(.error(error)) return diff --git a/Sources/URSession+Endpoint.swift b/Sources/URSession+Endpoint.swift new file mode 100644 index 0000000..678eb07 --- /dev/null +++ b/Sources/URSession+Endpoint.swift @@ -0,0 +1,45 @@ +// +// URSession+Endpoint.swift +// FTAPIKit +// +// Created by Matěj Kašpar Jirásek on 08/02/2019. +// Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved. +// + +import Foundation + +public extension URLSession { + func dataTask(to endpoint: APIEndpoint, with configuration: APIConfiguration) throws -> URLSessionDataTask { + let request = try endpoint.request(with: configuration) + return dataTask(with: request) + } + + func dataTask(to endpoint: APIEndpoint, with configuration: APIConfiguration, completion: @escaping (APIResult) -> Void) throws -> URLSessionDataTask { + let request = try endpoint.request(with: configuration) + return dataTask(with: request) { (data, response, error) in + if let error = configuration.error(from: data, response: response, error: error) { + completion(.error(error)) + return + } + switch (data, response, error) { + case let (nil, response as HTTPURLResponse, nil) where response.statusCode == 204: + completion(.value(Data())) + case let (data?, response as HTTPURLResponse, nil) where response.statusCode < 400: + completion(.value(data)) + case let (_, _, error as NSError) where error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled: + completion(.error(APIError.cancelled)) + case let (_, _, error?): + completion(.error(error)) + case let (data, response as HTTPURLResponse, nil): + completion(.error(APIError.errorCode(response.statusCode, data))) + default: + completion(.error(APIError.noResponse)) + } + } + } + + func downloadTask(to endpoint: APIEndpoint, with configuration: APIConfiguration) throws -> URLSessionDownloadTask { + let request = try endpoint.request(with: configuration) + return downloadTask(with: request) + } +} From e0c08f830bfa40716b87d01f56e781707cfdba43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ka=C5=A1par=20Jir=C3=A1sek?= Date: Thu, 14 Mar 2019 21:06:13 +0100 Subject: [PATCH 02/36] Rename URL session extension properly --- FTAPIKit.xcodeproj/project.pbxproj | 20 +++++++++---------- ...dpoint.swift => URLSession+Endpoint.swift} | 0 2 files changed, 10 insertions(+), 10 deletions(-) rename Sources/{URSession+Endpoint.swift => URLSession+Endpoint.swift} (100%) diff --git a/FTAPIKit.xcodeproj/project.pbxproj b/FTAPIKit.xcodeproj/project.pbxproj index 2b7ddd2..061bcbd 100644 --- a/FTAPIKit.xcodeproj/project.pbxproj +++ b/FTAPIKit.xcodeproj/project.pbxproj @@ -55,10 +55,10 @@ E7B74A1C220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1A220DD7E60055EC66 /* APIEndpoint+URLRequest.swift */; }; E7B74A1D220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1A220DD7E60055EC66 /* APIEndpoint+URLRequest.swift */; }; E7B74A1E220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1A220DD7E60055EC66 /* APIEndpoint+URLRequest.swift */; }; - E7B74A20220DDB300055EC66 /* URSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */; }; - E7B74A21220DDB300055EC66 /* URSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */; }; - E7B74A22220DDB300055EC66 /* URSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */; }; - E7B74A23220DDB300055EC66 /* URSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */; }; + E7B74A20220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URLSession+Endpoint.swift */; }; + E7B74A21220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URLSession+Endpoint.swift */; }; + E7B74A22220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URLSession+Endpoint.swift */; }; + E7B74A23220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URLSession+Endpoint.swift */; }; E7B7741B2152752A006E7585 /* APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774142152752A006E7585 /* APIAdapter.swift */; }; E7B7741C2152752A006E7585 /* APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774142152752A006E7585 /* APIAdapter.swift */; }; E7B7741D2152752A006E7585 /* APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774142152752A006E7585 /* APIAdapter.swift */; }; @@ -129,7 +129,7 @@ E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionAPIAdapter.swift; sourceTree = ""; }; E7B74A15220DC7690055EC66 /* APIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIConfiguration.swift; sourceTree = ""; }; E7B74A1A220DD7E60055EC66 /* APIEndpoint+URLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIEndpoint+URLRequest.swift"; sourceTree = ""; }; - E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URSession+Endpoint.swift"; sourceTree = ""; }; + E7B74A1F220DDB300055EC66 /* URLSession+Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Endpoint.swift"; sourceTree = ""; }; E7B774142152752A006E7585 /* APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIAdapter.swift; sourceTree = ""; }; E7B774152152752A006E7585 /* URL+APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+APIAdapter.swift"; sourceTree = ""; }; E7B774182152752A006E7585 /* AnyEncodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = ""; }; @@ -234,7 +234,7 @@ E7B774192152752A006E7585 /* APIEndpoint.swift */, E7B74A15220DC7690055EC66 /* APIConfiguration.swift */, E7B774142152752A006E7585 /* APIAdapter.swift */, - E7B74A1F220DDB300055EC66 /* URSession+Endpoint.swift */, + E7B74A1F220DDB300055EC66 /* URLSession+Endpoint.swift */, E7B7741A2152752A006E7585 /* APIAdapter+Types.swift */, E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */, E7B774182152752A006E7585 /* AnyEncodable.swift */, @@ -599,7 +599,7 @@ E7B74A16220DC7690055EC66 /* APIConfiguration.swift in Sources */, E7B774332152752A006E7585 /* APIAdapter+Types.swift in Sources */, E7B7741B2152752A006E7585 /* APIAdapter.swift in Sources */, - E7B74A20220DDB300055EC66 /* URSession+Endpoint.swift in Sources */, + E7B74A20220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */, E7B74A1B220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742B2152752A006E7585 /* AnyEncodable.swift in Sources */, E7197F77223510FF0059A54A /* MultipartBodyPart.swift in Sources */, @@ -628,7 +628,7 @@ E7B74A18220DC76C0055EC66 /* APIConfiguration.swift in Sources */, E7B774352152752A006E7585 /* APIAdapter+Types.swift in Sources */, E7B7741D2152752A006E7585 /* APIAdapter.swift in Sources */, - E7B74A22220DDB300055EC66 /* URSession+Endpoint.swift in Sources */, + E7B74A22220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */, E7B74A1D220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742D2152752A006E7585 /* AnyEncodable.swift in Sources */, E7197F79223510FF0059A54A /* MultipartBodyPart.swift in Sources */, @@ -648,7 +648,7 @@ E7B74A19220DC76C0055EC66 /* APIConfiguration.swift in Sources */, E7B774362152752A006E7585 /* APIAdapter+Types.swift in Sources */, E7B7741E2152752A006E7585 /* APIAdapter.swift in Sources */, - E7B74A23220DDB300055EC66 /* URSession+Endpoint.swift in Sources */, + E7B74A23220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */, E7B74A1E220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742E2152752A006E7585 /* AnyEncodable.swift in Sources */, E7197F7A223510FF0059A54A /* MultipartBodyPart.swift in Sources */, @@ -668,7 +668,7 @@ E7B74A17220DC76B0055EC66 /* APIConfiguration.swift in Sources */, E7B774342152752A006E7585 /* APIAdapter+Types.swift in Sources */, E7B7741C2152752A006E7585 /* APIAdapter.swift in Sources */, - E7B74A21220DDB300055EC66 /* URSession+Endpoint.swift in Sources */, + E7B74A21220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */, E7B74A1C220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742C2152752A006E7585 /* AnyEncodable.swift in Sources */, E7197F78223510FF0059A54A /* MultipartBodyPart.swift in Sources */, diff --git a/Sources/URSession+Endpoint.swift b/Sources/URLSession+Endpoint.swift similarity index 100% rename from Sources/URSession+Endpoint.swift rename to Sources/URLSession+Endpoint.swift From 6c5d3b9b777353fb7061b44efbc927ad2ab8189b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ka=C5=A1par=20Jir=C3=A1sek?= Date: Thu, 14 Mar 2019 21:06:28 +0100 Subject: [PATCH 03/36] Remove unneeded public annotations --- Sources/APIConfiguration.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/APIConfiguration.swift b/Sources/APIConfiguration.swift index 232f2ef..aa89ad6 100644 --- a/Sources/APIConfiguration.swift +++ b/Sources/APIConfiguration.swift @@ -23,15 +23,15 @@ public protocol APIJSONConfiguration: APIConfiguration { } public extension APIJSONConfiguration { - public func encode(_ value: Encodable) throws -> Data { + func encode(_ value: Encodable) throws -> Data { return try jsonEncoder.encode(AnyEncodable(value)) } - public func decode(from data: Data) throws -> T { + func decode(from data: Data) throws -> T { return try jsonDecoder.decode(T.self, from: data) } - public func error(from data: Data?, response: URLResponse?, error: Error?) -> Error? { + func error(from data: Data?, response: URLResponse?, error: Error?) -> Error? { return error } } From f34f8ebf5b38a1bd6c83310b6d87503b16fcfb27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ka=C5=A1par=20Jir=C3=A1sek?= Date: Thu, 14 Mar 2019 21:25:26 +0100 Subject: [PATCH 04/36] Remove API adapter --- FTAPIKit.xcodeproj/project.pbxproj | 20 ----- Sources/APIAdapter.swift | 63 --------------- Sources/URLSessionAPIAdapter.swift | 125 ----------------------------- 3 files changed, 208 deletions(-) delete mode 100644 Sources/APIAdapter.swift delete mode 100644 Sources/URLSessionAPIAdapter.swift diff --git a/FTAPIKit.xcodeproj/project.pbxproj b/FTAPIKit.xcodeproj/project.pbxproj index b5aced0..cc0f30e 100644 --- a/FTAPIKit.xcodeproj/project.pbxproj +++ b/FTAPIKit.xcodeproj/project.pbxproj @@ -47,10 +47,6 @@ E769282A22269D4C0031137D /* OutputStream+Write.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769282822269D4C0031137D /* OutputStream+Write.swift */; }; E769282B22269D4C0031137D /* OutputStream+Write.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769282822269D4C0031137D /* OutputStream+Write.swift */; }; E769282C22269D4C0031137D /* OutputStream+Write.swift in Sources */ = {isa = PBXBuildFile; fileRef = E769282822269D4C0031137D /* OutputStream+Write.swift */; }; - E792629D219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */; }; - E792629E219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */; }; - E792629F219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */; }; - E79262A0219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */; }; E7B74A16220DC7690055EC66 /* APIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A15220DC7690055EC66 /* APIConfiguration.swift */; }; E7B74A17220DC76B0055EC66 /* APIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A15220DC7690055EC66 /* APIConfiguration.swift */; }; E7B74A18220DC76C0055EC66 /* APIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A15220DC7690055EC66 /* APIConfiguration.swift */; }; @@ -63,10 +59,6 @@ E7B74A21220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URLSession+Endpoint.swift */; }; E7B74A22220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URLSession+Endpoint.swift */; }; E7B74A23220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B74A1F220DDB300055EC66 /* URLSession+Endpoint.swift */; }; - E7B7741B2152752A006E7585 /* APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774142152752A006E7585 /* APIAdapter.swift */; }; - E7B7741C2152752A006E7585 /* APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774142152752A006E7585 /* APIAdapter.swift */; }; - E7B7741D2152752A006E7585 /* APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774142152752A006E7585 /* APIAdapter.swift */; }; - E7B7741E2152752A006E7585 /* APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774142152752A006E7585 /* APIAdapter.swift */; }; E7B7741F2152752A006E7585 /* URL+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774152152752A006E7585 /* URL+APIAdapter.swift */; }; E7B774202152752A006E7585 /* URL+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774152152752A006E7585 /* URL+APIAdapter.swift */; }; E7B774212152752A006E7585 /* URL+APIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7B774152152752A006E7585 /* URL+APIAdapter.swift */; }; @@ -131,11 +123,9 @@ E766870A2236FCFE0090DC08 /* APIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = ""; }; E7692823222431CE0031137D /* MultipartFormData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartFormData.swift; sourceTree = ""; }; E769282822269D4C0031137D /* OutputStream+Write.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OutputStream+Write.swift"; sourceTree = ""; }; - E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionAPIAdapter.swift; sourceTree = ""; }; E7B74A15220DC7690055EC66 /* APIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIConfiguration.swift; sourceTree = ""; }; E7B74A1A220DD7E60055EC66 /* APIEndpoint+URLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIEndpoint+URLRequest.swift"; sourceTree = ""; }; E7B74A1F220DDB300055EC66 /* URLSession+Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Endpoint.swift"; sourceTree = ""; }; - E7B774142152752A006E7585 /* APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIAdapter.swift; sourceTree = ""; }; E7B774152152752A006E7585 /* URL+APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+APIAdapter.swift"; sourceTree = ""; }; E7B774182152752A006E7585 /* AnyEncodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = ""; }; E7B774192152752A006E7585 /* APIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIEndpoint.swift; sourceTree = ""; }; @@ -238,11 +228,9 @@ children = ( E7B774192152752A006E7585 /* APIEndpoint.swift */, E7B74A15220DC7690055EC66 /* APIConfiguration.swift */, - E7B774142152752A006E7585 /* APIAdapter.swift */, E7B74A1F220DDB300055EC66 /* URLSession+Endpoint.swift */, E7B7741A2152752A006E7585 /* APIAdapter+Types.swift */, E766870A2236FCFE0090DC08 /* APIError.swift */, - E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */, E7B774182152752A006E7585 /* AnyEncodable.swift */, E7B774152152752A006E7585 /* URL+APIAdapter.swift */, E74540E4223AE9E2007AD8C6 /* URLRequest+Endpoint.swift */, @@ -604,13 +592,11 @@ E7B7741F2152752A006E7585 /* URL+APIAdapter.swift in Sources */, E7B74A16220DC7690055EC66 /* APIConfiguration.swift in Sources */, E7B774332152752A006E7585 /* APIAdapter+Types.swift in Sources */, - E7B7741B2152752A006E7585 /* APIAdapter.swift in Sources */, E7B74A20220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */, E7B74A1B220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742B2152752A006E7585 /* AnyEncodable.swift in Sources */, E7197F77223510FF0059A54A /* MultipartBodyPart.swift in Sources */, E7692824222431CE0031137D /* MultipartFormData.swift in Sources */, - E792629D219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */, E766870B2236FCFE0090DC08 /* APIError.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -634,13 +620,11 @@ E7B774212152752A006E7585 /* URL+APIAdapter.swift in Sources */, E7B74A18220DC76C0055EC66 /* APIConfiguration.swift in Sources */, E7B774352152752A006E7585 /* APIAdapter+Types.swift in Sources */, - E7B7741D2152752A006E7585 /* APIAdapter.swift in Sources */, E7B74A22220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */, E7B74A1D220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742D2152752A006E7585 /* AnyEncodable.swift in Sources */, E7197F79223510FF0059A54A /* MultipartBodyPart.swift in Sources */, E7692826222431CE0031137D /* MultipartFormData.swift in Sources */, - E792629F219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */, E766870D2236FCFE0090DC08 /* APIError.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -655,13 +639,11 @@ E7B774222152752A006E7585 /* URL+APIAdapter.swift in Sources */, E7B74A19220DC76C0055EC66 /* APIConfiguration.swift in Sources */, E7B774362152752A006E7585 /* APIAdapter+Types.swift in Sources */, - E7B7741E2152752A006E7585 /* APIAdapter.swift in Sources */, E7B74A23220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */, E7B74A1E220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742E2152752A006E7585 /* AnyEncodable.swift in Sources */, E7197F7A223510FF0059A54A /* MultipartBodyPart.swift in Sources */, E7692827222431CE0031137D /* MultipartFormData.swift in Sources */, - E79262A0219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */, E766870E2236FCFE0090DC08 /* APIError.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -676,13 +658,11 @@ E7B774202152752A006E7585 /* URL+APIAdapter.swift in Sources */, E7B74A17220DC76B0055EC66 /* APIConfiguration.swift in Sources */, E7B774342152752A006E7585 /* APIAdapter+Types.swift in Sources */, - E7B7741C2152752A006E7585 /* APIAdapter.swift in Sources */, E7B74A21220DDB300055EC66 /* URLSession+Endpoint.swift in Sources */, E7B74A1C220DD7E60055EC66 /* APIEndpoint+URLRequest.swift in Sources */, E7B7742C2152752A006E7585 /* AnyEncodable.swift in Sources */, E7197F78223510FF0059A54A /* MultipartBodyPart.swift in Sources */, E7692825222431CE0031137D /* MultipartFormData.swift in Sources */, - E792629E219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */, E766870C2236FCFE0090DC08 /* APIError.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/APIAdapter.swift b/Sources/APIAdapter.swift deleted file mode 100644 index 2c3f56d..0000000 --- a/Sources/APIAdapter.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// APIAdapter.swift -// FTAPIKit -// -// Created by Matěj Kašpar Jirásek on 08/02/2018. -// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved. -// - -import Foundation - -/// Delegate of `APIAdapter` used for platform-specific functionality -/// (showing/hiding network activity indicator) and signing/manipulating -/// URL request before they are sent. -public protocol APIAdapterDelegate: class { - /// Delegate method updating number of currently running requests. Should be used mainly - /// for logging, debugging and/or presenting network activity indicator on iOS. See example - /// implementation in discussion. - /// - /// func apiAdapter(_ apiAdapter: APIAdapter, didUpdateRunningRequestCount runningRequestCount: UInt) { - /// let isVisible = UIApplication.shared.isNetworkActivityIndicatorVisible - /// if runningRequestCount > 0, !isVisible { - /// UIApplication.shared.isNetworkActivityIndicatorVisible = true - /// } else if runningRequestCount < 1 { - /// UIApplication.shared.isNetworkActivityIndicatorVisible = false - /// } - /// } - func apiAdapter(_ apiAdapter: APIAdapter, didUpdateRunningRequestCount runningRequestCount: UInt) - - /// Method for updating `URLRequest` created by API adapter with app-specific headers etc. - /// It can be completed asynchronously so actions like refreshing access token can be executed. - /// Changes to URL request, which are not due to authorization requirements should be provided - /// in custom `URLSession` with configuration when `APIAdapter` is created. - /// - /// The `authorization` property of `APIEndpoint` is provided for manual checking whether the - /// request should be signed, because signing non-authorized endpoints might pose as a security risk. - func apiAdapter(_ apiAdapter: APIAdapter, willRequest request: URLRequest, to endpoint: APIEndpoint, completion: @escaping (APIResult) -> Void) -} - -/// Protocol describing interface communicating with API resources (most probably over internet). -/// This interface encapsulates executing requests. -/// -/// Standard implementation of this interface using `URLSession` is available as -/// `URLSessionAPIAdapter`. -public protocol APIAdapter { - - /// Delegate used for notificating about the currently running request count - /// and asynchronously signing authorized requests. - var delegate: APIAdapterDelegate? { get set } - - /// Calls API request endpoint with JSON body and after finishing it calls completion handler with either decoded JSON model or error. - /// - /// - Parameters: - /// - endpoint: Response endpoint - /// - completion: Completion closure receiving result with automatically decoded JSON model taken from reponse endpoint associated type. - func request(response endpoint: Endpoint, completion: @escaping (APIResult) -> Void) - - /// Calls API endpoint and after finishing it calls completion handler with either data or error. - /// - /// - Parameters: - /// - endpoint: Standard endpoint with no response associated type. - /// - completion: Completion closure receiving result with data. - func request(data endpoint: APIEndpoint, completion: @escaping (APIResult) -> Void) -} diff --git a/Sources/URLSessionAPIAdapter.swift b/Sources/URLSessionAPIAdapter.swift deleted file mode 100644 index 1d29b0e..0000000 --- a/Sources/URLSessionAPIAdapter.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// URLSessionAPIAdapter.swift -// FTAPIKit -// -// Created by Matěj Kašpar Jirásek on 10/11/2018. -// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved. -// - -import Foundation - -/// Standard and default implementation of `APIAdapter` protocol using `URLSession`. -public final class URLSessionAPIAdapter: APIAdapter { - public weak var delegate: APIAdapterDelegate? - - private let urlSession: URLSession - private let baseUrl: URL - - private let jsonEncoder: JSONEncoder - private let jsonDecoder: JSONDecoder - - private let errorType: APIError.Type - - private var runningRequestCount: UInt = 0 { - didSet { - guard let delegate = delegate else { return } - DispatchQueue.main.async { - delegate.apiAdapter(self, didUpdateRunningRequestCount: self.runningRequestCount) - } - } - } - - /// Constructor for `APIAdapter` based on `URLSession`. - /// - /// - Parameters: - /// - baseUrl: Base URI for the server for all API calls this API adapter will be executing. - /// - jsonEncoder: Optional JSON encoder used for serialization of JSON models. - /// - jsonDecoder: Optional JSON decoder used for deserialization of JSON models. - /// - errorType: If we want custom method for error handling instead of returning `StandardAPIError` - /// This type needs to implement `APIError` protocol and its optional init requirement. - /// - urlSession: Optional URL session (otherwise the standard one will be used). Used mainly if we need - /// our own `URLSessionConfiguration` or another way of caching (ephemeral session). - public init(baseUrl: URL, jsonEncoder: JSONEncoder = JSONEncoder(), jsonDecoder: JSONDecoder = JSONDecoder(), errorType: APIError.Type = StandardAPIError.self, urlSession: URLSession = .shared) { - self.baseUrl = baseUrl - self.jsonDecoder = jsonDecoder - self.jsonEncoder = jsonEncoder - self.errorType = errorType - self.urlSession = urlSession - } - - public func request(response endpoint: Endpoint, completion: @escaping (APIResult) -> Void) { - dataTask(response: endpoint, creation: { _ in }, completion: completion) - } - - public func request(data endpoint: APIEndpoint, completion: @escaping (APIResult) -> Void) { - dataTask(data: endpoint, creation: { _ in }, completion: completion) - } - - public func dataTask(response endpoint: Endpoint, creation: @escaping (URLSessionTask) -> Void, completion: @escaping (APIResult) -> Void) { - dataTask(data: endpoint, creation: creation) { result in - switch result { - case .value(let data): - do { - let model = try self.jsonDecoder.decode(Endpoint.Response.self, from: data) - completion(.value(model)) - } catch { - completion(.error(error)) - } - case .error(let error): - completion(.error(error)) - } - } - } - - public func dataTask(data endpoint: APIEndpoint, creation: @escaping (URLSessionTask) -> Void, completion: @escaping (APIResult) -> Void) { - let url = baseUrl.appendingPathComponent(endpoint.path) - var request = URLRequest(url: url) - request.httpMethod = endpoint.method.description - - let encode: (Encodable) throws -> Data = { encodable in - try self.jsonEncoder.encode(AnyEncodable(encodable)) - } - - do { - try request.setRequestType(endpoint.type, parameters: endpoint.parameters, using: encode) - } catch { - completion(.error(error)) - return - } - - if let delegate = delegate { - delegate.apiAdapter(self, willRequest: request, to: endpoint) { result in - switch result { - case .value(let request): - let task = self.send(request: request, completion: completion) - creation(task) - case .error(let error): - completion(.error(error)) - } - } - } else { - let task = self.send(request: request, completion: completion) - creation(task) - } - } - - private func send(request: URLRequest, completion: @escaping (APIResult) -> Void) -> URLSessionTask { - runningRequestCount += 1 - return resumeDataTask(with: request) { result in - self.runningRequestCount -= 1 - completion(result) - } - } - - private func resumeDataTask(with request: URLRequest, completion: @escaping (APIResult) -> Void) -> URLSessionTask { - let task = urlSession.dataTask(with: request) { [jsonDecoder, errorType] data, response, error in - if let error = errorType.init(data: data, response: response, error: error, decoder: jsonDecoder) { - completion(.error(error)) - } else { - completion(.value(data ?? Data())) - } - } - task.resume() - return task - } -} From 89e708337c1432be279c7c1b2b7a4f57754d3548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ka=C5=A1par=20Jir=C3=A1sek?= Date: Thu, 14 Mar 2019 21:25:59 +0100 Subject: [PATCH 05/36] Add coding protocols --- Sources/APIConfiguration.swift | 30 +++++++++------------------- Sources/APIEndpoint+URLRequest.swift | 2 +- Sources/APIError.swift | 4 ++-- Sources/URLRequest+Endpoint.swift | 14 ++++++------- Sources/URLSession+Endpoint.swift | 19 +++--------------- 5 files changed, 22 insertions(+), 47 deletions(-) diff --git a/Sources/APIConfiguration.swift b/Sources/APIConfiguration.swift index aa89ad6..d47340c 100644 --- a/Sources/APIConfiguration.swift +++ b/Sources/APIConfiguration.swift @@ -8,30 +8,18 @@ import Foundation -public protocol APIConfiguration { - var baseUrl: URL { get } - - func encode(_ value: Encodable) throws -> Data - func decode(from data: Data) throws -> T - - func error(from data: Data?, response: URLResponse?, error: Error?) -> Error? +public protocol APIDecoder { + func decode(_ type: T.Type, from data: Data) throws -> T where T: Decodable } -public protocol APIJSONConfiguration: APIConfiguration { - var jsonDecoder: JSONDecoder { get } - var jsonEncoder: JSONEncoder { get } +public protocol APIEncoder { + func encode(_ value: T) throws -> Data where T: Encodable } -public extension APIJSONConfiguration { - func encode(_ value: Encodable) throws -> Data { - return try jsonEncoder.encode(AnyEncodable(value)) - } - - func decode(from data: Data) throws -> T { - return try jsonDecoder.decode(T.self, from: data) - } +public protocol APIConfiguration { + var baseUrl: URL { get } + var apiErrorType: APIError.Type { get } - func error(from data: Data?, response: URLResponse?, error: Error?) -> Error? { - return error - } + var decoder: APIDecoder { get } + var encoder: APIEncoder { get } } diff --git a/Sources/APIEndpoint+URLRequest.swift b/Sources/APIEndpoint+URLRequest.swift index c487be1..d3e7b2e 100644 --- a/Sources/APIEndpoint+URLRequest.swift +++ b/Sources/APIEndpoint+URLRequest.swift @@ -14,7 +14,7 @@ extension APIEndpoint { var request = URLRequest(url: url) request.httpMethod = method.description - try request.setRequestType(type, parameters: parameters, using: configuration.encode) + try request.setRequestType(type, parameters: parameters, using: configuration.encoder) return request } } diff --git a/Sources/APIError.swift b/Sources/APIError.swift index df2ddae..4d0c587 100644 --- a/Sources/APIError.swift +++ b/Sources/APIError.swift @@ -9,7 +9,7 @@ import Foundation public protocol APIError: Error { - init?(data: Data?, response: URLResponse?, error: Error?, decoder: JSONDecoder) + init?(data: Data?, response: URLResponse?, error: Error?, decoder: APIDecoder) } /// Standard API error returned in `APIResult` when no custom error @@ -31,7 +31,7 @@ public enum StandardAPIError: APIError { /// or the temporary request body stream cannot be opened. case multipartStreamCannotBeOpened - public init?(data: Data?, response: URLResponse?, error: Error?, decoder: JSONDecoder) { + public init?(data: Data?, response: URLResponse?, error: Error?, decoder: APIDecoder) { switch (data, response as? HTTPURLResponse, error) { case let (_, _, error as NSError) where error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled: self = .cancelled diff --git a/Sources/URLRequest+Endpoint.swift b/Sources/URLRequest+Endpoint.swift index 5143a9d..ae85f7c 100644 --- a/Sources/URLRequest+Endpoint.swift +++ b/Sources/URLRequest+Endpoint.swift @@ -9,14 +9,14 @@ import Foundation extension URLRequest { - mutating func setRequestType(_ requestType: RequestType, parameters: HTTPParameters, using encode: (Encodable) throws -> Data) throws { + mutating func setRequestType(_ requestType: RequestType, parameters: HTTPParameters, using encoder: APIEncoder) throws { switch requestType { case .jsonBody(let encodable): - try setJSONBody(encodable: encodable, parameters: parameters, using: encode) + try setJSONBody(encodable: encodable, parameters: parameters, using: encoder) case .urlEncoded: setURLEncoded(parameters: parameters) case .jsonParams: - setJSON(parameters: parameters, using: encode) + setJSON(parameters: parameters, using: encoder) case let .multipart(files): try setMultipart(parameters: parameters, files: files) case .base64Upload: @@ -46,7 +46,7 @@ extension URLRequest { } } - private mutating func setJSON(parameters: HTTPParameters, body: Data? = nil, using encode: (Encodable) throws -> Data) { + private mutating func setJSON(parameters: HTTPParameters, body: Data? = nil, using encoder: APIEncoder) { setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") httpBody = body url?.appendQuery(parameters: parameters) @@ -59,8 +59,8 @@ extension URLRequest { setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") } - private mutating func setJSONBody(encodable: Encodable, parameters: HTTPParameters, using encode: (Encodable) throws -> Data) throws { - let body = try encode(encodable) - setJSON(parameters: parameters, body: body, using: encode) + private mutating func setJSONBody(encodable: Encodable, parameters: HTTPParameters, using encoder: APIEncoder) throws { + let body = try encoder.encode(AnyEncodable(encodable)) + setJSON(parameters: parameters, body: body, using: encoder) } } diff --git a/Sources/URLSession+Endpoint.swift b/Sources/URLSession+Endpoint.swift index 678eb07..3633fc3 100644 --- a/Sources/URLSession+Endpoint.swift +++ b/Sources/URLSession+Endpoint.swift @@ -17,23 +17,10 @@ public extension URLSession { func dataTask(to endpoint: APIEndpoint, with configuration: APIConfiguration, completion: @escaping (APIResult) -> Void) throws -> URLSessionDataTask { let request = try endpoint.request(with: configuration) return dataTask(with: request) { (data, response, error) in - if let error = configuration.error(from: data, response: response, error: error) { + if let error = configuration.apiErrorType.init(data: data, response: response, error: error, decoder: configuration.decoder) { completion(.error(error)) - return - } - switch (data, response, error) { - case let (nil, response as HTTPURLResponse, nil) where response.statusCode == 204: - completion(.value(Data())) - case let (data?, response as HTTPURLResponse, nil) where response.statusCode < 400: - completion(.value(data)) - case let (_, _, error as NSError) where error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled: - completion(.error(APIError.cancelled)) - case let (_, _, error?): - completion(.error(error)) - case let (data, response as HTTPURLResponse, nil): - completion(.error(APIError.errorCode(response.statusCode, data))) - default: - completion(.error(APIError.noResponse)) + } else { + completion(.value(data ?? Data())) } } } From c30390e20d502c22ece767b5f77b699c607ea3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Sat, 4 Jan 2020 20:57:35 +0100 Subject: [PATCH 06/36] Remove prefix from endpoint --- .../{APIEndpoint.swift => Endpoint.swift} | 24 +++------ .../APIAdapter+PromiseKit.swift | 4 +- .../URLSessionAPIAdapter+PromiseKit.swift | 4 +- Tests/FTAPIKitTests/APIAdapterTests.swift | 52 +++++++++---------- .../Mockups/MockupAPIAdapterDelegate.swift | 2 +- Tests/FTAPIKitTests/StressTests.swift | 2 +- 6 files changed, 40 insertions(+), 48 deletions(-) rename Sources/FTAPIKit/{APIEndpoint.swift => Endpoint.swift} (80%) diff --git a/Sources/FTAPIKit/APIEndpoint.swift b/Sources/FTAPIKit/Endpoint.swift similarity index 80% rename from Sources/FTAPIKit/APIEndpoint.swift rename to Sources/FTAPIKit/Endpoint.swift index d05881b..e277de3 100644 --- a/Sources/FTAPIKit/APIEndpoint.swift +++ b/Sources/FTAPIKit/Endpoint.swift @@ -1,11 +1,3 @@ -// -// APIEndpoint.swift -// FTAPIKit-iOS -// -// Created by Matěj Kašpar Jirásek on 04/09/2018. -// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved. -// - /// Protocol describing API endpoint. API Endpoint describes one URI with all the /// data and parameters which are sent to it. /// @@ -16,7 +8,7 @@ /// cases and information about one endpoint is spreaded all over the files. Also, /// structs offer us generated initializers, which is very helpful /// -public protocol APIEndpoint { +public protocol Endpoint { /// URL path component without base URI. var path: String { get } @@ -41,7 +33,7 @@ public protocol APIEndpoint { var authorized: Bool { get } } -public extension APIEndpoint { +public extension Endpoint { var parameters: HTTPParameters { return [:] } @@ -59,17 +51,17 @@ public extension APIEndpoint { } } -/// Endpoint protocol extending `APIEndpoint` having decodable associated type, which is used +/// Endpoint protocol extending `Endpoint` having decodable associated type, which is used /// for automatic deserialization. -public protocol APIResponseEndpoint: APIEndpoint { +public protocol ResponseEndpoint: Endpoint { /// Associated type describing the return type conforming to `Decodable` /// protocol. This is only a phantom-type used by `APIAdapter` /// for automatic decoding/deserialization of API results. associatedtype Response: Decodable } -/// Endpoint protocol extending `APIEndpoint` encapsulating and improving sending JSON models to API. -public protocol APIRequestEndpoint: APIEndpoint { +/// Endpoint protocol extending `Endpoint` encapsulating and improving sending JSON models to API. +public protocol RequestEndpoint: Endpoint { /// Associated type describing the encodable request model for /// JSON serialization. The associated type is derived from /// the body property. @@ -78,7 +70,7 @@ public protocol APIRequestEndpoint: APIEndpoint { var body: Request { get } } -public extension APIRequestEndpoint { +public extension RequestEndpoint { var method: HTTPMethod { return .post } @@ -90,4 +82,4 @@ public extension APIRequestEndpoint { /// Typealias combining request and response API endpoint. For describing JSON /// request which both sends and expects JSON model from the server. -public typealias APIRequestResponseEndpoint = APIRequestEndpoint & APIResponseEndpoint +public typealias RequestResponseEndpoint = RequestEndpoint & ResponseEndpoint diff --git a/Sources/FTAPIKitPromiseKit/APIAdapter+PromiseKit.swift b/Sources/FTAPIKitPromiseKit/APIAdapter+PromiseKit.swift index ff3deac..3a2f1f5 100644 --- a/Sources/FTAPIKitPromiseKit/APIAdapter+PromiseKit.swift +++ b/Sources/FTAPIKitPromiseKit/APIAdapter+PromiseKit.swift @@ -13,13 +13,13 @@ import FTAPIKit #endif extension APIAdapter { - public func request(response endpoint: Endpoint) -> Promise { + public func request(response endpoint: Endpoint) -> Promise { let (promise, seal) = Promise.pending() request(response: endpoint, completion: seal.resolve) return promise } - public func request(data endpoint: APIEndpoint) -> Promise { + public func request(data endpoint: Endpoint) -> Promise { let (promise, seal) = Promise.pending() request(data: endpoint, completion: seal.resolve) return promise diff --git a/Sources/FTAPIKitPromiseKit/URLSessionAPIAdapter+PromiseKit.swift b/Sources/FTAPIKitPromiseKit/URLSessionAPIAdapter+PromiseKit.swift index aa85d90..b7daa1f 100644 --- a/Sources/FTAPIKitPromiseKit/URLSessionAPIAdapter+PromiseKit.swift +++ b/Sources/FTAPIKitPromiseKit/URLSessionAPIAdapter+PromiseKit.swift @@ -18,7 +18,7 @@ public struct APIDataTask { } extension URLSessionAPIAdapter { - public func dataTask(response endpoint: Endpoint) -> APIDataTask { + public func dataTask(response endpoint: Endpoint) -> APIDataTask { let task = Guarantee.pending() let response = Promise.pending() @@ -31,7 +31,7 @@ extension URLSessionAPIAdapter { return APIDataTask(sessionTask: task.guarantee, response: response.promise) } - public func dataTask(data endpoint: APIEndpoint) -> APIDataTask { + public func dataTask(data endpoint: Endpoint) -> APIDataTask { let task = Guarantee.pending() let response = Promise.pending() diff --git a/Tests/FTAPIKitTests/APIAdapterTests.swift b/Tests/FTAPIKitTests/APIAdapterTests.swift index 8e438a8..d23942f 100644 --- a/Tests/FTAPIKitTests/APIAdapterTests.swift +++ b/Tests/FTAPIKitTests/APIAdapterTests.swift @@ -20,7 +20,7 @@ final class APIAdapterTests: XCTestCase { private let timeout: TimeInterval = 30.0 func testGet() { - struct Endpoint: APIEndpoint { + struct TestEndpoint: Endpoint { let path = "get" } @@ -28,7 +28,7 @@ final class APIAdapterTests: XCTestCase { var adapter: APIAdapter = apiAdapter() adapter.delegate = delegate let expectation = self.expectation(description: "Result") - adapter.request(data: Endpoint()) { result in + adapter.request(data: TestEndpoint()) { result in if case let .failure(error) = result { XCTFail(error.localizedDescription) } @@ -38,7 +38,7 @@ final class APIAdapterTests: XCTestCase { } func testClientError() { - struct Endpoint: APIEndpoint { + struct TestEndpoint: Endpoint { let path = "status/404" } @@ -46,7 +46,7 @@ final class APIAdapterTests: XCTestCase { var adapter: APIAdapter = apiAdapter() adapter.delegate = delegate let expectation = self.expectation(description: "Result") - adapter.request(data: Endpoint()) { result in + adapter.request(data: TestEndpoint()) { result in switch result { case .success: XCTFail("404 endpoint must return error") @@ -61,7 +61,7 @@ final class APIAdapterTests: XCTestCase { } func testServerError() { - struct Endpoint: APIEndpoint { + struct TestEndpoint: Endpoint { let path = "status/500" } @@ -69,7 +69,7 @@ final class APIAdapterTests: XCTestCase { var adapter: APIAdapter = apiAdapter() adapter.delegate = delegate let expectation = self.expectation(description: "Result") - adapter.request(data: Endpoint()) { result in + adapter.request(data: TestEndpoint()) { result in switch result { case .success: XCTFail("500 endpoint must return error") @@ -84,7 +84,7 @@ final class APIAdapterTests: XCTestCase { } func testConnectionError() { - struct Endpoint: APIEndpoint { + struct TestEndpoint: Endpoint { let path = "some-failing-path" } @@ -92,7 +92,7 @@ final class APIAdapterTests: XCTestCase { var adapter: APIAdapter = URLSessionAPIAdapter(baseUrl: URL(string: "https://www.tato-stranka-urcite-neexistuje.cz/")!) adapter.delegate = delegate let expectation = self.expectation(description: "Result") - adapter.request(data: Endpoint()) { result in + adapter.request(data: TestEndpoint()) { result in switch result { case .success: XCTFail("Non-existing domain must fail") @@ -107,7 +107,7 @@ final class APIAdapterTests: XCTestCase { } func testEmptyResult() { - struct Endpoint: APIEndpoint { + struct TestEndpoint: Endpoint { let path = "status/204" } @@ -115,7 +115,7 @@ final class APIAdapterTests: XCTestCase { var adapter: APIAdapter = apiAdapter() adapter.delegate = delegate let expectation = self.expectation(description: "Result") - adapter.request(data: Endpoint()) { result in + adapter.request(data: TestEndpoint()) { result in if case let .failure(error) = result { XCTFail(error.localizedDescription) } @@ -125,7 +125,7 @@ final class APIAdapterTests: XCTestCase { } func testCustomError() { - struct Endpoint: APIEndpoint { + struct TestEndpoint: Endpoint { let path = "get" } @@ -141,7 +141,7 @@ final class APIAdapterTests: XCTestCase { var adapter: APIAdapter = URLSessionAPIAdapter(baseUrl: URL(string: "http://httpbin.org/")!, errorType: CustomError.self) adapter.delegate = delegate let expectation = self.expectation(description: "Result") - adapter.request(data: Endpoint()) { result in + adapter.request(data: TestEndpoint()) { result in if case let .failure(error) = result { XCTAssertTrue(error is CustomError) } else { @@ -153,7 +153,7 @@ final class APIAdapterTests: XCTestCase { } func testURLEncodedPost() { - struct Endpoint: APIEndpoint { + struct TestEndpoint: Endpoint { let data: RequestType = .urlEncoded let parameters: HTTPParameters = [ "someParameter": "someValue", @@ -167,7 +167,7 @@ final class APIAdapterTests: XCTestCase { var adapter: APIAdapter = apiAdapter() adapter.delegate = delegate let expectation = self.expectation(description: "Result") - adapter.request(data: Endpoint()) { result in + adapter.request(data: TestEndpoint()) { result in if case let .failure(error) = result { XCTFail(error.localizedDescription) } @@ -192,7 +192,7 @@ final class APIAdapterTests: XCTestCase { let items: [String]? } - struct Endpoint: APIResponseEndpoint { + struct TestEndpoint: ResponseEndpoint { typealias Response = TopLevel let path = "json" @@ -202,7 +202,7 @@ final class APIAdapterTests: XCTestCase { var adapter: APIAdapter = apiAdapter() adapter.delegate = delegate let expectation = self.expectation(description: "Result") - adapter.request(response: Endpoint()) { result in + adapter.request(response: TestEndpoint()) { result in if case let .failure(error) = result { XCTFail(error.localizedDescription) } @@ -227,7 +227,7 @@ final class APIAdapterTests: XCTestCase { let items: [String]? } - struct Endpoint: APIResponseEndpoint { + struct TestEndpoint: ResponseEndpoint { typealias Response = TopLevel let path = "json" @@ -237,7 +237,7 @@ final class APIAdapterTests: XCTestCase { let adapter = apiAdapter() adapter.delegate = delegate let expectation = self.expectation(description: "Result") - adapter.dataTask(response: Endpoint(), creation: { $0.cancel() }, completion: { result in + adapter.dataTask(response: TestEndpoint(), creation: { $0.cancel() }, completion: { result in if case .failure(StandardAPIError.cancelled) = result { XCTAssert(true) } else { @@ -259,7 +259,7 @@ final class APIAdapterTests: XCTestCase { let json: User } - struct Endpoint: APIRequestResponseEndpoint { + struct TestEndpoint: RequestResponseEndpoint { typealias Response = TopLevel @@ -268,7 +268,7 @@ final class APIAdapterTests: XCTestCase { } let user = User(uuid: UUID(), name: "Some Name", age: .random(in: 0...120)) - let endpoint = Endpoint(body: user) + let endpoint = TestEndpoint(body: user) let delegate = MockupAPIAdapterDelegate() var adapter: APIAdapter = apiAdapter() adapter.delegate = delegate @@ -292,7 +292,7 @@ final class APIAdapterTests: XCTestCase { let age: UInt } - struct Endpoint: APIRequestResponseEndpoint { + struct TestEndpoint: RequestResponseEndpoint { typealias Response = User let body: User @@ -300,7 +300,7 @@ final class APIAdapterTests: XCTestCase { } let user = User(uuid: UUID(), name: "Some Name", age: .random(in: 0...120)) - let endpoint = Endpoint(body: user) + let endpoint = TestEndpoint(body: user) let delegate = MockupAPIAdapterDelegate() var adapter: APIAdapter = apiAdapter() adapter.delegate = delegate @@ -315,7 +315,7 @@ final class APIAdapterTests: XCTestCase { } func testAuthorization() { - struct Endpoint: APIEndpoint { + struct TestEndpoint: Endpoint { let path = "bearer" let authorized = true } @@ -325,7 +325,7 @@ final class APIAdapterTests: XCTestCase { adapter.delegate = delegate let expectation = self.expectation(description: "Result") - adapter.request(data: Endpoint()) { result in + adapter.request(data: TestEndpoint()) { result in if case let .failure(error) = result { XCTFail(error.localizedDescription) } @@ -344,7 +344,7 @@ final class APIAdapterTests: XCTestCase { ] } - struct Endpoint: APIEndpoint { + struct TestEndpoint: Endpoint { let file: MockupFile var type: RequestType { @@ -369,7 +369,7 @@ final class APIAdapterTests: XCTestCase { var adapter: APIAdapter = apiAdapter() adapter.delegate = delegate let expectation = self.expectation(description: "Result") - adapter.request(data: Endpoint(file: file)) { result in + adapter.request(data: TestEndpoint(file: file)) { result in if case let .failure(error) = result { XCTFail(error.localizedDescription) } diff --git a/Tests/FTAPIKitTests/Mockups/MockupAPIAdapterDelegate.swift b/Tests/FTAPIKitTests/Mockups/MockupAPIAdapterDelegate.swift index ef0a7b1..43db405 100644 --- a/Tests/FTAPIKitTests/Mockups/MockupAPIAdapterDelegate.swift +++ b/Tests/FTAPIKitTests/Mockups/MockupAPIAdapterDelegate.swift @@ -10,7 +10,7 @@ import FTAPIKit import Foundation final class MockupAPIAdapterDelegate: APIAdapterDelegate { - func apiAdapter(_ apiAdapter: APIAdapter, willRequest request: URLRequest, to endpoint: APIEndpoint, completion: @escaping (Result) -> Void) { + func apiAdapter(_ apiAdapter: APIAdapter, willRequest request: URLRequest, to endpoint: Endpoint, completion: @escaping (Result) -> Void) { if endpoint.authorized { var newRequest = request newRequest.addValue("Bearer " + UUID().uuidString, forHTTPHeaderField: "Authorization") diff --git a/Tests/FTAPIKitTests/StressTests.swift b/Tests/FTAPIKitTests/StressTests.swift index a23aa08..b54bb21 100644 --- a/Tests/FTAPIKitTests/StressTests.swift +++ b/Tests/FTAPIKitTests/StressTests.swift @@ -18,7 +18,7 @@ final class StressTests: XCTestCase { private let extendedTimeout: TimeInterval = 120.0 func testStressMultipleRequestsViaGet() { - struct Endpoint: APIEndpoint { + struct Endpoint: Endpoint { let path = "get" } From 051bd64b3bd1f94f511f5e93a2aae33a9d714da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Sat, 4 Jan 2020 21:01:50 +0100 Subject: [PATCH 07/36] Remove API configuration --- Sources/APIConfiguration.swift | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 Sources/APIConfiguration.swift diff --git a/Sources/APIConfiguration.swift b/Sources/APIConfiguration.swift deleted file mode 100644 index d47340c..0000000 --- a/Sources/APIConfiguration.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// APIConfiguration.swift -// FTAPIKit-iOS -// -// Created by Matěj Kašpar Jirásek on 08/02/2019. -// Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved. -// - -import Foundation - -public protocol APIDecoder { - func decode(_ type: T.Type, from data: Data) throws -> T where T: Decodable -} - -public protocol APIEncoder { - func encode(_ value: T) throws -> Data where T: Encodable -} - -public protocol APIConfiguration { - var baseUrl: URL { get } - var apiErrorType: APIError.Type { get } - - var decoder: APIDecoder { get } - var encoder: APIEncoder { get } -} From 7f26a64d681e519ee70114240bcd8c24c2c4887d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Sat, 4 Jan 2020 21:02:45 +0100 Subject: [PATCH 08/36] Add protocols for custom coding --- Sources/FTAPIKit/APIError.swift | 4 +- Sources/FTAPIKit/Coding.swift | 46 ++++++++++++++++++++++ Sources/FTAPIKit/URLRequest+Endpoint.swift | 14 +++---- 3 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 Sources/FTAPIKit/Coding.swift diff --git a/Sources/FTAPIKit/APIError.swift b/Sources/FTAPIKit/APIError.swift index 4d0c587..003d4c5 100644 --- a/Sources/FTAPIKit/APIError.swift +++ b/Sources/FTAPIKit/APIError.swift @@ -9,7 +9,7 @@ import Foundation public protocol APIError: Error { - init?(data: Data?, response: URLResponse?, error: Error?, decoder: APIDecoder) + init?(data: Data?, response: URLResponse?, error: Error?, decoding: Decoding) } /// Standard API error returned in `APIResult` when no custom error @@ -31,7 +31,7 @@ public enum StandardAPIError: APIError { /// or the temporary request body stream cannot be opened. case multipartStreamCannotBeOpened - public init?(data: Data?, response: URLResponse?, error: Error?, decoder: APIDecoder) { + public init?(data: Data?, response: URLResponse?, error: Error?, decoding: Decoding) { switch (data, response as? HTTPURLResponse, error) { case let (_, _, error as NSError) where error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled: self = .cancelled diff --git a/Sources/FTAPIKit/Coding.swift b/Sources/FTAPIKit/Coding.swift new file mode 100644 index 0000000..7ce7e86 --- /dev/null +++ b/Sources/FTAPIKit/Coding.swift @@ -0,0 +1,46 @@ +import Foundation + +public protocol Encoding { + func encode(_ object: T) throws -> Data +} + +public protocol Decoding { + func decode(data: Data) throws -> T +} + +public struct JSONEncoding: Encoding { + let encoder: JSONEncoder + + public init(encoder: JSONEncoder = .init()) { + self.encoder = encoder + } + + public init(configure: (JSONEncoder) -> Void) { + let encoder = JSONEncoder() + configure(encoder) + self.encoder = encoder + } + + public func encode(_ object: T) throws -> Data { + try encoder.encode(object) + } +} + + +public struct JSONDecoding: Decoding { + let decoder: JSONDecoder + + public init(decoder: JSONDecoder = .init()) { + self.decoder = decoder + } + + public init(configure: (JSONDecoder) -> Void) { + let decoder = JSONDecoder() + configure(decoder) + self.decoder = decoder + } + + public func decode(data: Data) throws -> T { + try decoder.decode(T.self, from: data) + } +} diff --git a/Sources/FTAPIKit/URLRequest+Endpoint.swift b/Sources/FTAPIKit/URLRequest+Endpoint.swift index ae85f7c..183a774 100644 --- a/Sources/FTAPIKit/URLRequest+Endpoint.swift +++ b/Sources/FTAPIKit/URLRequest+Endpoint.swift @@ -9,14 +9,14 @@ import Foundation extension URLRequest { - mutating func setRequestType(_ requestType: RequestType, parameters: HTTPParameters, using encoder: APIEncoder) throws { + mutating func setRequestType(_ requestType: RequestType, parameters: HTTPParameters, using encoding: Encoding) throws { switch requestType { case .jsonBody(let encodable): - try setJSONBody(encodable: encodable, parameters: parameters, using: encoder) + try setJSONBody(encodable: encodable, parameters: parameters, using: encoding) case .urlEncoded: setURLEncoded(parameters: parameters) case .jsonParams: - setJSON(parameters: parameters, using: encoder) + setJSON(parameters: parameters, using: encoding) case let .multipart(files): try setMultipart(parameters: parameters, files: files) case .base64Upload: @@ -46,7 +46,7 @@ extension URLRequest { } } - private mutating func setJSON(parameters: HTTPParameters, body: Data? = nil, using encoder: APIEncoder) { + private mutating func setJSON(parameters: HTTPParameters, body: Data? = nil, using encoding: Encoding) { setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") httpBody = body url?.appendQuery(parameters: parameters) @@ -59,8 +59,8 @@ extension URLRequest { setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") } - private mutating func setJSONBody(encodable: Encodable, parameters: HTTPParameters, using encoder: APIEncoder) throws { - let body = try encoder.encode(AnyEncodable(encodable)) - setJSON(parameters: parameters, body: body, using: encoder) + private mutating func setJSONBody(encodable: Encodable, parameters: HTTPParameters, using encoding: Encoding) throws { + let body = try encoding.encode(AnyEncodable(encodable)) + setJSON(parameters: parameters, body: body, using: encoding) } } From 9b869fb498db06258161a2454e927ad6289d5041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Sat, 4 Jan 2020 21:13:28 +0100 Subject: [PATCH 09/36] Add server protocols --- Sources/FTAPIKit/Server.swift | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Sources/FTAPIKit/Server.swift diff --git a/Sources/FTAPIKit/Server.swift b/Sources/FTAPIKit/Server.swift new file mode 100644 index 0000000..67752ac --- /dev/null +++ b/Sources/FTAPIKit/Server.swift @@ -0,0 +1,34 @@ +import Foundation + +public protocol ReadonlyServer { + associatedtype APIError: Error + + var baseUri: URL { get } + var urlSession: URLSession { get } + var decoding: Decoding { get } + var requestConfiguration: (inout URLRequest) -> Void { get } +} + +public protocol Server: ReadonlyServer { + var encoding: Encoding { get } +} + +public extension ReadonlyServer { + var urlSession: URLSession { + .shared + } + + var decoding: Decoding { + JSONDecoding() + } + + var requestConfiguration: (inout URLRequest) -> Void { + { _ in } + } +} + +public extension Server { + var encoding: Encoding { + JSONEncoding() + } +} From 6b33260abb9bc266d51e262d9adb14e5853ccb1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Sat, 4 Jan 2020 21:13:51 +0100 Subject: [PATCH 10/36] Make decoder/encoder private --- Sources/FTAPIKit/Coding.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/FTAPIKit/Coding.swift b/Sources/FTAPIKit/Coding.swift index 7ce7e86..c006144 100644 --- a/Sources/FTAPIKit/Coding.swift +++ b/Sources/FTAPIKit/Coding.swift @@ -9,7 +9,7 @@ public protocol Decoding { } public struct JSONEncoding: Encoding { - let encoder: JSONEncoder + private let encoder: JSONEncoder public init(encoder: JSONEncoder = .init()) { self.encoder = encoder @@ -28,7 +28,7 @@ public struct JSONEncoding: Encoding { public struct JSONDecoding: Decoding { - let decoder: JSONDecoder + private let decoder: JSONDecoder public init(decoder: JSONDecoder = .init()) { self.decoder = decoder From 9681503ec92b350a75d8870dfa8012394e6aeb4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Sat, 4 Jan 2020 21:14:01 +0100 Subject: [PATCH 11/36] Increase Swift tools version --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index b50a279..b7bfca6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.1 import PackageDescription From fb73c54f30b94542647e825c8b36ebaad3bdd101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Wed, 29 Jan 2020 14:47:32 +0100 Subject: [PATCH 12/36] Remove PromiseKit extension --- FTAPIKit.podspec | 20 ++------ Package.swift | 9 ---- .../APIAdapter+PromiseKit.swift | 27 ----------- .../FTAPIKitPromiseKit/Resolver+Result.swift | 20 -------- .../URLSessionAPIAdapter+PromiseKit.swift | 47 ------------------- 5 files changed, 5 insertions(+), 118 deletions(-) delete mode 100644 Sources/FTAPIKitPromiseKit/APIAdapter+PromiseKit.swift delete mode 100644 Sources/FTAPIKitPromiseKit/Resolver+Result.swift delete mode 100644 Sources/FTAPIKitPromiseKit/URLSessionAPIAdapter+PromiseKit.swift diff --git a/FTAPIKit.podspec b/FTAPIKit.podspec index fb41c34..79a260d 100644 --- a/FTAPIKit.podspec +++ b/FTAPIKit.podspec @@ -12,25 +12,15 @@ Pod::Spec.new do |s| s.license = { :type => "MIT", :file => "LICENSE" } s.author = { "Matěj Kašpar Jirásek" => "matej.jirasek@thefuntasty.com" } s.social_media_url = "https://twitter.com/thefuntasty" - s.default_subspec = 'Core' + s.source_files = "Sources/FTAPIKit/*" + s.framework = "Foundation" + s.ios.framework = "MobileCoreServices" + s.tvos.framework = "MobileCoreServices" + s.watchos.framework = "MobileCoreServices" s.swift_version = "5.0" s.ios.deployment_target = "8.0" s.osx.deployment_target = "10.10" s.watchos.deployment_target = "2.0" s.tvos.deployment_target = "9.0" s.source = { :git => "https://github.com/thefuntasty/FTAPIKit.git", :tag => s.version.to_s } - - s.subspec 'Core' do |ss| - ss.source_files = "Sources/FTAPIKit/*" - ss.framework = "Foundation" - ss.ios.framework = "MobileCoreServices" - ss.tvos.framework = "MobileCoreServices" - ss.watchos.framework = "MobileCoreServices" - end - - s.subspec 'PromiseKit' do |ss| - ss.source_files = Dir['Sources/FTAPIKitPromiseKit/*'] - ss.dependency 'PromiseKit', '~> 6.0' - ss.dependency 'FTAPIKit/Core' - end end diff --git a/Package.swift b/Package.swift index b7bfca6..61c4086 100644 --- a/Package.swift +++ b/Package.swift @@ -8,20 +8,11 @@ let package = Package( .library( name: "FTAPIKit", targets: ["FTAPIKit"]), - .library( - name: "FTAPIKitPromiseKit", - targets: ["FTAPIKitPromiseKit"]) - ], - dependencies: [ - .package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.8.4") ], targets: [ .target( name: "FTAPIKit", dependencies: []), - .target( - name: "FTAPIKitPromiseKit", - dependencies: ["FTAPIKit", "PromiseKit"]), .testTarget( name: "FTAPIKitTests", dependencies: ["FTAPIKit"]) diff --git a/Sources/FTAPIKitPromiseKit/APIAdapter+PromiseKit.swift b/Sources/FTAPIKitPromiseKit/APIAdapter+PromiseKit.swift deleted file mode 100644 index 3a2f1f5..0000000 --- a/Sources/FTAPIKitPromiseKit/APIAdapter+PromiseKit.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// APIAdapter+PromiseKit.swift -// FTAPIKit -// -// Created by Matěj Jirásek on 03/01/2019. -// Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved. -// - -import PromiseKit -import Foundation -#if !COCOAPODS -import FTAPIKit -#endif - -extension APIAdapter { - public func request(response endpoint: Endpoint) -> Promise { - let (promise, seal) = Promise.pending() - request(response: endpoint, completion: seal.resolve) - return promise - } - - public func request(data endpoint: Endpoint) -> Promise { - let (promise, seal) = Promise.pending() - request(data: endpoint, completion: seal.resolve) - return promise - } -} diff --git a/Sources/FTAPIKitPromiseKit/Resolver+Result.swift b/Sources/FTAPIKitPromiseKit/Resolver+Result.swift deleted file mode 100644 index fa24a2d..0000000 --- a/Sources/FTAPIKitPromiseKit/Resolver+Result.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Resolver+Result.swift -// FTAPIKit -// -// Created by Matěj Jirásek on 06/03/2019. -// Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved. -// - -import PromiseKit - -extension Resolver { - func resolve(result: Swift.Result) { - switch result { - case .success(let value): - fulfill(value) - case .failure(let error): - reject(error) - } - } -} diff --git a/Sources/FTAPIKitPromiseKit/URLSessionAPIAdapter+PromiseKit.swift b/Sources/FTAPIKitPromiseKit/URLSessionAPIAdapter+PromiseKit.swift deleted file mode 100644 index b7daa1f..0000000 --- a/Sources/FTAPIKitPromiseKit/URLSessionAPIAdapter+PromiseKit.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// URLSessionAPIAdapter+PromiseKit.swift -// FTAPIKit -// -// Created by Matěj Jirásek on 03/01/2019. -// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved. -// - -import PromiseKit -import Foundation -#if !COCOAPODS -import FTAPIKit -#endif - -public struct APIDataTask { - public let sessionTask: Guarantee - public let response: Promise -} - -extension URLSessionAPIAdapter { - public func dataTask(response endpoint: Endpoint) -> APIDataTask { - let task = Guarantee.pending() - let response = Promise.pending() - - dataTask(response: endpoint, creation: task.resolve, completion: { result in - if task.guarantee.isPending { - task.resolve(nil) - } - response.resolver.resolve(result: result) - }) - return APIDataTask(sessionTask: task.guarantee, response: response.promise) - } - - public func dataTask(data endpoint: Endpoint) -> APIDataTask { - let task = Guarantee.pending() - let response = Promise.pending() - - dataTask(data: endpoint, creation: task.resolve, completion: { result in - if task.guarantee.isPending { - task.resolve(nil) - } - - response.resolver.resolve(result: result) - }) - return APIDataTask(sessionTask: task.guarantee, response: response.promise) - } -} From 769271e0ac331382adca96c9dddc951bc28d26f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Wed, 29 Jan 2020 15:46:52 +0100 Subject: [PATCH 13/36] Rewrite request calling, transformation and tests --- Sources/APIEndpoint+URLRequest.swift | 20 - Sources/FTAPIKit/APIAdapter+Types.swift | 62 --- Sources/FTAPIKit/APIError+Standard.swift | 35 ++ Sources/FTAPIKit/APIError.swift | 39 +- Sources/FTAPIKit/AnyEncodable.swift | 21 - Sources/FTAPIKit/Coding.swift | 5 + Sources/FTAPIKit/Endpoint.swift | 50 ++- Sources/FTAPIKit/HTTPMethod.swift | 34 ++ Sources/FTAPIKit/MultipartBodyPart.swift | 6 +- Sources/FTAPIKit/MultipartFormData.swift | 4 +- Sources/FTAPIKit/Serialized.swift | 38 -- Sources/FTAPIKit/Server.swift | 33 +- Sources/FTAPIKit/URL+MIME.swift | 13 + .../{URL+APIAdapter.swift => URL+Query.swift} | 15 - Sources/FTAPIKit/URLRequest+Endpoint.swift | 66 --- Sources/FTAPIKit/URLRequestBuilder.swift | 20 + Sources/FTAPIKit/URLServer+Call.swift | 103 +++++ Sources/FTAPIKit/URLServer.swift | 24 ++ Sources/URLSession+Endpoint.swift | 32 -- Tests/FTAPIKitTests/APIAdapterTests.swift | 395 ------------------ Tests/FTAPIKitTests/Mockups/Endpoints.swift | 61 +++ Tests/FTAPIKitTests/Mockups/Errors.swift | 12 + .../Mockups/MockupAPIAdapterDelegate.swift | 25 -- Tests/FTAPIKitTests/Mockups/Models.swift | 7 + Tests/FTAPIKitTests/Mockups/Servers.swift | 25 ++ Tests/FTAPIKitTests/ResponseTests.swift | 169 ++++++++ Tests/FTAPIKitTests/StressTests.swift | 86 ---- Tests/LinuxMain.swift | 2 +- 28 files changed, 544 insertions(+), 858 deletions(-) delete mode 100644 Sources/APIEndpoint+URLRequest.swift delete mode 100644 Sources/FTAPIKit/APIAdapter+Types.swift create mode 100644 Sources/FTAPIKit/APIError+Standard.swift delete mode 100644 Sources/FTAPIKit/AnyEncodable.swift create mode 100644 Sources/FTAPIKit/HTTPMethod.swift delete mode 100644 Sources/FTAPIKit/Serialized.swift create mode 100644 Sources/FTAPIKit/URL+MIME.swift rename Sources/FTAPIKit/{URL+APIAdapter.swift => URL+Query.swift} (53%) delete mode 100644 Sources/FTAPIKit/URLRequest+Endpoint.swift create mode 100644 Sources/FTAPIKit/URLRequestBuilder.swift create mode 100644 Sources/FTAPIKit/URLServer+Call.swift create mode 100644 Sources/FTAPIKit/URLServer.swift delete mode 100644 Sources/URLSession+Endpoint.swift delete mode 100644 Tests/FTAPIKitTests/APIAdapterTests.swift create mode 100644 Tests/FTAPIKitTests/Mockups/Endpoints.swift create mode 100644 Tests/FTAPIKitTests/Mockups/Errors.swift delete mode 100644 Tests/FTAPIKitTests/Mockups/MockupAPIAdapterDelegate.swift create mode 100644 Tests/FTAPIKitTests/Mockups/Models.swift create mode 100644 Tests/FTAPIKitTests/Mockups/Servers.swift create mode 100644 Tests/FTAPIKitTests/ResponseTests.swift delete mode 100644 Tests/FTAPIKitTests/StressTests.swift diff --git a/Sources/APIEndpoint+URLRequest.swift b/Sources/APIEndpoint+URLRequest.swift deleted file mode 100644 index d3e7b2e..0000000 --- a/Sources/APIEndpoint+URLRequest.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// APIEndpoint+URLRequest.swift -// FTAPIKit -// -// Created by Matěj Kašpar Jirásek on 08/02/2019. -// Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved. -// - -import Foundation - -extension APIEndpoint { - func request(with configuration: APIConfiguration) throws -> URLRequest { - let url = configuration.baseUrl.appendingPathComponent(path) - var request = URLRequest(url: url) - request.httpMethod = method.description - - try request.setRequestType(type, parameters: parameters, using: configuration.encoder) - return request - } -} diff --git a/Sources/FTAPIKit/APIAdapter+Types.swift b/Sources/FTAPIKit/APIAdapter+Types.swift deleted file mode 100644 index e522ca6..0000000 --- a/Sources/FTAPIKit/APIAdapter+Types.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// APIAdapter+Types.swift -// FTAPIKit -// -// Created by Matěj Kašpar Jirásek on 08/02/2018. -// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved. -// - -import Foundation - -/// HTTP method enum with all commonly used verbs. -public enum HTTPMethod: String, CustomStringConvertible { - /// `OPTIONS` HTTP method - case options - /// `GET` HTTP method - case get - /// `HEAD` HTTP method - case head - /// `POST` HTTP method - case post - /// `PUT` HTTP method - case put - /// `PATCH` HTTP method - case patch - /// `DELETE` HTTP method - case delete - /// `TRACE` HTTP method - case trace - /// `CONNECT` HTTP method - case connect - - /// Uppercased HTTP method, used for sending requests. - public var description: String { - return rawValue.uppercased() - } -} - -/// Alias for URL query or URL encoded parameter dictionary. -public typealias HTTPParameters = [String: String] -/// Alias for HTTP header dictionary.CustomStringConvertible -public typealias HTTPHeaders = [String: String] - -/// Type of the API request. JSON body and multipart requests -/// have associated values which are used as a body. The other -/// types only describe how the `HTTPParameters` are encoded. -public enum RequestType { - /// The HTTP parameters will be added to URL as query. - case urlQuery - /// HTTP parameters will be sent as a URL encoded body. - case urlEncoded - /// The parameters will be sent as JSON body. - case jsonParams - /// The encodable model will be serialized and sent as JSON, - /// parameters will be added as URL query. - case jsonBody(Encodable) - /// All the parameters will be sent as multipart - /// and files too. - case multipart([MultipartBodyPart]) - /// The parameters will be encoded using Base64 encoding - /// and sent in request body. - case base64Upload -} diff --git a/Sources/FTAPIKit/APIError+Standard.swift b/Sources/FTAPIKit/APIError+Standard.swift new file mode 100644 index 0000000..8ec373a --- /dev/null +++ b/Sources/FTAPIKit/APIError+Standard.swift @@ -0,0 +1,35 @@ +import Foundation + +/// Standard API error returned in `APIResult` when no custom error +/// was parsed in the `APIAdapter` first and the response from server +/// was invalid. +public enum APIErrorStandard: APIError { + /// Error raised by URLSession. + case connection(URLError) + case encoding(EncodingError) + case decoding(DecodingError) + /// Status code error when the response status code + /// is larger or equal to 500 and less than 600. + case server(Int, URLResponse, Data?) + /// Status code error when the response status code + /// is larger or equal to 400 and less than 500. + case client(Int, URLResponse, Data?) + case unhandled(data: Data?, response: URLResponse?, error: Error?) + + public init?(data: Data?, response: URLResponse?, error: Error?, decoding: Decoding) { + switch (data, response as? HTTPURLResponse, error) { + case let (_, _, error as URLError): + self = .connection(error) + case let (data, response?, nil) where 400..<500 ~= response.statusCode: + self = .client(response.statusCode, response, data) + case let (data, response?, nil) where 500..<600 ~= response.statusCode: + self = .server(response.statusCode, response, data) + case (_, .some, nil), (.some, nil, nil): + return nil + default: + self = .unhandled(data: data, response: response, error: error) + } + } + + public static var unhandled: Standard = .unhandled(data: nil, response: nil, error: nil) +} diff --git a/Sources/FTAPIKit/APIError.swift b/Sources/FTAPIKit/APIError.swift index 003d4c5..cfea5f2 100644 --- a/Sources/FTAPIKit/APIError.swift +++ b/Sources/FTAPIKit/APIError.swift @@ -9,42 +9,9 @@ import Foundation public protocol APIError: Error { - init?(data: Data?, response: URLResponse?, error: Error?, decoding: Decoding) -} + typealias Standard = APIErrorStandard -/// Standard API error returned in `APIResult` when no custom error -/// was parsed in the `APIAdapter` first and the response from server -/// was invalid. -public enum StandardAPIError: APIError { - /// Error raised by NSURLSession corresponding to NSURLErrorCancelled at - /// domain NSURLErrorDomain. - case cancelled - /// Connection error when no response and data was received. - case connection(Error) - /// Status code error when the response status code - /// is larger or equal to 500 and less than 600. - case server(Int, Data?) - /// Status code error when the response status code - /// is larger or equal to 400 and less than 500. - case client(Int, Data?) - /// Multipart body part error, when the stream for the part - /// or the temporary request body stream cannot be opened. - case multipartStreamCannotBeOpened + init?(data: Data?, response: URLResponse?, error: Error?, decoding: Decoding) - public init?(data: Data?, response: URLResponse?, error: Error?, decoding: Decoding) { - switch (data, response as? HTTPURLResponse, error) { - case let (_, _, error as NSError) where error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled: - self = .cancelled - case let (_, _, error?): - self = .connection(error) - case let (data, response?, nil) where 400..<500 ~= response.statusCode: - self = .client(response.statusCode, data) - case let (data, response?, nil) where 500..<600 ~= response.statusCode: - self = .server(response.statusCode, data) - case (_, .some, nil), (.some, nil, nil): - return nil - case (nil, nil, nil): - fatalError("No response, data or error was returned from URLSession") - } - } + static var unhandled: Self { get } } diff --git a/Sources/FTAPIKit/AnyEncodable.swift b/Sources/FTAPIKit/AnyEncodable.swift deleted file mode 100644 index e9c0069..0000000 --- a/Sources/FTAPIKit/AnyEncodable.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// AnyEncodable.swift -// FTAPIKit -// -// Created by Patrik Potoček on 27.3.18. -// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved. -// - -struct AnyEncodable: Encodable { - private let anyEncode: (Encoder) throws -> Void - - init(_ encodable: Encodable) { - anyEncode = { encoder in - try encodable.encode(to: encoder) - } - } - - func encode(to encoder: Encoder) throws { - try anyEncode(encoder) - } -} diff --git a/Sources/FTAPIKit/Coding.swift b/Sources/FTAPIKit/Coding.swift index c006144..f3a70af 100644 --- a/Sources/FTAPIKit/Coding.swift +++ b/Sources/FTAPIKit/Coding.swift @@ -2,6 +2,7 @@ import Foundation public protocol Encoding { func encode(_ object: T) throws -> Data + func configure(request: inout URLRequest) throws } public protocol Decoding { @@ -24,6 +25,10 @@ public struct JSONEncoding: Encoding { public func encode(_ object: T) throws -> Data { try encoder.encode(object) } + + public func configure(request: inout URLRequest) throws { + request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") + } } diff --git a/Sources/FTAPIKit/Endpoint.swift b/Sources/FTAPIKit/Endpoint.swift index e277de3..7505de6 100644 --- a/Sources/FTAPIKit/Endpoint.swift +++ b/Sources/FTAPIKit/Endpoint.swift @@ -1,3 +1,5 @@ +import struct Foundation.Data + /// Protocol describing API endpoint. API Endpoint describes one URI with all the /// data and parameters which are sent to it. /// @@ -13,44 +15,42 @@ public protocol Endpoint { /// URL path component without base URI. var path: String { get } - /// URL parameters is string dictionary sent either as URL query, multipart, - /// JSON parameters or URL/Base64 encoded body. The `type` parameter of `APIEndpoint` - /// protocol describes the way how to send the parameters. - var parameters: HTTPParameters { get } + var headers: [String: String] { get } + + var query: [String: String] { get } /// HTTP method/verb describing the action. var method: HTTPMethod { get } - /// Type of the request describing how the parameters and data should be encoded and - /// sent to the server. If additional data (not only parameters) are sent, then they - /// are returned as an associated value of the type. - var type: RequestType { get } - - /// Boolean marking whether the endpoint should be signed and authorization is required. - /// - /// This value is not used inside the framework. This value should be checked and handled - /// accordingly when signing using the `APIAdapterDelegate`. - var authorized: Bool { get } + func body(encoding: Encoding) throws -> Data? } public extension Endpoint { - var parameters: HTTPParameters { + var headers: [String: String] { return [:] } - var type: RequestType { - return .jsonParams + var query: [String: String] { + return [:] } var method: HTTPMethod { return .get } - var authorized: Bool { - return false + func body(encoding: Encoding) throws -> Data? { + return nil } } +public protocol DataEndpoint: Endpoint { + var data: Data? { get } +} + +public extension DataEndpoint { + func body(encoding: Encoding) throws -> Data? { data } +} + /// Endpoint protocol extending `Endpoint` having decodable associated type, which is used /// for automatic deserialization. public protocol ResponseEndpoint: Endpoint { @@ -65,18 +65,16 @@ public protocol RequestEndpoint: Endpoint { /// Associated type describing the encodable request model for /// JSON serialization. The associated type is derived from /// the body property. - associatedtype Request: Encodable + associatedtype Parameters: Encodable /// Generic encodable model, which will be sent as JSON body. - var body: Request { get } + var parameters: Parameters { get } } public extension RequestEndpoint { - var method: HTTPMethod { - return .post - } + var method: HTTPMethod { .post } - var type: RequestType { - return RequestType.jsonBody(body) + func body(encoding: Encoding) throws -> Data? { + try encoding.encode(parameters) } } diff --git a/Sources/FTAPIKit/HTTPMethod.swift b/Sources/FTAPIKit/HTTPMethod.swift new file mode 100644 index 0000000..81ade44 --- /dev/null +++ b/Sources/FTAPIKit/HTTPMethod.swift @@ -0,0 +1,34 @@ +// +// APIAdapter+Types.swift +// FTAPIKit +// +// Created by Matěj Kašpar Jirásek on 08/02/2018. +// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved. +// + +/// HTTP method enum with all commonly used verbs. +public enum HTTPMethod: String, CustomStringConvertible { + /// `OPTIONS` HTTP method + case options + /// `GET` HTTP method + case get + /// `HEAD` HTTP method + case head + /// `POST` HTTP method + case post + /// `PUT` HTTP method + case put + /// `PATCH` HTTP method + case patch + /// `DELETE` HTTP method + case delete + /// `TRACE` HTTP method + case trace + /// `CONNECT` HTTP method + case connect + + /// Uppercased HTTP method, used for sending requests. + public var description: String { + return rawValue.uppercased() + } +} diff --git a/Sources/FTAPIKit/MultipartBodyPart.swift b/Sources/FTAPIKit/MultipartBodyPart.swift index fb868b0..00f023a 100644 --- a/Sources/FTAPIKit/MultipartBodyPart.swift +++ b/Sources/FTAPIKit/MultipartBodyPart.swift @@ -6,9 +6,7 @@ // Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved. // -import struct Foundation.Data -import struct Foundation.URL -import class Foundation.InputStream +import Foundation /// Structure representing part in `multipart/form-data` request. /// These parts must have valid headers according @@ -59,7 +57,7 @@ public struct MultipartBodyPart: Hashable { /// - Throws: `APIError.multipartStreamCannotBeOpened` if stream was not created from the file. public init(name: String, url: URL) throws { guard let inputStream = InputStream(url: url) else { - throw StandardAPIError.multipartStreamCannotBeOpened + throw URLError(.cannotOpenFile, userInfo: ["url": url]) } self.headers = [ "Content-Type": url.mimeType, diff --git a/Sources/FTAPIKit/MultipartFormData.swift b/Sources/FTAPIKit/MultipartFormData.swift index b947570..e9fd18f 100644 --- a/Sources/FTAPIKit/MultipartFormData.swift +++ b/Sources/FTAPIKit/MultipartFormData.swift @@ -35,14 +35,14 @@ struct MultipartFormData { func inputStream() throws -> InputStream { try outputStream() guard let inputStream = InputStream(url: temporaryUrl) else { - throw StandardAPIError.multipartStreamCannotBeOpened + throw URLError(.cannotOpenFile, userInfo: ["url": temporaryUrl]) } return inputStream } private func outputStream() throws { guard let outputStream = OutputStream(url: temporaryUrl, append: false) else { - throw StandardAPIError.multipartStreamCannotBeOpened + throw URLError(.cannotOpenFile, userInfo: ["url": temporaryUrl]) } outputStream.open() defer { diff --git a/Sources/FTAPIKit/Serialized.swift b/Sources/FTAPIKit/Serialized.swift deleted file mode 100644 index 005dcd0..0000000 --- a/Sources/FTAPIKit/Serialized.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// Serialized.swift -// FTAPIKit-iOS -// -// Created by Patrik Potoček on 27/06/2019. -// Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved. -// - -import Foundation - -/// This class provides user with easy way to serialize access to a property in multiplatform environment. This class is written with future PropertyWrapper feature of swift in mind. -final class Serialized { - - /// Synchronization queue for the property. Read or write to the property must be perforimed on this queue - private let queue = DispatchQueue(label: "com.thefuntasty.ftapikit.serialization") - - /// The value itself with did-set observing. - private var value: Value { - didSet { - didSet?(value) - } - } - - /// Did set observer for stored property. Notice, that didSet event is called on the synchronization queue. You should free this thread asap with async call, since complex operations would slow down sync access to the property. - var didSet: ((Value) -> Void)? - - /// Inserting initial value to the property. Notice, that this operation is NOT DONE on the synchronization queue. - init(initialValue: Value) { - value = initialValue - } - - /// It is enouraged to use this method to make more complex operations with the stored property, like read-and-write. Do not perform any time-demading operations in this block since it will stop other uses of the stored property. - func asyncAccess(transform: @escaping (Value) -> Value) { - queue.async { - self.value = transform(self.value) - } - } -} diff --git a/Sources/FTAPIKit/Server.swift b/Sources/FTAPIKit/Server.swift index 67752ac..e4ae712 100644 --- a/Sources/FTAPIKit/Server.swift +++ b/Sources/FTAPIKit/Server.swift @@ -1,34 +1,9 @@ -import Foundation -public protocol ReadonlyServer { - associatedtype APIError: Error +public protocol Server { + associatedtype E: APIError = APIError.Standard + associatedtype Request - var baseUri: URL { get } - var urlSession: URLSession { get } var decoding: Decoding { get } - var requestConfiguration: (inout URLRequest) -> Void { get } -} - -public protocol Server: ReadonlyServer { var encoding: Encoding { get } -} - -public extension ReadonlyServer { - var urlSession: URLSession { - .shared - } - - var decoding: Decoding { - JSONDecoding() - } - - var requestConfiguration: (inout URLRequest) -> Void { - { _ in } - } -} - -public extension Server { - var encoding: Encoding { - JSONEncoding() - } + var configureRequest: (inout Request, Endpoint) throws -> Void { get } } diff --git a/Sources/FTAPIKit/URL+MIME.swift b/Sources/FTAPIKit/URL+MIME.swift new file mode 100644 index 0000000..5498cfd --- /dev/null +++ b/Sources/FTAPIKit/URL+MIME.swift @@ -0,0 +1,13 @@ +import Foundation +#if os(iOS) || os(watchOS) || os(tvOS) +import MobileCoreServices +#endif + +extension URL { + var mimeType: String { + if let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { + return contentType as String + } + return "application/octet-stream" + } +} diff --git a/Sources/FTAPIKit/URL+APIAdapter.swift b/Sources/FTAPIKit/URL+Query.swift similarity index 53% rename from Sources/FTAPIKit/URL+APIAdapter.swift rename to Sources/FTAPIKit/URL+Query.swift index d863489..0a68dd0 100644 --- a/Sources/FTAPIKit/URL+APIAdapter.swift +++ b/Sources/FTAPIKit/URL+Query.swift @@ -1,11 +1,3 @@ -// -// Data+APIAdapter.swift -// FTAPIKit-iOS -// -// Created by Matěj Kašpar Jirásek on 03/09/2018. -// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved. -// - import Foundation #if os(iOS) || os(watchOS) || os(tvOS) import MobileCoreServices @@ -25,11 +17,4 @@ extension URL { components?.queryItems = oldItems + parameters.map(URLQueryItem.init) return components?.url ?? self } - - var mimeType: String { - if let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { - return contentType as String - } - return "application/octet-stream" - } } diff --git a/Sources/FTAPIKit/URLRequest+Endpoint.swift b/Sources/FTAPIKit/URLRequest+Endpoint.swift deleted file mode 100644 index 183a774..0000000 --- a/Sources/FTAPIKit/URLRequest+Endpoint.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// URLRequest+Endpoint.swift -// FTAPIKit -// -// Created by Matěj Kašpar Jirásek on 02/09/2018. -// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved. -// - -import Foundation - -extension URLRequest { - mutating func setRequestType(_ requestType: RequestType, parameters: HTTPParameters, using encoding: Encoding) throws { - switch requestType { - case .jsonBody(let encodable): - try setJSONBody(encodable: encodable, parameters: parameters, using: encoding) - case .urlEncoded: - setURLEncoded(parameters: parameters) - case .jsonParams: - setJSON(parameters: parameters, using: encoding) - case let .multipart(files): - try setMultipart(parameters: parameters, files: files) - case .base64Upload: - appendBase64(parameters: parameters) - case .urlQuery: - url?.appendQuery(parameters: parameters) - } - } - - private mutating func appendBase64(parameters: HTTPParameters) { - var urlComponents = URLComponents() - urlComponents.queryItems = parameters.map(URLQueryItem.init) - httpBody = urlComponents.query?.data(using: String.Encoding.ascii, allowLossyConversion: true) - setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - } - - private mutating func setMultipart(parameters: HTTPParameters = [:], files: [MultipartBodyPart] = [], boundary: String = "FTAPIKit-" + UUID().uuidString) throws { - - let parameterParts = parameters.map(MultipartBodyPart.init) - let multipartData = MultipartFormData(parts: parameterParts + files, boundary: "--" + boundary) - - httpBodyStream = try multipartData.inputStream() - - setValue("multipart/form-data; charset=utf-8; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - if let contentLength = multipartData.contentLength { - setValue(contentLength.description, forHTTPHeaderField: "Content-Length") - } - } - - private mutating func setJSON(parameters: HTTPParameters, body: Data? = nil, using encoding: Encoding) { - setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") - httpBody = body - url?.appendQuery(parameters: parameters) - } - - private mutating func setURLEncoded(parameters: HTTPParameters) { - var urlComponents = URLComponents() - urlComponents.queryItems = parameters.map(URLQueryItem.init) - httpBody = urlComponents.query?.data(using: .ascii, allowLossyConversion: true) - setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - } - - private mutating func setJSONBody(encodable: Encodable, parameters: HTTPParameters, using encoding: Encoding) throws { - let body = try encoding.encode(AnyEncodable(encodable)) - setJSON(parameters: parameters, body: body, using: encoding) - } -} diff --git a/Sources/FTAPIKit/URLRequestBuilder.swift b/Sources/FTAPIKit/URLRequestBuilder.swift new file mode 100644 index 0000000..e523657 --- /dev/null +++ b/Sources/FTAPIKit/URLRequestBuilder.swift @@ -0,0 +1,20 @@ +import Foundation + +struct URLRequestBuilder { + let server: S + let endpoint: Endpoint + + func build() throws -> URLRequest { + let url = server.baseUri + .appendingPathComponent(endpoint.path) + .appendingQuery(parameters: endpoint.query) + var request = URLRequest(url: url) + + request.httpMethod = endpoint.method.description + request.allHTTPHeaderFields = endpoint.headers + request.httpBody = try endpoint.body(encoding: server.encoding) + try server.encoding.configure(request: &request) + try server.configureRequest(&request, endpoint) + return request + } +} diff --git a/Sources/FTAPIKit/URLServer+Call.swift b/Sources/FTAPIKit/URLServer+Call.swift new file mode 100644 index 0000000..fcf8fae --- /dev/null +++ b/Sources/FTAPIKit/URLServer+Call.swift @@ -0,0 +1,103 @@ +import Foundation + +extension URLServer { + @discardableResult + public func call(request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + task(request: request, process: { data, response, error in + if let error = E(data: data, response: response, error: error, decoding: self.decoding) { + return .failure(error) + } + return .success(()) + }, completion: completion) + } + + @discardableResult + public func call(data request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + task(request: request, process: { data, response, error in + if let error = E(data: data, response: response, error: error, decoding: self.decoding) { + return .failure(error) + } else if let data = data { + return .success(data) + } + return .failure(.unhandled) + }, completion: completion) + } + + @discardableResult + public func call(response request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + task(request: request, process: { data, response, error in + if let error = E(data: data, response: response, error: error, decoding: self.decoding) { + return .failure(error) + } else if let data = data { + do { + let response: R = try self.decoding.decode(data: data) + return .success(response) + } catch { + return self.apiError(error: error) + } + } + return .failure(.unhandled) + }, completion: completion) + } + + @discardableResult + public func call(endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + switch request(endpoint: endpoint) { + case .success(let request): + return call(request: request, completion: completion) + case .failure(let error): + completion(.failure(error)) + return nil + } + } + + + @discardableResult + public func call(data endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + switch request(endpoint: endpoint) { + case .success(let request): + return call(data: request, completion: completion) + case .failure(let error): + completion(.failure(error)) + return nil + } + } + + @discardableResult + public func call(response endpoint: EP, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + switch request(endpoint: endpoint) { + case .success(let request): + return call(response: request, completion: completion) + case .failure(let error): + completion(.failure(error)) + return nil + } + } + + func task( + request: URLRequest, + process: @escaping (Data?, URLResponse?, Error?) -> Result, + completion: @escaping (Result) -> Void + ) -> URLSessionDataTask? { + let task = urlSession.dataTask(with: request) { data, response, error in + completion(process(data, response, error)) + } + task.resume() + return task + } + + func request(endpoint: Endpoint) -> Result { + do { + let builder = URLRequestBuilder(server: self, endpoint: endpoint) + let request = try builder.build() + return .success(request) + } catch { + return apiError(error: error) + } + } + + func apiError(error: Error?) -> Result { + let error = E(data: nil, response: nil, error: error, decoding: decoding) ?? .unhandled + return .failure(error) + } +} diff --git a/Sources/FTAPIKit/URLServer.swift b/Sources/FTAPIKit/URLServer.swift new file mode 100644 index 0000000..ed8340a --- /dev/null +++ b/Sources/FTAPIKit/URLServer.swift @@ -0,0 +1,24 @@ +import Foundation + +public protocol URLServer: Server where Request == URLRequest { + var baseUri: URL { get } + var urlSession: URLSession { get } +} + +public extension URLServer { + var urlSession: URLSession { + .shared + } + + var decoding: Decoding { + JSONDecoding() + } + + var encoding: Encoding { + JSONEncoding() + } + + var configureRequest: (inout URLRequest, Endpoint) throws -> Void { + { _, _ in } + } +} diff --git a/Sources/URLSession+Endpoint.swift b/Sources/URLSession+Endpoint.swift deleted file mode 100644 index 3633fc3..0000000 --- a/Sources/URLSession+Endpoint.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// URSession+Endpoint.swift -// FTAPIKit -// -// Created by Matěj Kašpar Jirásek on 08/02/2019. -// Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved. -// - -import Foundation - -public extension URLSession { - func dataTask(to endpoint: APIEndpoint, with configuration: APIConfiguration) throws -> URLSessionDataTask { - let request = try endpoint.request(with: configuration) - return dataTask(with: request) - } - - func dataTask(to endpoint: APIEndpoint, with configuration: APIConfiguration, completion: @escaping (APIResult) -> Void) throws -> URLSessionDataTask { - let request = try endpoint.request(with: configuration) - return dataTask(with: request) { (data, response, error) in - if let error = configuration.apiErrorType.init(data: data, response: response, error: error, decoder: configuration.decoder) { - completion(.error(error)) - } else { - completion(.value(data ?? Data())) - } - } - } - - func downloadTask(to endpoint: APIEndpoint, with configuration: APIConfiguration) throws -> URLSessionDownloadTask { - let request = try endpoint.request(with: configuration) - return downloadTask(with: request) - } -} diff --git a/Tests/FTAPIKitTests/APIAdapterTests.swift b/Tests/FTAPIKitTests/APIAdapterTests.swift deleted file mode 100644 index d23942f..0000000 --- a/Tests/FTAPIKitTests/APIAdapterTests.swift +++ /dev/null @@ -1,395 +0,0 @@ -// -// APIAdapterTests.swift -// FTAPIKit -// -// Created by Matěj Kašpar Jirásek on 03/09/2018. -// Copyright © 2018 The Funtasty. All rights reserved. -// - -// swiftlint:disable nesting - -import XCTest -@testable import FTAPIKit - -final class APIAdapterTests: XCTestCase { - - private func apiAdapter() -> URLSessionAPIAdapter { - return URLSessionAPIAdapter(baseUrl: URL(string: "http://httpbin.org/")!) - } - - private let timeout: TimeInterval = 30.0 - - func testGet() { - struct TestEndpoint: Endpoint { - let path = "get" - } - - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = apiAdapter() - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.request(data: TestEndpoint()) { result in - if case let .failure(error) = result { - XCTFail(error.localizedDescription) - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testClientError() { - struct TestEndpoint: Endpoint { - let path = "status/404" - } - - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = apiAdapter() - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.request(data: TestEndpoint()) { result in - switch result { - case .success: - XCTFail("404 endpoint must return error") - case .failure(StandardAPIError.client): - XCTAssert(true) - case .failure: - XCTFail("404 endpoint must return client error") - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testServerError() { - struct TestEndpoint: Endpoint { - let path = "status/500" - } - - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = apiAdapter() - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.request(data: TestEndpoint()) { result in - switch result { - case .success: - XCTFail("500 endpoint must return error") - case .failure(StandardAPIError.server): - XCTAssert(true) - case .failure: - XCTFail("500 endpoint must return server error") - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testConnectionError() { - struct TestEndpoint: Endpoint { - let path = "some-failing-path" - } - - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = URLSessionAPIAdapter(baseUrl: URL(string: "https://www.tato-stranka-urcite-neexistuje.cz/")!) - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.request(data: TestEndpoint()) { result in - switch result { - case .success: - XCTFail("Non-existing domain must fail") - case .failure(StandardAPIError.connection): - XCTAssert(true) - case .failure: - XCTFail("Non-existing domain must throw connection error") - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testEmptyResult() { - struct TestEndpoint: Endpoint { - let path = "status/204" - } - - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = apiAdapter() - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.request(data: TestEndpoint()) { result in - if case let .failure(error) = result { - XCTFail(error.localizedDescription) - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testCustomError() { - struct TestEndpoint: Endpoint { - let path = "get" - } - - struct CustomError: APIError { - private init() {} - - init?(data: Data?, response: URLResponse?, error: Error?, decoder: JSONDecoder) { - self = CustomError() - } - } - - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = URLSessionAPIAdapter(baseUrl: URL(string: "http://httpbin.org/")!, errorType: CustomError.self) - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.request(data: TestEndpoint()) { result in - if case let .failure(error) = result { - XCTAssertTrue(error is CustomError) - } else { - XCTFail("Custom error must be returned") - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testURLEncodedPost() { - struct TestEndpoint: Endpoint { - let data: RequestType = .urlEncoded - let parameters: HTTPParameters = [ - "someParameter": "someValue", - "anotherParameter": "anotherValue" - ] - let path = "post" - let method: HTTPMethod = .post - } - - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = apiAdapter() - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.request(data: TestEndpoint()) { result in - if case let .failure(error) = result { - XCTFail(error.localizedDescription) - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testValidJSONResponse() { - struct TopLevel: Codable { - let slideshow: Slideshow - } - - struct Slideshow: Codable { - let author, date: String - let slides: [Slide] - let title: String - } - - struct Slide: Codable { - let title, type: String - let items: [String]? - } - - struct TestEndpoint: ResponseEndpoint { - typealias Response = TopLevel - - let path = "json" - } - - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = apiAdapter() - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.request(response: TestEndpoint()) { result in - if case let .failure(error) = result { - XCTFail(error.localizedDescription) - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testTaskCancellation() { - struct TopLevel: Codable { - let slideshow: Slideshow - } - - struct Slideshow: Codable { - let author, date: String - let slides: [Slide] - let title: String - } - - struct Slide: Codable { - let title, type: String - let items: [String]? - } - - struct TestEndpoint: ResponseEndpoint { - typealias Response = TopLevel - - let path = "json" - } - - let delegate = MockupAPIAdapterDelegate() - let adapter = apiAdapter() - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.dataTask(response: TestEndpoint(), creation: { $0.cancel() }, completion: { result in - if case .failure(StandardAPIError.cancelled) = result { - XCTAssert(true) - } else { - XCTFail("Task not cancelled") - } - expectation.fulfill() - }) - wait(for: [expectation], timeout: timeout) - } - - func testValidJSONRequestResponse() { - struct User: Codable, Equatable { - let uuid: UUID - let name: String - let age: UInt - } - - struct TopLevel: Decodable { - let json: User - } - - struct TestEndpoint: RequestResponseEndpoint { - - typealias Response = TopLevel - - let body: User - let path = "anything" - } - - let user = User(uuid: UUID(), name: "Some Name", age: .random(in: 0...120)) - let endpoint = TestEndpoint(body: user) - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = apiAdapter() - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.request(response: endpoint) { result in - switch result { - case .success(let response): - XCTAssertEqual(user, response.json) - case .failure(let error): - XCTFail(error.localizedDescription) - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testInvalidJSONRequestResponse() { - struct User: Codable, Equatable { - let uuid: UUID - let name: String - let age: UInt - } - - struct TestEndpoint: RequestResponseEndpoint { - typealias Response = User - - let body: User - let path = "anything" - } - - let user = User(uuid: UUID(), name: "Some Name", age: .random(in: 0...120)) - let endpoint = TestEndpoint(body: user) - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = apiAdapter() - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.request(response: endpoint) { result in - if case .success = result { - XCTFail("Received valid value, decoding must fail") - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testAuthorization() { - struct TestEndpoint: Endpoint { - let path = "bearer" - let authorized = true - } - - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = apiAdapter() - adapter.delegate = delegate - - let expectation = self.expectation(description: "Result") - adapter.request(data: TestEndpoint()) { result in - if case let .failure(error) = result { - XCTFail(error.localizedDescription) - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testMultipartData() { - struct MockupFile { - let url: URL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("\(UUID()).txt") - let data = Data(repeating: UInt8(ascii: "a"), count: 1024 * 1024) - let headers: [String: String] = [ - "Content-Disposition": "form-data; name=jpegFile", - "Content-Type": "image/jpeg" - ] - } - - struct TestEndpoint: Endpoint { - let file: MockupFile - - var type: RequestType { - return .multipart([ - MultipartBodyPart(name: "anotherParameter", value: "valueForParameter"), - try! MultipartBodyPart(name: "urlImage", url: file.url), - MultipartBodyPart(headers: file.headers, data: file.data), - MultipartBodyPart(headers: file.headers, inputStream: InputStream(url: file.url)!) - ]) - } - let parameters: HTTPParameters = [ - "someParameter": "someValue" - ] - let path = "post" - let method: HTTPMethod = .post - } - - let file = MockupFile() - try! file.data.write(to: file.url) - - let delegate = MockupAPIAdapterDelegate() - var adapter: APIAdapter = apiAdapter() - adapter.delegate = delegate - let expectation = self.expectation(description: "Result") - adapter.request(data: TestEndpoint(file: file)) { result in - if case let .failure(error) = result { - XCTFail(error.localizedDescription) - } - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - static var allTests = [ - ("testGet", testGet), - ("testClientError", testClientError), - ("testServerError", testServerError), - ("testConnectionError", testConnectionError), - ("testEmptyResult", testEmptyResult), - ("testCustomError", testCustomError), - ("testURLEncodedPost", testURLEncodedPost), - ("testValidJSONResponse", testValidJSONResponse), - ("testValidJSONRequestResponse", testValidJSONRequestResponse), - ("testInvalidJSONRequestResponse", testInvalidJSONRequestResponse), - ("testAuthorization", testAuthorization), - ("testMultipartData", testMultipartData) - ] -} diff --git a/Tests/FTAPIKitTests/Mockups/Endpoints.swift b/Tests/FTAPIKitTests/Mockups/Endpoints.swift new file mode 100644 index 0000000..b78cf9a --- /dev/null +++ b/Tests/FTAPIKitTests/Mockups/Endpoints.swift @@ -0,0 +1,61 @@ +import FTAPIKit +import Foundation + +struct GetEndpoint: Endpoint { + let path = "get" +} + +struct NoContentEndpoint: Endpoint { + let path = "status/204" +} + +struct NotFoundEndpoint: Endpoint { + let path = "status/404" +} + +struct AuthorizedEndpoint: Endpoint { + let path = "bearer" +} + +struct ServerErrorEndpoint: Endpoint { + let path = "status/500" +} + +struct JSONResponseEndpoint: ResponseEndpoint { + typealias Response = TopLevel + + let path = "json" + + struct TopLevel: Decodable { + let slideshow: Slideshow + } + + struct Slideshow: Decodable { + let author, date: String + let slides: [Slide] + let title: String + } + + struct Slide: Decodable { + let title, type: String + let items: [String]? + } +} + +struct UpdateUserEndpoint: RequestResponseEndpoint { + typealias Response = Wrapper + + let parameters: User + let path = "anything" + + struct Wrapper: Decodable { + let json: User + } +} + +struct FailingUpdateUserEndpoint: RequestResponseEndpoint { + typealias Response = User + + let parameters: User + let path = "anything" +} diff --git a/Tests/FTAPIKitTests/Mockups/Errors.swift b/Tests/FTAPIKitTests/Mockups/Errors.swift new file mode 100644 index 0000000..0d329b2 --- /dev/null +++ b/Tests/FTAPIKitTests/Mockups/Errors.swift @@ -0,0 +1,12 @@ +import FTAPIKit +import Foundation + +struct ThrowawayAPIError: APIError { + private init() {} + + init?(data: Data?, response: URLResponse?, error: Error?, decoding: Decoding) { + self.init() + } + + static var unhandled = Self.init() +} diff --git a/Tests/FTAPIKitTests/Mockups/MockupAPIAdapterDelegate.swift b/Tests/FTAPIKitTests/Mockups/MockupAPIAdapterDelegate.swift deleted file mode 100644 index 43db405..0000000 --- a/Tests/FTAPIKitTests/Mockups/MockupAPIAdapterDelegate.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// MockupAPIAdapterDelegate.swift -// FTAPIKit-iOS -// -// Created by Matěj Kašpar Jirásek on 03/09/2018. -// Copyright © 2018 The Funtasty. All rights reserved. -// - -import FTAPIKit -import Foundation - -final class MockupAPIAdapterDelegate: APIAdapterDelegate { - func apiAdapter(_ apiAdapter: APIAdapter, willRequest request: URLRequest, to endpoint: Endpoint, completion: @escaping (Result) -> Void) { - if endpoint.authorized { - var newRequest = request - newRequest.addValue("Bearer " + UUID().uuidString, forHTTPHeaderField: "Authorization") - completion(.success(newRequest)) - } else { - completion(.success(request)) - } - } - - func apiAdapter(_ apiAdapter: APIAdapter, didUpdateRunningRequestCount runningRequestCount: UInt) { - } -} diff --git a/Tests/FTAPIKitTests/Mockups/Models.swift b/Tests/FTAPIKitTests/Mockups/Models.swift new file mode 100644 index 0000000..fe5b5b2 --- /dev/null +++ b/Tests/FTAPIKitTests/Mockups/Models.swift @@ -0,0 +1,7 @@ +import Foundation + +struct User: Codable, Equatable { + let uuid: UUID + let name: String + let age: UInt +} diff --git a/Tests/FTAPIKitTests/Mockups/Servers.swift b/Tests/FTAPIKitTests/Mockups/Servers.swift new file mode 100644 index 0000000..b1afd46 --- /dev/null +++ b/Tests/FTAPIKitTests/Mockups/Servers.swift @@ -0,0 +1,25 @@ +import FTAPIKit +import Foundation + +struct HTTPBinServer: URLServer { + let urlSession = URLSession(configuration: .ephemeral) + let baseUri = URL(string: "http://httpbin.org/")! + let configureRequest: (inout URLRequest, Endpoint) throws -> Void = { request, endpoint in + if endpoint is AuthorizedEndpoint { + request.addValue("Bearer \(UUID().uuidString)", forHTTPHeaderField: "Authorization") + } + print(request) + } +} + +struct NonExistingServer: URLServer { + let urlSession = URLSession(configuration: .ephemeral) + let baseUri = URL(string: "https://www.tato-stranka-urcite-neexistuje.cz/")! +} + +struct ErrorThrowingServer: URLServer { + typealias E = ThrowawayAPIError + + let urlSession = URLSession(configuration: .ephemeral) + let baseUri = URL(string: "http://httpbin.org/")! +} diff --git a/Tests/FTAPIKitTests/ResponseTests.swift b/Tests/FTAPIKitTests/ResponseTests.swift new file mode 100644 index 0000000..edee004 --- /dev/null +++ b/Tests/FTAPIKitTests/ResponseTests.swift @@ -0,0 +1,169 @@ +import XCTest +@testable import FTAPIKit + +final class ResponseTests: XCTestCase { + private let timeout: TimeInterval = 30.0 + + func testGet() { + let server = HTTPBinServer() + let endpoint = GetEndpoint() + let expectation = self.expectation(description: "Result") + server.call(endpoint: endpoint) { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + func testClientError() { + let server = HTTPBinServer() + let endpoint = NotFoundEndpoint() + let expectation = self.expectation(description: "Result") + server.call(endpoint: endpoint) { result in + switch result { + case .success: + XCTFail("404 endpoint must return error") + case .failure(.client): + XCTAssert(true) + case .failure: + XCTFail("404 endpoint must return client error") + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + func testServerError() { + let server = HTTPBinServer() + let endpoint = ServerErrorEndpoint() + let expectation = self.expectation(description: "Result") + server.call(endpoint: endpoint) { result in + switch result { + case .success: + XCTFail("500 endpoint must return error") + case .failure(.server): + XCTAssert(true) + case .failure: + XCTFail("500 endpoint must return server error") + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + func testConnectionError() { + let server = NonExistingServer() + let endpoint = NotFoundEndpoint() + let expectation = self.expectation(description: "Result") + server.call(endpoint: endpoint) { result in + switch result { + case .success: + XCTFail("Non-existing domain must fail") + case .failure(.connection): + XCTAssert(true) + case .failure: + XCTFail("Non-existing domain must throw connection error") + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + func testEmptyResult() { + let server = HTTPBinServer() + let endpoint = NoContentEndpoint() + let expectation = self.expectation(description: "Result") + server.call(endpoint: endpoint) { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + func testCustomError() { + let server = ErrorThrowingServer() + let endpoint = GetEndpoint() + let expectation = self.expectation(description: "Result") + server.call(endpoint: endpoint) { result in + if case .success = result { + XCTFail("Custom error must be returned") + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + func testValidJSONResponse() { + let server = HTTPBinServer() + let endpoint = JSONResponseEndpoint() + let expectation = self.expectation(description: "Result") + server.call(response: endpoint) { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + func testValidJSONRequestResponse() { + let server = HTTPBinServer() + let user = User(uuid: UUID(), name: "Some Name", age: .random(in: 0...120)) + let endpoint = UpdateUserEndpoint(parameters: user) + let expectation = self.expectation(description: "Result") + server.call(response: endpoint) { result in + switch result { + case .success(let response): + XCTAssertEqual(user, response.json) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + func testInvalidJSONRequestResponse() { + let server = HTTPBinServer() + let user = User(uuid: UUID(), name: "Some Name", age: .random(in: 0...120)) + let endpoint = FailingUpdateUserEndpoint(parameters: user) + let expectation = self.expectation(description: "Result") + server.call(response: endpoint) { result in + if case .success = result { + XCTFail("Received valid value, decoding must fail") + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + func testAuthorization() { + let server = HTTPBinServer() + let endpoint = AuthorizedEndpoint() + let expectation = self.expectation(description: "Result") + server.call(endpoint: endpoint) { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + + static var allTests = [ + ("testGet", testGet), + ("testClientError", testClientError), + ("testServerError", testServerError), + ("testConnectionError", testConnectionError), + ("testEmptyResult", testEmptyResult), + ("testCustomError", testCustomError), + ("testValidJSONResponse", testValidJSONResponse), + ("testValidJSONRequestResponse", testValidJSONRequestResponse), + ("testInvalidJSONRequestResponse", testInvalidJSONRequestResponse), + ("testAuthorization", testAuthorization), + ] +} diff --git a/Tests/FTAPIKitTests/StressTests.swift b/Tests/FTAPIKitTests/StressTests.swift deleted file mode 100644 index b54bb21..0000000 --- a/Tests/FTAPIKitTests/StressTests.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// FTAPIKit_iOS_Stress_Tests.swift -// FTAPIKit-iOS Stress Tests -// -// Created by Mikoláš Stuchlík on 24/06/2019. -// Copyright © 2019 FUNTASTY Digital s.r.o. All rights reserved. -// - -import XCTest -@testable import FTAPIKit - -final class StressTests: XCTestCase { - - private func apiAdapter() -> URLSessionAPIAdapter { - return URLSessionAPIAdapter(baseUrl: URL(string: "http://httpbin.org/")!) - } - - private let extendedTimeout: TimeInterval = 120.0 - - func testStressMultipleRequestsViaGet() { - struct Endpoint: Endpoint { - let path = "get" - } - - let adapter: APIAdapter = apiAdapter() - let expectation = self.expectation(description: "Result") - - let testingRange = 0...9 - let testingRequests = testingRange.count * 4 - - let counter: Serialized = Serialized(initialValue: 0) - counter.didSet = { count in - if testingRequests > count { - //nop - } else if testingRequests == count { - expectation.fulfill() - } else if testingRequests < count { - print(testingRequests) - print(count) - XCTFail("Number of responses exceeded number of requests") - } - } - - for _ in testingRange { - DispatchQueue.global(qos: .background).async { - adapter.request(data: Endpoint()) { result in - if case let .failure(error) = result { - XCTFail(error.localizedDescription) - } - counter.asyncAccess { $0 + 1 } - } - } - DispatchQueue.global(qos: .userInitiated).async { - adapter.request(data: Endpoint()) { result in - if case let .failure(error) = result { - XCTFail(error.localizedDescription) - } - counter.asyncAccess { $0 + 1 } - } - } - DispatchQueue.global(qos: .userInteractive).async { - adapter.request(data: Endpoint()) { result in - if case let .failure(error) = result { - XCTFail(error.localizedDescription) - } - counter.asyncAccess { $0 + 1 } - } - } - DispatchQueue.global(qos: .utility).async { - adapter.request(data: Endpoint()) { result in - if case let .failure(error) = result { - XCTFail(error.localizedDescription) - } - counter.asyncAccess { $0 + 1 } - } - } - } - - wait(for: [expectation], timeout: extendedTimeout) - } - - static var allTests = [ - ("testStressMultipleRequestsViaGet", testStressMultipleRequestsViaGet) - ] - -} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 804932a..57985e7 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -2,5 +2,5 @@ import XCTest @testable import FTAPIKitTests XCTMain([ - testCase(APIAdapterTests.allTests) + testCase(ResponseTests.allTests) ]) From a3b87b599c035dca52e46d09411b33c9ca42ffb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Wed, 29 Jan 2020 15:56:55 +0100 Subject: [PATCH 14/36] Remove forgotten print --- Tests/FTAPIKitTests/Mockups/Servers.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/FTAPIKitTests/Mockups/Servers.swift b/Tests/FTAPIKitTests/Mockups/Servers.swift index b1afd46..07399f6 100644 --- a/Tests/FTAPIKitTests/Mockups/Servers.swift +++ b/Tests/FTAPIKitTests/Mockups/Servers.swift @@ -8,7 +8,6 @@ struct HTTPBinServer: URLServer { if endpoint is AuthorizedEndpoint { request.addValue("Bearer \(UUID().uuidString)", forHTTPHeaderField: "Authorization") } - print(request) } } From ec48d6d640f11459fbf0e9490f0f1d5504b5ab31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Thu, 30 Jan 2020 12:06:15 +0100 Subject: [PATCH 15/36] Remove testable import --- Tests/FTAPIKitTests/ResponseTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/FTAPIKitTests/ResponseTests.swift b/Tests/FTAPIKitTests/ResponseTests.swift index edee004..3cd6977 100644 --- a/Tests/FTAPIKitTests/ResponseTests.swift +++ b/Tests/FTAPIKitTests/ResponseTests.swift @@ -1,5 +1,5 @@ import XCTest -@testable import FTAPIKit +import FTAPIKit final class ResponseTests: XCTestCase { private let timeout: TimeInterval = 30.0 From 0046b8953b5b9abab4aa7dabe625555ef34a6830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Fri, 31 Jan 2020 16:13:34 +0100 Subject: [PATCH 16/36] Use input stream instead of data --- Sources/FTAPIKit/Endpoint.swift | 14 +++++++------- Sources/FTAPIKit/URLRequestBuilder.swift | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Sources/FTAPIKit/Endpoint.swift b/Sources/FTAPIKit/Endpoint.swift index 7505de6..e5b5481 100644 --- a/Sources/FTAPIKit/Endpoint.swift +++ b/Sources/FTAPIKit/Endpoint.swift @@ -1,4 +1,4 @@ -import struct Foundation.Data +import Foundation /// Protocol describing API endpoint. API Endpoint describes one URI with all the /// data and parameters which are sent to it. @@ -22,7 +22,7 @@ public protocol Endpoint { /// HTTP method/verb describing the action. var method: HTTPMethod { get } - func body(encoding: Encoding) throws -> Data? + func body(encoding: Encoding) throws -> InputStream? } public extension Endpoint { @@ -38,17 +38,17 @@ public extension Endpoint { return .get } - func body(encoding: Encoding) throws -> Data? { + func body(encoding: Encoding) throws -> InputStream? { return nil } } public protocol DataEndpoint: Endpoint { - var data: Data? { get } + var data: Data { get } } public extension DataEndpoint { - func body(encoding: Encoding) throws -> Data? { data } + func body(encoding: Encoding) throws -> InputStream? { InputStream(data: data) } } /// Endpoint protocol extending `Endpoint` having decodable associated type, which is used @@ -73,8 +73,8 @@ public protocol RequestEndpoint: Endpoint { public extension RequestEndpoint { var method: HTTPMethod { .post } - func body(encoding: Encoding) throws -> Data? { - try encoding.encode(parameters) + func body(encoding: Encoding) throws -> InputStream? { + InputStream(data: try encoding.encode(parameters)) } } diff --git a/Sources/FTAPIKit/URLRequestBuilder.swift b/Sources/FTAPIKit/URLRequestBuilder.swift index e523657..08bdafa 100644 --- a/Sources/FTAPIKit/URLRequestBuilder.swift +++ b/Sources/FTAPIKit/URLRequestBuilder.swift @@ -12,7 +12,6 @@ struct URLRequestBuilder { request.httpMethod = endpoint.method.description request.allHTTPHeaderFields = endpoint.headers - request.httpBody = try endpoint.body(encoding: server.encoding) try server.encoding.configure(request: &request) try server.configureRequest(&request, endpoint) return request From 47e724d7692edfce9e8527541df6d1f46cbf4ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Tue, 4 Feb 2020 12:16:04 +0100 Subject: [PATCH 17/36] Shorten default extensions --- Sources/FTAPIKit/Endpoint.swift | 18 +++--------------- Sources/FTAPIKit/URLServer.swift | 19 ++++--------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/Sources/FTAPIKit/Endpoint.swift b/Sources/FTAPIKit/Endpoint.swift index e5b5481..9a28c3f 100644 --- a/Sources/FTAPIKit/Endpoint.swift +++ b/Sources/FTAPIKit/Endpoint.swift @@ -26,21 +26,9 @@ public protocol Endpoint { } public extension Endpoint { - var headers: [String: String] { - return [:] - } - - var query: [String: String] { - return [:] - } - - var method: HTTPMethod { - return .get - } - - func body(encoding: Encoding) throws -> InputStream? { - return nil - } + var headers: [String: String] { [:] } + var query: [String: String] { [:] } + var method: HTTPMethod { .get } } public protocol DataEndpoint: Endpoint { diff --git a/Sources/FTAPIKit/URLServer.swift b/Sources/FTAPIKit/URLServer.swift index ed8340a..5c061de 100644 --- a/Sources/FTAPIKit/URLServer.swift +++ b/Sources/FTAPIKit/URLServer.swift @@ -6,19 +6,8 @@ public protocol URLServer: Server where Request == URLRequest { } public extension URLServer { - var urlSession: URLSession { - .shared - } - - var decoding: Decoding { - JSONDecoding() - } - - var encoding: Encoding { - JSONEncoding() - } - - var configureRequest: (inout URLRequest, Endpoint) throws -> Void { - { _, _ in } - } + var urlSession: URLSession { .shared } + var decoding: Decoding { JSONDecoding() } + var encoding: Encoding { JSONEncoding() } + var requestBuilder: (Self, Endpoint) throws -> URLRequest { Self.buildStandardRequest } } From 2d33c5b39439c0b3ece30256d0694af1b0caec9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Tue, 4 Feb 2020 12:17:04 +0100 Subject: [PATCH 18/36] Rename server error type --- Sources/FTAPIKit/Server.swift | 2 +- Sources/FTAPIKit/URLServer+Call.swift | 28 +++++++++++------------ Tests/FTAPIKitTests/Mockups/Servers.swift | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/FTAPIKit/Server.swift b/Sources/FTAPIKit/Server.swift index e4ae712..95fa204 100644 --- a/Sources/FTAPIKit/Server.swift +++ b/Sources/FTAPIKit/Server.swift @@ -1,6 +1,6 @@ public protocol Server { - associatedtype E: APIError = APIError.Standard + associatedtype ErrorType: APIError = APIError.Standard associatedtype Request var decoding: Decoding { get } diff --git a/Sources/FTAPIKit/URLServer+Call.swift b/Sources/FTAPIKit/URLServer+Call.swift index fcf8fae..c71345e 100644 --- a/Sources/FTAPIKit/URLServer+Call.swift +++ b/Sources/FTAPIKit/URLServer+Call.swift @@ -2,9 +2,9 @@ import Foundation extension URLServer { @discardableResult - public func call(request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + public func call(request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { task(request: request, process: { data, response, error in - if let error = E(data: data, response: response, error: error, decoding: self.decoding) { + if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { return .failure(error) } return .success(()) @@ -12,9 +12,9 @@ extension URLServer { } @discardableResult - public func call(data request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + public func call(data request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { task(request: request, process: { data, response, error in - if let error = E(data: data, response: response, error: error, decoding: self.decoding) { + if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { return .failure(error) } else if let data = data { return .success(data) @@ -24,9 +24,9 @@ extension URLServer { } @discardableResult - public func call(response request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + public func call(response request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { task(request: request, process: { data, response, error in - if let error = E(data: data, response: response, error: error, decoding: self.decoding) { + if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { return .failure(error) } else if let data = data { do { @@ -41,7 +41,7 @@ extension URLServer { } @discardableResult - public func call(endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + public func call(endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { switch request(endpoint: endpoint) { case .success(let request): return call(request: request, completion: completion) @@ -53,7 +53,7 @@ extension URLServer { @discardableResult - public func call(data endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + public func call(data endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { switch request(endpoint: endpoint) { case .success(let request): return call(data: request, completion: completion) @@ -64,7 +64,7 @@ extension URLServer { } @discardableResult - public func call(response endpoint: EP, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + public func call(response endpoint: EP, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { switch request(endpoint: endpoint) { case .success(let request): return call(response: request, completion: completion) @@ -76,8 +76,8 @@ extension URLServer { func task( request: URLRequest, - process: @escaping (Data?, URLResponse?, Error?) -> Result, - completion: @escaping (Result) -> Void + process: @escaping (Data?, URLResponse?, Error?) -> Result, + completion: @escaping (Result) -> Void ) -> URLSessionDataTask? { let task = urlSession.dataTask(with: request) { data, response, error in completion(process(data, response, error)) @@ -86,7 +86,7 @@ extension URLServer { return task } - func request(endpoint: Endpoint) -> Result { + func request(endpoint: Endpoint) -> Result { do { let builder = URLRequestBuilder(server: self, endpoint: endpoint) let request = try builder.build() @@ -96,8 +96,8 @@ extension URLServer { } } - func apiError(error: Error?) -> Result { - let error = E(data: nil, response: nil, error: error, decoding: decoding) ?? .unhandled + func apiError(error: Error?) -> Result { + let error = ErrorType(data: nil, response: nil, error: error, decoding: decoding) ?? .unhandled return .failure(error) } } diff --git a/Tests/FTAPIKitTests/Mockups/Servers.swift b/Tests/FTAPIKitTests/Mockups/Servers.swift index 07399f6..bbd0a54 100644 --- a/Tests/FTAPIKitTests/Mockups/Servers.swift +++ b/Tests/FTAPIKitTests/Mockups/Servers.swift @@ -17,7 +17,7 @@ struct NonExistingServer: URLServer { } struct ErrorThrowingServer: URLServer { - typealias E = ThrowawayAPIError + typealias ErrorType = ThrowawayAPIError let urlSession = URLSession(configuration: .ephemeral) let baseUri = URL(string: "http://httpbin.org/")! From 3e52bdb178aadb92ec32ab9dc41a415528f29f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Tue, 4 Feb 2020 12:18:25 +0100 Subject: [PATCH 19/36] Rename parameters to request --- Sources/FTAPIKit/Endpoint.swift | 4 ++-- Tests/FTAPIKitTests/Mockups/Endpoints.swift | 4 ++-- Tests/FTAPIKitTests/ResponseTests.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/FTAPIKit/Endpoint.swift b/Sources/FTAPIKit/Endpoint.swift index 9a28c3f..77962dc 100644 --- a/Sources/FTAPIKit/Endpoint.swift +++ b/Sources/FTAPIKit/Endpoint.swift @@ -53,9 +53,9 @@ public protocol RequestEndpoint: Endpoint { /// Associated type describing the encodable request model for /// JSON serialization. The associated type is derived from /// the body property. - associatedtype Parameters: Encodable + associatedtype Request: Encodable /// Generic encodable model, which will be sent as JSON body. - var parameters: Parameters { get } + var request: Request { get } } public extension RequestEndpoint { diff --git a/Tests/FTAPIKitTests/Mockups/Endpoints.swift b/Tests/FTAPIKitTests/Mockups/Endpoints.swift index b78cf9a..39563c7 100644 --- a/Tests/FTAPIKitTests/Mockups/Endpoints.swift +++ b/Tests/FTAPIKitTests/Mockups/Endpoints.swift @@ -45,7 +45,7 @@ struct JSONResponseEndpoint: ResponseEndpoint { struct UpdateUserEndpoint: RequestResponseEndpoint { typealias Response = Wrapper - let parameters: User + let request: User let path = "anything" struct Wrapper: Decodable { @@ -56,6 +56,6 @@ struct UpdateUserEndpoint: RequestResponseEndpoint { struct FailingUpdateUserEndpoint: RequestResponseEndpoint { typealias Response = User - let parameters: User + let request: User let path = "anything" } diff --git a/Tests/FTAPIKitTests/ResponseTests.swift b/Tests/FTAPIKitTests/ResponseTests.swift index 3cd6977..a2c2cba 100644 --- a/Tests/FTAPIKitTests/ResponseTests.swift +++ b/Tests/FTAPIKitTests/ResponseTests.swift @@ -113,7 +113,7 @@ final class ResponseTests: XCTestCase { func testValidJSONRequestResponse() { let server = HTTPBinServer() let user = User(uuid: UUID(), name: "Some Name", age: .random(in: 0...120)) - let endpoint = UpdateUserEndpoint(parameters: user) + let endpoint = UpdateUserEndpoint(request: user) let expectation = self.expectation(description: "Result") server.call(response: endpoint) { result in switch result { @@ -130,7 +130,7 @@ final class ResponseTests: XCTestCase { func testInvalidJSONRequestResponse() { let server = HTTPBinServer() let user = User(uuid: UUID(), name: "Some Name", age: .random(in: 0...120)) - let endpoint = FailingUpdateUserEndpoint(parameters: user) + let endpoint = FailingUpdateUserEndpoint(request: user) let expectation = self.expectation(description: "Result") server.call(response: endpoint) { result in if case .success = result { From 5f6812b2a4e5618f71125a7e45ad406db68df69f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Tue, 4 Feb 2020 12:20:00 +0100 Subject: [PATCH 20/36] Move all endpoint conversion to request builder --- Sources/FTAPIKit/Endpoint.swift | 24 +++++++++---------- Sources/FTAPIKit/Server.swift | 2 +- Sources/FTAPIKit/URLRequestBuilder.swift | 28 ++++++++++++++++++++--- Sources/FTAPIKit/URLServer+Call.swift | 3 +-- Tests/FTAPIKitTests/Mockups/Servers.swift | 5 +++- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/Sources/FTAPIKit/Endpoint.swift b/Sources/FTAPIKit/Endpoint.swift index 77962dc..a2acb00 100644 --- a/Sources/FTAPIKit/Endpoint.swift +++ b/Sources/FTAPIKit/Endpoint.swift @@ -21,8 +21,6 @@ public protocol Endpoint { /// HTTP method/verb describing the action. var method: HTTPMethod { get } - - func body(encoding: Encoding) throws -> InputStream? } public extension Endpoint { @@ -32,11 +30,7 @@ public extension Endpoint { } public protocol DataEndpoint: Endpoint { - var data: Data { get } -} - -public extension DataEndpoint { - func body(encoding: Encoding) throws -> InputStream? { InputStream(data: data) } + var body: Data { get } } /// Endpoint protocol extending `Endpoint` having decodable associated type, which is used @@ -49,7 +43,7 @@ public protocol ResponseEndpoint: Endpoint { } /// Endpoint protocol extending `Endpoint` encapsulating and improving sending JSON models to API. -public protocol RequestEndpoint: Endpoint { +public protocol RequestEndpoint: AnyRequestEndpoint { /// Associated type describing the encodable request model for /// JSON serialization. The associated type is derived from /// the body property. @@ -59,13 +53,19 @@ public protocol RequestEndpoint: Endpoint { } public extension RequestEndpoint { - var method: HTTPMethod { .post } - - func body(encoding: Encoding) throws -> InputStream? { - InputStream(data: try encoding.encode(parameters)) + func body(encoding: Encoding) throws -> Data { + try encoding.encode(request) } } +public protocol AnyRequestEndpoint: Endpoint { + func body(encoding: Encoding) throws -> Data +} + +public extension RequestEndpoint { + var method: HTTPMethod { .post } +} + /// Typealias combining request and response API endpoint. For describing JSON /// request which both sends and expects JSON model from the server. public typealias RequestResponseEndpoint = RequestEndpoint & ResponseEndpoint diff --git a/Sources/FTAPIKit/Server.swift b/Sources/FTAPIKit/Server.swift index 95fa204..353834e 100644 --- a/Sources/FTAPIKit/Server.swift +++ b/Sources/FTAPIKit/Server.swift @@ -5,5 +5,5 @@ public protocol Server { var decoding: Decoding { get } var encoding: Encoding { get } - var configureRequest: (inout Request, Endpoint) throws -> Void { get } + var requestBuilder: (Self, Endpoint) throws -> Request { get } } diff --git a/Sources/FTAPIKit/URLRequestBuilder.swift b/Sources/FTAPIKit/URLRequestBuilder.swift index 08bdafa..a0e991f 100644 --- a/Sources/FTAPIKit/URLRequestBuilder.swift +++ b/Sources/FTAPIKit/URLRequestBuilder.swift @@ -1,8 +1,19 @@ import Foundation +public extension URLServer { + static func buildStandardRequest(server: Self, endpoint: Endpoint) throws -> URLRequest { + try URLRequestBuilder(server: server, endpoint: endpoint).build() + } +} + struct URLRequestBuilder { - let server: S - let endpoint: Endpoint + public let server: S + public let endpoint: Endpoint + + init(server: S, endpoint: Endpoint) { + self.server = server + self.endpoint = endpoint + } func build() throws -> URLRequest { let url = server.baseUri @@ -12,8 +23,19 @@ struct URLRequestBuilder { request.httpMethod = endpoint.method.description request.allHTTPHeaderFields = endpoint.headers + try buildBody(to: &request) try server.encoding.configure(request: &request) - try server.configureRequest(&request, endpoint) return request } + + private func buildBody(to request: inout URLRequest) throws { + switch endpoint { + case let endpoint as DataEndpoint: + request.httpBody = endpoint.body + case let endpoint as AnyRequestEndpoint: + request.httpBody = try endpoint.body(encoding: server.encoding) + default: + break + } + } } diff --git a/Sources/FTAPIKit/URLServer+Call.swift b/Sources/FTAPIKit/URLServer+Call.swift index c71345e..a6b136e 100644 --- a/Sources/FTAPIKit/URLServer+Call.swift +++ b/Sources/FTAPIKit/URLServer+Call.swift @@ -88,8 +88,7 @@ extension URLServer { func request(endpoint: Endpoint) -> Result { do { - let builder = URLRequestBuilder(server: self, endpoint: endpoint) - let request = try builder.build() + let request = try requestBuilder(self, endpoint) return .success(request) } catch { return apiError(error: error) diff --git a/Tests/FTAPIKitTests/Mockups/Servers.swift b/Tests/FTAPIKitTests/Mockups/Servers.swift index bbd0a54..bc41862 100644 --- a/Tests/FTAPIKitTests/Mockups/Servers.swift +++ b/Tests/FTAPIKitTests/Mockups/Servers.swift @@ -4,10 +4,13 @@ import Foundation struct HTTPBinServer: URLServer { let urlSession = URLSession(configuration: .ephemeral) let baseUri = URL(string: "http://httpbin.org/")! - let configureRequest: (inout URLRequest, Endpoint) throws -> Void = { request, endpoint in + + let requestBuilder: (Self, Endpoint) throws -> URLRequest = { server, endpoint in + var request = try buildStandardRequest(server: server, endpoint: endpoint) if endpoint is AuthorizedEndpoint { request.addValue("Bearer \(UUID().uuidString)", forHTTPHeaderField: "Authorization") } + return request } } From 2c26f577680faa7b8ce4cb35a6fba58f3576344c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Tue, 4 Feb 2020 12:57:22 +0100 Subject: [PATCH 21/36] Add back multipart functionality --- Sources/FTAPIKit/Endpoint.swift | 4 ++++ Sources/FTAPIKit/MultipartFormData.swift | 13 +++++++++---- Sources/FTAPIKit/URLRequestBuilder.swift | 7 +++++++ Tests/FTAPIKitTests/Mockups/Endpoints.swift | 15 +++++++++++++++ Tests/FTAPIKitTests/Mockups/Models.swift | 9 +++++++++ Tests/FTAPIKitTests/ResponseTests.swift | 15 +++++++++++++++ 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/Sources/FTAPIKit/Endpoint.swift b/Sources/FTAPIKit/Endpoint.swift index a2acb00..9999839 100644 --- a/Sources/FTAPIKit/Endpoint.swift +++ b/Sources/FTAPIKit/Endpoint.swift @@ -33,6 +33,10 @@ public protocol DataEndpoint: Endpoint { var body: Data { get } } +public protocol MultipartEndpoint: Endpoint { + var parts: [MultipartBodyPart] { get } +} + /// Endpoint protocol extending `Endpoint` having decodable associated type, which is used /// for automatic deserialization. public protocol ResponseEndpoint: Endpoint { diff --git a/Sources/FTAPIKit/MultipartFormData.swift b/Sources/FTAPIKit/MultipartFormData.swift index e0b6e64..b21f268 100644 --- a/Sources/FTAPIKit/MultipartFormData.swift +++ b/Sources/FTAPIKit/MultipartFormData.swift @@ -3,16 +3,20 @@ import Foundation struct MultipartFormData { private let parts: [MultipartBodyPart] - private let boundaryData: Data + private let boundary: String private let temporaryUrl: URL = makeTemporaryUrl() - init(parts: [MultipartBodyPart], boundary: String) { + init(parts: [MultipartBodyPart], boundary: String = "FTAPIKit-" + UUID().uuidString) { self.parts = parts - self.boundaryData = Data(boundary.utf8) + self.boundary = boundary } var contentLength: Int64? { - return (try? FileManager.default.attributesOfItem(atPath: temporaryUrl.path)[.size] as? Int64)?.flatMap { $0 } + (try? FileManager.default.attributesOfItem(atPath: temporaryUrl.path)[.size] as? Int64)?.flatMap { $0 } + } + + var contentType: String { + "multipart/form-data; charset=utf-8; boundary=\(boundary)" } private static func makeTemporaryUrl() -> URL { @@ -40,6 +44,7 @@ struct MultipartFormData { defer { outputStream.close() } + let boundaryData = Data(boundary.utf8) for part in parts { try outputStream.write(data: boundaryData) try outputStream.writeLine() diff --git a/Sources/FTAPIKit/URLRequestBuilder.swift b/Sources/FTAPIKit/URLRequestBuilder.swift index a0e991f..c12c950 100644 --- a/Sources/FTAPIKit/URLRequestBuilder.swift +++ b/Sources/FTAPIKit/URLRequestBuilder.swift @@ -34,6 +34,13 @@ struct URLRequestBuilder { request.httpBody = endpoint.body case let endpoint as AnyRequestEndpoint: request.httpBody = try endpoint.body(encoding: server.encoding) + case let endpoint as MultipartEndpoint: + let formData = MultipartFormData(parts: endpoint.parts) + request.httpBodyStream = try formData.inputStream() + request.setValue(formData.contentType, forHTTPHeaderField: "Content-Type") + if let contentLength = formData.contentLength { + request.setValue(contentLength.description, forHTTPHeaderField: "Content-Length") + } default: break } diff --git a/Tests/FTAPIKitTests/Mockups/Endpoints.swift b/Tests/FTAPIKitTests/Mockups/Endpoints.swift index 39563c7..0f3abb3 100644 --- a/Tests/FTAPIKitTests/Mockups/Endpoints.swift +++ b/Tests/FTAPIKitTests/Mockups/Endpoints.swift @@ -59,3 +59,18 @@ struct FailingUpdateUserEndpoint: RequestResponseEndpoint { let request: User let path = "anything" } + +struct TestMultipartEndpoint: MultipartEndpoint { + let parts: [MultipartBodyPart] + let path = "post" + let method: HTTPMethod = .post + + init(file: File) throws { + self.parts = [ + MultipartBodyPart(name: "anotherParameter", value: "valueForParameter"), + try MultipartBodyPart(name: "urlImage", url: file.url), + MultipartBodyPart(headers: file.headers, data: file.data), + MultipartBodyPart(headers: file.headers, inputStream: InputStream(url: file.url) ?? InputStream()) + ] + } +} diff --git a/Tests/FTAPIKitTests/Mockups/Models.swift b/Tests/FTAPIKitTests/Mockups/Models.swift index fe5b5b2..e4e8848 100644 --- a/Tests/FTAPIKitTests/Mockups/Models.swift +++ b/Tests/FTAPIKitTests/Mockups/Models.swift @@ -5,3 +5,12 @@ struct User: Codable, Equatable { let name: String let age: UInt } + +struct File { + let url: URL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent("\(UUID()).txt") + let data = Data(repeating: UInt8(ascii: "a"), count: 1024 * 1024) + let headers: [String: String] = [ + "Content-Disposition": "form-data; name=jpegFile", + "Content-Type": "image/jpeg" + ] +} diff --git a/Tests/FTAPIKitTests/ResponseTests.swift b/Tests/FTAPIKitTests/ResponseTests.swift index a2c2cba..7b4e983 100644 --- a/Tests/FTAPIKitTests/ResponseTests.swift +++ b/Tests/FTAPIKitTests/ResponseTests.swift @@ -154,6 +154,21 @@ final class ResponseTests: XCTestCase { wait(for: [expectation], timeout: timeout) } + func testMultipartData() { + let server = HTTPBinServer() + let file = File() + try! file.data.write(to: file.url) + let endpoint = try! TestMultipartEndpoint(file: file) + let expectation = self.expectation(description: "Result") + server.call(endpoint: endpoint) { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + static var allTests = [ ("testGet", testGet), ("testClientError", testClientError), From 1ed6f12aa0aa3ce40b46c07ad99baf1d55132404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Tue, 4 Feb 2020 16:03:27 +0100 Subject: [PATCH 22/36] Add uploading support --- Sources/FTAPIKit/Endpoint.swift | 4 + Sources/FTAPIKit/URLServer+Call.swift | 106 ++++++++------------ Sources/FTAPIKit/URLServer+Task.swift | 73 ++++++++++++++ Tests/FTAPIKitTests/Mockups/Endpoints.swift | 10 ++ Tests/FTAPIKitTests/Mockups/Models.swift | 4 + Tests/FTAPIKitTests/ResponseTests.swift | 22 +++- 6 files changed, 151 insertions(+), 68 deletions(-) create mode 100644 Sources/FTAPIKit/URLServer+Task.swift diff --git a/Sources/FTAPIKit/Endpoint.swift b/Sources/FTAPIKit/Endpoint.swift index 9999839..3c9ab85 100644 --- a/Sources/FTAPIKit/Endpoint.swift +++ b/Sources/FTAPIKit/Endpoint.swift @@ -33,6 +33,10 @@ public protocol DataEndpoint: Endpoint { var body: Data { get } } +public protocol UploadEndpoint: Endpoint { + var file: URL { get } +} + public protocol MultipartEndpoint: Endpoint { var parts: [MultipartBodyPart] { get } } diff --git a/Sources/FTAPIKit/URLServer+Call.swift b/Sources/FTAPIKit/URLServer+Call.swift index a6b136e..f3a9d38 100644 --- a/Sources/FTAPIKit/URLServer+Call.swift +++ b/Sources/FTAPIKit/URLServer+Call.swift @@ -1,50 +1,11 @@ import Foundation -extension URLServer { +public extension URLServer { @discardableResult - public func call(request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { - task(request: request, process: { data, response, error in - if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { - return .failure(error) - } - return .success(()) - }, completion: completion) - } - - @discardableResult - public func call(data request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { - task(request: request, process: { data, response, error in - if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { - return .failure(error) - } else if let data = data { - return .success(data) - } - return .failure(.unhandled) - }, completion: completion) - } - - @discardableResult - public func call(response request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { - task(request: request, process: { data, response, error in - if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { - return .failure(error) - } else if let data = data { - do { - let response: R = try self.decoding.decode(data: data) - return .success(response) - } catch { - return self.apiError(error: error) - } - } - return .failure(.unhandled) - }, completion: completion) - } - - @discardableResult - public func call(endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + func call(endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionTask? { switch request(endpoint: endpoint) { case .success(let request): - return call(request: request, completion: completion) + return call(request: request, file: uploadFile(endpoint: endpoint), completion: completion) case .failure(let error): completion(.failure(error)) return nil @@ -53,10 +14,10 @@ extension URLServer { @discardableResult - public func call(data endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + func call(data endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionTask? { switch request(endpoint: endpoint) { case .success(let request): - return call(data: request, completion: completion) + return call(data: request, file: uploadFile(endpoint: endpoint), completion: completion) case .failure(let error): completion(.failure(error)) return nil @@ -64,39 +25,52 @@ extension URLServer { } @discardableResult - public func call(response endpoint: EP, completion: @escaping (Result) -> Void) -> URLSessionDataTask? { + func call(response endpoint: EP, completion: @escaping (Result) -> Void) -> URLSessionTask? { switch request(endpoint: endpoint) { case .success(let request): - return call(response: request, completion: completion) + return call(response: request, file: uploadFile(endpoint: endpoint), completion: completion) case .failure(let error): completion(.failure(error)) return nil } } - func task( - request: URLRequest, - process: @escaping (Data?, URLResponse?, Error?) -> Result, - completion: @escaping (Result) -> Void - ) -> URLSessionDataTask? { - let task = urlSession.dataTask(with: request) { data, response, error in - completion(process(data, response, error)) - } - task.resume() - return task + @discardableResult + private func call(request: URLRequest, file: URL?, completion: @escaping (Result) -> Void) -> URLSessionTask? { + task(request: request, file: file, process: { data, response, error in + if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { + return .failure(error) + } + return .success(()) + }, completion: completion) } - func request(endpoint: Endpoint) -> Result { - do { - let request = try requestBuilder(self, endpoint) - return .success(request) - } catch { - return apiError(error: error) - } + @discardableResult + private func call(data request: URLRequest, file: URL?, completion: @escaping (Result) -> Void) -> URLSessionTask? { + task(request: request, file: file, process: { data, response, error in + if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { + return .failure(error) + } else if let data = data { + return .success(data) + } + return .failure(.unhandled) + }, completion: completion) } - func apiError(error: Error?) -> Result { - let error = ErrorType(data: nil, response: nil, error: error, decoding: decoding) ?? .unhandled - return .failure(error) + @discardableResult + private func call(response request: URLRequest, file: URL?, completion: @escaping (Result) -> Void) -> URLSessionTask? { + task(request: request, file: file, process: { data, response, error in + if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { + return .failure(error) + } else if let data = data { + do { + let response: R = try self.decoding.decode(data: data) + return .success(response) + } catch { + return self.apiError(error: error) + } + } + return .failure(.unhandled) + }, completion: completion) } } diff --git a/Sources/FTAPIKit/URLServer+Task.swift b/Sources/FTAPIKit/URLServer+Task.swift new file mode 100644 index 0000000..3e42f02 --- /dev/null +++ b/Sources/FTAPIKit/URLServer+Task.swift @@ -0,0 +1,73 @@ +import Foundation + +extension URLServer { + func task( + request: URLRequest, + file: URL?, + process: @escaping (Data?, URLResponse?, Error?) -> Result, + completion: @escaping (Result) -> Void + ) -> URLSessionDataTask? { + if let file = file { + return uploadTask(request: request, file: file, process: process, completion: completion) + } + return dataTask(request: request, process: process, completion: completion) + } + + private func dataTask( + request: URLRequest, + process: @escaping (Data?, URLResponse?, Error?) -> Result, + completion: @escaping (Result) -> Void + ) -> URLSessionDataTask? { + let task = urlSession.dataTask(with: request) { data, response, error in + completion(process(data, response, error)) + } + task.resume() + return task + } + + private func uploadTask( + request: URLRequest, + file: URL, + process: @escaping (Data?, URLResponse?, Error?) -> Result, + completion: @escaping (Result) -> Void + ) -> URLSessionUploadTask? { + let task = urlSession.uploadTask(with: request, fromFile: file) { data, response, error in + completion(process(data, response, error)) + } + task.resume() + return task + } + + func downloadTask( + request: URLRequest, + process: @escaping (URL?, URLResponse?, Error?) -> Result, + completion: @escaping (Result) -> Void + ) -> URLSessionDownloadTask? { + let task = urlSession.downloadTask(with: request) { url, response, error in + completion(process(url, response, error)) + } + task.resume() + return task + } + + func request(endpoint: Endpoint) -> Result { + do { + let request = try requestBuilder(self, endpoint) + return .success(request) + } catch { + return apiError(error: error) + } + } + + func uploadFile(endpoint: Endpoint) -> URL? { + if let endpoint = endpoint as? UploadEndpoint { + return endpoint.file + } + return nil + } + + func apiError(error: Error?) -> Result { + let error = ErrorType(data: nil, response: nil, error: error, decoding: decoding) ?? .unhandled + return .failure(error) + } +} diff --git a/Tests/FTAPIKitTests/Mockups/Endpoints.swift b/Tests/FTAPIKitTests/Mockups/Endpoints.swift index 0f3abb3..3c1f536 100644 --- a/Tests/FTAPIKitTests/Mockups/Endpoints.swift +++ b/Tests/FTAPIKitTests/Mockups/Endpoints.swift @@ -74,3 +74,13 @@ struct TestMultipartEndpoint: MultipartEndpoint { ] } } + +struct TestUploadEndpoint: UploadEndpoint { + let file: URL + let path: String = "put" + let method: HTTPMethod = .put + + init(file: File) { + self.file = file.url + } +} diff --git a/Tests/FTAPIKitTests/Mockups/Models.swift b/Tests/FTAPIKitTests/Mockups/Models.swift index e4e8848..2ccacea 100644 --- a/Tests/FTAPIKitTests/Mockups/Models.swift +++ b/Tests/FTAPIKitTests/Mockups/Models.swift @@ -13,4 +13,8 @@ struct File { "Content-Disposition": "form-data; name=jpegFile", "Content-Type": "image/jpeg" ] + + func write() throws { + try data.write(to: url) + } } diff --git a/Tests/FTAPIKitTests/ResponseTests.swift b/Tests/FTAPIKitTests/ResponseTests.swift index 7b4e983..ae6b664 100644 --- a/Tests/FTAPIKitTests/ResponseTests.swift +++ b/Tests/FTAPIKitTests/ResponseTests.swift @@ -157,10 +157,10 @@ final class ResponseTests: XCTestCase { func testMultipartData() { let server = HTTPBinServer() let file = File() - try! file.data.write(to: file.url) + XCTAssertNoThrow(try file.write()) let endpoint = try! TestMultipartEndpoint(file: file) let expectation = self.expectation(description: "Result") - server.call(endpoint: endpoint) { result in + server.call(data: endpoint) { result in if case let .failure(error) = result { XCTFail(error.localizedDescription) } @@ -169,6 +169,22 @@ final class ResponseTests: XCTestCase { wait(for: [expectation], timeout: timeout) } + func testUploadTask() { + let server = HTTPBinServer() + let file = File() + XCTAssertNoThrow(try file.write()) + let endpoint = TestUploadEndpoint(file: file) + let expectation = self.expectation(description: "Result") + server.call(data: endpoint) { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + + } + static var allTests = [ ("testGet", testGet), ("testClientError", testClientError), @@ -180,5 +196,7 @@ final class ResponseTests: XCTestCase { ("testValidJSONRequestResponse", testValidJSONRequestResponse), ("testInvalidJSONRequestResponse", testInvalidJSONRequestResponse), ("testAuthorization", testAuthorization), + ("testMultipartData", testMultipartData), + ("testUploadTask", testUploadTask), ] } From f0affb6ba20e7013a7bd4f0fdef033d5ed6d51a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Tue, 4 Feb 2020 16:29:18 +0100 Subject: [PATCH 23/36] Remove discardable results --- Sources/FTAPIKit/URLServer+Call.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/FTAPIKit/URLServer+Call.swift b/Sources/FTAPIKit/URLServer+Call.swift index f3a9d38..2e6892c 100644 --- a/Sources/FTAPIKit/URLServer+Call.swift +++ b/Sources/FTAPIKit/URLServer+Call.swift @@ -35,7 +35,6 @@ public extension URLServer { } } - @discardableResult private func call(request: URLRequest, file: URL?, completion: @escaping (Result) -> Void) -> URLSessionTask? { task(request: request, file: file, process: { data, response, error in if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { @@ -45,7 +44,6 @@ public extension URLServer { }, completion: completion) } - @discardableResult private func call(data request: URLRequest, file: URL?, completion: @escaping (Result) -> Void) -> URLSessionTask? { task(request: request, file: file, process: { data, response, error in if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { @@ -57,7 +55,6 @@ public extension URLServer { }, completion: completion) } - @discardableResult private func call(response request: URLRequest, file: URL?, completion: @escaping (Result) -> Void) -> URLSessionTask? { task(request: request, file: file, process: { data, response, error in if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { From 55d073015431feecf649070d9b76976b46cef351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Tue, 4 Feb 2020 16:29:57 +0100 Subject: [PATCH 24/36] Add download task support --- Sources/FTAPIKit/URLServer+Download.swift | 26 +++++++++++++++++++++ Tests/FTAPIKitTests/Mockups/Endpoints.swift | 6 ++++- Tests/FTAPIKitTests/ResponseTests.swift | 13 +++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 Sources/FTAPIKit/URLServer+Download.swift diff --git a/Sources/FTAPIKit/URLServer+Download.swift b/Sources/FTAPIKit/URLServer+Download.swift new file mode 100644 index 0000000..d5f6f0d --- /dev/null +++ b/Sources/FTAPIKit/URLServer+Download.swift @@ -0,0 +1,26 @@ +import Foundation + +public extension URLServer { + @discardableResult + func download(endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionTask? { + switch request(endpoint: endpoint) { + case .success(let request): + return download(request: request, completion: completion) + case .failure(let error): + completion(.failure(error)) + return nil + } + } + + private func download(request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionTask? { + downloadTask(request: request, process: { url, response, error in + let urlData = (url?.absoluteString.utf8).flatMap { Data($0) } + if let error = ErrorType(data: urlData, response: response, error: error, decoding: self.decoding) { + return .failure(error) + } else if let url = url { + return .success(url) + } + return .failure(.unhandled) + }, completion: completion) + } +} diff --git a/Tests/FTAPIKitTests/Mockups/Endpoints.swift b/Tests/FTAPIKitTests/Mockups/Endpoints.swift index 3c1f536..4dcdcaa 100644 --- a/Tests/FTAPIKitTests/Mockups/Endpoints.swift +++ b/Tests/FTAPIKitTests/Mockups/Endpoints.swift @@ -77,10 +77,14 @@ struct TestMultipartEndpoint: MultipartEndpoint { struct TestUploadEndpoint: UploadEndpoint { let file: URL - let path: String = "put" + let path = "put" let method: HTTPMethod = .put init(file: File) { self.file = file.url } } + +struct ImageEndpoint: Endpoint { + let path = "image/jpeg" +} diff --git a/Tests/FTAPIKitTests/ResponseTests.swift b/Tests/FTAPIKitTests/ResponseTests.swift index ae6b664..1a6a350 100644 --- a/Tests/FTAPIKitTests/ResponseTests.swift +++ b/Tests/FTAPIKitTests/ResponseTests.swift @@ -182,7 +182,19 @@ final class ResponseTests: XCTestCase { expectation.fulfill() } wait(for: [expectation], timeout: timeout) + } + func testDownloadTask() { + let server = HTTPBinServer() + let endpoint = ImageEndpoint() + let expectation = self.expectation(description: "Result") + server.download(endpoint: endpoint) { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) } static var allTests = [ @@ -198,5 +210,6 @@ final class ResponseTests: XCTestCase { ("testAuthorization", testAuthorization), ("testMultipartData", testMultipartData), ("testUploadTask", testUploadTask), + ("testDownloadTask", testDownloadTask), ] } From 7a4d9a2b47c4cc1edfe7594f673953ffd7e2ac6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Tue, 4 Feb 2020 16:44:55 +0100 Subject: [PATCH 25/36] Add decoding and encoding errors --- Sources/FTAPIKit/APIError+Standard.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/FTAPIKit/APIError+Standard.swift b/Sources/FTAPIKit/APIError+Standard.swift index 8ec373a..993d833 100644 --- a/Sources/FTAPIKit/APIError+Standard.swift +++ b/Sources/FTAPIKit/APIError+Standard.swift @@ -20,6 +20,10 @@ public enum APIErrorStandard: APIError { switch (data, response as? HTTPURLResponse, error) { case let (_, _, error as URLError): self = .connection(error) + case let (_, _, error as EncodingError): + self = .encoding(error) + case let (_, _, error as DecodingError): + self = .decoding(error) case let (data, response?, nil) where 400..<500 ~= response.statusCode: self = .client(response.statusCode, response, data) case let (data, response?, nil) where 500..<600 ~= response.statusCode: From 3da5df7bb79bfc081837317fc72ee2354061fdd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Wed, 5 Feb 2020 16:17:53 +0100 Subject: [PATCH 26/36] Rename type-erased protocols to more EncodableEndpoint --- Sources/FTAPIKit/Endpoint.swift | 4 ++-- Sources/FTAPIKit/URLRequestBuilder.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/FTAPIKit/Endpoint.swift b/Sources/FTAPIKit/Endpoint.swift index 3c9ab85..c2fc99e 100644 --- a/Sources/FTAPIKit/Endpoint.swift +++ b/Sources/FTAPIKit/Endpoint.swift @@ -51,7 +51,7 @@ public protocol ResponseEndpoint: Endpoint { } /// Endpoint protocol extending `Endpoint` encapsulating and improving sending JSON models to API. -public protocol RequestEndpoint: AnyRequestEndpoint { +public protocol RequestEndpoint: EncodableEndpoint { /// Associated type describing the encodable request model for /// JSON serialization. The associated type is derived from /// the body property. @@ -66,7 +66,7 @@ public extension RequestEndpoint { } } -public protocol AnyRequestEndpoint: Endpoint { +public protocol EncodableEndpoint: Endpoint { func body(encoding: Encoding) throws -> Data } diff --git a/Sources/FTAPIKit/URLRequestBuilder.swift b/Sources/FTAPIKit/URLRequestBuilder.swift index c12c950..03c6ca6 100644 --- a/Sources/FTAPIKit/URLRequestBuilder.swift +++ b/Sources/FTAPIKit/URLRequestBuilder.swift @@ -32,7 +32,7 @@ struct URLRequestBuilder { switch endpoint { case let endpoint as DataEndpoint: request.httpBody = endpoint.body - case let endpoint as AnyRequestEndpoint: + case let endpoint as EncodableEndpoint: request.httpBody = try endpoint.body(encoding: server.encoding) case let endpoint as MultipartEndpoint: let formData = MultipartFormData(parts: endpoint.parts) From 5abc6d8be95f4f71b1281348cc05c4d29aeb1c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Wed, 5 Feb 2020 16:18:08 +0100 Subject: [PATCH 27/36] Remove unused washable implementation --- Sources/FTAPIKit/MultipartBodyPart.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/FTAPIKit/MultipartBodyPart.swift b/Sources/FTAPIKit/MultipartBodyPart.swift index 8c3b300..ea1081c 100644 --- a/Sources/FTAPIKit/MultipartBodyPart.swift +++ b/Sources/FTAPIKit/MultipartBodyPart.swift @@ -5,7 +5,7 @@ import Foundation /// to [RFC-7578](https://tools.ietf.org/html/rfc7578). /// Everything passed to it is converted to `InputStream` /// in order to limit memory usage when sending files to a server. -public struct MultipartBodyPart: Hashable { +public struct MultipartBodyPart { let headers: [String: String] let inputStream: InputStream From 365f8b5a3289fa63e63f2cc8f95f0d4c6e5073fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Wed, 5 Feb 2020 16:19:47 +0100 Subject: [PATCH 28/36] Remove unused URL encoded extension --- Sources/FTAPIKit/CharacterSet+APIAdapter.swift | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 Sources/FTAPIKit/CharacterSet+APIAdapter.swift diff --git a/Sources/FTAPIKit/CharacterSet+APIAdapter.swift b/Sources/FTAPIKit/CharacterSet+APIAdapter.swift deleted file mode 100644 index 7fd6a15..0000000 --- a/Sources/FTAPIKit/CharacterSet+APIAdapter.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -extension CharacterSet { - private static let urlGeneralDelimiters: CharacterSet = [":", "/", "?", "#", "[", "]", "@"] - private static let urlSubDelimiters: CharacterSet = ["!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="] - private static let urlDelimiters = CharacterSet.urlGeneralDelimiters.union(.urlSubDelimiters) - - /// https://tools.ietf.org/html/rfc3986#section-2.2 - static let urlQueryNameValueAllowed = CharacterSet.urlQueryAllowed.subtracting(.urlDelimiters) -} From f40625b58f0e0ee8e29f9832bd2251e69e64e481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Wed, 5 Feb 2020 16:20:04 +0100 Subject: [PATCH 29/36] Update podspec info and bump to 1.0.0 --- FTAPIKit.podspec | 49 +++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/FTAPIKit.podspec b/FTAPIKit.podspec index fd6faa0..797f3cd 100644 --- a/FTAPIKit.podspec +++ b/FTAPIKit.podspec @@ -1,26 +1,29 @@ Pod::Spec.new do |s| - s.name = "FTAPIKit" - s.version = "0.6.0" - s.summary = "Declarative, generic REST API framework using URLSession and Codable" - s.description = <<-DESC - Protocol-oriented REST API library for communication with REST API. - APIEndpoint protocols allow description of the API access points - and the requests/responses codable types. APIAdapter handles execution - of calls to this endpoints. + s.name = "FTAPIKit" + s.version = "1.0.0" + s.summary = "Declarative, generic and protocol-orented REST API framework using URLSession and Codable" + s.description = <<-DESC + Protocol-oriented framework for communication with REST APIs. + Endpoint protocols describe the API resource access points + and the requests/responses codable types. Server protocol describes web services + and enables the user to call endoints in a type-safe manner. DESC - s.homepage = "https://github.com/futuredapp/FTAPIKit" - s.license = { :type => "MIT", :file => "LICENSE" } - s.author = { "Matěj Kašpar Jirásek" => "matej.jirasek@futured.app" } - s.social_media_url = "https://twitter.com/Futuredapps" - s.source_files = "Sources/FTAPIKit/*" - s.framework = "Foundation" - s.ios.framework = "MobileCoreServices" - s.tvos.framework = "MobileCoreServices" - s.watchos.framework = "MobileCoreServices" - s.swift_version = "5.0" - s.ios.deployment_target = "8.0" - s.osx.deployment_target = "10.10" - s.watchos.deployment_target = "2.0" - s.tvos.deployment_target = "9.0" - s.source = { :git => "https://github.com/futuredapp/FTAPIKit.git", :tag => s.version.to_s } + s.homepage = "https://github.com/futuredapp/FTAPIKit" + s.license = { type: "MIT", file: "LICENSE" } + s.author = { "Matěj Kašpar Jirásek": "matej.jirasek@futured.app" } + s.social_media_url = "https://twitter.com/Futuredapps" + + s.source = { git: "https://github.com/futuredapp/FTAPIKit.git", tag: s.version.to_s } + s.source_files = "Sources/FTAPIKit/*" + + s.framework = "Foundation" + s.ios.framework = "MobileCoreServices" + s.tvos.framework = "MobileCoreServices" + s.watchos.framework = "MobileCoreServices" + + s.swift_version = "5.0" + s.ios.deployment_target = "8.0" + s.osx.deployment_target = "10.10" + s.watchos.deployment_target = "2.0" + s.tvos.deployment_target = "9.0" end From 31260ad3df4ee43924dfb6301c6b4d99b7301d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Fri, 7 Feb 2020 09:46:53 +0100 Subject: [PATCH 30/36] Move error type to URLServer --- Sources/FTAPIKit/Server.swift | 1 - Sources/FTAPIKit/URLServer.swift | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/FTAPIKit/Server.swift b/Sources/FTAPIKit/Server.swift index 353834e..abd80ab 100644 --- a/Sources/FTAPIKit/Server.swift +++ b/Sources/FTAPIKit/Server.swift @@ -1,6 +1,5 @@ public protocol Server { - associatedtype ErrorType: APIError = APIError.Standard associatedtype Request var decoding: Decoding { get } diff --git a/Sources/FTAPIKit/URLServer.swift b/Sources/FTAPIKit/URLServer.swift index 5c061de..7b17f1f 100644 --- a/Sources/FTAPIKit/URLServer.swift +++ b/Sources/FTAPIKit/URLServer.swift @@ -1,6 +1,8 @@ import Foundation public protocol URLServer: Server where Request == URLRequest { + associatedtype ErrorType: APIError = APIError.Standard + var baseUri: URL { get } var urlSession: URLSession { get } } From d8f167a8be62304019bd5c64acd94f90257d1630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Fri, 7 Feb 2020 09:49:00 +0100 Subject: [PATCH 31/36] Change request builder to methods --- Sources/FTAPIKit/Server.swift | 3 ++- Sources/FTAPIKit/URLRequestBuilder.swift | 4 ++-- Sources/FTAPIKit/URLServer+Task.swift | 2 +- Sources/FTAPIKit/URLServer.swift | 5 ++++- Tests/FTAPIKitTests/Mockups/Servers.swift | 4 ++-- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/FTAPIKit/Server.swift b/Sources/FTAPIKit/Server.swift index abd80ab..7378b21 100644 --- a/Sources/FTAPIKit/Server.swift +++ b/Sources/FTAPIKit/Server.swift @@ -4,5 +4,6 @@ public protocol Server { var decoding: Decoding { get } var encoding: Encoding { get } - var requestBuilder: (Self, Endpoint) throws -> Request { get } + + func buildRequest(endpoint: Endpoint) throws -> Request } diff --git a/Sources/FTAPIKit/URLRequestBuilder.swift b/Sources/FTAPIKit/URLRequestBuilder.swift index 03c6ca6..09b73d9 100644 --- a/Sources/FTAPIKit/URLRequestBuilder.swift +++ b/Sources/FTAPIKit/URLRequestBuilder.swift @@ -1,8 +1,8 @@ import Foundation public extension URLServer { - static func buildStandardRequest(server: Self, endpoint: Endpoint) throws -> URLRequest { - try URLRequestBuilder(server: server, endpoint: endpoint).build() + func buildStandardRequest(endpoint: Endpoint) throws -> URLRequest { + try URLRequestBuilder(server: self, endpoint: endpoint).build() } } diff --git a/Sources/FTAPIKit/URLServer+Task.swift b/Sources/FTAPIKit/URLServer+Task.swift index 3e42f02..63fa5f3 100644 --- a/Sources/FTAPIKit/URLServer+Task.swift +++ b/Sources/FTAPIKit/URLServer+Task.swift @@ -52,7 +52,7 @@ extension URLServer { func request(endpoint: Endpoint) -> Result { do { - let request = try requestBuilder(self, endpoint) + let request = try buildRequest(endpoint: endpoint) return .success(request) } catch { return apiError(error: error) diff --git a/Sources/FTAPIKit/URLServer.swift b/Sources/FTAPIKit/URLServer.swift index 7b17f1f..f7d55ee 100644 --- a/Sources/FTAPIKit/URLServer.swift +++ b/Sources/FTAPIKit/URLServer.swift @@ -11,5 +11,8 @@ public extension URLServer { var urlSession: URLSession { .shared } var decoding: Decoding { JSONDecoding() } var encoding: Encoding { JSONEncoding() } - var requestBuilder: (Self, Endpoint) throws -> URLRequest { Self.buildStandardRequest } + + func buildRequest(endpoint: Endpoint) throws -> URLRequest { + try buildStandardRequest(endpoint: endpoint) + } } diff --git a/Tests/FTAPIKitTests/Mockups/Servers.swift b/Tests/FTAPIKitTests/Mockups/Servers.swift index bc41862..35ec731 100644 --- a/Tests/FTAPIKitTests/Mockups/Servers.swift +++ b/Tests/FTAPIKitTests/Mockups/Servers.swift @@ -5,8 +5,8 @@ struct HTTPBinServer: URLServer { let urlSession = URLSession(configuration: .ephemeral) let baseUri = URL(string: "http://httpbin.org/")! - let requestBuilder: (Self, Endpoint) throws -> URLRequest = { server, endpoint in - var request = try buildStandardRequest(server: server, endpoint: endpoint) + func buildRequest(endpoint: Endpoint) throws -> URLRequest { + var request = try buildStandardRequest(endpoint: endpoint) if endpoint is AuthorizedEndpoint { request.addValue("Bearer \(UUID().uuidString)", forHTTPHeaderField: "Authorization") } From e300a9852bfbce08d98df23d0a445024491dd884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Fri, 7 Feb 2020 10:16:42 +0100 Subject: [PATCH 32/36] Add basic URL encoded endpoint --- Sources/FTAPIKit/CaracterSet+UrlQuery.swift | 10 ++++++++++ Sources/FTAPIKit/Endpoint.swift | 4 ++++ Sources/FTAPIKit/URL+Query.swift | 6 +++--- Sources/FTAPIKit/URLRequestBuilder.swift | 19 +++++++++++++++++-- Tests/FTAPIKitTests/Mockups/Endpoints.swift | 9 +++++++++ Tests/FTAPIKitTests/ResponseTests.swift | 13 +++++++++++++ 6 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 Sources/FTAPIKit/CaracterSet+UrlQuery.swift diff --git a/Sources/FTAPIKit/CaracterSet+UrlQuery.swift b/Sources/FTAPIKit/CaracterSet+UrlQuery.swift new file mode 100644 index 0000000..7fd6a15 --- /dev/null +++ b/Sources/FTAPIKit/CaracterSet+UrlQuery.swift @@ -0,0 +1,10 @@ +import Foundation + +extension CharacterSet { + private static let urlGeneralDelimiters: CharacterSet = [":", "/", "?", "#", "[", "]", "@"] + private static let urlSubDelimiters: CharacterSet = ["!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="] + private static let urlDelimiters = CharacterSet.urlGeneralDelimiters.union(.urlSubDelimiters) + + /// https://tools.ietf.org/html/rfc3986#section-2.2 + static let urlQueryNameValueAllowed = CharacterSet.urlQueryAllowed.subtracting(.urlDelimiters) +} diff --git a/Sources/FTAPIKit/Endpoint.swift b/Sources/FTAPIKit/Endpoint.swift index c2fc99e..c2d26ba 100644 --- a/Sources/FTAPIKit/Endpoint.swift +++ b/Sources/FTAPIKit/Endpoint.swift @@ -41,6 +41,10 @@ public protocol MultipartEndpoint: Endpoint { var parts: [MultipartBodyPart] { get } } +public protocol URLEncodedEndpoint: Endpoint { + var body: [String: String] { get } +} + /// Endpoint protocol extending `Endpoint` having decodable associated type, which is used /// for automatic deserialization. public protocol ResponseEndpoint: Endpoint { diff --git a/Sources/FTAPIKit/URL+Query.swift b/Sources/FTAPIKit/URL+Query.swift index 0a68dd0..df3054e 100644 --- a/Sources/FTAPIKit/URL+Query.swift +++ b/Sources/FTAPIKit/URL+Query.swift @@ -4,17 +4,17 @@ import MobileCoreServices #endif extension URL { - mutating func appendQuery(parameters: [String: String]) { + mutating func appendQuery(parameters: [URLQueryItem]) { self = appendingQuery(parameters: parameters) } - func appendingQuery(parameters: [String: String]) -> URL { + func appendingQuery(parameters: [URLQueryItem]) -> URL { guard !parameters.isEmpty else { return self } var components = URLComponents(url: self, resolvingAgainstBaseURL: true) let oldItems = components?.queryItems ?? [] - components?.queryItems = oldItems + parameters.map(URLQueryItem.init) + components?.queryItems = oldItems + parameters return components?.url ?? self } } diff --git a/Sources/FTAPIKit/URLRequestBuilder.swift b/Sources/FTAPIKit/URLRequestBuilder.swift index 09b73d9..1146329 100644 --- a/Sources/FTAPIKit/URLRequestBuilder.swift +++ b/Sources/FTAPIKit/URLRequestBuilder.swift @@ -18,13 +18,13 @@ struct URLRequestBuilder { func build() throws -> URLRequest { let url = server.baseUri .appendingPathComponent(endpoint.path) - .appendingQuery(parameters: endpoint.query) + .appendingQuery(parameters: queryItems(parameters: endpoint.query)) var request = URLRequest(url: url) request.httpMethod = endpoint.method.description request.allHTTPHeaderFields = endpoint.headers - try buildBody(to: &request) try server.encoding.configure(request: &request) + try buildBody(to: &request) return request } @@ -34,6 +34,11 @@ struct URLRequestBuilder { request.httpBody = endpoint.body case let endpoint as EncodableEndpoint: request.httpBody = try endpoint.body(encoding: server.encoding) + case let endpoint as URLEncodedEndpoint: + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + var urlComponents = URLComponents() + urlComponents.queryItems = queryItems(parameters: endpoint.body) + request.httpBody = urlComponents.query?.data(using: .ascii) case let endpoint as MultipartEndpoint: let formData = MultipartFormData(parts: endpoint.parts) request.httpBodyStream = try formData.inputStream() @@ -45,4 +50,14 @@ struct URLRequestBuilder { break } } + + private func queryItems(parameters: [String: String]) -> [URLQueryItem] { + parameters.compactMap { key, value in + guard let encodedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryNameValueAllowed), + let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryNameValueAllowed) else { + return nil + } + return URLQueryItem(name: encodedKey, value: encodedValue) + } + } } diff --git a/Tests/FTAPIKitTests/Mockups/Endpoints.swift b/Tests/FTAPIKitTests/Mockups/Endpoints.swift index 4dcdcaa..a94c94b 100644 --- a/Tests/FTAPIKitTests/Mockups/Endpoints.swift +++ b/Tests/FTAPIKitTests/Mockups/Endpoints.swift @@ -75,6 +75,15 @@ struct TestMultipartEndpoint: MultipartEndpoint { } } +struct TestURLEncodedEndpoint: URLEncodedEndpoint { + let path = "post" + let method: HTTPMethod = .post + let body: [String: String] = [ + "param1": "value1", + "param2": "value2", + ] +} + struct TestUploadEndpoint: UploadEndpoint { let file: URL let path = "put" diff --git a/Tests/FTAPIKitTests/ResponseTests.swift b/Tests/FTAPIKitTests/ResponseTests.swift index 1a6a350..5aa074b 100644 --- a/Tests/FTAPIKitTests/ResponseTests.swift +++ b/Tests/FTAPIKitTests/ResponseTests.swift @@ -169,6 +169,19 @@ final class ResponseTests: XCTestCase { wait(for: [expectation], timeout: timeout) } + func testURLEncodedEndpoint() { + let server = HTTPBinServer() + let endpoint = TestURLEncodedEndpoint() + let expectation = self.expectation(description: "Result") + server.call(data: endpoint) { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + } + func testUploadTask() { let server = HTTPBinServer() let file = File() From 3853a34afd4763e019204f1862133ba6bc9c1d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Fri, 7 Feb 2020 10:26:25 +0100 Subject: [PATCH 33/36] Fix linting --- Package.swift | 2 +- Sources/FTAPIKit/Coding.swift | 1 - Sources/FTAPIKit/Server.swift | 1 - Sources/FTAPIKit/URLServer+Call.swift | 3 +-- Tests/FTAPIKitTests/Mockups/Endpoints.swift | 2 +- Tests/FTAPIKitTests/Mockups/Errors.swift | 2 +- Tests/FTAPIKitTests/ResponseTests.swift | 21 +++++++++++++-------- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Package.swift b/Package.swift index 61c4086..76b577d 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ let package = Package( products: [ .library( name: "FTAPIKit", - targets: ["FTAPIKit"]), + targets: ["FTAPIKit"]) ], targets: [ .target( diff --git a/Sources/FTAPIKit/Coding.swift b/Sources/FTAPIKit/Coding.swift index f3a70af..366f645 100644 --- a/Sources/FTAPIKit/Coding.swift +++ b/Sources/FTAPIKit/Coding.swift @@ -31,7 +31,6 @@ public struct JSONEncoding: Encoding { } } - public struct JSONDecoding: Decoding { private let decoder: JSONDecoder diff --git a/Sources/FTAPIKit/Server.swift b/Sources/FTAPIKit/Server.swift index 7378b21..25d9ce5 100644 --- a/Sources/FTAPIKit/Server.swift +++ b/Sources/FTAPIKit/Server.swift @@ -1,4 +1,3 @@ - public protocol Server { associatedtype Request diff --git a/Sources/FTAPIKit/URLServer+Call.swift b/Sources/FTAPIKit/URLServer+Call.swift index 2e6892c..0686bcb 100644 --- a/Sources/FTAPIKit/URLServer+Call.swift +++ b/Sources/FTAPIKit/URLServer+Call.swift @@ -12,7 +12,6 @@ public extension URLServer { } } - @discardableResult func call(data endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionTask? { switch request(endpoint: endpoint) { @@ -44,7 +43,7 @@ public extension URLServer { }, completion: completion) } - private func call(data request: URLRequest, file: URL?, completion: @escaping (Result) -> Void) -> URLSessionTask? { + private func call(data request: URLRequest, file: URL?, completion: @escaping (Result) -> Void) -> URLSessionTask? { task(request: request, file: file, process: { data, response, error in if let error = ErrorType(data: data, response: response, error: error, decoding: self.decoding) { return .failure(error) diff --git a/Tests/FTAPIKitTests/Mockups/Endpoints.swift b/Tests/FTAPIKitTests/Mockups/Endpoints.swift index a94c94b..00d2a77 100644 --- a/Tests/FTAPIKitTests/Mockups/Endpoints.swift +++ b/Tests/FTAPIKitTests/Mockups/Endpoints.swift @@ -80,7 +80,7 @@ struct TestURLEncodedEndpoint: URLEncodedEndpoint { let method: HTTPMethod = .post let body: [String: String] = [ "param1": "value1", - "param2": "value2", + "param2": "value2" ] } diff --git a/Tests/FTAPIKitTests/Mockups/Errors.swift b/Tests/FTAPIKitTests/Mockups/Errors.swift index 0d329b2..55344b0 100644 --- a/Tests/FTAPIKitTests/Mockups/Errors.swift +++ b/Tests/FTAPIKitTests/Mockups/Errors.swift @@ -8,5 +8,5 @@ struct ThrowawayAPIError: APIError { self.init() } - static var unhandled = Self.init() + static var unhandled = Self() } diff --git a/Tests/FTAPIKitTests/ResponseTests.swift b/Tests/FTAPIKitTests/ResponseTests.swift index 5aa074b..ec7bab6 100644 --- a/Tests/FTAPIKitTests/ResponseTests.swift +++ b/Tests/FTAPIKitTests/ResponseTests.swift @@ -158,15 +158,20 @@ final class ResponseTests: XCTestCase { let server = HTTPBinServer() let file = File() XCTAssertNoThrow(try file.write()) - let endpoint = try! TestMultipartEndpoint(file: file) - let expectation = self.expectation(description: "Result") - server.call(data: endpoint) { result in - if case let .failure(error) = result { - XCTFail(error.localizedDescription) + do { + let endpoint = try TestMultipartEndpoint(file: file) + let expectation = self.expectation(description: "Result") + server.call(data: endpoint) { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation.fulfill() } - expectation.fulfill() + wait(for: [expectation], timeout: timeout) + } catch { + let errorFunc = { throw error } + XCTAssertNoThrow(errorFunc) } - wait(for: [expectation], timeout: timeout) } func testURLEncodedEndpoint() { @@ -223,6 +228,6 @@ final class ResponseTests: XCTestCase { ("testAuthorization", testAuthorization), ("testMultipartData", testMultipartData), ("testUploadTask", testUploadTask), - ("testDownloadTask", testDownloadTask), + ("testDownloadTask", testDownloadTask) ] } From 025fa01fb7a66527de2906143fe6450403491bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Fri, 7 Feb 2020 16:45:11 +0100 Subject: [PATCH 34/36] Add Combine support --- Sources/FTAPIKit/URLServer+Combine.swift | 30 ++++++++++ .../{ResponseTests.swift => CallTests.swift} | 2 +- Tests/FTAPIKitTests/PublisherTests.swift | 55 +++++++++++++++++++ Tests/LinuxMain.swift | 2 +- 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 Sources/FTAPIKit/URLServer+Combine.swift rename Tests/FTAPIKitTests/{ResponseTests.swift => CallTests.swift} (99%) create mode 100644 Tests/FTAPIKitTests/PublisherTests.swift diff --git a/Sources/FTAPIKit/URLServer+Combine.swift b/Sources/FTAPIKit/URLServer+Combine.swift new file mode 100644 index 0000000..ab87803 --- /dev/null +++ b/Sources/FTAPIKit/URLServer+Combine.swift @@ -0,0 +1,30 @@ +import Combine +import Foundation + +@available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +public extension URLServer { + func publisher(endpoint: Endpoint) -> AnyPublisher { + request(endpoint: endpoint) + .publisher + .flatMap { request in + self.urlSession + .dataTaskPublisher(for: request) + .mapError { ErrorType(data: nil, response: nil, error: $0, decoding: self.decoding) ?? .unhandled } + } + .map(\.data) + .eraseToAnyPublisher() + } + + func publisher(response endpoint: EP) -> AnyPublisher { + publisher(endpoint: endpoint) + .tryMap(self.decoding.decode) + .mapError { error in + if let error = error as? ErrorType { + return error + } else if let error = ErrorType(data: nil, response: nil, error: error, decoding: self.decoding) { + return error + } + return .unhandled + }.eraseToAnyPublisher() + } +} diff --git a/Tests/FTAPIKitTests/ResponseTests.swift b/Tests/FTAPIKitTests/CallTests.swift similarity index 99% rename from Tests/FTAPIKitTests/ResponseTests.swift rename to Tests/FTAPIKitTests/CallTests.swift index ec7bab6..f8f9d65 100644 --- a/Tests/FTAPIKitTests/ResponseTests.swift +++ b/Tests/FTAPIKitTests/CallTests.swift @@ -1,7 +1,7 @@ import XCTest import FTAPIKit -final class ResponseTests: XCTestCase { +final class CallTests: XCTestCase { private let timeout: TimeInterval = 30.0 func testGet() { diff --git a/Tests/FTAPIKitTests/PublisherTests.swift b/Tests/FTAPIKitTests/PublisherTests.swift new file mode 100644 index 0000000..fbaf985 --- /dev/null +++ b/Tests/FTAPIKitTests/PublisherTests.swift @@ -0,0 +1,55 @@ + +import XCTest +import FTAPIKit +import Combine + +@available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +final class PublisherTests: XCTestCase { + private let timeout: TimeInterval = 30.0 + + private var cancellable: AnyCancellable? + + override func tearDown() { + super.tearDown() + cancellable = nil + } + + func testGet() { + let server = HTTPBinServer() + let endpoint = GetEndpoint() + let expectation = self.expectation(description: "Result") + cancellable = server.publisher(endpoint: endpoint) + .assertNoFailure() + .sink { _ in expectation.fulfill() } + wait(for: [expectation], timeout: timeout) + } + + func testConnectionError() { + let server = NonExistingServer() + let endpoint = NotFoundEndpoint() + let expectation = self.expectation(description: "Result") + cancellable = server.publisher(endpoint: endpoint) + .map { _ in XCTFail() } + .mapError { error -> APIErrorStandard in + if case .connection = error { + XCTAssertTrue(true) + } else { + XCTFail() + } + return error + } + .replaceError(with: ()) + .sink { _ in expectation.fulfill() } + wait(for: [expectation], timeout: timeout) + } + + func testValidJSONResponse() { + let server = HTTPBinServer() + let endpoint = JSONResponseEndpoint() + let expectation = self.expectation(description: "Result") + cancellable = server.publisher(response: endpoint) + .assertNoFailure() + .sink { _ in expectation.fulfill() } + wait(for: [expectation], timeout: timeout) + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 57985e7..f3eeec8 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -2,5 +2,5 @@ import XCTest @testable import FTAPIKitTests XCTMain([ - testCase(ResponseTests.allTests) + testCase(CallTests.allTests) ]) From 5c796a6ec0e20e9e99b235d1c4d9b7870f5af301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Mon, 10 Feb 2020 14:53:38 +0100 Subject: [PATCH 35/36] Fix multipart boundaries --- Sources/FTAPIKit/MultipartFormData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/FTAPIKit/MultipartFormData.swift b/Sources/FTAPIKit/MultipartFormData.swift index b21f268..fae0ac4 100644 --- a/Sources/FTAPIKit/MultipartFormData.swift +++ b/Sources/FTAPIKit/MultipartFormData.swift @@ -44,7 +44,7 @@ struct MultipartFormData { defer { outputStream.close() } - let boundaryData = Data(boundary.utf8) + let boundaryData = Data("--\(boundary)".utf8) for part in parts { try outputStream.write(data: boundaryData) try outputStream.writeLine() From 44c1acccf07ec24734289a1452b8a5f6a0888845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mate=CC=8Cj=20Kas=CC=8Cpar=20Jira=CC=81sek?= Date: Mon, 10 Feb 2020 14:55:32 +0100 Subject: [PATCH 36/36] Revert "Add Combine support" This reverts commit 025fa01fb7a66527de2906143fe6450403491bdf. --- Sources/FTAPIKit/URLServer+Combine.swift | 30 ---------- Tests/FTAPIKitTests/PublisherTests.swift | 55 ------------------- .../{CallTests.swift => ResponseTests.swift} | 2 +- Tests/LinuxMain.swift | 2 +- 4 files changed, 2 insertions(+), 87 deletions(-) delete mode 100644 Sources/FTAPIKit/URLServer+Combine.swift delete mode 100644 Tests/FTAPIKitTests/PublisherTests.swift rename Tests/FTAPIKitTests/{CallTests.swift => ResponseTests.swift} (99%) diff --git a/Sources/FTAPIKit/URLServer+Combine.swift b/Sources/FTAPIKit/URLServer+Combine.swift deleted file mode 100644 index ab87803..0000000 --- a/Sources/FTAPIKit/URLServer+Combine.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Combine -import Foundation - -@available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -public extension URLServer { - func publisher(endpoint: Endpoint) -> AnyPublisher { - request(endpoint: endpoint) - .publisher - .flatMap { request in - self.urlSession - .dataTaskPublisher(for: request) - .mapError { ErrorType(data: nil, response: nil, error: $0, decoding: self.decoding) ?? .unhandled } - } - .map(\.data) - .eraseToAnyPublisher() - } - - func publisher(response endpoint: EP) -> AnyPublisher { - publisher(endpoint: endpoint) - .tryMap(self.decoding.decode) - .mapError { error in - if let error = error as? ErrorType { - return error - } else if let error = ErrorType(data: nil, response: nil, error: error, decoding: self.decoding) { - return error - } - return .unhandled - }.eraseToAnyPublisher() - } -} diff --git a/Tests/FTAPIKitTests/PublisherTests.swift b/Tests/FTAPIKitTests/PublisherTests.swift deleted file mode 100644 index fbaf985..0000000 --- a/Tests/FTAPIKitTests/PublisherTests.swift +++ /dev/null @@ -1,55 +0,0 @@ - -import XCTest -import FTAPIKit -import Combine - -@available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -final class PublisherTests: XCTestCase { - private let timeout: TimeInterval = 30.0 - - private var cancellable: AnyCancellable? - - override func tearDown() { - super.tearDown() - cancellable = nil - } - - func testGet() { - let server = HTTPBinServer() - let endpoint = GetEndpoint() - let expectation = self.expectation(description: "Result") - cancellable = server.publisher(endpoint: endpoint) - .assertNoFailure() - .sink { _ in expectation.fulfill() } - wait(for: [expectation], timeout: timeout) - } - - func testConnectionError() { - let server = NonExistingServer() - let endpoint = NotFoundEndpoint() - let expectation = self.expectation(description: "Result") - cancellable = server.publisher(endpoint: endpoint) - .map { _ in XCTFail() } - .mapError { error -> APIErrorStandard in - if case .connection = error { - XCTAssertTrue(true) - } else { - XCTFail() - } - return error - } - .replaceError(with: ()) - .sink { _ in expectation.fulfill() } - wait(for: [expectation], timeout: timeout) - } - - func testValidJSONResponse() { - let server = HTTPBinServer() - let endpoint = JSONResponseEndpoint() - let expectation = self.expectation(description: "Result") - cancellable = server.publisher(response: endpoint) - .assertNoFailure() - .sink { _ in expectation.fulfill() } - wait(for: [expectation], timeout: timeout) - } -} diff --git a/Tests/FTAPIKitTests/CallTests.swift b/Tests/FTAPIKitTests/ResponseTests.swift similarity index 99% rename from Tests/FTAPIKitTests/CallTests.swift rename to Tests/FTAPIKitTests/ResponseTests.swift index f8f9d65..ec7bab6 100644 --- a/Tests/FTAPIKitTests/CallTests.swift +++ b/Tests/FTAPIKitTests/ResponseTests.swift @@ -1,7 +1,7 @@ import XCTest import FTAPIKit -final class CallTests: XCTestCase { +final class ResponseTests: XCTestCase { private let timeout: TimeInterval = 30.0 func testGet() { diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index f3eeec8..57985e7 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -2,5 +2,5 @@ import XCTest @testable import FTAPIKitTests XCTMain([ - testCase(CallTests.allTests) + testCase(ResponseTests.allTests) ])