Skip to content

Commit 9f52b84

Browse files
committed
Merge pull request #161 from ishkawa/2.0-development
APIKit 2.0
2 parents 1cbf85e + 91e262e commit 9f52b84

File tree

60 files changed

+2564
-2272
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+2564
-2272
lines changed

.gitmodules

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
url = https://github.com/antitypical/Result.git
44
[submodule "Carthage/Checkouts/OHHTTPStubs"]
55
path = Carthage/Checkouts/OHHTTPStubs
6-
url = https://github.com/AliSoftware/OHHTTPStubs.git
6+
url = https://github.com/ishkawa/OHHTTPStubs.git

.travis.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ script:
1717
- pod lib lint
1818
- set -o pipefail
1919
- xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit | xcpretty -c
20-
- xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit -sdk iphonesimulator -destination 'name=iPhone 6,OS=9.1' | xcpretty -c
21-
- xcodebuild build -workspace APIKit.xcworkspace -scheme APIKit -sdk appletvsimulator -destination 'name=Apple TV 1080p,OS=9.1' | xcpretty -c
20+
- xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit -sdk iphonesimulator | xcpretty -c
21+
- xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit -sdk appletvsimulator | xcpretty -c
2222

2323
before_deploy:
2424
- ./script/import-certificates

APIKit.podspec

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = "APIKit"
3-
s.version = "1.4.1"
3+
s.version = "2.0.0"
44
s.summary = "A networking library for building type safe web API client in Swift."
55
s.homepage = "https://github.com/ishkawa/APIKit"
66

@@ -9,15 +9,15 @@ Pod::Spec.new do |s|
99
}
1010

1111
s.ios.deployment_target = "8.0"
12-
s.osx.deployment_target = "10.9"
12+
s.osx.deployment_target = "10.10"
1313
if s.respond_to?(:watchos)
1414
s.watchos.deployment_target = "2.0"
1515
end
1616
if s.respond_to?(:tvos)
1717
s.tvos.deployment_target = "9.0"
1818
end
1919

