diff --git a/Api/Classes/ApiError.swift b/Api/Classes/ApiError.swift new file mode 100644 index 0000000000000000000000000000000000000000..abc9d304c441aeaad52cc7197a73cb2d69dec5ed --- /dev/null +++ b/Api/Classes/ApiError.swift @@ -0,0 +1,27 @@ +// +// ApiError.swift +// Pods +// +// Created by Mauro Bender on 25/8/16. +// +// + +public enum ApiError: ErrorType { + case Network( error: NSError, response: NSURLResponse ) + case JSONSerialization(error: NSError) + case Unknown( error: NSError ) +} + +public extension ApiError { + init( response: Response ) { + 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 diff --git a/Api/Classes/ApiResult.swift b/Api/Classes/ApiResult.swift new file mode 100644 index 0000000000000000000000000000000000000000..8d32f211d41d95e6d7412015b87da515ce208335 --- /dev/null +++ b/Api/Classes/ApiResult.swift @@ -0,0 +1,171 @@ +// +// Result.swift +// Pods +// +// Created by Mauro Bender on 14/8/16. +// +// + +import Alamofire + +public enum ApiResult { + 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( response: Response, + 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 ) -> EmptyResult { + switch(response.result) { + case .Success: + return .Success + case .Failure(let error): + return .Failure( ApiError( response: response ) ) + } +} + +public func listResult( response: Response, + 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( response: Response, + 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 ) ) + } +} diff --git a/Api/Classes/Pagination.swift b/Api/Classes/Pagination.swift new file mode 100644 index 0000000000000000000000000000000000000000..2515c350ecd5ce9021092bf390e452767e432a00 --- /dev/null +++ b/Api/Classes/Pagination.swift @@ -0,0 +1,36 @@ +// +// 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 + } +} diff --git a/Api/Classes/Resource.swift b/Api/Classes/Resource.swift index 40edeb8d5a8c8f885f2cd4263a4db0e727bdb4d1..acbf53b7d65fcb91c7c7cd09359d43285902f6fc 100644 --- a/Api/Classes/Resource.swift +++ b/Api/Classes/Resource.swift @@ -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 @@ -72,89 +68,50 @@ extension Resource { // MARK: CRUD extension Resource { public func item( 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( 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 + completion( listResult( response, keyPath: self.responseKeyPath ) ) + } + } + + public func paginate( parameters: [ String: String ]? = nil, headers: [ String: String]? = nil, + completion: ( ( ApiResult< ([T], Pagination)? > ) -> Void ) ) { get( parameters: parameters, headers: headers ) { response in - self.handleListResult( response, handler: completion ) + completion( paginatedListResult( response, + keyPath: self.responseKeyPath, paginationKeyPath: self.paginationKeyPath ) ) } } public func create( 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 ) + completion( itemResult( response, keyPath: self.responseKeyPath ) ) } } public func destroy( id: AnyObject, parameters: [ String: String ]? = nil, - headers: [ String: String]? = nil, completion: ( ( error: ApiError? ) -> Void ) ) + headers: [ String: String]? = nil, completion: ( ( EmptyResult ) -> Void ) ) { + ApiResult.Success() delete( "\(id)", parameters: parameters, headers: headers ) { response in - self.handleEmptyResult( response, handler: completion ) + completion( emptyResult( response ) ) } } - - - // MARK: UTILS - private func handleItemResult( response: Response, - 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( response: Response, - handler: ( [ T ]?, error: ApiError? ) -> 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 ) ) - } - } - - private func handleEmptyResult( response: Response, handler: ( error: ApiError? ) -> Void ) - { - switch(response.result) { - case .Success(let JSON): - handler( error: nil ) - case .Failure(let error): - handler( error: handleError( response ) ) - } - } - - private func handleError( response: Response ) -> 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 ) - } - - return .Unknown( error: error ) - } } // MARK: NESTED RESOURCES diff --git a/Example/Tests/ResourceTests.swift b/Example/Tests/ResourceTests.swift index b8adfb5abd395a8192d804a2412486fe1c81f6a8..3e9ada6fe31ae0d3fc22703741ef33f58f763a61 100644 --- a/Example/Tests/ResourceTests.swift +++ b/Example/Tests/ResourceTests.swift @@ -16,13 +16,13 @@ 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: .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 +62,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 ) in + switch result { + case .Success( let item ): + expect( item?.id ) == 1 + expect( item?.name ) == "Item 1" + case .Failure(_): + fail() + } done() } @@ -77,11 +82,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 +104,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() } @@ -109,9 +124,9 @@ class ResourceTests: QuickSpec { describe( "destroy" ) { context( "when the request is successful" ) { - it( "should call the correct endpoint and return the created item" ) { + it( "should call the correct endpoint and return an empty result" ) { waitUntil { done in - resource.destroy( 1 ) { ( error: ApiError? ) in + resource.destroy( 1 ) { ( emptyResult: EmptyResult ) in done() } } @@ -123,9 +138,13 @@ class ResourceTests: QuickSpec { let itemToCreate = Item( id: 0, name: "Item 2" ) it( "should call the correct endpoint and return the correct error" ) { waitUntil { done in - failResource.create( itemToCreate ) { ( item: Item?, error: ApiError? ) in - expect( item ).to( beNil() ) - expect( error ).toNot( beNil() ) + failResource.create( itemToCreate ) { ( result: ApiResult ) in + switch result { + case .Success(_): + fail() + case .Failure(_): + break + } done() } @@ -157,9 +176,14 @@ class ResourceTests: QuickSpec { it( "should call the correct endpoint and return correct the item" ) { waitUntil { done in - nestedResource.item( 1 ) { ( component: Component?, error: ApiError? ) in - expect( component?.id ) == 1 - expect( component?.name ) == "Component 1" + nestedResource.item( 1 ) { ( result: ApiResult ) in + switch result { + case .Success(let component): + expect( component?.id ) == 1 + expect( component?.name ) == "Component 1" + case .Failure(_): + fail() + } done() }