Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge branch 'develop/4.0' into 'master' #281

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b0da296
Add APIKit 4 migration guide
ishkawa Dec 25, 2016
263ba3f
Add QueryParameters protocol to provide interface for URL query.
matsuda Jan 8, 2017
6bb7961
Added .DS_Store to gitignore
gormster Jan 17, 2017
261af5f
Allow DataParser to return typed objects
gormster Jan 17, 2017
758f24f
Merge pull request #229 from matsuda/feature/queryParameters
ishkawa Mar 12, 2017
5f67418
Merge pull request #230 from gormster/DataParserAssociatedTypes
ishkawa Mar 12, 2017
57c3837
Add a way to get progress of uploading
taish Mar 22, 2017
8ebecd0
rename argment `progressHandler` to `completionHandler` in `Session` …
taish Mar 25, 2017
aa08818
rename argment `progressHandler` to `completionHandler` in `SessionAd…
taish Mar 25, 2017
8748820
rename argment `progressHandler` to `completionHandler` in `TestSessi…
taish Mar 25, 2017
184b827
Merge pull request #234 from taish/new-feature-upload-progress
ishkawa Mar 25, 2017
20eebe8
Fixed `progressHandler` to work properly
taish Mar 29, 2017
8807e49
Merge pull request #236 from taish/fix-progress
ishkawa Mar 30, 2017
3593a02
Rename Request.Parser to Request.DataParser
ishkawa Apr 10, 2017
ccc4e97
Merge pull request #237 from ishkawa/rename-request-parser
ishkawa May 1, 2017
cb83ca4
Merge branch 'master' into develop/4.0
ishkawa Jul 23, 2017
734f7b3
Merge branch 'develop/4.0'
davdroman Mar 14, 2020
1afc176
Remove stale document
davdroman Mar 14, 2020
01656bd
Merge branch 'master' of github.com:ishkawa/APIKit
davdroman Jun 8, 2020
e3c12f7
Merge branch 'master' of github.com:ishkawa/APIKit
davdroman Apr 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ Carthage/Build

fastlane/report.xml
fastlane/screenshots

