Commit 577a5f81 authored by Ezequiel R. Aguerre's avatar Ezequiel R. Aguerre

Merge branch '9-mb-add_put_patch_and_delete_methods_to_api' into 'master'

Add PUT, PATCH and DELETE methods to the API

__IMPORTANT__: Merge !9 first!

Closes #9

See merge request !7
parents 98b83f0a f994be68
......@@ -6,8 +6,12 @@ build_code:
script:
- cd Example/
- bundle install
- bundle exec pod install
- bundle exec scan --scheme Api-Example
- bundle exec pod install
- bundle exec scan --scheme Api-Example --code_coverage --derived_data_path build
- bundle exec slather coverage -s --scheme Api-Example --workspace Api.xcworkspace --build-directory build --binary-basename Api Api.xcodeproj
- bundle exec slather coverage --html --output-directory test_output/coverage --scheme Api-Example --workspace Api.xcworkspace --build-directory build --binary-basename Api Api.xcodeproj
- cd ..
- lizard -X Api/ | lizardf.rb > Example/test_output/complexity.html
tags:
- ios
artifacts:
......
......@@ -49,6 +49,27 @@ public class Api {
headers: headers, completion: completion)
}
public func put( path: String, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL,
headers: [String: String]? = nil, completion: Response<AnyObject, NSError> -> Void )
{
return request( .PUT, path: path, parameters: parameters, encoding: encoding,
headers: headers, completion: completion)
}
public func patch( path: String, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL,
headers: [String: String]? = nil, completion: Response<AnyObject, NSError> -> Void )
{
return request( .PATCH, path: path, parameters: parameters, encoding: encoding,
headers: headers, completion: completion)
}
public func delete( path: String, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL,
headers: [String: String]? = nil, completion: Response<AnyObject, NSError> -> Void )
{
return request( .DELETE, path: path, parameters: parameters, encoding: encoding,
headers: headers, completion: completion)
}
func urlForPath( path: String ) -> NSURL {
return apiURL.URLByAppendingPathComponent( path )
}
......
//
// ApiError.swift
// Pods
//
// Created by Mauro Bender on 25/8/16.
//
//
import Alamofire
public enum ApiError: ErrorType {
case Network( error: NSError, response: NSURLResponse )
case JSONSerialization(error: NSError)
case Unknown( error: NSError )
}
public extension ApiError {
init( response: Response<AnyObject, NSError> ) {
let error = response.result.error!
if let statusCode = response.response?.statusCode where !(200...299).contains( statusCode ) {
self = .Network( error: error, response: response.response! )
} else if error.code == -6006 {
self = .JSONSerialization( error: error )
} else {
self = .Unknown( error: error )
}
}
}
\ No newline at end of file
//
// Result.swift
// Pods
//
// Created by Mauro Bender on 14/8/16.
//
//
import Alamofire
public enum ApiResult<Value> {
case Success(Value)
case Failure(ApiError)
/// Returns `true` if the result is a success, `false` otherwise.
public var isSuccess: Bool {
switch self {
case .Success:
return true
case .Failure:
return false
}
}
/// Returns `true` if the result is a failure, `false` otherwise.
public var isFailure: Bool {
return !isSuccess
}
/// Returns the associated value if the result is a success, `nil` otherwise.
public var value: Value? {
switch self {
case .Success(let value):
return value
case .Failure:
return nil
}
}
/// Returns the associated error value if the result is a failure, `nil` otherwise.
public var error: ApiError? {
switch self {
case .Success:
return nil
case .Failure(let error):
return error
}
}
}
public enum EmptyResult {
case Success
case Failure(ApiError)
/// Returns `true` if the result is a success, `false` otherwise.
public var isSuccess: Bool {
switch self {
case .Success:
return true
case .Failure:
return false
}
}
/// Returns `true` if the result is a failure, `false` otherwise.
public var isFailure: Bool {
return !isSuccess
}
/// Returns the associated error value if the result is a failure, `nil` otherwise.
public var error: ApiError? {
switch self {
case .Success:
return nil
case .Failure(let error):
return error
}
}
}
// MARK: - CustomStringConvertible
extension ApiResult: CustomStringConvertible {
/// The textual representation used when written to an output stream, which includes whether the result was a
/// success or failure.
public var description: String {
switch self {
case .Success:
return "SUCCESS"
case .Failure:
return "FAILURE"
}
}
}
extension ApiResult: CustomDebugStringConvertible {
/// The debug textual representation used when written to an output stream, which includes whether the result was a
/// success or failure in addition to the value or error.
public var debugDescription: String {
switch self {
case .Success(let value):
return "SUCCESS: \(value)"
case .Failure(let error):
return "FAILURE: \(error)"
}
}
}
// MARK: Factories
// FIXME: Encapsulate
public func itemResult<T: JSONSerializable>( response: Response<AnyObject, NSError>,
keyPath: String? ) -> ApiResult< T? >
{
switch(response.result) {
case .Success(let JSON):
if let itemJSON = (keyPath != nil ? JSON[ keyPath! ] : JSON) as? [String : AnyObject] {
return .Success( T( fromJSON: itemJSON ) )
} else {
return .Success( nil )
}
case .Failure(let error):
return .Failure( ApiError( response: response ) )
}
}
public func emptyResult( response: Response<AnyObject, NSError> ) -> EmptyResult {
switch(response.result) {
case .Success:
return .Success
case .Failure(let error):
return .Failure( ApiError( response: response ) )
}
}
public func listResult<T: JSONSerializable>( response: Response<AnyObject, NSError>,
keyPath: String? ) -> ApiResult< [T]? >
{
switch(response.result) {
case .Success(let JSON):
if let listJSON = (keyPath != nil ? JSON[ keyPath! ] : JSON) as? [[String : AnyObject]] {
let list = listJSON.map { T( fromJSON: $0 ) }
return .Success( list )
} else {
return .Success( nil )
}
case .Failure(let error):
return .Failure( ApiError( response: response ) )
}
}
public func paginatedListResult<T: JSONSerializable>( response: Response<AnyObject, NSError>,
keyPath: String?, paginationKeyPath: String? ) -> ApiResult< ([T], Pagination)? >
{
switch(response.result) {
case .Success(let JSON):
if let listJSON = (keyPath != nil ? JSON[ keyPath! ] : JSON) as? [[String : AnyObject]],
let paginationJSON = (paginationKeyPath != nil ? JSON[ paginationKeyPath! ] : JSON) as? [String : AnyObject]
{
let list = listJSON.map { T( fromJSON: $0 ) }
let pagination = Pagination( fromJSON: paginationJSON )
return .Success( (list, pagination) )
} else {
return .Success( nil )
}
case .Failure(let error):
return .Failure( ApiError( response: response ) )
}
}
//
// Pagination.swift
// Pods
//
// Created by Mauro Bender on 25/8/16.
//
//
public struct Pagination {
let page: Int
let pages: Int
let perPage: Int
let count: Int
var hasNext: Bool { return self.page < self.pages }
var hasPrev: Bool { return self.page > 1 }
var next: Int? {
guard hasNext else { return nil }
return page + 1
}
var prev: Int? {
guard hasPrev else { return nil }
return page - 1
}
}
public extension Pagination {
init( fromJSON: [String: AnyObject] ) {
self.page = fromJSON["page"] as! Int
self.pages = fromJSON["pages"] as! Int
self.perPage = fromJSON["perPage"] as! Int
self.count = fromJSON["count"] as! Int
}
}
......@@ -13,15 +13,11 @@ public protocol JSONSerializable {
func toJSON() -> [ String: AnyObject ]
}
public enum ApiError: ErrorType {
case Network( error: NSError, response: NSURLResponse )
case JSONSerialization(error: NSError)
case Unknown( error: NSError )
}
public class Resource {
public let api: Api
public let path: String
public var responseKeyPath: String? = "response"
public var paginationKeyPath: String? = "pagination"
init( api: Api, path: String ) {
self.api = api
......@@ -61,6 +57,13 @@ extension Resource {
headers: headers, completion: completion)
}
public func patch( path: String? = nil, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL,
headers: [String: String]? = nil, completion: Response<AnyObject, NSError> -> Void )
{
return request( .PATCH, path: path, parameters: parameters, encoding: encoding,
headers: headers, completion: completion)
}
public func delete( path: String? = nil, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL,
headers: [String: String]? = nil, completion: Response<AnyObject, NSError> -> Void )
{
......@@ -72,88 +75,60 @@ extension Resource {
// MARK: CRUD
extension Resource {
public func item<T: JSONSerializable>( id: AnyObject, parameters: [ String: String ]? = nil,
headers: [ String: String]? = nil, completion: ( ( T?, error: ApiError? ) -> Void ) )
headers: [ String: String]? = nil, completion: ( ( ApiResult< T? > ) -> Void ) )
{
get( "\(id)", parameters: parameters, headers: headers ) { response in
self.handleItemResult( response, handler: completion )
completion( itemResult( response, keyPath: self.responseKeyPath ) )
}
}
public func list<T: JSONSerializable>( parameters: [ String: String ]? = nil,
headers: [ String: String]? = nil, completion: ( ( [ T ]?, error: ApiError? ) -> Void ) )
headers: [ String: String]? = nil, completion: ( ( ApiResult< [T]? > ) -> Void ) )
{
get( parameters: parameters, headers: headers ) { response in
self.handleListResult( response, handler: completion )
completion( listResult( response, keyPath: self.responseKeyPath ) )
}
}
public func paginate<T: JSONSerializable>( parameters: [ String: String ]? = nil, headers: [ String: String]? = nil,
completion: ( ( ApiResult< ([T], Pagination)? > ) -> Void ) )
{
get( parameters: parameters, headers: headers ) { response in
completion( paginatedListResult( response,
keyPath: self.responseKeyPath, paginationKeyPath: self.paginationKeyPath ) )
}
}
public func create<T: JSONSerializable>( item: T, parameters: [ String: String ]? = nil,
headers: [ String: String]? = nil, encoding: ParameterEncoding = .JSON,
completion: ( ( T?, error: ApiError? ) -> Void ) )
completion: ( ( ApiResult< T? > ) -> Void ) )
{
var requestParameters = item.toJSON()
if parameters != nil { requestParameters += parameters! }
post( parameters: requestParameters, encoding: encoding, headers: headers ) { response in
self.handleItemResult( response, handler: completion )
}
}
public func destroy( id: AnyObject, parameters: [ String: String ]? = nil,
headers: [ String: String]? = nil, completion: ( ( error: ApiError? ) -> Void ) )
{
delete( "\(id)", parameters: parameters, headers: headers ) { response in
self.handleEmptyResult( response, handler: completion )
completion( itemResult( response, keyPath: self.responseKeyPath ) )
}
}
// MARK: UTILS
private func handleItemResult<T: JSONSerializable>( response: Response<AnyObject, NSError>,
handler: ( T?, error: ApiError? ) -> Void )
{
switch(response.result) {
case .Success(let JSON):
handler( T( fromJSON: JSON as! [String : AnyObject] ), error: nil )
case .Failure(let error):
handler( nil, error: handleError( response ) )
}
}
private func handleListResult<T: JSONSerializable>( response: Response<AnyObject, NSError>,
handler: ( [ T ]?, error: ApiError? ) -> Void )
public func edit<T: JSONSerializable>( id: AnyObject, item: T, parameters: [ String: String ]? = nil,
headers: [ String: String]? = nil, encoding: ParameterEncoding = .JSON,
completion: ( ( ApiResult< T? > ) -> Void ) )
{
switch(response.result) {
case .Success(let JSON):
if let JSONList = JSON as? [ [String: AnyObject] ] {
let list = JSONList.map { T( fromJSON: $0 ) }
handler( list, error: nil )
}
case .Failure(let error):
handler( nil, error: handleError( response ) )
var requestParameters = item.toJSON()
if parameters != nil { requestParameters += parameters! }
patch( "\(id)", parameters: requestParameters, encoding: encoding, headers: headers ) { response in
completion( itemResult( response, keyPath: self.responseKeyPath ) )
}
}
private func handleEmptyResult( response: Response<AnyObject, NSError>, handler: ( error: ApiError? ) -> Void )
public func destroy( id: AnyObject, parameters: [ String: String ]? = nil,
headers: [ String: String]? = nil, completion: ( ( EmptyResult ) -> Void ) )
{
switch(response.result) {
case .Success(let JSON):
handler( error: nil )
case .Failure(let error):
handler( error: handleError( response ) )
}
}
private func handleError( response: Response<AnyObject, NSError> ) -> ApiError {
let error = response.result.error!
if let statusCode = response.response?.statusCode where !(200...299).contains( statusCode ) {
return .Network( error: error, response: response.response! )
} else if error.code == -6006 {
return .JSONSerialization( error: error )
delete( "\(id)", parameters: parameters, headers: headers ) { response in
completion( emptyResult( response ) )
}
return .Unknown( error: error )
}
}
......
......@@ -34,13 +34,28 @@
ReferencedContainer = "container:Api.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FEA2BAA9368CEF9B0243D288F81759E9"
BuildableName = "Api.framework"
BlueprintName = "Api"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
......
......@@ -4,3 +4,4 @@ source "https://rubygems.org"
gem 'cocoapods', git: 'https://github.com/CocoaPods/CocoaPods.git'
gem 'fastlane'
gem 'slather'
......@@ -36,6 +36,7 @@ GEM
fastlane_core (>= 0.29.1, < 1.0.0)
spaceship (>= 0.22.0, < 1.0.0)
claide (1.0.0)
clamp (0.6.5)
cocoapods-core (1.1.0.beta.1)
activesupport (>= 4.0.2)
fuzzy_match (~> 2.0.4)
......@@ -189,6 +190,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_magick (4.5.1)
mini_portile2 (2.1.0)
minitest (5.9.0)
molinillo (0.5.0)
multi_json (1.12.1)
......@@ -199,6 +201,9 @@ GEM
net-ssh (>= 2.6.5)
net-ssh (3.2.0)
netrc (0.7.8)
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
os (0.9.6)
pem (1.3.2)
fastlane_core (>= 0.43.1, < 1.0.0)
......@@ -208,6 +213,7 @@ GEM
fastlane_core (>= 0.46.2, < 1.0.0)
spaceship (>= 0.29.0, < 1.0.0)
terminal-table (~> 1.4.5)
pkg-config (1.1.7)
plist (3.1.0)
produce (1.2.0)
fastlane_core (>= 0.30.0, < 1.0.0)
......@@ -236,6 +242,11 @@ GEM
jwt (~> 1.5)
multi_json (~> 1.10)
slack-notifier (1.5.1)
slather (2.3.0)
activesupport (>= 4.0.2, < 5)
clamp (~> 0.6)
nokogiri (~> 1.6.3)
xcodeproj (>= 0.20, < 2.0.0)
snapshot (1.14.0)
fastimage (~> 1.6.3)
fastlane_core (>= 0.50.3, < 1.0.0)
......@@ -283,6 +294,7 @@ PLATFORMS
DEPENDENCIES
cocoapods!
fastlane
slather
BUNDLED WITH
1.12.5
......@@ -60,7 +60,7 @@ func createItemWithIDFromRequest( id: Int, request: NSURLRequest ) -> NSData? {
guard json != nil else { return nil }
json![ "id" ] = id
return try? NSJSONSerialization.dataWithJSONObject( json!, options: .PrettyPrinted )
return try? NSJSONSerialization.dataWithJSONObject( [ "response": json! ], options: .PrettyPrinted )
}
func jsonFromRequest( request: NSURLRequest ) -> [ String: AnyObject ]? {
......
......@@ -16,13 +16,14 @@ class ResourceTests: QuickSpec {
super.setUp()
// Register stubs
stub( matchApiPath( "items/1" ), builder: json( [ "id": 1, "name": "Item 1" ] ) )
stub( matchApiPath( "items/1/components/1" ), builder: json( [ "id": 1, "name": "Component 1" ] ) )
stub( matchApiPath( "items/1" ), builder: json( [ "response": [ "id": 1, "name": "Item 1" ] ] ) )
stub( matchApiPath( "items/1/components/1" ), builder: json( [ "response": [ "id": 1, "name": "Component 1" ] ] ) )
stub( matchApiPath( "items", method: .POST ), builder: buildItemFromRequest( 2 ) )
stub( matchApiPath( "items/1", method: .DELETE ), builder: http( 200 ) )
stub( matchApiPath( "items/1", method: .PATCH ), builder: buildItemFromRequest( 1 ) )
stub( matchApiPath( "items/1", method: .PUT ), builder: http( 200 ) )
stub( matchApiPath( "failedItems", method: .POST ), builder: http( 422 ) )
stub( matchApiPath( "items" ), builder: json( [ [ "id": 1, "name": "Item 1" ], [ "id": 2, "name": "Item 2" ] ] ) )
stub( matchApiPath( "items" ), builder: json( [ "response": [ [ "id": 1, "name": "Item 1" ], [ "id": 2, "name": "Item 2" ] ] ] ) )
}
override func spec() {
......@@ -62,9 +63,14 @@ class ResourceTests: QuickSpec {
context( "when the request is successful" ) {
it( "should call the correct endpoint and return the item" ) {
waitUntil { done in
resource.item( 1 ) { ( item: Item?, error: ApiError? ) in
expect( item?.id ) == 1
expect( item?.name ) == "Item 1"
resource.item( 1 ) { ( result: ApiResult<Item?> ) in
switch result {
case .Success( let item ):
expect( item?.id ) == 1
expect( item?.name ) == "Item 1"
case .Failure(_):
fail()
}
done()
}
......@@ -77,11 +83,16 @@ class ResourceTests: QuickSpec {
context( "when the request is successful" ) {
it( "should call the correct endpoint and return the item" ) {
waitUntil { done in
resource.list() { ( items: [ Item ]?, error: ApiError? ) in
expect( items?[0].id ) == 1
expect( items?[0].name ) == "Item 1"
expect( items?[1].id ) == 2
expect( items?[1].name ) == "Item 2"
resource.list() { ( result: ApiResult< [Item]? > ) in
switch result {
case .Success( let items ):
expect( items?[0].id ) == 1
expect( items?[0].name ) == "Item 1"
expect( items?[1].id ) == 2
expect( items?[1].name ) == "Item 2"
case .Failure(_):
fail()
}
done()
}
......@@ -94,11 +105,16 @@ class ResourceTests: QuickSpec {
context( "when the request is successful" ) {
it( "should call the correct endpoint and return the item" ) {
waitUntil { done in
resource.list() { ( items: [ Item ]?, error: ApiError? ) in
expect( items?[0].id ) == 1
expect( items?[0].name ) == "Item 1"
expect( items?[1].id ) == 2
expect( items?[1].name ) == "Item 2"
resource.list() { ( result: ApiResult< [Item]? > ) in
switch result {
case .Success( let items ):
expect( items?[0].id ) == 1
expect( items?[0].name ) == "Item 1"
expect( items?[1].id ) == 2
expect( items?[1].name ) == "Item 2"
case .Failure(_):
fail()
}
done()
}
......@@ -107,11 +123,20 @@ class ResourceTests: QuickSpec {
}
}
describe( "destroy" ) {
describe( "create" ) {
context( "when the request is successful" ) {
it( "should call the correct endpoint and return the created item" ) {
waitUntil { done in
resource.destroy( 1 ) { ( error: ApiError? ) in
let item = Item(id: 0, name: "Item 2")
resource.create( item ) { ( result: ApiResult<Item?> ) in
switch result {
case .Success( let item ):
expect( item?.id ) == 2
expect( item?.name ) == "Item 2"
case .Failure(_):
fail()
}
done()
}