Skip to content

Commit

Permalink
Merge pull request #161 from ishkawa/2.0-development
Browse files Browse the repository at this point in the history
APIKit 2.0
  • Loading branch information
ishkawa committed May 23, 2016
2 parents 1cbf85e + 91e262e commit 9f52b84
Show file tree
Hide file tree
Showing 60 changed files with 2,564 additions and 2,272 deletions.
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
url = https://github.com/antitypical/Result.git
[submodule "Carthage/Checkouts/OHHTTPStubs"]
path = Carthage/Checkouts/OHHTTPStubs
url = https://github.com/AliSoftware/OHHTTPStubs.git
url = https://github.com/ishkawa/OHHTTPStubs.git
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ script:
- pod lib lint
- set -o pipefail
- xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit | xcpretty -c
- xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit -sdk iphonesimulator -destination 'name=iPhone 6,OS=9.1' | xcpretty -c
- xcodebuild build -workspace APIKit.xcworkspace -scheme APIKit -sdk appletvsimulator -destination 'name=Apple TV 1080p,OS=9.1' | xcpretty -c
- xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit -sdk iphonesimulator | xcpretty -c
- xcodebuild test -workspace APIKit.xcworkspace -scheme APIKit -sdk appletvsimulator | xcpretty -c

before_deploy:
- ./script/import-certificates
Expand Down
8 changes: 4 additions & 4 deletions APIKit.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "APIKit"
s.version = "1.4.1"
s.version = "2.0.0"
s.summary = "A networking library for building type safe web API client in Swift."
s.homepage = "https://github.com/ishkawa/APIKit"

Expand All @@ -9,15 +9,15 @@ Pod::Spec.new do |s|
}

s.ios.deployment_target = "8.0"
s.osx.deployment_target = "10.9"
s.osx.deployment_target = "10.10"
if s.respond_to?(:watchos)
s.watchos.deployment_target = "2.0"
end
if s.respond_to?(:tvos)
s.tvos.deployment_target = "9.0"
end

