Skip to content

Commit

Permalink
Merge pull request #144 from ishkawa/feature/update-readme
Browse files Browse the repository at this point in the history
Documentation for 2.0
  • Loading branch information
ishkawa committed Mar 21, 2016
2 parents 02ab6ef + 677cd38 commit 26c8bda
Show file tree
Hide file tree
Showing 7 changed files with 522 additions and 397 deletions.
104 changes: 104 additions & 0 deletions Documentation/APIKit2MigrationGuide.md
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`
93 changes: 93 additions & 0 deletions Documentation/ConvenienceParametersAndActualParameters.md
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]`|
52 changes: 52 additions & 0 deletions Documentation/CustomizingNetworkingBackend.md
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
...
}
}
```
134 changes: 134 additions & 0 deletions Documentation/DefiningRequestProtocolForWebService.md
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:")
}
}
```
Loading

0 comments on commit 26c8bda

Please sign in to comment.