.DS_Store
29 changes: 29 additions & 0 deletions APIKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
objects = {

/* Begin PBXBuildFile section */
0ED17E891E229EC700EC1114 /* QueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED17E881E229EC700EC1114 /* QueryParameters.swift */; };
0ED17E8B1E22A22900EC1114 /* URLEncodedQueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED17E8A1E22A22900EC1114 /* URLEncodedQueryParameters.swift */; };
0ED17E8E1E22AC8300EC1114 /* URLEncodedQueryParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED17E8D1E22AC8300EC1114 /* URLEncodedQueryParametersTests.swift */; };
0969AE0F259DEC6D00C498AF /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0969AE0E259DEC6D00C498AF /* Combine.swift */; };
0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0973EE34259E2DDC00879BA2 /* CombineTests.swift */; };
7F698E501D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F698E3C1D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift */; };
Expand Down Expand Up @@ -78,6 +81,9 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
0ED17E881E229EC700EC1114 /* QueryParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryParameters.swift; sourceTree = "<group>"; };
0ED17E8A1E22A22900EC1114 /* URLEncodedQueryParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedQueryParameters.swift; sourceTree = "<group>"; };
0ED17E8D1E22AC8300EC1114 /* URLEncodedQueryParametersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLEncodedQueryParametersTests.swift; sourceTree = "<group>"; };
0969AE0E259DEC6D00C498AF /* Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Combine.swift; sourceTree = "<group>"; };
0973EE34259E2DDC00879BA2 /* CombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTests.swift; sourceTree = "<group>"; };
141F120F1C1C96820026D415 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Base.xcconfig; path = Configurations/Base.xcconfig; sourceTree = "<group>"; };
Expand Down Expand Up @@ -153,6 +159,24 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
0ED17E871E229EAC00EC1114 /* QueryParameters */ = {
isa = PBXGroup;
children = (
0ED17E881E229EC700EC1114 /* QueryParameters.swift */,
0ED17E8A1E22A22900EC1114 /* URLEncodedQueryParameters.swift */,
);
name = QueryParameters;
path = APIKit/QueryParameters;
sourceTree = "<group>";
};
0ED17E8C1E22AC5D00EC1114 /* QueryParameters */ = {
isa = PBXGroup;
children = (
0ED17E8D1E22AC8300EC1114 /* URLEncodedQueryParametersTests.swift */,
);
path = QueryParameters;
sourceTree = "<group>";
};
0969AE0D259DEC3C00C498AF /* Combine */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -240,6 +264,7 @@
7F698E451D9D680C00F1561D /* RequestTests.swift */,
7F698E491D9D680C00F1561D /* SessionCallbackQueueTests.swift */,
7F698E4A1D9D680C00F1561D /* SessionTests.swift */,
0ED17E8C1E22AC5D00EC1114 /* QueryParameters */,
0973EE33259E2DD000879BA2 /* Combine */,
7F698E3B1D9D680C00F1561D /* BodyParametersType */,
7F698E401D9D680C00F1561D /* DataParserType */,
Expand Down Expand Up @@ -312,6 +337,7 @@
7F7048CC1D9D89BE003C99F6 /* Unavailable.swift */,
0969AE0D259DEC3C00C498AF /* Combine */,
7F85FB8B1C9D317300CEE132 /* SessionAdapter */,
0ED17E871E229EAC00EC1114 /* QueryParameters */,
7F18BD0D1C972C38003A31DF /* BodyParameters */,
7FA19A441C9CC9A2005D25AE /* DataParser */,
7F18BD161C9730ED003A31DF /* Serializations */,
Expand Down Expand Up @@ -477,12 +503,14 @@
files = (
7F7048D31D9D89BE003C99F6 /* Unavailable.swift in Sources */,
7F7048D11D9D89BE003C99F6 /* Request.swift in Sources */,
0ED17E891E229EC700EC1114 /* QueryParameters.swift in Sources */,
7F7048E81D9D8A08003C99F6 /* DataParser.swift in Sources */,
7F7048CE1D9D89BE003C99F6 /* CallbackQueue.swift in Sources */,
7F7048DE1D9D89FB003C99F6 /* AbstractInputStream.m in Sources */,
7F7048E31D9D89FB003C99F6 /* MultipartFormDataBodyParameters.swift in Sources */,
7F7048F01D9D8A12003C99F6 /* ResponseError.swift in Sources */,
7F7048EA1D9D8A08003C99F6 /* JSONDataParser.swift in Sources */,
0ED17E8B1E22A22900EC1114 /* URLEncodedQueryParameters.swift in Sources */,
7F7048D21D9D89BE003C99F6 /* Session.swift in Sources */,
7F7048E01D9D89FB003C99F6 /* Data+InputStream.swift in Sources */,
7F7048DF1D9D89FB003C99F6 /* BodyParameters.swift in Sources */,
Expand Down Expand Up @@ -512,6 +540,7 @@
7F698E501D9D680C00F1561D /* FormURLEncodedBodyParametersTests.swift in Sources */,
7F698E581D9D680C00F1561D /* RequestTests.swift in Sources */,
ECA8314A1DE4DEBE004EB1B5 /* ProtobufDataParserTests.swift in Sources */,
0ED17E8E1E22AC8300EC1114 /* URLEncodedQueryParametersTests.swift in Sources */,
7F698E5E1D9D680C00F1561D /* TestRequest.swift in Sources */,
7F698E601D9D680C00F1561D /* TestSessionTask.swift in Sources */,
0973EE35259E2DDC00879BA2 /* CombineTests.swift in Sources */,
Expand Down
4 changes: 3 additions & 1 deletion Sources/APIKit/DataParser/DataParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import Foundation

/// `DataParser` protocol provides interface to parse HTTP response body and to state Content-Type to accept.
public protocol DataParser {
associatedtype Parsed

/// Value for `Accept` header field of HTTP request.
var contentType: String? { get }

/// Return `Any` that expresses structure of response such as JSON and XML.
/// - Throws: `Error` when parser encountered invalid format data.
func parse(data: Data) throws -> Any
func parse(data: Data) throws -> Parsed
}
7 changes: 7 additions & 0 deletions Sources/APIKit/QueryParameters/QueryParameters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

/// `QueryParameters` provides interface to generate HTTP URL query strings.
public protocol QueryParameters {
/// Generate URL query strings.
func encode() -> String?
}
20 changes: 20 additions & 0 deletions Sources/APIKit/QueryParameters/URLEncodedQueryParameters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

/// `URLEncodedQueryParameters` serializes form object for HTTP URL query.
public struct URLEncodedQueryParameters: QueryParameters {
/// The parameters to be url encoded.
public let parameters: Any

/// Returns `URLEncodedQueryParameters` that is initialized with parameters.
public init(parameters: Any) {
self.parameters = parameters
}

/// Generate url encoded `String`.
public func encode() -> String? {
guard let parameters = parameters as? [String: Any], !parameters.isEmpty else {
return nil
}
return URLEncodedSerialization.string(from: parameters)
}
}
31 changes: 18 additions & 13 deletions Sources/APIKit/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation
public protocol Request {
/// The response type associated with the request type.
associatedtype Response
associatedtype DataParser: APIKit.DataParser

/// The base URL.
var baseURL: URL { get }
Expand All @@ -28,7 +29,7 @@ public protocol Request {
/// The actual parameters for the URL query. The values of this property will be escaped using `URLEncodedSerialization`.
/// If this property is not implemented and `method.prefersQueryParameter` is `true`, the value of this property
/// will be computed from `parameters`.
var queryParameters: [String: Any]? { get }
var queryParameters: QueryParameters? { get }

/// The actual parameters for the HTTP body. If this property is not implemented and `method.prefersQueryParameter` is `false`,
/// the value of this property will be computed from `parameters` using `JSONBodyParameters`.
Expand All @@ -52,25 +53,25 @@ public protocol Request {
/// The default implementation of this method is provided to throw `ResponseError.unacceptableStatusCode`
/// if the HTTP status code is not in `200..<300`.
/// - Throws: `Error`
func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any
func intercept(object: DataParser.Parsed, urlResponse: HTTPURLResponse) throws -> DataParser.Parsed

/// Build `Response` instance from raw response object. This method is called after
/// `intercept(object:urlResponse:)` if it does not throw any error.
/// - Throws: `Error`
func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response
func response(from object: DataParser.Parsed, urlResponse: HTTPURLResponse) throws -> Response
}

public extension Request {
var parameters: Any? {
return nil
}

var queryParameters: [String: Any]? {
guard let parameters = parameters as? [String: Any], method.prefersQueryParameters else {
var queryParameters: QueryParameters? {
guard let parameters = parameters, method.prefersQueryParameters else {
return nil
}

return parameters
return URLEncodedQueryParameters(parameters: parameters)
}

var bodyParameters: BodyParameters? {
Expand All @@ -85,15 +86,11 @@ public extension Request {
return [:]
}

var dataParser: DataParser {
return JSONDataParser(readingOptions: [])
}

func intercept(urlRequest: URLRequest) throws -> URLRequest {
return urlRequest
}

func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any {
func intercept(object: DataParser.Parsed, urlResponse: HTTPURLResponse) throws -> DataParser.Parsed {
guard 200..<300 ~= urlResponse.statusCode else {
throw ResponseError.unacceptableStatusCode(urlResponse.statusCode)
}
Expand All @@ -110,8 +107,8 @@ public extension Request {

var urlRequest = URLRequest(url: url)

if let queryParameters = queryParameters, !queryParameters.isEmpty {
components.percentEncodedQuery = URLEncodedSerialization.string(from: queryParameters)
if let queryString = queryParameters?.encode(), !queryString.isEmpty {
components.percentEncodedQuery = queryString
}

if let bodyParameters = bodyParameters {
Expand Down Expand Up @@ -145,3 +142,11 @@ public extension Request {
return try response(from: passedObject, urlResponse: urlResponse)
}
}

public protocol JSONRequest: Request {}

public extension JSONRequest {
var dataParser: JSONDataParser {
return JSONDataParser(readingOptions: [])
}
}
49 changes: 27 additions & 22 deletions Sources/APIKit/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ open class Session {
/// - parameter handler: The closure that receives result of the request.
/// - returns: The new session task.
@discardableResult
open class func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
return shared.send(request, callbackQueue: callbackQueue, handler: handler)
open class func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _,_,_ in }, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
return shared.send(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler)
}

/// Calls `cancelRequests(with:passingTest:)` of `Session.shared`.
Expand All @@ -54,41 +54,46 @@ open class Session {
/// - parameter handler: The closure that receives result of the request.
/// - returns: The new session task.
@discardableResult
open func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
open func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _,_,_ in }, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
let callbackQueue = callbackQueue ?? self.callbackQueue

let urlRequest: URLRequest
do {
urlRequest = try request.buildURLRequest()
} catch {
callbackQueue.execute {
handler(.failure(.requestError(error)))
completionHandler(.failure(.requestError(error)))
}
return nil
}

let task = adapter.createTask(with: urlRequest) { data, urlResponse, error in
let result: Result<Request.Response, SessionTaskError>

switch (data, urlResponse, error) {
case (_, _, let error?):
result = .failure(.connectionError(error))
let task = adapter.createTask(with: urlRequest,
progressHandler: { bytesSent, totalBytesSent, totalBytesExpectedToSend in
progressHandler(bytesSent, totalBytesSent, totalBytesExpectedToSend)
},
completionHandler: { data, urlResponse, error in
let result: Result<Request.Response, SessionTaskError>

switch (data, urlResponse, error) {
case (_, _, let error?):
result = .failure(.connectionError(error))

case (let data?, let urlResponse as HTTPURLResponse, _):
do {
result = .success(try request.parse(data: data as Data, urlResponse: urlResponse))
} catch {
result = .failure(.responseError(error))
}

case (let data?, let urlResponse as HTTPURLResponse, _):
do {
result = .success(try request.parse(data: data as Data, urlResponse: urlResponse))
} catch {
result = .failure(.responseError(error))
default:
result = .failure(.responseError(ResponseError.nonHTTPURLResponse(urlResponse)))
}

default:
result = .failure(.responseError(ResponseError.nonHTTPURLResponse(urlResponse)))
}

callbackQueue.execute {
handler(result)
callbackQueue.execute {
completionHandler(result)
}
}
}
)

setRequest(request, forTask: task)
task.resume()
Expand Down
2 changes: 1 addition & 1 deletion Sources/APIKit/SessionAdapter/SessionAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public protocol SessionTask: class {
/// with `Session`.
public protocol SessionAdapter {
/// Returns instance that conforms to `SessionTask`. `handler` must be called after success or failure.
func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask
func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask

/// Collects tasks from backend networking stack. `handler` must be called after collecting.
func getTasks(with handler: @escaping ([SessionTask]) -> Void)
Expand Down
18 changes: 16 additions & 2 deletions Sources/APIKit/SessionAdapter/URLSessionAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ extension URLSessionTask: SessionTask {

private var dataTaskResponseBufferKey = 0
private var taskAssociatedObjectCompletionHandlerKey = 0
private var taskAssociatedObjectProgressHandlerKey = 0

/// `URLSessionAdapter` connects `URLSession` with `Session`.
///
Expand All @@ -25,11 +26,12 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS
}

/// Creates `URLSessionDataTask` instance using `dataTaskWithRequest(_:completionHandler:)`.
open func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask {
open func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask {
let task = urlSession.dataTask(with: URLRequest)

setBuffer(NSMutableData(), forTask: task)
setHandler(handler, forTask: task)
setHandler(completionHandler, forTask: task)
setProgressHandler(progressHandler, forTask: task)

return task
}
Expand Down Expand Up @@ -58,6 +60,13 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS
return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Data?, URLResponse?, Error?) -> Void
}

private func setProgressHandler(_ progressHandler: @escaping (Int64, Int64, Int64) -> Void, forTask task: URLSessionTask) {
objc_setAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}

private func progressHandler(for task: URLSessionTask) -> ((Int64, Int64, Int64) -> Void)? {
return objc_getAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey) as? (Int64, Int64, Int64) -> Void
}
// MARK: URLSessionTaskDelegate
open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
handler(for: task)?(buffer(for: task) as Data?, task.response, error)
Expand All @@ -67,4 +76,9 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
buffer(for: dataTask)?.append(data)
}

// MARK: URLSessionDataDelegate
open func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
progressHandler(for: task)?(bytesSent, totalBytesSent, totalBytesExpectedToSend)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import XCTest
import APIKit

class URLEncodedQueryParametersTests: XCTestCase {
func testURLEncodedSuccess() {
let object: [String: Any] = ["foo": "string", "bar": 1, "q": "こんにちは"]
let parameters = URLEncodedQueryParameters(parameters: object)
guard let query = parameters.encode() else {
XCTFail()
return
}

let items = query.components(separatedBy: "&")
XCTAssertEqual(items.count, 3)
XCTAssertTrue(items.contains("foo=string"))
XCTAssertTrue(items.contains("bar=1"))
XCTAssertTrue(items.contains("q=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF"))
}
}
Loading