-
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 #144 from ishkawa/feature/update-readme
Documentation for 2.0
- Loading branch information
Showing
7 changed files
with
522 additions
and
397 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# 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()` 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 define in `SessionTaskError`. For example, if you throw `SomeError` in `responseFromObject()`, the closure of `Session.sendRequest()` 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` | ||
|
||
### 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` | ||
|
||
## NSURLSessionDelegate | ||
|
||
- [**Deleted**] `class URLSessionDelegate` | ||
- [**Added**] `protocol SessionAdapterType` | ||
- [**Added**] `class NSURLSessionAdapter: SessionAdapterType` |
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,93 @@ | ||
# 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`. | ||
|
||
## 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,52 @@ | ||
# 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 { | ||
func resumedTaskWithURLRequest(URLRequest: NSURLRequest, handler: (NSData?, NSURLResponse?, NSError?) -> Void) -> SessionTaskType | ||
func getTasksWithHandler(handler: [SessionTaskType] -> Void) | ||
} | ||
|
||
public protocol SessionTaskType: class { | ||
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.resumedTaskWithURLRequest(URLRequest) { data, URLResponse, error in | ||
... | ||
} | ||
} | ||
``` |
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,134 @@ | ||
# Defining Request Protocol for Web Service | ||
|
||
Most web APIs have common configurations such as base URL, authorization header fields and MIME type to accept. For example, GitHub API has common base URL `https://api.github.com`, authorization header field `Authorization` and MIME type `application/json`. Protocol to express such common interfaces and default implementations is useful in defining many request types. | ||
|
||
We define `GitHubRequestType` to give common configuration for example. | ||
|
||
## Giving default implementation to RequestType components | ||
|
||
### Base URL | ||
|
||
First of all, we give default implementation for `baseURL`. | ||
|
||
```swift | ||
protocol GitHubRequestType: RequestType { | ||
|
||
} | ||
|
||
extension GitHubRequestType { | ||
var baseURL: NSURL { | ||
return NSURL(string: "https://api.github.com")! | ||
} | ||
} | ||
``` | ||
|
||
### JSON Mapping | ||
|
||
There are several JSON mapping library such as [Himotoki](https://github.com/ikesyo/Himotoki), [Argo](https://github.com/thoughtbot/Argo) and [Unbox](https://github.com/JohnSundell/Unbox). These libraries provide protocol that define interface to decode `AnyObject` into JSON model type. If you adopt one of them, you can give default implementation to `responseFromObject()`. Here is an example of default implementation with Himotoki 2: | ||
|
||
```swift | ||
import Himotoki | ||
|
||
extension GitHubRequestType where Response: Decodable { | ||
func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { | ||
return try decodeValue(object) | ||
} | ||
} | ||
``` | ||
|
||
### Defining request types | ||
|
||
Since `GitHubRequestType` has default implementations of `baseURL` and `responseFromObject()`, all you have to implement to conform to `GitHubRequestType` are 3 components, `Response`, `method` and `path`. | ||
|
||
```swift | ||
final class GitHubAPI { | ||
struct RateLimitRequest { | ||
typealias Response = RateLimit | ||
|
||
var method: HTTPMethod { | ||
return .GET | ||
} | ||
|
||
var path: String { | ||
return "/rate_limit" | ||
} | ||
} | ||
|
||
struct SearchRepositoriesRequest: GitHubRequestType { | ||
let query: String | ||
|
||
// MARK: RequestType | ||
typealias Response = SearchResponse<Repository> | ||
|
||
var method: HTTPMethod { | ||
return .GET | ||
} | ||
|
||
var path: String { | ||
return "/search/repositories" | ||
} | ||
|
||
var parameters: AnyObject? { | ||
return ["q": query] | ||
} | ||
} | ||
} | ||
``` | ||
|
||
It is useful for code completion to nest request types in a utility class like `GitHubAPI` above. | ||
|
||
## Throwing custom errors web API returns | ||
|
||
Most web APIs define error response to notify what happened on the server. For example, GitHub API defines errors [like this](https://developer.github.com/v3/#client-errors). `interceptObject()` in `RequestType` gives us a chance to determine if the response is an error. If the response is an error, you can create custom error object from the response object and throw the error in `interceptObject()`. | ||
|
||
Here is an example of handling [GitHub API errors](https://developer.github.com/v3/#client-errors): | ||
|
||
```swift | ||
// https://developer.github.com/v3/#client-errors | ||
struct GitHubError { | ||
let message: String | ||
|
||
init(object: AnyObject) { | ||
message = object["message"] as? String ?? "Unknown error occurred" | ||
} | ||
} | ||
|
||
extension GitHubRequestType { | ||
func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response { | ||
guard (200..<300).contains(URLResponse.statusCode) else { | ||
throw GitHubError(object: AnyObject) | ||
} | ||
|
||
return object | ||
} | ||
} | ||
``` | ||
|
||
The custom error you throw in `interceptObject()` can be retrieved from call-site as `.Failure(.ResponseError(GitHubError))`. | ||
|
||
```swift | ||
let request = SomeGitHubRequest() | ||
|
||
Session.sendRequest(request) { result in | ||
switch result { | ||
case .Success(let response): | ||
print(response) | ||
|
||
case .Failure(let error): | ||
printSessionTaskError(error) | ||
} | ||
} | ||
|
||
func printSessionTaskError(error: SessionTaskError) { | ||
switch sessionTaskError { | ||
case .ResponseError(let error as GitHubError): | ||
print(error.message) // Prints message from GitHub API | ||
|
||
case .ConnectionError(let error): | ||
print("Connection error: \(error.localizedDescription)") | ||
|
||
default: | ||
print("System error :bow:") | ||
} | ||
} | ||
``` |
Oops, something went wrong.