s.source_files = "Sources/*.swift"
s.source_files = "Sources/**/*.{swift,h,m}"
s.source = {
:git => "https://github.com/ishkawa/APIKit.git",
:tag => "#{s.version}",
Expand All @@ -26,7 +26,7 @@ Pod::Spec.new do |s|
s.license = {
:type => "MIT",
:text => <<-LICENSE
Copyright (c) 2015 Yosuke Ishikawa
Copyright (c) 2015 - 2016 Yosuke Ishikawa
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
Expand Down
235 changes: 202 additions & 33 deletions APIKit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cartfile.private
Original file line number Diff line number Diff line change
@@ -1 +1 @@
github "AliSoftware/OHHTTPStubs" ~> 4.6.0
github "ishkawa/OHHTTPStubs" "75f74c9c19620a37f436b38e2bc20a07310b999e"
2 changes: 1 addition & 1 deletion Cartfile.resolved
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"
2 changes: 1 addition & 1 deletion Carthage/Checkouts/OHHTTPStubs
Submodule OHHTTPStubs updated 46 files
+33 −1 CHANGELOG.md
+2 −1 Examples/ObjC/MainViewController.m
+8 −8 Examples/ObjC/Podfile.lock
+3 −3 Examples/ObjC/Pods/Local Podspecs/OHHTTPStubs.podspec.json
+8 −8 Examples/ObjC/Pods/Manifest.lock
+1 −1 Examples/ObjC/Pods/Pods.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs.xcscheme
+1 −1 Examples/Swift/MainViewController.swift
+9 −9 Examples/Swift/Podfile.lock
+3 −3 Examples/Swift/Pods/Local Podspecs/OHHTTPStubs.podspec.json
+9 −9 Examples/Swift/Pods/Manifest.lock
+1 −1 Examples/Swift/Pods/Pods.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs.xcscheme
+1 −1 Examples/Swift/Pods/Target Support Files/OHHTTPStubs/Info.plist
+3 −3 OHHTTPStubs.podspec
+30 −12 OHHTTPStubs/OHHTTPStubs.xcodeproj/project.pbxproj
+1 −1 OHHTTPStubs/Podfile
+13 −24 OHHTTPStubs/Podfile.lock
+67 −21 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.h
+49 −13 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.m
+8 −9 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFNetworkReachabilityManager.h
+42 −44 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFNetworkReachabilityManager.m
+5 −18 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFSecurityPolicy.h
+18 −20 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFSecurityPolicy.m
+5 −26 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLRequestSerialization.h
+4 −56 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLRequestSerialization.m
+4 −4 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLResponseSerialization.h
+8 −8 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLResponseSerialization.m
+37 −98 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLSessionManager.h
+188 −125 OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLSessionManager.m
+24 −12 OHHTTPStubs/Pods/AFNetworking/README.md
+0 −4 OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFAutoPurgingImageCache.m
+22 −2 OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.h
+13 −11 OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.m
+11 −7 OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIButton+AFNetworking.m
+19 −6 OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIImageView+AFNetworking.m
+1 −1 OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIWebView+AFNetworking.h
+5 −4 OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIWebView+AFNetworking.m
+13 −24 OHHTTPStubs/Pods/Manifest.lock
+35 −1 OHHTTPStubs/Sources/OHHTTPStubs.h
+88 −10 OHHTTPStubs/Sources/OHHTTPStubs.m
+49 −0 OHHTTPStubs/Sources/Swift/OHHTTPStubsSwift.swift
+1 −3 OHHTTPStubs/Supporting Files/OHHTTPStubs Mac-Info.plist
+3 −1 OHHTTPStubs/Supporting Files/OHHTTPStubs iOS-Info.plist
+6 −5 OHHTTPStubs/UnitTests/Test Suites/AFNetworkingTests.m
+1 −1 OHHTTPStubs/UnitTests/Test Suites/NSURLConnectionTests.m
+93 −5 OHHTTPStubs/UnitTests/Test Suites/SwiftHelpersTests.swift
+4 −1 README.md
2 changes: 1 addition & 1 deletion Configurations/Base.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ VERSION_INFO_PREFIX =
VERSIONING_SYSTEM = apple-generic

CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer
MACOSX_DEPLOYMENT_TARGET = 10.9
MACOSX_DEPLOYMENT_TARGET = 10.10
IPHONEOS_DEPLOYMENT_TARGET = 8.0
WATCHOS_DEPLOYMENT_TARGET = 2.0
TVOS_DEPLOYMENT_TARGET = 9.0
17 changes: 7 additions & 10 deletions Demo.playground/Contents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,10 @@ struct GetRateLimitRequest: GitHubRequestType {
return "/rate_limit"
}

func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? {
guard let dictionary = object as? [String: AnyObject] else {
return nil
}

guard let rateLimit = RateLimit(dictionary: dictionary) else {
return nil
func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) throws -> Response {
guard let dictionary = object as? [String: AnyObject],
let rateLimit = RateLimit(dictionary: dictionary) else {
throw ResponseError.UnexpectedObject(object)
}

return rateLimit
Expand All @@ -66,10 +63,10 @@ let request = GetRateLimitRequest()
Session.sendRequest(request) { result in
switch result {
case .Success(let rateLimit):
debugPrint("count: \(rateLimit.count)")
debugPrint("reset: \(rateLimit.resetDate)")
print("count: \(rateLimit.count)")
print("reset: \(rateLimit.resetDate)")

case .Failure(let error):
debugPrint("error: \(error)")
print("error: \(error)")
}
}
2 changes: 1 addition & 1 deletion Demo.playground/contents.xcplayground
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>
106 changes: 106 additions & 0 deletions Documentation/APIKit2MigrationGuide.md
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`
96 changes: 96 additions & 0 deletions Documentation/ConvenienceParametersAndActualParameters.md
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]`|
57 changes: 57 additions & 0 deletions Documentation/CustomizingNetworkingBackend.md
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
}
```
Loading

0 comments on commit 9f52b84

Please sign in to comment.