Commit d9af1e47 authored by Mauro E. Bender's avatar Mauro E. Bender

Close #7 - Refactor resource results to api results

parent fa39df7d
//
// 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<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
......@@ -72,89 +68,50 @@ 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
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
self.handleListResult( response, handler: completion )
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 )
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<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 )
{
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<AnyObject, NSError>, 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<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 )
}
return .Unknown( error: error )
}
}
// MARK: NESTED RESOURCES
......
......@@ -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<Item?> ) 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<Item?> ) 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<Component?> ) in
switch result {
case .Success(let component):
expect( component?.id ) == 1
expect( component?.name ) == "Component 1"
case .Failure(_):
fail()
}
done()
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment