Skip to content

Commit

Permalink
Merge pull request #4 from thefuntasty/housekeep/urlsession-adapter-f…
Browse files Browse the repository at this point in the history
…ile-separation

Housekeep: URLSession adapter file separation
  • Loading branch information
mkj-is authored Nov 12, 2018
2 parents 844e338 + 5483ba5 commit 3e6ceef
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 124 deletions.
10 changes: 10 additions & 0 deletions FTAPIKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
52D6D9871BEFF229002C0205 /* FTAPIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* FTAPIKit.framework */; };
DD7502881C68FEDE006590AF /* FTAPIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6DA0F1BF000BD002C0205 /* FTAPIKit.framework */; };
DD7502921C690C7A006590AF /* FTAPIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D9F01BEFFFBE002C0205 /* FTAPIKit.framework */; };
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 */; };
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 */; };
Expand Down Expand Up @@ -94,6 +98,7 @@
AD2FAA281CD0B6E100659CF4 /* FTAPIKitTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = FTAPIKitTests.plist; sourceTree = "<group>"; };
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 = "<group>"; };
E7B774142152752A006E7585 /* APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIAdapter.swift; sourceTree = "<group>"; };
E7B774152152752A006E7585 /* URL+APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+APIAdapter.swift"; sourceTree = "<group>"; };
E7B774162152752A006E7585 /* Data+APIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+APIAdapter.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -200,6 +205,7 @@
E7B774192152752A006E7585 /* APIEndpoint.swift */,
E7B774142152752A006E7585 /* APIAdapter.swift */,
E7B7741A2152752A006E7585 /* APIAdapter+Types.swift */,
E792629C219710A700CDBB7E /* URLSessionAPIAdapter.swift */,
E7B774182152752A006E7585 /* AnyEncodable.swift */,
E7B774162152752A006E7585 /* Data+APIAdapter.swift */,
E7B774152152752A006E7585 /* URL+APIAdapter.swift */,
Expand Down Expand Up @@ -554,6 +560,7 @@
E7B774332152752A006E7585 /* APIAdapter+Types.swift in Sources */,
E7B7741B2152752A006E7585 /* APIAdapter.swift in Sources */,
E7B7742B2152752A006E7585 /* AnyEncodable.swift in Sources */,
E792629D219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -577,6 +584,7 @@
E7B774352152752A006E7585 /* APIAdapter+Types.swift in Sources */,
E7B7741D2152752A006E7585 /* APIAdapter.swift in Sources */,
E7B7742D2152752A006E7585 /* AnyEncodable.swift in Sources */,
E792629F219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -591,6 +599,7 @@
E7B774362152752A006E7585 /* APIAdapter+Types.swift in Sources */,
E7B7741E2152752A006E7585 /* APIAdapter.swift in Sources */,
E7B7742E2152752A006E7585 /* AnyEncodable.swift in Sources */,
E79262A0219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -605,6 +614,7 @@
E7B774342152752A006E7585 /* APIAdapter+Types.swift in Sources */,
E7B7741C2152752A006E7585 /* APIAdapter.swift in Sources */,
E7B7742C2152752A006E7585 /* AnyEncodable.swift in Sources */,
E792629E219710A700CDBB7E /* URLSessionAPIAdapter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
120 changes: 2 additions & 118 deletions Sources/APIAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved.
//

import Foundation
import struct Foundation.URLRequest
import struct Foundation.Data

/// Delegate of `APIAdapter` used for platform-specific functionality
/// (showing/hiding network activity indicator) and signing/manipulating
Expand Down Expand Up @@ -60,120 +61,3 @@ public protocol APIAdapter {
/// - completion: Completion closure receiving result with data.
func request(data endpoint: APIEndpoint, completion: @escaping (APIResult<Data>) -> Void)
}

/// Standard and default implementation of `APIAdapter` protocol using `URLSession`.
public final class URLSessionAPIAdapter: APIAdapter {

/// Custom error custructor typealias recieving values from data task execution
/// and JSON decoder, if it needs to decode custom error from returned JSON.
public typealias ErrorConstructor = (Data?, URLResponse?, Error?, JSONDecoder) -> Error?

public weak var delegate: APIAdapterDelegate?

private let urlSession: URLSession
private let baseUrl: URL

private let jsonEncoder: JSONEncoder
private let jsonDecoder: JSONDecoder

private let customErrorConstructor: ErrorConstructor?

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.
/// - customErrorConstructor: Optional custom error constructor if we want the API adapter to not return
/// the standard `APIError`, but to handle the errors our own way.
/// - 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(), customErrorConstructor: ErrorConstructor? = nil, urlSession: URLSession = .shared) {
self.baseUrl = baseUrl
self.jsonDecoder = jsonDecoder
self.jsonEncoder = jsonEncoder
self.customErrorConstructor = customErrorConstructor
self.urlSession = urlSession
}

public func request<Endpoint: APIResponseEndpoint>(response endpoint: Endpoint, completion: @escaping (APIResult<Endpoint.Response>) -> Void) {
request(data: endpoint) { 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 request(data endpoint: APIEndpoint, completion: @escaping (APIResult<Data>) -> 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(.error(error))
return
}

if let delegate = delegate {
delegate.apiAdapter(self, willRequest: request, to: endpoint) { result in
switch result {
case .value(let request):
self.send(request: request, completion: completion)
case .error(let error):
completion(.error(error))
}
}
} else {
send(request: request, completion: completion)
}
}

private func send(request: URLRequest, completion: @escaping (APIResult<Data>) -> Void) {
runningRequestCount += 1
resumeDataTask(with: request) { result in
self.runningRequestCount -= 1
completion(result)
}
}

private func resumeDataTask(with request: URLRequest, completion: @escaping (APIResult<Data>) -> Void) {
let task = urlSession.dataTask(with: request) { [customErrorConstructor, jsonDecoder] data, response, error in
if let constructor = customErrorConstructor, let error = constructor(data, response, error, jsonDecoder) {
completion(.error(error))
return
}
switch (data, response, error) {
case let (_, response as HTTPURLResponse, _) where response.statusCode == 204:
completion(.value(Data()))
case let (data?, response as HTTPURLResponse, nil) where response.statusCode < 400:
completion(.value(data))
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))
}
}
task.resume()
}
}
2 changes: 0 additions & 2 deletions Sources/APIEndpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved.
//

import Foundation

/// Protocol describing API endpoint. API Endpoint describes one URI with all the
/// data and parameters which are sent to it.
///
Expand Down
2 changes: 0 additions & 2 deletions Sources/AnyEncodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved.
//

import Foundation

struct AnyEncodable: Encodable {
private let anyEncode: (Encoder) throws -> Void

Expand Down
2 changes: 1 addition & 1 deletion Sources/Data+APIAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved.
//

import Foundation
import struct Foundation.Data

extension Data {
mutating func append(_ string: String) {
Expand Down
4 changes: 3 additions & 1 deletion Sources/URL+APIAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved.
//

import Foundation
import struct Foundation.URL
import struct Foundation.URLComponents
import struct Foundation.URLQueryItem

extension URL {
mutating func appendQuery(parameters: [String: String]) {
Expand Down
126 changes: 126 additions & 0 deletions Sources/URLSessionAPIAdapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// 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 {

/// Custom error constructor typealias receiving values from data task execution
/// and JSON decoder, if it needs to decode custom error from returned JSON.
public typealias ErrorConstructor = (Data?, URLResponse?, Error?, JSONDecoder) -> Error?

public weak var delegate: APIAdapterDelegate?

private let urlSession: URLSession
private let baseUrl: URL

private let jsonEncoder: JSONEncoder
private let jsonDecoder: JSONDecoder

private let customErrorConstructor: ErrorConstructor?

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.
/// - customErrorConstructor: Optional custom error constructor if we want the API adapter to not return
/// the standard `APIError`, but to handle the errors our own way.
/// - 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(), customErrorConstructor: ErrorConstructor? = nil, urlSession: URLSession = .shared) {
self.baseUrl = baseUrl
self.jsonDecoder = jsonDecoder
self.jsonEncoder = jsonEncoder
self.customErrorConstructor = customErrorConstructor
self.urlSession = urlSession
}

public func request<Endpoint: APIResponseEndpoint>(response endpoint: Endpoint, completion: @escaping (APIResult<Endpoint.Response>) -> Void) {
request(data: endpoint) { 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 request(data endpoint: APIEndpoint, completion: @escaping (APIResult<Data>) -> 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(.error(error))
return
}

if let delegate = delegate {
delegate.apiAdapter(self, willRequest: request, to: endpoint) { result in
switch result {
case .value(let request):
self.send(request: request, completion: completion)
case .error(let error):
completion(.error(error))
}
}
} else {
send(request: request, completion: completion)
}
}

private func send(request: URLRequest, completion: @escaping (APIResult<Data>) -> Void) {
runningRequestCount += 1
resumeDataTask(with: request) { result in
self.runningRequestCount -= 1
completion(result)
}
}

private func resumeDataTask(with request: URLRequest, completion: @escaping (APIResult<Data>) -> Void) {
let task = urlSession.dataTask(with: request) { [customErrorConstructor, jsonDecoder] data, response, error in
if let constructor = customErrorConstructor, let error = constructor(data, response, error, jsonDecoder) {
completion(.error(error))
return
}
switch (data, response, error) {
case let (_, response as HTTPURLResponse, _) where response.statusCode == 204:
completion(.value(Data()))
case let (data?, response as HTTPURLResponse, nil) where response.statusCode < 400:
completion(.value(data))
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))
}
}
task.resume()
}
}

0 comments on commit 3e6ceef

Please sign in to comment.