20-
s.source_files = "Sources/*.swift"
20+
s.source_files = "Sources/**/*.{swift,h,m}"
2121
s.source = {
2222
:git => "https://github.com/ishkawa/APIKit.git",
2323
:tag => "#{s.version}",
@@ -26,7 +26,7 @@ Pod::Spec.new do |s|
2626
s.license = {
2727
:type => "MIT",
2828
:text => <<-LICENSE
29-
Copyright (c) 2015 Yosuke Ishikawa
29+
Copyright (c) 2015 - 2016 Yosuke Ishikawa
3030
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
3131
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
3232
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

APIKit.xcodeproj/project.pbxproj

+202-33
Large diffs are not rendered by default.

Cartfile.private

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
github "AliSoftware/OHHTTPStubs" ~> 4.6.0
1+
github "ishkawa/OHHTTPStubs" "75f74c9c19620a37f436b38e2bc20a07310b999e"

Cartfile.resolved

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
github "AliSoftware/OHHTTPStubs" "4.6.0"
1+
github "ishkawa/OHHTTPStubs" "75f74c9c19620a37f436b38e2bc20a07310b999e"
22
github "antitypical/Result" "2.0.0"

Carthage/Checkouts/OHHTTPStubs

Submodule OHHTTPStubs updated 46 files

Configurations/Base.xcconfig

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ VERSION_INFO_PREFIX =
2525
VERSIONING_SYSTEM = apple-generic
2626

2727
CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer
28-
MACOSX_DEPLOYMENT_TARGET = 10.9
28+
MACOSX_DEPLOYMENT_TARGET = 10.10
2929
IPHONEOS_DEPLOYMENT_TARGET = 8.0
3030
WATCHOS_DEPLOYMENT_TARGET = 2.0
3131
TVOS_DEPLOYMENT_TARGET = 9.0

Demo.playground/Contents.swift

+7-10
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,10 @@ struct GetRateLimitRequest: GitHubRequestType {
4747
return "/rate_limit"
4848
}
4949

50-
func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? {
51-
guard let dictionary = object as? [String: AnyObject] else {
52-
return nil
53-
}
54-
55-
guard let rateLimit = RateLimit(dictionary: dictionary) else {
56-
return nil
50+
func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response {
51+
guard let dictionary = object as? [String: AnyObject],
52+
let rateLimit = RateLimit(dictionary: dictionary) else {
53+
throw ResponseError.UnexpectedObject(object)
5754
}
5855

5956
return rateLimit
@@ -66,10 +63,10 @@ let request = GetRateLimitRequest()
6663
Session.sendRequest(request) { result in
6764
switch result {
6865
case .Success(let rateLimit):
69-
debugPrint("count: \(rateLimit.count)")
70-
debugPrint("reset: \(rateLimit.resetDate)")
66+
print("count: \(rateLimit.count)")
67+
print("reset: \(rateLimit.resetDate)")
7168

7269
case .Failure(let error):
73-
debugPrint("error: \(error)")
70+
print("error: \(error)")
7471
}
7572
}

Demo.playground/contents.xcplayground

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<playground version='5.0' target-platform='ios' requires-full-environment='true' display-mode='raw'>
2+
<playground version='5.0' target-platform='ios' display-mode='raw'>
33
<timeline fileName='timeline.xctimeline'/>
44
</playground>
+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# APIKit 2 Migration Guide
2+
3+
APIKit 2.0 introduces several breaking changes to add functionality and to improve modeling of web API.
4+
5+
- Abstraction of backend
6+
- Improved error handling modeling
7+
- Separation of convenience parameters and type-safe parameters
8+
9+
## Errors
10+
11+
- [**Deleted**] `APIError`
12+
- [**Added**] `SessionTaskError`
13+
14+
Errors cases of `Session.sendRequest(_:handler:)` is reduced to 3 cases listed below:
15+
16+
```swift
17+
public enum SessionTaskError: ErrorType {
18+
/// Error of networking backend such as `NSURLSession`.
19+
case ConnectionError(NSError)
20+
21+
/// Error while creating `NSURLRequest` from `Request`.
22+
case RequestError(ErrorType)
23+
24+
/// Error while creating `RequestType.Response` from `(NSData, NSURLResponse)`.
25+
case ResponseError(ErrorType)
26+
}
27+
```
28+
29+
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))`.
30+
31+
## RequestType
32+
33+
### Converting AnyObject to Response
34+
35+
- [**Deleted**] `func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response?`
36+
- [**Added**] `func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response`
37+
38+
### Handling response errors
39+
40+
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.
41+
42+
- [**Deleted**] `var acceptableStatusCodes: Set<Int> { get }`
43+
- [**Deleted**] `func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType?`
44+
- [**Added**] `func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> AnyObject`
45+
46+
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.
47+
48+
```swift
49+
func interceptObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> AnyObject {
50+
guard (200..<300).contains(URLResponse.statusCode) else {
51+
// https://developer.github.com/v3/#client-errors
52+
throw GitHubError(object: object)
53+
}
54+
55+
return object
56+
}
57+
```
58+
59+
### Parameters
60+
61+
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`.
62+
63+
- [**Deleted**] `var parameters: [String: AnyObject]`
64+
- [**Deleted**] `var objectParameters: AnyObject`
65+
- [**Deleted**] `var requestBodyBuilder: RequestBodyBuilder`
66+
- [**Added**] `var parameters: AnyObject?` (convenience property)
67+
- [**Added**] `var bodyParameters: BodyParametersType?` (actual property)
68+
- [**Added**] `var queryParameters: [String: AnyObject]?` (actual property)
69+
70+
Related types:
71+
72+
- [**Deleted**] `enum RequestBodyBuilder`
73+
- [**Added**] `protocol BodyParametersType`
74+
75+
APIKit provides 3 parameters types that conform to `BodyParametersType`:
76+
77+
- [**Added**] `class JSONBodyParameters`
78+
- [**Added**] `class FormURLEncodedBodyParameters`
79+
- [**Added**] `class MultipartFormDataBodyParameters`
80+
81+
### Data parsers
82+
83+
- [**Deleted**] `var responseBodyParser: ResponseBodyParser`
84+
- [**Added**] `var dataParser: DataParserType`
85+
86+
Related types:
87+
88+
- [**Deleted**] `enum ResponseBodyParser`
89+
- [**Added**] `protocol DataParserType`
90+
- [**Added**] `class JSONDataParser`
91+
- [**Added**] `class FormURLEncodedDataParser`
92+
- [**Added**] `class StringDataParser`
93+
94+
### Configuring NSURLRequest
95+
96+
`configureURLRequest()` in 1.x is renamed to `interceptURLRequest()` for the consistency with `interceptObject()`.
97+
98+
- [**Deleted**] `func configureURLRequest(URLRequest: NSMutableURLRequest) -> NSMutableURLRequest`
99+
- [**Added**] `func interceptURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest`
100+
101+
## NSURLSession
102+
103+
- [**Deleted**] `class URLSessionDelegate`
104+
- [**Added**] `protocol SessionTaskType`
105+
- [**Added**] `protocol SessionAdapterType`
106+
- [**Added**] `class NSURLSessionAdapter`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Convenience Parameters and Actual Parameters
2+
3+
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`.
4+
5+
1. [Convenience parameters](#convenience-parameters)
6+
2. [Actual parameters](#actual-parameters)
7+
8+
## Convenience parameters
9+
10+
Most documentations of web APIs express parameters in dictionary-like notation:
11+
12+
|Name |Type |Description |
13+
|-------|--------|-------------------------------------------------------------------------------------------------|
14+
|`q` |`string`|The search keywords, as well as any qualifiers. |
15+
|`sort` |`string`|The sort field. One of `stars`, `forks`, or `updated`. Default: results are sorted by best match.|
16+
|`order`|`string`|The sort order if `sort` parameter is provided. One of `asc` or `desc`. Default: `desc` |
17+
18+
`RequestType` has a property `var parameter: AnyObject?` to express parameters in this kind of notation. That is the convenience parameters.
19+
20+
```swift
21+
struct SomeRequest: RequestType {
22+
...
23+
24+
var parameters: AnyObject? {
25+
return [
26+
"q": "Swift",
27+
"sort": "stars",
28+
"order": "desc",
29+
]
30+
}
31+
}
32+
```
33+
34+
`RequestType` provides default implementation of `parameters` `nil`.
35+
36+
```swift
37+
public extension RequestType {
38+
public var parameters: AnyObject? {
39+
return nil
40+
}
41+
}
42+
```
43+
44+
## Actual parameters
45+
46+
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.
47+
48+
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:
49+
50+
```swift
51+
public extension RequestType {
52+
public var queryParameters: [String: AnyObject]? {
53+
guard let parameters = parameters as? [String: AnyObject] where method.prefersQueryParameters else {
54+
return nil
55+
}
56+
57+
return parameters
58+
}
59+
60+
public var bodyParameters: BodyParametersType? {
61+
guard let parameters = parameters where !method.prefersQueryParameters else {
62+
return nil
63+
}
64+
65+
return JSONBodyParameters(JSONObject: parameters)
66+
}
67+
}
68+
```
69+
70+
If you implement actual parameters for the HTTP method, the convenience parameters will be ignored.
71+
72+
### BodyParametersType
73+
74+
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`.
75+
76+
`BodyParametersType` defines 2 components, `contentType` and `buildEntity()`. You can create custom body parameters type that conforms to `BodyParametersType`.
77+
78+
```swift
79+
public enum RequestBodyEntity {
80+
case Data(NSData)
81+
case InputStream(NSInputStream)
82+
}
83+
84+
public protocol BodyParametersType {
85+
var contentType: String { get }
86+
func buildEntity() throws -> RequestBodyEntity
87+
}
88+
```
89+
90+
APIKit provides 3 body parameters type listed below:
91+
92+
|Name |Parameters Type |
93+
|---------------------------------|----------------------------------------|
94+
|`JSONBodyParameters` |`AnyObject` |
95+
|`FormURLEncodedBodyParameters` |`[String: AnyObject]` |
96+
|`MultipartFormDataBodyParameters`|`[MultipartFormDataBodyParameters.Part]`|
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Customizing Networking Backend
2+
3+
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:
4+
5+
- Third party HTTP client like [Alamofire](https://github.com/Alamofire/Alamofire)
6+
- Mock backend like [`TestSessionAdapter`](../Tests/APIKit/TestComponents/TestSessionAdapter.swift)
7+
- `NSURLSession` with custom configuration and delegate
8+
9+
Demo implementation of Alamofire adapter is available [here](https://github.com/ishkawa/APIKit-AlamofireAdapter).
10+
11+
## SessionAdapterType
12+
13+
`SessionAdapterType` provides an interface to get `(NSData?, NSURLResponse?, NSError?)` from `NSURLRequest` and returns `SessionTaskType` for cancellation.
14+
15+
```swift
16+
public protocol SessionAdapterType {
17+
public func createTaskWithURLRequest(URLRequest: NSURLRequest, handler: (NSData?, NSURLResponse?, ErrorType?) -> Void) -> SessionTaskType
18+
public func getTasksWithHandler(handler: [SessionTaskType] -> Void)
19+
}
20+
21+
public protocol SessionTaskType : class {
22+
public func resume()
23+
public func cancel()
24+
}
25+
```
26+
27+
28+
## How Session works with SessionAdapterType
29+
30+
`Session` takes an instance of type that conforms `SessionAdapterType` as a parameter of initializer.
31+
32+
```swift
33+
public class Session {
34+
public let adapter: SessionAdapterType
35+
36+
public init(adapter: SessionAdapterType) {
37+
self.adapter = adapter
38+
}
39+
40+
...
41+
}
42+
```
43+
44+
Once it is initialized with a session adapter, it sends `NSURLRequest` and receives `(NSData?, NSURLResponse?, NSError?)` via the interfaces which are defined in `SessionAdapterType`.
45+
46+
```swift
47+
func sendRequest<T: RequestType>(request: T, handler: (Result<T.Response, APIError>) -> Void = {r in}) -> SessionTaskType? {
48+
let URLRequest: NSURLRequest = ...
49+
let task = adapter.createTaskWithURLRequest(URLRequest) { data, URLResponse, error in
50+
...
51+
}
52+
53+
task.resume()
54+
55+
return task
56+
}
57+
```

0 commit comments

Comments
 (0)