Visit github/erkie/ApiModel for the official project.
This is just my fork where I'm trying to learn more about ApiModel and modify for for my needs. (see feature/objectmapper for my current work. I'm attempting integrate ObjectMapper to replace the current transforms.
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, and also intuitive to set up.
This project is very much inspired by @idlefingers' excellent api-model.
The key part is to implmenet the ApiTransformable
protocol.
import RealmSwift
import ApiModel
class Post: Object, 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
// MUST BE singular ie `post` not `posts`
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 apiRoutes() -> ApiRoutes {
return ApiRoutes(
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
]
}
}
To represent the API itself, you have to create an object of the API
class. This holds a ApiConfiguration
object defining the host URL for all requests. After it has been created it can be accessed from the func api() -> API
singleton function.
To set it up:
// Put this somewhere in your AppDelegate or together with other initialization code
var apiConfig = ApiConfiguration(host: "https://service.io/api/v1/")
ApiSingleton.setInstance(API(configuration: apiConfig))
If you would like to disable request logging, you can do so by setting requestLogging
to false
:
apiConfig.requestLogging = false
The base of ApiModel
is the ApiForm
wrapper class. This class wraps an Object
type and takes care of fetching objects, saving objects and dealing with validation errors.
ApiModel
supports querying API's using basic HTTP verbs.
// GET call without parameters
ApiForm<Post>.get("/v1/posts.json") { response in
println("response.isSuccessful: \(response.isSuccessful)")
println("Response as an array: \(response.array)")
println("Response as a dictionary: \(response.dictionary)")
println("Response errors?: \(response.errors)")
}
// Other supported methods:
ApiForm<Post>.get(path, parameters: [String:AnyObject]) { response // ...
ApiForm<Post>.post(path, parameters: [String:AnyObject]) { response // ...
ApiForm<Post>.put(path, parameters: [String:AnyObject]) { response // ...
ApiForm<Post>.delete(path, parameters: [String:AnyObject]) { response // ...
// no parameters
ApiForm<Post>.get(path) { response // ...
ApiForm<Post>.post(path) { response // ...
ApiForm<Post>.put(path) { response // ...
ApiForm<Post>.delete(path) { response // ...
Most of the time you'll want to use the ActiveRecord
-style verbs index/show/create/update
for interacting with a REST API, as described below.
Using the index
of a REST resource:
GET /posts.json
ApiForm<Post>.findArray { posts in
for post in posts {
println("... \(post.title)")
}
}
Using the show
of a REST resource:
GET /user.json
ApiForm<User>.find { userResponse in
if let user = userResponse {
println("User is: \(user.email)")
} else {
println("Error loading user")
}
}
var post = Post()
post.title = "Hello world - A prologue"
post.contents = "Hello!"
post.createdAt = NSDate()
var form = ApiForm<Post>(model: post)
form.save { _ in
if form.hasErrors {
println("Could not save:")
for 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 (or which ever field is defined as primaryKey
in Realm). 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 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: Object, ApiTransformable {
dynamic var id = ApiId()
dynamic var email = ""
let posts = List<Post>()
static func fromJSONMapping() -> JSONMapping {
return [
"posts": ArrayTransform<Post>()
]
}
}
ApiForm<User>.find { response in
let user = response!.model
println("User: \(user.email)")
for post in user.posts {
println("\(post.title)")
}
}
Default transforms are:
- StringTransform
- IntTransform
- FloatTransform
- DoubleTransform
- BoolTransform
- NSDateTransform
- ModelTransform
- ArrayTransform
- PercentageTransform
However, it is really easy to define your own. Go nuts!
ApiModel
uses Alamofire for sending and receiving requests. To hook into this, the API
class currently has before
- and after
-hooks that you can use to modify or log the requests. An example of sending user credentials with each request:
// Put this somewhere in your AppDelegate or together with other initialization code
api().beforeRequest { request in
if let loginToken = User.loginToken() {
request.parameters["access_token"] = loginToken
}
}
There is also a afterRequest
which passes in a ApiRequest
and ApiResponse
:
api().afterRequest { request, response in
println("... Got: \(response.status)")
println("... \(request)")
println("... \(response)")
}
Given the setup for the Post
model above, if you wanted to get the full url with replacements for the show resource (like https://service.io/api/v1/posts/123.json
), you can use:
post.apiUrlForRoute(Post.apiRoutes().show)
// NOT IMPLEMENTED YET BECAUSE LIMITATIONS IN SWIFT: post.apiUrlForResource(.Show)
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.
Some API's wrap all their responses in an "envelope", a container that is generic for all responses. For example an API might wrap all response data within a data
-property of the root JSON:
{
"data": {
"user": { ... }
}
}
To deal with this gracefully there is a configuration option on the ApiConfiguration
class called rootNamespace
. This is a dot-separated path that is traversed for each response. To deal with the above example you would simply:
let config = ApiConfiguration()
config.rootNamespace = "data"
It can also be more complex, for example if the envelope looked something like this:
{
"JsonResponseEnvelope": {
"SuccessFullJsonResponse": {
"SoapResponseContainer": {
"EnterpriseBeanData": {
"user": { ... }
}
}
}
}
}
This would then convert into the rootNamespace
:
let config = ApiConfiguration()
config.rootNamespace = "JsonResponseEnvelope.SuccessFullJsonResponse.SoapResponseContainer.EnterpriseBeanData"
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.
The MIT License (MIT)
Copyright (c) 2015 Rootof Creations HB
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.