idlefingers / APIModel

Interact with REST apis using realm.io to represent objects

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

APIModel

Interact with REST apis using realm.io to represent objects. The goal of APIModel is to be easy to setup, easy to grasp, and fun to use. Boilerplate should be kept to a minimum, but still to intuitive to set up.

This project is very much inspired by @idlefinger's excellent api-model.

Getting started

The key part is to implmenet the ApiTransformable protocol.

import Realm
import APIModel

class Post: RLMObject, ApiTransformable {
    // Standard Realm boilerplate
    dynamic var id = ""
	dynamic var title = ""
	dynamic var contents = ""
	dynamic var createdAt = NSDate()

	override class func primaryKey() -> String {
        return "id"
    }
    
    // Define the standard namespace this class usually resides in JSON responses
    class func apiNamespace() -> String {
        return "post"
    }
    
    // Define where and how to get these. Routes are assumed to use Rails style REST (index, show, update, destroy)
    class func apiResource() -> ApiResource {
        return ApiResource(
            index: "/posts.json",
            show: "/post/:id:.json"
        )
    }
    
    // Define how it is converted from JSON responses into Realm objects. A host of transforms are available
    // See section "Transforms" in README. They are super easy to create as well!
    class func fromJSONMapping() -> JSONMapping {
        return [
            "id": ApiIdTransform(),
            "title": StringTransform(),
            "contents": StringTransform(),
            "createdAt": NSDateTransform()
        ]
    }

    // Define how this object is to be serialized back into a server response format
    func JSONDictionary() -> [String:AnyObject] {
        return [
            "id": id,
            "title": email,
            "contents": contents,
            "created_at": createdAt
        ]
    }
}

Interacting with APIs

The base of APIModel is the ApiForm wrapper class. This class wraps a RLMObject and takes care of fetching objects, saving objects and dealing with validation errors.

Fetching objects

Using the index of a REST resource:

GET /posts.json

ApiForm<Post>.findArray { objects in
    for var post in posts {
        println("... \(post.title)")
    }
}

Using the show of a REST resource:

GET /user.json

ApiForm<User>.load { userResponse in
    if let user = userResponse {
        println("User is: \(user.email)")
    } else {
        println("Error loading user")
    }
}

Storing objects

var post = Post()
post.title = "Hello world - A prologue"
post.contents = "Hello!"
post.createdAt = NSDate()

var form = ApiForm<Post>(model: post)
form.save {
    if form.hasErrors {
        println("Could not save:")
        for var error in form.errorMessages {
            println("... \(error)")
        }
    } else {
        println("Saved! Post #\(post.id)")
    }
}

ApiForm will know that the object is not persisted, since it does not have an id set. So a POST request will be made as follows:

POST /posts.json

{
    "post": {
        "title": "Hello world - A prologue",
        "contents": "Hello!",
        "created_at": "2015-03-08T14:19:31-01:00"
    }
}

If the response is successful, the attributes returned by the server will be updated on the model.

200 OK

{
    "post": {
        "id": 1
    }
}

The errors are expected to be in the format:

400 BAD REQUEST

{
    "post": {
        "errors": {
            "contents": [
                "must be longer than 140 characters"
            ]
        }
    }
}

And this will make it possible to access the errors as follows:

form.errors["contents"] // -> [String]
// or
form.errorMessages // -> [String]

Transforms

Transforms are used to convert attributes from JSON responses to rich types. The easiest way to explain is to show a simple transform.

APIModel comes with a host of standard transforms. An example is the IntTransform:

class IntTransform: Transform {
    func perform(value: AnyObject) -> AnyObject {
        if let asInt = value.integerValue {
            return asInt
        } else {
            return 0
        }
    }
}

This takes an object and attempts to convert it into an integer. If that fails, it returns the default value 0.

Transforms can be quite complex, and even convert nested models. For example:

class User: RLMObject, ApiTransformable {
    dynamic var id = ApiId()
    dynamic var email = ""
    dynamic var posts: RLMArray = RLMArray(objectClassName: Post.className())

    static func fromJSONMapping() -> JSONMapping {
        return [
            "posts": ArrayTransform(modelType: Post.self)
        ]
    }
}

ApiForm<User>.load { response in
    let user = response!.model

    println("User: \(user.email)")
    for var post in user.posts {
        println("\(post.title)")
    }
}

Default transforms are:

  • ArrayTransform
  • BoolTransform
  • IntTransform
  • ModelTransform
  • NSDateTransform
  • PercentageTransform
  • StringTransform

However, it is really easy to define your own. Go nuts!

Dealing with IDs

As a consumer of an API, you never want to make assumptions about the ID structure used for their models. Do not use Int or anything similar for ID types, strings are to be recommended. Therefor APIModel defines a typealias to String, called ApiId. There is also an ApiIdTransform available for IDs.

Caching and storage

It is up to you to cache and store the results of any calls. APIModel does not do that for you, and will not do that, since strategies vary wildly depending on needs.

About

Interact with REST apis using realm.io to represent objects

License:MIT License


Languages

Language:Swift 98.6%Language:Ruby 1.4%