diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..4b9e36c
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @futuredapp/ios
diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml
new file mode 100644
index 0000000..68df7ec
--- /dev/null
+++ b/.github/workflows/swift.yml
@@ -0,0 +1,19 @@
+name: Swift
+
+on:
+ pull_request:
+ branches:
+ - develop
+ - master
+
+jobs:
+ build:
+
+ runs-on: macos-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Build
+ run: swift build
+ - name: Run tests
+ run: swift test
diff --git a/Documentation/Architecture.svg b/Documentation/Architecture.svg
new file mode 100644
index 0000000..b310924
--- /dev/null
+++ b/Documentation/Architecture.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Documentation/Endpoints.svg b/Documentation/Endpoints.svg
new file mode 100644
index 0000000..7664ca2
--- /dev/null
+++ b/Documentation/Endpoints.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/FTAPIKit.podspec b/FTAPIKit.podspec
index 37e1fe5..797f3cd 100644
--- a/FTAPIKit.podspec
+++ b/FTAPIKit.podspec
@@ -1,36 +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.default_subspec = 'Core'
- 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.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.source = { git: "https://github.com/futuredapp/FTAPIKit.git", tag: s.version.to_s }
+ s.source_files = "Sources/FTAPIKit/*"
- s.subspec 'PromiseKit' do |ss|
- ss.source_files = Dir['Sources/FTAPIKitPromiseKit/*']
- ss.dependency 'PromiseKit', '~> 6.0'
- ss.dependency 'FTAPIKit/Core'
- end
+ 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
diff --git a/Package.swift b/Package.swift
index b50a279..76b577d 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.0
+// swift-tools-version:5.1
import PackageDescription
@@ -7,21 +7,12 @@ let package = Package(
products: [
.library(
name: "FTAPIKit",
- targets: ["FTAPIKit"]),
- .library(
- name: "FTAPIKitPromiseKit",
- targets: ["FTAPIKitPromiseKit"])
- ],
- dependencies: [
- .package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.8.4")
+ targets: ["FTAPIKit"])
],
targets: [
.target(
name: "FTAPIKit",
dependencies: []),
- .target(
- name: "FTAPIKitPromiseKit",
- dependencies: ["FTAPIKit", "PromiseKit"]),
.testTarget(
name: "FTAPIKitTests",
dependencies: ["FTAPIKit"])
diff --git a/README.md b/README.md
index 238e98f..2c917ee 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,156 @@
# FTAPIKit
-Declarative, generic REST API framework using URLSession and Codable
+![Swift](https://github.com/futuredapp/FTAPIKit/workflows/Swift/badge.svg)
+![Cocoapods](https://img.shields.io/cocoapods/v/FTAPIKit)
+![Cocoapods platforms](https://img.shields.io/cocoapods/p/FTAPIKit)
+![License](https://img.shields.io/cocoapods/l/FTAPIKit)
-## FTAPIKit templates installation
+Declarative and generic REST API framework using Codable.
+With standard implementation using URLSesssion and JSON encoder/decoder.
+Easily extensible for your asynchronous framework or networking stack.
-The templates can be added to Xcode simply running `make` inside `Templates/` directory.
+## Installation
+
+When using Swift package manager install using Xcode 11+
+or add following line to your dependencies:
+
+```swift
+.package(url: "https://github.com/futuredapp/FTAPIKit.git", from: "1.0.0")
+```
+
+When using CocoaPods add following line to your `Podfile`:
+
+```ruby
+pod 'FTAPIKit', '~> 1.0'
+```
+
+# Features
+
+The main feature of this library is to provide documentation-like API
+for defining web services. This is achieved using declarative
+and protocol-oriented programming in Swift.
+
+The framework provides two core protocols reflecting the physical infrastructure:
+
+- `Server` protocol defining single web service.
+- `Endpoint` protocol defining access points for resources.
+
+Combining instances of type conforming to `Server` and `Endpoint` we can build request.
+`URLServer` has convenience method for calling endpoints using `URLSession`.
+If some advanced features are required then we recommend implementing API client.
+This client should encapsulate logic which is not provided by this framework
+(like signing authorized endpoints or conforming to `URLSessionDelegate`).
+
+![Architecture](Documentation/Architecture.svg)
+
+This package contains predefined `Endpoint` protocols.
+Use cases like multipart upload, automatic encoding/decoding
+are separated in various protocols for convenience.
+
+- `Endpoint` protocol has empty body. Typically used in `GET` endpoints.
+- `DataEndpoint` sends provided data in body.
+- `UploadEndpoint` uploads file using `InputStream`.
+- `MultipartEndpoint` combines body parts into `InputStream` and sends them to server.
+ Body parts are represented by `MultipartBodyPart` struct and provided to the endpoint
+ in an array.
+- `RequestEndpoint` has encodable request which is encoded using encoding
+ of the `Server` instance.
+
+![Endpoint types](Documentation/Endpoints.svg)
+
+## Usage
+
+### Defining web service (server)
+
+Firstly we need to define our server. Structs are preferred but not required:
+
+```swift
+struct HTTPBinServer: URLServer {
+ let baseUri = URL(string: "http://httpbin.org/")!
+ let urlSession = URLSession(configuration: .default)
+}
+```
+
+If we want to use custom formatting we just need to add our encoding/decoding configuration:
+
+```swift
+struct HTTPBinServer: URLServer {
+ ...
+
+ let decoding: Decoding = JSONDecoding { decoder in
+ decoder.keyDecodingStrategy = .convertFromSnakeCase
+ }
+ let encoding: Encoding = JSONEncoding { encoder in
+ encoder.keyEncodingStrategy = .convertToSnakeCase
+ }
+}
+```
+
+If we need to create specific request, add some headers, usually to provide
+authorization we can override default request building mechanism.
+
+```swift
+struct HTTPBinServer: URLServer {
+ ...
+ func buildRequest(endpoint: Endpoint) throws -> URLRequest {
+ var request = try buildStandardRequest(endpoint: endpoint)
+ request.addValue("MyApp/1.0.0", forHTTPHeaderField: "User-Agent")
+ return request
+ }
+}
+```
+
+### Defining endpoints
+
+Most basic `GET` endpoint can be implemented using `Endpoint` protocol,
+all default propertires are inferred.
+
+```swift
+struct GetEndpoint: Endpoint {
+ let path = "get"
+}
+```
+
+Let's take more complicated example like updating some model.
+We need to supply encodable request and decodable response.
+
+```swift
+struct UpdateUserEndpoint: RequestResponseEndpoint {
+ typealias Response = User
+
+ let request: User
+ let path = "user"
+}
+```
+
+### Executing the request
+
+When we have server and enpoint defined we can call the web service:
+
+```swift
+let server = HTTPBinServer()
+let endpoint = UpdateUserEndpoint(request: user)
+server.call(response: endpoint) { result in
+ switch result {
+ case .success(let updatedUser):
+ ...
+ case .failure(let error):
+ ...
+ }
+}
+```
+
+## Contributors
+
+Current maintainer and main contributor is [Matěj Kašpar Jirásek](https://github.com/mkj-is), .
+
+We want to thank other contributors, namely:
+
+- [Mikoláš Stuchlík](https://github.com/mikolasstuchlik)
+- [Radek Doležal](https://github.com/eRDe33)
+- [Adam Bezák](https://github.com/bezoadam)
+- [Patrik Potoček](https://github.com/Patrez)
+
+## License
+
+FTAPIKit is available under the MIT license. See the [LICENSE file](LICENSE) for more information.
diff --git a/Sources/FTAPIKit/APIAdapter+Types.swift b/Sources/FTAPIKit/APIAdapter+Types.swift
deleted file mode 100644
index 2ded909..0000000
--- a/Sources/FTAPIKit/APIAdapter+Types.swift
+++ /dev/null
@@ -1,54 +0,0 @@
-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/APIAdapter.swift b/Sources/FTAPIKit/APIAdapter.swift
deleted file mode 100644
index 162da1f..0000000
--- a/Sources/FTAPIKit/APIAdapter.swift
+++ /dev/null
@@ -1,55 +0,0 @@
-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 (Result) -> 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 (Result) -> 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 (Result) -> Void)
-}
diff --git a/Sources/FTAPIKit/APIEndpoint.swift b/Sources/FTAPIKit/APIEndpoint.swift
deleted file mode 100644
index 6ef2e73..0000000
--- a/Sources/FTAPIKit/APIEndpoint.swift
+++ /dev/null
@@ -1,85 +0,0 @@
-/// Protocol describing API endpoint. API Endpoint describes one URI with all the
-/// data and parameters which are sent to it.
-///
-/// Recommended conformance of this protocol is implemented using `struct`. It is
-/// of course possible using `enum` or `class`. Endpoints are are not designed
-/// to be referenced and used instantly after creation, so no memory usage is required.
-/// The case for not using enums is long-term sustainability. Enums tend to have many
-/// 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 {
-
- /// 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 }
-
- /// 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 }
-}
-
-public extension APIEndpoint {
- var parameters: HTTPParameters {
- return [:]
- }
-
- var type: RequestType {
- return .jsonParams
- }
-
- var method: HTTPMethod {
- return .get
- }
-
- var authorized: Bool {
- return false
- }
-}
-
-/// Endpoint protocol extending `APIEndpoint` having decodable associated type, which is used
-/// for automatic deserialization.
-public protocol APIResponseEndpoint: APIEndpoint {
- /// 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 {
- /// Associated type describing the encodable request model for
- /// JSON serialization. The associated type is derived from
- /// the body property.
- associatedtype Request: Encodable
- /// Generic encodable model, which will be sent as JSON body.
- var body: Request { get }
-}
-
-public extension APIRequestEndpoint {
- var method: HTTPMethod {
- return .post
- }
-
- var type: RequestType {
- return RequestType.jsonBody(body)
- }
-}
-
-/// 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
diff --git a/Sources/FTAPIKit/APIError+Standard.swift b/Sources/FTAPIKit/APIError+Standard.swift
new file mode 100644
index 0000000..993d833
--- /dev/null
+++ b/Sources/FTAPIKit/APIError+Standard.swift
@@ -0,0 +1,39 @@
+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 (_, _, 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:
+ 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 90834bf..29c4e6b 100644
--- a/Sources/FTAPIKit/APIError.swift
+++ b/Sources/FTAPIKit/APIError.swift
@@ -1,42 +1,9 @@
import Foundation
public protocol APIError: Error {
- init?(data: Data?, response: URLResponse?, error: Error?, decoder: JSONDecoder)
-}
+ 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?, decoder: JSONDecoder) {
- 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 7b319aa..0000000
--- a/Sources/FTAPIKit/AnyEncodable.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-
-struct AnyEncodable: Encodable {
- private let anyEncode: (Encoder) throws -> Void
-
- init(_ encodable: Encodable) {
- anyEncode = encodable.encode
- }
-
- func encode(to encoder: Encoder) throws {
- try anyEncode(encoder)
- }
-}
diff --git a/Sources/FTAPIKit/CharacterSet+APIAdapter.swift b/Sources/FTAPIKit/CaracterSet+UrlQuery.swift
similarity index 100%
rename from Sources/FTAPIKit/CharacterSet+APIAdapter.swift
rename to Sources/FTAPIKit/CaracterSet+UrlQuery.swift
diff --git a/Sources/FTAPIKit/Coding.swift b/Sources/FTAPIKit/Coding.swift
new file mode 100644
index 0000000..366f645
--- /dev/null
+++ b/Sources/FTAPIKit/Coding.swift
@@ -0,0 +1,50 @@
+import Foundation
+
+public protocol Encoding {
+ func encode(_ object: T) throws -> Data
+ func configure(request: inout URLRequest) throws
+}
+
+public protocol Decoding {
+ func decode(data: Data) throws -> T
+}
+
+public struct JSONEncoding: Encoding {
+ private 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 func configure(request: inout URLRequest) throws {
+ request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
+ }
+}
+
+public struct JSONDecoding: Decoding {
+ private 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/Endpoint.swift b/Sources/FTAPIKit/Endpoint.swift
new file mode 100644
index 0000000..c2d26ba
--- /dev/null
+++ b/Sources/FTAPIKit/Endpoint.swift
@@ -0,0 +1,83 @@
+import Foundation
+
+/// Protocol describing API endpoint. API Endpoint describes one URI with all the
+/// data and parameters which are sent to it.
+///
+/// Recommended conformance of this protocol is implemented using `struct`. It is
+/// of course possible using `enum` or `class`. Endpoints are are not designed
+/// to be referenced and used instantly after creation, so no memory usage is required.
+/// The case for not using enums is long-term sustainability. Enums tend to have many
+/// cases and information about one endpoint is spreaded all over the files. Also,
+/// structs offer us generated initializers, which is very helpful
+///
+public protocol Endpoint {
+
+ /// URL path component without base URI.
+ var path: String { get }
+
+ var headers: [String: String] { get }
+
+ var query: [String: String] { get }
+
+ /// HTTP method/verb describing the action.
+ var method: HTTPMethod { get }
+}
+
+public extension Endpoint {
+ var headers: [String: String] { [:] }
+ var query: [String: String] { [:] }
+ var method: HTTPMethod { .get }
+}
+
+public protocol DataEndpoint: Endpoint {
+ var body: Data { get }
+}
+
+public protocol UploadEndpoint: Endpoint {
+ var file: URL { get }
+}
+
+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 {
+ /// 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 `Endpoint` encapsulating and improving sending JSON models to API.
+public protocol RequestEndpoint: EncodableEndpoint {
+ /// Associated type describing the encodable request model for
+ /// JSON serialization. The associated type is derived from
+ /// the body property.
+ associatedtype Request: Encodable
+ /// Generic encodable model, which will be sent as JSON body.
+ var request: Request { get }
+}
+
+public extension RequestEndpoint {
+ func body(encoding: Encoding) throws -> Data {
+ try encoding.encode(request)
+ }
+}
+
+public protocol EncodableEndpoint: 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/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 2f02109..ea1081c 100644
--- a/Sources/FTAPIKit/MultipartBodyPart.swift
+++ b/Sources/FTAPIKit/MultipartBodyPart.swift
@@ -1,13 +1,11 @@
-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
/// 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
@@ -51,7 +49,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 037aaa1..fae0ac4 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 {
@@ -27,19 +31,20 @@ 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 {
outputStream.close()
}
+ let boundaryData = Data("--\(boundary)".utf8)
for part in parts {
try outputStream.write(data: boundaryData)
try outputStream.writeLine()
diff --git a/Sources/FTAPIKit/Serialized.swift b/Sources/FTAPIKit/Serialized.swift
deleted file mode 100644
index 5aa75a4..0000000
--- a/Sources/FTAPIKit/Serialized.swift
+++ /dev/null
@@ -1,30 +0,0 @@
-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
new file mode 100644
index 0000000..25d9ce5
--- /dev/null
+++ b/Sources/FTAPIKit/Server.swift
@@ -0,0 +1,8 @@
+public protocol Server {
+ associatedtype Request
+
+ var decoding: Decoding { get }
+ var encoding: Encoding { get }
+
+ func buildRequest(endpoint: Endpoint) throws -> Request
+}
diff --git a/Sources/FTAPIKit/URL+APIAdapter.swift b/Sources/FTAPIKit/URL+APIAdapter.swift
deleted file mode 100644
index 00369f7..0000000
--- a/Sources/FTAPIKit/URL+APIAdapter.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-import Foundation
-#if os(iOS) || os(watchOS) || os(tvOS)
-import MobileCoreServices
-#endif
-
-extension URL {
- mutating func appendQuery(parameters: [String: String]) {
- self = appendingQuery(parameters: parameters)
- }
-
- func appendingQuery(parameters: [String: String]) -> 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)
- 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/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+Query.swift b/Sources/FTAPIKit/URL+Query.swift
new file mode 100644
index 0000000..df3054e
--- /dev/null
+++ b/Sources/FTAPIKit/URL+Query.swift
@@ -0,0 +1,20 @@
+import Foundation
+#if os(iOS) || os(watchOS) || os(tvOS)
+import MobileCoreServices
+#endif
+
+extension URL {
+ mutating func appendQuery(parameters: [URLQueryItem]) {
+ self = appendingQuery(parameters: parameters)
+ }
+
+ 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
+ return components?.url ?? self
+ }
+}
diff --git a/Sources/FTAPIKit/URLRequest+APIAdapter.swift b/Sources/FTAPIKit/URLRequest+APIAdapter.swift
deleted file mode 100644
index 7ab3036..0000000
--- a/Sources/FTAPIKit/URLRequest+APIAdapter.swift
+++ /dev/null
@@ -1,65 +0,0 @@
-import Foundation
-
-extension URLRequest {
- mutating func setRequestType(_ requestType: RequestType, parameters: HTTPParameters, using jsonEncoder: JSONEncoder) throws {
- switch requestType {
- case .jsonBody(let encodable):
- try setJSONBody(encodable: encodable, parameters: parameters, using: jsonEncoder)
- case .urlEncoded:
- setURLEncoded(parameters: parameters)
- case .jsonParams:
- setJSON(parameters: parameters, using: jsonEncoder)
- 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 jsonEncoder: JSONEncoder) {
- setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
- httpBody = body
- url?.appendQuery(parameters: parameters)
- }
-
- private mutating func setURLEncoded(parameters: HTTPParameters) {
- let queryItems: [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)
- }
- var urlComponents = URLComponents()
- urlComponents.queryItems = queryItems
- httpBody = urlComponents.query?.data(using: .ascii)
- 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)
- }
-}
diff --git a/Sources/FTAPIKit/URLRequestBuilder.swift b/Sources/FTAPIKit/URLRequestBuilder.swift
new file mode 100644
index 0000000..1146329
--- /dev/null
+++ b/Sources/FTAPIKit/URLRequestBuilder.swift
@@ -0,0 +1,63 @@
+import Foundation
+
+public extension URLServer {
+ func buildStandardRequest(endpoint: Endpoint) throws -> URLRequest {
+ try URLRequestBuilder(server: self, endpoint: endpoint).build()
+ }
+}
+
+struct URLRequestBuilder {
+ 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
+ .appendingPathComponent(endpoint.path)
+ .appendingQuery(parameters: queryItems(parameters: endpoint.query))
+ var request = URLRequest(url: url)
+
+ request.httpMethod = endpoint.method.description
+ request.allHTTPHeaderFields = endpoint.headers
+ try server.encoding.configure(request: &request)
+ try buildBody(to: &request)
+ 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 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()
+ request.setValue(formData.contentType, forHTTPHeaderField: "Content-Type")
+ if let contentLength = formData.contentLength {
+ request.setValue(contentLength.description, forHTTPHeaderField: "Content-Length")
+ }
+ default:
+ 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/Sources/FTAPIKit/URLServer+Call.swift b/Sources/FTAPIKit/URLServer+Call.swift
new file mode 100644
index 0000000..0686bcb
--- /dev/null
+++ b/Sources/FTAPIKit/URLServer+Call.swift
@@ -0,0 +1,72 @@
+import Foundation
+
+public extension URLServer {
+ @discardableResult
+ func call(endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionTask? {
+ switch request(endpoint: endpoint) {
+ case .success(let request):
+ return call(request: request, file: uploadFile(endpoint: endpoint), completion: completion)
+ case .failure(let error):
+ completion(.failure(error))
+ return nil
+ }
+ }
+
+ @discardableResult
+ func call(data endpoint: Endpoint, completion: @escaping (Result) -> Void) -> URLSessionTask? {
+ switch request(endpoint: endpoint) {
+ case .success(let request):
+ return call(data: request, file: uploadFile(endpoint: endpoint), completion: completion)
+ case .failure(let error):
+ completion(.failure(error))
+ return nil
+ }
+ }
+
+ @discardableResult
+ func call(response endpoint: EP, completion: @escaping (Result) -> Void) -> URLSessionTask? {
+ switch request(endpoint: endpoint) {
+ case .success(let request):
+ return call(response: request, file: uploadFile(endpoint: endpoint), completion: completion)
+ case .failure(let error):
+ completion(.failure(error))
+ return nil
+ }
+ }
+
+ 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)
+ }
+
+ 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)
+ }
+
+ 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+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/Sources/FTAPIKit/URLServer+Task.swift b/Sources/FTAPIKit/URLServer+Task.swift
new file mode 100644
index 0000000..63fa5f3
--- /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 buildRequest(endpoint: 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/Sources/FTAPIKit/URLServer.swift b/Sources/FTAPIKit/URLServer.swift
new file mode 100644
index 0000000..f7d55ee
--- /dev/null
+++ b/Sources/FTAPIKit/URLServer.swift
@@ -0,0 +1,18 @@
+import Foundation
+
+public protocol URLServer: Server where Request == URLRequest {
+ associatedtype ErrorType: APIError = APIError.Standard
+
+ var baseUri: URL { get }
+ var urlSession: URLSession { get }
+}
+
+public extension URLServer {
+ var urlSession: URLSession { .shared }
+ var decoding: Decoding { JSONDecoding() }
+ var encoding: Encoding { JSONEncoding() }
+
+ func buildRequest(endpoint: Endpoint) throws -> URLRequest {
+ try buildStandardRequest(endpoint: endpoint)
+ }
+}
diff --git a/Sources/FTAPIKit/URLSessionAPIAdapter.swift b/Sources/FTAPIKit/URLSessionAPIAdapter.swift
deleted file mode 100644
index 85d8b93..0000000
--- a/Sources/FTAPIKit/URLSessionAPIAdapter.swift
+++ /dev/null
@@ -1,108 +0,0 @@
-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: Serialized
-
- /// 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
- self.runningRequestCount = Serialized(initialValue: 0)
-
- runningRequestCount.didSet = { [weak self] count in
- DispatchQueue.main.async {
- guard let self = self else {
- return
- }
- self.delegate?.apiAdapter(self, didUpdateRunningRequestCount: count)
- }
- }
- }
-
- public func request(response endpoint: Endpoint, completion: @escaping (Result) -> Void) {
- dataTask(response: endpoint, creation: { _ in }, completion: completion)
- }
-
- public func request(data endpoint: APIEndpoint, completion: @escaping (Result) -> Void) {
- dataTask(data: endpoint, creation: { _ in }, completion: completion)
- }
-
- public func dataTask(response endpoint: Endpoint, creation: @escaping (URLSessionTask) -> Void, completion: @escaping (Result) -> Void) {
- dataTask(data: endpoint, creation: creation) { result in
- completion(result.flatMap { data in
- Result(catching: { try self.jsonDecoder.decode(Endpoint.Response.self, from: data) })
- })
- }
- }
-
- public func dataTask(data endpoint: APIEndpoint, creation: @escaping (URLSessionTask) -> Void, completion: @escaping (Result) -> Void) {
- let url = baseUrl.appendingPathComponent(endpoint.path)
- var request = URLRequest(url: url)
- request.httpMethod = endpoint.method.description
-
- do {
- try request.setRequestType(endpoint.type, parameters: endpoint.parameters, using: jsonEncoder)
- } catch {
- completion(.failure(error))
- return
- }
-
- if let delegate = delegate {
- delegate.apiAdapter(self, willRequest: request, to: endpoint) { result in
- switch result {
- case .success(let request):
- let task = self.send(request: request, completion: completion)
- creation(task)
- case .failure(let error):
- completion(.failure(error))
- }
- }
- } else {
- let task = self.send(request: request, completion: completion)
- creation(task)
- }
- }
-
- private func send(request: URLRequest, completion: @escaping (Result) -> Void) -> URLSessionTask {
- runningRequestCount.asyncAccess { $0 + 1 }
- return resumeDataTask(with: request) { result in
- self.runningRequestCount.asyncAccess { $0 - 1 }
- completion(result)
- }
- }
-
- private func resumeDataTask(with request: URLRequest, completion: @escaping (Result) -> 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(.failure(error))
- } else {
- completion(.success(data ?? Data()))
- }
- }
- task.resume()
- return task
- }
-}
diff --git a/Sources/FTAPIKitPromiseKit/APIAdapter+PromiseKit.swift b/Sources/FTAPIKitPromiseKit/APIAdapter+PromiseKit.swift
deleted file mode 100644
index 68e8d93..0000000
--- a/Sources/FTAPIKitPromiseKit/APIAdapter+PromiseKit.swift
+++ /dev/null
@@ -1,19 +0,0 @@
-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: APIEndpoint) -> 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 6750418..0000000
--- a/Sources/FTAPIKitPromiseKit/Resolver+Result.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-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 7731105..0000000
--- a/Sources/FTAPIKitPromiseKit/URLSessionAPIAdapter+PromiseKit.swift
+++ /dev/null
@@ -1,39 +0,0 @@
-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: APIEndpoint) -> 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)
- }
-}
diff --git a/Templates/FTAPIKit/Endpoints.xctemplate/Default/___VARIABLE_endpointName___Endpoints.swift b/Templates/FTAPIKit/Endpoints.xctemplate/Default/___VARIABLE_endpointName___Endpoints.swift
deleted file mode 100644
index 4452dbd..0000000
--- a/Templates/FTAPIKit/Endpoints.xctemplate/Default/___VARIABLE_endpointName___Endpoints.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-//
-// ___FILENAME___
-// ___PROJECTNAME___
-//
-// Created by ___FULLUSERNAME___ on ___DATE___ using FTAPIKit Endpoint Template (v1.0).
-// Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved.
-//
-
-import FTAPIKit
-
-struct ___VARIABLE_templateName___Endpoint: APIEndpoint {
-
- let path: String = ""
-
-}
diff --git a/Templates/FTAPIKit/Endpoints.xctemplate/HasRequest/___VARIABLE_endpointName___Endpoints.swift b/Templates/FTAPIKit/Endpoints.xctemplate/HasRequest/___VARIABLE_endpointName___Endpoints.swift
deleted file mode 100644
index c2c5dd6..0000000
--- a/Templates/FTAPIKit/Endpoints.xctemplate/HasRequest/___VARIABLE_endpointName___Endpoints.swift
+++ /dev/null
@@ -1,16 +0,0 @@
-//
-// ___FILENAME___
-// ___PROJECTNAME___
-//
-// Created by ___FULLUSERNAME___ on ___DATE___ using FTAPIKit Request Endpoint Template (v1.0).
-// Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved.
-//
-
-import FTAPIKit
-
-struct ___VARIABLE_templateName___Endpoint: APIRequestEndpoint {
- typealias Request = ___VARIABLE_templateName___Request
-
- var body: ___VARIABLE_templateName___Request
- var path: String
-}
diff --git a/Templates/FTAPIKit/Endpoints.xctemplate/HasRequest/___VARIABLE_endpointName___Requests.swift b/Templates/FTAPIKit/Endpoints.xctemplate/HasRequest/___VARIABLE_endpointName___Requests.swift
deleted file mode 100644
index 2b7f84e..0000000
--- a/Templates/FTAPIKit/Endpoints.xctemplate/HasRequest/___VARIABLE_endpointName___Requests.swift
+++ /dev/null
@@ -1,11 +0,0 @@
-//
-// ___FILENAME___
-// ___PROJECTNAME___
-//
-// Created by ___FULLUSERNAME___ on ___DATE___ using FTAPIKit Request Endpoint Template (v1.0).
-// Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved.
-//
-
-import Foundation
-
-struct ___VARIABLE_templateName___Request: Encodable {}
diff --git a/Templates/FTAPIKit/Endpoints.xctemplate/HasRequestHasResponse/___VARIABLE_endpointName___Endpoints.swift b/Templates/FTAPIKit/Endpoints.xctemplate/HasRequestHasResponse/___VARIABLE_endpointName___Endpoints.swift
deleted file mode 100644
index cb3caab..0000000
--- a/Templates/FTAPIKit/Endpoints.xctemplate/HasRequestHasResponse/___VARIABLE_endpointName___Endpoints.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-//
-// ___FILENAME___
-// ___PROJECTNAME___
-//
-// Created by ___FULLUSERNAME___ on ___DATE___ using FTAPIKit Request Response Endpoint Template (v1.0).
-// Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved.
-//
-
-import FTAPIKit
-
-struct ___VARIABLE_templateName___Endpoint: APIRequestResponseEndpoint {
- typealias Response = ___VARIABLE_templateName___Response
- typealias Request = ___VARIABLE_templateName___Request
-
- var body: ___VARIABLE_templateName___Request
- let path: String = ""
-
-}
diff --git a/Templates/FTAPIKit/Endpoints.xctemplate/HasRequestHasResponse/___VARIABLE_endpointName___Requests.swift b/Templates/FTAPIKit/Endpoints.xctemplate/HasRequestHasResponse/___VARIABLE_endpointName___Requests.swift
deleted file mode 100644
index 3c43843..0000000
--- a/Templates/FTAPIKit/Endpoints.xctemplate/HasRequestHasResponse/___VARIABLE_endpointName___Requests.swift
+++ /dev/null
@@ -1,11 +0,0 @@
-//
-// ___FILENAME___
-// ___PROJECTNAME___
-//
-// Created by ___FULLUSERNAME___ on ___DATE___ using FTAPIKit Request Response Endpoint Template (v1.0).
-// Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved.
-//
-
-import Foundation
-
-struct ___VARIABLE_templateName___Request: Encodable {}
diff --git a/Templates/FTAPIKit/Endpoints.xctemplate/HasRequestHasResponse/___VARIABLE_endpointName___Responses.swift b/Templates/FTAPIKit/Endpoints.xctemplate/HasRequestHasResponse/___VARIABLE_endpointName___Responses.swift
deleted file mode 100644
index ab8c38b..0000000
--- a/Templates/FTAPIKit/Endpoints.xctemplate/HasRequestHasResponse/___VARIABLE_endpointName___Responses.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-//
-// ___FILENAME___
-// ___PROJECTNAME___
-//
-// Created by ___FULLUSERNAME___ on ___DATE___ using FTAPIKit Request Response Endpoint Template (v1.0).
-// Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved.
-//
-
-import Foundation
-
-struct ___VARIABLE_templateName___Response: Decodable {
-
-}
diff --git a/Templates/FTAPIKit/Endpoints.xctemplate/HasResponse/___VARIABLE_endpointName___Endpoints.swift b/Templates/FTAPIKit/Endpoints.xctemplate/HasResponse/___VARIABLE_endpointName___Endpoints.swift
deleted file mode 100644
index ed0dfa2..0000000
--- a/Templates/FTAPIKit/Endpoints.xctemplate/HasResponse/___VARIABLE_endpointName___Endpoints.swift
+++ /dev/null
@@ -1,16 +0,0 @@
-//
-// ___FILENAME___
-// ___PROJECTNAME___
-//
-// Created by ___FULLUSERNAME___ on ___DATE___ using FTAPIKit Response Endpoint Template (v1.0).
-// Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved.
-//
-
-import FTAPIKit
-
-struct ___VARIABLE_templateName___Endpoint: APIResponseEndpoint {
- typealias Response = ___VARIABLE_templateName___Response
-
- let path: String = ""
-
-}
diff --git a/Templates/FTAPIKit/Endpoints.xctemplate/HasResponse/___VARIABLE_endpointName___Responses.swift b/Templates/FTAPIKit/Endpoints.xctemplate/HasResponse/___VARIABLE_endpointName___Responses.swift
deleted file mode 100644
index 6156933..0000000
--- a/Templates/FTAPIKit/Endpoints.xctemplate/HasResponse/___VARIABLE_endpointName___Responses.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-//
-// ___FILENAME___
-// ___PROJECTNAME___
-//
-// Created by ___FULLUSERNAME___ on ___DATE___ using FTAPIKit Response Endpoint Template (v1.0).
-// Copyright (c) ___YEAR___ ___ORGANIZATIONNAME___. All rights reserved.
-//
-
-import Foundation
-
-struct ___VARIABLE_templateName___Response: Decodable {
-
-}
diff --git a/Templates/FTAPIKit/Endpoints.xctemplate/TemplateIcon.png b/Templates/FTAPIKit/Endpoints.xctemplate/TemplateIcon.png
deleted file mode 100644
index bd48cc0..0000000
Binary files a/Templates/FTAPIKit/Endpoints.xctemplate/TemplateIcon.png and /dev/null differ
diff --git a/Templates/FTAPIKit/Endpoints.xctemplate/TemplateIcon@2x.png b/Templates/FTAPIKit/Endpoints.xctemplate/TemplateIcon@2x.png
deleted file mode 100644
index 3a32393..0000000
Binary files a/Templates/FTAPIKit/Endpoints.xctemplate/TemplateIcon@2x.png and /dev/null differ
diff --git a/Templates/FTAPIKit/Endpoints.xctemplate/TemplateInfo.plist b/Templates/FTAPIKit/Endpoints.xctemplate/TemplateInfo.plist
deleted file mode 100644
index d95b457..0000000
--- a/Templates/FTAPIKit/Endpoints.xctemplate/TemplateInfo.plist
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
- DefaultCompletionName
- Endpoint
- Description
- This generates a new endpoint for use with the APIAdapter from the FTAPIKit.
- Kind
- Xcode.IDEKit.TextSubstitutionFileTemplateKind
- Options
-
-
- Description
- The name of the endpoint
- Identifier
- endpointName
- Name
- Endpoint file name:
- NotPersisted
-
- Required
-
- Type
- text
-
-
- Description
- The name of template request.
- Identifier
- templateName
- Name
- Template endpoint name:
- NotPersisted
-
- Required
-
- Type
- text
-
-
- Identifier
- hasRequest
- Name
- Generate Request file
- Description
- Check whethe you want to create file containing requests.
- Type
- checkbox
-
-
- Identifier
- hasResponse
- Name
- Generate Response file
- Description
- Check whether you want to create a file containing responses.
- Type
- checkbox
-
-
- Platforms
-
- com.apple.platform.iphoneos
-
- SortOrder
- 99
- Summary
- This generates a new endpoint for use with the APIAdapter from the FTAPIKit.
-
-
diff --git a/Templates/Makefile b/Templates/Makefile
deleted file mode 100755
index 176a69a..0000000
--- a/Templates/Makefile
+++ /dev/null
@@ -1,11 +0,0 @@
-
-TEMPLATE_NAME = 'FTAPIKit'
-TEMPLATE_DIR = $(HOME)/Library/Developer/Xcode/Templates/
-
-install:
- mkdir -p $(TEMPLATE_DIR)$(TEMPLATE_NAME) ; \
- cd $(TEMPLATE_NAME) ; \
- cp -R *.xctemplate $(TEMPLATE_DIR)$(TEMPLATE_NAME) ; \
- cd .. ; \
-
-.PHONY = install
diff --git a/Tests/FTAPIKitTests/APIAdapterTests.swift b/Tests/FTAPIKitTests/APIAdapterTests.swift
deleted file mode 100644
index bbefb45..0000000
--- a/Tests/FTAPIKitTests/APIAdapterTests.swift
+++ /dev/null
@@ -1,387 +0,0 @@
-// 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 Endpoint: APIEndpoint {
- let path = "get"
- }
-
- let delegate = MockupAPIAdapterDelegate()
- var adapter: APIAdapter = apiAdapter()
- adapter.delegate = delegate
- let expectation = self.expectation(description: "Result")
- adapter.request(data: Endpoint()) { result in
- if case let .failure(error) = result {
- XCTFail(error.localizedDescription)
- }
- expectation.fulfill()
- }
- wait(for: [expectation], timeout: timeout)
- }
-
- func testClientError() {
- struct Endpoint: APIEndpoint {
- let path = "status/404"
- }
-
- let delegate = MockupAPIAdapterDelegate()
- var adapter: APIAdapter = apiAdapter()
- adapter.delegate = delegate
- let expectation = self.expectation(description: "Result")
- adapter.request(data: Endpoint()) { 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 Endpoint: APIEndpoint {
- let path = "status/500"
- }
-
- let delegate = MockupAPIAdapterDelegate()
- var adapter: APIAdapter = apiAdapter()
- adapter.delegate = delegate
- let expectation = self.expectation(description: "Result")
- adapter.request(data: Endpoint()) { 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 Endpoint: APIEndpoint {
- 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: Endpoint()) { 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 Endpoint: APIEndpoint {
- let path = "status/204"
- }
-
- let delegate = MockupAPIAdapterDelegate()
- var adapter: APIAdapter = apiAdapter()
- adapter.delegate = delegate
- let expectation = self.expectation(description: "Result")
- adapter.request(data: Endpoint()) { result in
- if case let .failure(error) = result {
- XCTFail(error.localizedDescription)
- }
- expectation.fulfill()
- }
- wait(for: [expectation], timeout: timeout)
- }
-
- func testCustomError() {
- struct Endpoint: APIEndpoint {
- 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: Endpoint()) { 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 Endpoint: APIEndpoint {
- 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: Endpoint()) { 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 Endpoint: APIResponseEndpoint {
- 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: Endpoint()) { 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 Endpoint: APIResponseEndpoint {
- typealias Response = TopLevel
-
- let path = "json"
- }
-
- let delegate = MockupAPIAdapterDelegate()
- let adapter = apiAdapter()
- adapter.delegate = delegate
- let expectation = self.expectation(description: "Result")
- adapter.dataTask(response: Endpoint(), 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 Endpoint: APIRequestResponseEndpoint {
-
- typealias Response = TopLevel
-
- let body: User
- let path = "anything"
- }
-
- let user = User(uuid: UUID(), name: "Some Name", age: .random(in: 0...120))
- let endpoint = Endpoint(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 Endpoint: APIRequestResponseEndpoint {
- typealias Response = User
-
- let body: User
- let path = "anything"
- }
-
- let user = User(uuid: UUID(), name: "Some Name", age: .random(in: 0...120))
- let endpoint = Endpoint(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 Endpoint: APIEndpoint {
- 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: Endpoint()) { 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 Endpoint: APIEndpoint {
- 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: Endpoint(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..00d2a77
--- /dev/null
+++ b/Tests/FTAPIKitTests/Mockups/Endpoints.swift
@@ -0,0 +1,99 @@
+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 request: User
+ let path = "anything"
+
+ struct Wrapper: Decodable {
+ let json: User
+ }
+}
+
+struct FailingUpdateUserEndpoint: RequestResponseEndpoint {
+ typealias Response = User
+
+ 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())
+ ]
+ }
+}
+
+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"
+ let method: HTTPMethod = .put
+
+ init(file: File) {
+ self.file = file.url
+ }
+}
+
+struct ImageEndpoint: Endpoint {
+ let path = "image/jpeg"
+}
diff --git a/Tests/FTAPIKitTests/Mockups/Errors.swift b/Tests/FTAPIKitTests/Mockups/Errors.swift
new file mode 100644
index 0000000..55344b0
--- /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()
+}
diff --git a/Tests/FTAPIKitTests/Mockups/MockupAPIAdapterDelegate.swift b/Tests/FTAPIKitTests/Mockups/MockupAPIAdapterDelegate.swift
deleted file mode 100644
index dfc023b..0000000
--- a/Tests/FTAPIKitTests/Mockups/MockupAPIAdapterDelegate.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-import FTAPIKit
-import Foundation
-
-final class MockupAPIAdapterDelegate: APIAdapterDelegate {
- func apiAdapter(_ apiAdapter: APIAdapter, willRequest request: URLRequest, to endpoint: APIEndpoint, 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..2ccacea
--- /dev/null
+++ b/Tests/FTAPIKitTests/Mockups/Models.swift
@@ -0,0 +1,20 @@
+import Foundation
+
+struct User: Codable, Equatable {
+ let uuid: UUID
+ 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"
+ ]
+
+ func write() throws {
+ try data.write(to: url)
+ }
+}
diff --git a/Tests/FTAPIKitTests/Mockups/Servers.swift b/Tests/FTAPIKitTests/Mockups/Servers.swift
new file mode 100644
index 0000000..35ec731
--- /dev/null
+++ b/Tests/FTAPIKitTests/Mockups/Servers.swift
@@ -0,0 +1,27 @@
+import FTAPIKit
+import Foundation
+
+struct HTTPBinServer: URLServer {
+ let urlSession = URLSession(configuration: .ephemeral)
+ let baseUri = URL(string: "http://httpbin.org/")!
+
+ func buildRequest(endpoint: Endpoint) throws -> URLRequest {
+ var request = try buildStandardRequest(endpoint: endpoint)
+ if endpoint is AuthorizedEndpoint {
+ request.addValue("Bearer \(UUID().uuidString)", forHTTPHeaderField: "Authorization")
+ }
+ return request
+ }
+}
+
+struct NonExistingServer: URLServer {
+ let urlSession = URLSession(configuration: .ephemeral)
+ let baseUri = URL(string: "https://www.tato-stranka-urcite-neexistuje.cz/")!
+}
+
+struct ErrorThrowingServer: URLServer {
+ typealias ErrorType = 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..ec7bab6
--- /dev/null
+++ b/Tests/FTAPIKitTests/ResponseTests.swift
@@ -0,0 +1,233 @@
+import XCTest
+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(request: 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(request: 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)
+ }
+
+ func testMultipartData() {
+ let server = HTTPBinServer()
+ let file = File()
+ XCTAssertNoThrow(try file.write())
+ 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()
+ }
+ wait(for: [expectation], timeout: timeout)
+ } catch {
+ let errorFunc = { throw error }
+ XCTAssertNoThrow(errorFunc)
+ }
+ }
+
+ 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()
+ 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)
+ }
+
+ 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 = [
+ ("testGet", testGet),
+ ("testClientError", testClientError),
+ ("testServerError", testServerError),
+ ("testConnectionError", testConnectionError),
+ ("testEmptyResult", testEmptyResult),
+ ("testCustomError", testCustomError),
+ ("testValidJSONResponse", testValidJSONResponse),
+ ("testValidJSONRequestResponse", testValidJSONRequestResponse),
+ ("testInvalidJSONRequestResponse", testInvalidJSONRequestResponse),
+ ("testAuthorization", testAuthorization),
+ ("testMultipartData", testMultipartData),
+ ("testUploadTask", testUploadTask),
+ ("testDownloadTask", testDownloadTask)
+ ]
+}
diff --git a/Tests/FTAPIKitTests/StressTests.swift b/Tests/FTAPIKitTests/StressTests.swift
deleted file mode 100644
index 65c4a0d..0000000
--- a/Tests/FTAPIKitTests/StressTests.swift
+++ /dev/null
@@ -1,78 +0,0 @@
-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: APIEndpoint {
- 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)
])