-
Notifications
You must be signed in to change notification settings - Fork 205
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #161 from ishkawa/2.0-development
APIKit 2.0
- Loading branch information
Showing
60 changed files
with
2,564 additions
and
2,272 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
github "AliSoftware/OHHTTPStubs" ~> 4.6.0 | ||
github "ishkawa/OHHTTPStubs" "75f74c9c19620a37f436b38e2bc20a07310b999e" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
github "AliSoftware/OHHTTPStubs" "4.6.0" | ||
github "ishkawa/OHHTTPStubs" "75f74c9c19620a37f436b38e2bc20a07310b999e" | ||
github "antitypical/Result" "2.0.0" |
Submodule OHHTTPStubs
updated
46 files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | ||
<playground version='5.0' target-platform='ios' requires-full-environment='true' display-mode='raw'> | ||
<playground version='5.0' target-platform='ios' display-mode='raw'> | ||
<timeline fileName='timeline.xctimeline'/> | ||
</playground> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# APIKit 2 Migration Guide | ||
|
||
APIKit 2.0 introduces several breaking changes to add functionality and to improve modeling of web API. | ||
|
||
- Abstraction of backend | ||
- Improved error handling modeling | ||
- Separation of convenience parameters and type-safe parameters | ||
|
||
## Errors | ||
|
||
- [**Deleted**] `APIError` | ||
- [**Added**] `SessionTaskError` | ||
|
||
Errors cases of `Session.sendRequest(_:handler:)` is reduced to 3 cases listed below: | ||
|
||
```swift | ||
public enum SessionTaskError: ErrorType { | ||
/// Error of networking backend such as `NSURLSession`. | ||
case ConnectionError(NSError) | ||
|
||
/// Error while creating `NSURLRequest` from `Request`. | ||
case RequestError(ErrorType) | ||
|
||
/// Error while creating `RequestType.Response` from `(NSData, NSURLResponse)`. | ||
case ResponseError(ErrorType) | ||
} | ||
``` | ||
|
||
These error cases describes *where* the error occurred, not *what* is the error. You can throw any kind of error while building `NSURLRequest` and converting `NSData` to `Response`. `Session` catches the error you threw and wrap it into one of the cases defined in `SessionTaskError`. For example, if you throw `SomeError` in `responseFromObject(_:URLResponse:)`, the closure of `Session.sendRequest(_:handler:)` receives `.Failure(.ResponseError(SomeError))`. | ||
|
||
## RequestType | ||
|
||
### Converting AnyObject to Response | ||
|
||
- [**Deleted**] `func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response?` | ||
- [**Added**] `func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response` | ||
|
||
### Handling response errors | ||
|
||
In 1.x, `Session` checks if the actual status code is contained in `RequestType.acceptableStatusCodes`. If it is not, `Session` calls `errorFromObject()` to obtain custom error from response object. In 2.x, `Session` always call `interceptObject()` before calling `responseFromObject()`, so you can validate `AnyObject` and `NSHTTPURLResponse` in `interceptObject()` and throw error initialized with them. | ||
|
||
- [**Deleted**] `var acceptableStatusCodes: Set<Int> { get }` | ||
- [**Deleted**] `func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType?` | ||
- [**Added**] `func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> AnyObject` | ||
|
||
For example, the code below checks HTTP status code, and if the status code is not 2xx, it throws an error initialized with error JSON GitHub API returns. | ||
|
||
```swift | ||
func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> AnyObject { | ||
guard (200..<300).contains(URLResponse.statusCode) else { | ||
// https://developer.github.com/v3/#client-errors | ||
throw GitHubError(object: object) | ||
} | ||
|
||
return object | ||
} | ||
``` | ||
|
||
### Parameters | ||
|
||
To satisfy both ease and accuracy, `parameters` property is separated into 1 convenience property and 2 actual properties. If you implement convenience parameters only, 2 actual parameters are computed by default implementation of `RequestType`. | ||
|
||
- [**Deleted**] `var parameters: [String: AnyObject]` | ||
- [**Deleted**] `var objectParameters: AnyObject` | ||
- [**Deleted**] `var requestBodyBuilder: RequestBodyBuilder` | ||
- [**Added**] `var parameters: AnyObject?` (convenience property) | ||
- [**Added**] `var bodyParameters: BodyParametersType?` (actual property) | ||
- [**Added**] `var queryParameters: [String: AnyObject]?` (actual property) | ||
|
||
Related types: | ||
|
||
- [**Deleted**] `enum RequestBodyBuilder` | ||
- [**Added**] `protocol BodyParametersType` | ||
|
||
APIKit provides 3 parameters types that conform to `BodyParametersType`: | ||
|
||
- [**Added**] `class JSONBodyParameters` | ||
- [**Added**] `class FormURLEncodedBodyParameters` | ||
- [**Added**] `class MultipartFormDataBodyParameters` | ||
|
||
### Data parsers | ||
|
||
- [**Deleted**] `var responseBodyParser: ResponseBodyParser` | ||
- [**Added**] `var dataParser: DataParserType` | ||
|
||
Related types: | ||
|
||
- [**Deleted**] `enum ResponseBodyParser` | ||
- [**Added**] `protocol DataParserType` | ||
- [**Added**] `class JSONDataParser` | ||
- [**Added**] `class FormURLEncodedDataParser` | ||
- [**Added**] `class StringDataParser` | ||
|
||
### Configuring NSURLRequest | ||
|
||
`configureURLRequest()` in 1.x is renamed to `interceptURLRequest()` for the consistency with `interceptObject()`. | ||
|
||
- [**Deleted**] `func configureURLRequest(URLRequest: NSMutableURLRequest) -> NSMutableURLRequest` | ||
- [**Added**] `func interceptURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest` | ||
|
||
## NSURLSession | ||
|
||
- [**Deleted**] `class URLSessionDelegate` | ||
- [**Added**] `protocol SessionTaskType` | ||
- [**Added**] `protocol SessionAdapterType` | ||
- [**Added**] `class NSURLSessionAdapter` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
# Convenience Parameters and Actual Parameters | ||
|
||
To satisfy both ease and accuracy, `RequestType` has 2 kind of parameters properties, convenience property and actual properties. If you implement convenience parameters only, actual parameters are computed by default implementation of `RequestType`. | ||
|
||
1. [Convenience parameters](#convenience-parameters) | ||
2. [Actual parameters](#actual-parameters) | ||
|
||
## Convenience parameters | ||
|
||
Most documentations of web APIs express parameters in dictionary-like notation: | ||
|
||
|Name |Type |Description | | ||
|-------|--------|-------------------------------------------------------------------------------------------------| | ||
|`q` |`string`|The search keywords, as well as any qualifiers. | | ||
|`sort` |`string`|The sort field. One of `stars`, `forks`, or `updated`. Default: results are sorted by best match.| | ||
|`order`|`string`|The sort order if `sort` parameter is provided. One of `asc` or `desc`. Default: `desc` | | ||
|
||
`RequestType` has a property `var parameter: AnyObject?` to express parameters in this kind of notation. That is the convenience parameters. | ||
|
||
```swift | ||
struct SomeRequest: RequestType { | ||
... | ||
|
||
var parameters: AnyObject? { | ||
return [ | ||
"q": "Swift", | ||
"sort": "stars", | ||
"order": "desc", | ||
] | ||
} | ||
} | ||
``` | ||
|
||
`RequestType` provides default implementation of `parameters` `nil`. | ||
|
||
```swift | ||
public extension RequestType { | ||
public var parameters: AnyObject? { | ||
return nil | ||
} | ||
} | ||
``` | ||
|
||
## Actual parameters | ||
|
||
Actually, we have to translate dictionary-like notation in API docs into HTTP/HTTPS request. There are 2 places to express parameters, URL query and body. `RequestType` has interface to express them, `var queryParameters: [String: AnyObject]?` and `var bodyParameters: BodyParametersType?`. Those are the actual parameters. | ||
|
||
If you implement convenience parameters only, the actual parameters are computed from the convenience parameters depending on HTTP method. Here is the default implementation of actual parameters: | ||
|
||
```swift | ||
public extension RequestType { | ||
public var queryParameters: [String: AnyObject]? { | ||
guard let parameters = parameters as? [String: AnyObject] where method.prefersQueryParameters else { | ||
return nil | ||
} | ||
|
||
return parameters | ||
} | ||
|
||
public var bodyParameters: BodyParametersType? { | ||
guard let parameters = parameters where !method.prefersQueryParameters else { | ||
return nil | ||
} | ||
|
||
return JSONBodyParameters(JSONObject: parameters) | ||
} | ||
} | ||
``` | ||
|
||
If you implement actual parameters for the HTTP method, the convenience parameters will be ignored. | ||
|
||
### BodyParametersType | ||
|
||
There are several MIME types to express parameters such as `application/json`, `application/x-www-form-urlencoded` and `multipart/form-data; boundary=foobarbaz`. Because parameters types to express these MIME types are different, type of `bodyParameters` is a protocol `BodyParametersType`. | ||
|
||
`BodyParametersType` defines 2 components, `contentType` and `buildEntity()`. You can create custom body parameters type that conforms to `BodyParametersType`. | ||
|
||
```swift | ||
public enum RequestBodyEntity { | ||
case Data(NSData) | ||
case InputStream(NSInputStream) | ||
} | ||
|
||
public protocol BodyParametersType { | ||
var contentType: String { get } | ||
func buildEntity() throws -> RequestBodyEntity | ||
} | ||
``` | ||
|
||
APIKit provides 3 body parameters type listed below: | ||
|
||
|Name |Parameters Type | | ||
|---------------------------------|----------------------------------------| | ||
|`JSONBodyParameters` |`AnyObject` | | ||
|`FormURLEncodedBodyParameters` |`[String: AnyObject]` | | ||
|`MultipartFormDataBodyParameters`|`[MultipartFormDataBodyParameters.Part]`| |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Customizing Networking Backend | ||
|
||
APIKit uses `NSURLSession` as networking backend by default. Since `Session` has abstraction layer of backend called `SessionAdapterType`, you can change the backend of `Session` like below: | ||
|
||
- Third party HTTP client like [Alamofire](https://github.com/Alamofire/Alamofire) | ||
- Mock backend like [`TestSessionAdapter`](../Tests/APIKit/TestComponents/TestSessionAdapter.swift) | ||
- `NSURLSession` with custom configuration and delegate | ||
|
||
Demo implementation of Alamofire adapter is available [here](https://github.com/ishkawa/APIKit-AlamofireAdapter). | ||
|
||
## SessionAdapterType | ||
|
||
`SessionAdapterType` provides an interface to get `(NSData?, NSURLResponse?, NSError?)` from `NSURLRequest` and returns `SessionTaskType` for cancellation. | ||
|
||
```swift | ||
public protocol SessionAdapterType { | ||
public func createTaskWithURLRequest(URLRequest: NSURLRequest, handler: (NSData?, NSURLResponse?, ErrorType?) -> Void) -> SessionTaskType | ||
public func getTasksWithHandler(handler: [SessionTaskType] -> Void) | ||
} | ||
|
||
public protocol SessionTaskType : class { | ||
public func resume() | ||
public func cancel() | ||
} | ||
``` | ||
|
||
|
||
## How Session works with SessionAdapterType | ||
|
||
`Session` takes an instance of type that conforms `SessionAdapterType` as a parameter of initializer. | ||
|
||
```swift | ||
public class Session { | ||
public let adapter: SessionAdapterType | ||
|
||
public init(adapter: SessionAdapterType) { | ||
self.adapter = adapter | ||
} | ||
|
||
... | ||
} | ||
``` | ||
|
||
Once it is initialized with a session adapter, it sends `NSURLRequest` and receives `(NSData?, NSURLResponse?, NSError?)` via the interfaces which are defined in `SessionAdapterType`. | ||
|
||
```swift | ||
func sendRequest<T: RequestType>(request: T, handler: (Result<T.Response, APIError>) -> Void = {r in}) -> SessionTaskType? { | ||
let URLRequest: NSURLRequest = ... | ||
let task = adapter.createTaskWithURLRequest(URLRequest) { data, URLResponse, error in | ||
... | ||
} | ||
|
||
task.resume() | ||
|
||
return task | ||
} | ||
``` |
Oops, something went wrong.