README.md
# SmoothOperator [![Code Climate](https://codeclimate.com/github/goncalvesjoao/smooth_operator/badges/gpa.svg)](https://codeclimate.com/github/goncalvesjoao/smooth_operator)
Ruby gem, that mimics the ActiveRecord behaviour but through external API's.
It's a lightweight and flexible alternative to ActiveResource, that responds to a REST API like you expect it too.
Be sure to check out this micro-services example: https://github.com/goncalvesjoao/micro-services-example
Where a Rails4 app lists/creates/edits and destroys blog posts from a Padrino (aka Sinatra) app, using SmoothOperator::Rails instead of ActiveRecord::Base classes.
This micro-services example will also feature other cool stuff like:
- parallel requests;
- using HTTP PATCH verb for saving instead of PUT;
- form errors with simple_form gem;
- nested objects using cocoon gem;
- endless-pagination with kaminari gem
- and others...
---
## 1) Installation
Add this line to your application's Gemfile:
gem 'smooth_operator'
And then execute:
$ bundle
Or install it yourself as:
$ gem install smooth_operator
---
## 2) Usage and Examples
```ruby
class MyBlogResource < SmoothOperator::Base
# HTTP BASIC AUTH
options endpoint_user: 'admin',
endpoint_pass: 'admin',
endpoint: 'http://myblog.com/api/v0'
# OR
# smooth_operator_options
end
class Post < MyBlogResource
end
```
---
### 2.1) Creating a .new 'Post' and #save it
```ruby
post = Post.new(body: 'my first post', author: 'John Doe')
post.new_record? # true
post.persisted? # false
post.body # 'my first post'
post.author # 'John Doe'
post.something_else # will raise NoMethodError
save_result = post.save # will make a http POST call to 'http://myblog.com/api/v0/posts'
# with `{ post: { body: 'my first post', author: 'John Doe' } }`
post.last_remote_call # will contain a SmoothOperator::RemoteCall instance containing relevant information about the save remote call.
# If the server response is positive (http code between 200 and 299):
save_result # true
post.new_record? # false
post.persisted? # true
# server response contains { id: 1 } on its body
post.id # 1
# If the server response is negative (http code between 400 and 499):
save_result # false
post.new_record? # true
post.persisted? # false
# server response contains { errors: { body: ['must be less then 10 letters'] } }
post.errors.body # Array
# If the server response is an error (http code between 500 and 599), or the connection was broke:
save_result # nil
post.new_record? # true
post.persisted? # false
# server response contains { errors: { body: ['must be less then 10 letters'] } }
post.errors # will raise NoMethodError
# In the positive and negative server response comes with a json,
# e.g. { id: 1 }, post will reflect that new data
post.id # 1
# In case of error and the server response contains a json,
# e.g. { id: 1 }, post will NOT reflect that data
post.id # raise NoMethodError
```
---
### 2.2) Editing an existing record
```ruby
post = Post.find(2)
post.body = 'editing my second page'
post.save
```
---
### 2.3) Customize #save 'url', 'params' and 'options'
```ruby
post = Post.new(id: 2, body: 'editing my second page')
post.new_record? # false
post.persisted? # true
post.save("save_and_add_to_list", { admin: true, post: { author: 'Agent Smith', list_id: 1 } }, { timeout: 1 })
# Will make a PUT to 'http://myblog.com/api/v0/posts/2/save_and_add_to_list'
# with { admin: true, post: { body: 'editing my second page', list_id: 1 } }
# and will only wait 1sec for the server to respond.
post.save('/#{post.id}/save_and_add_to_list')
# Will make a PUT to 'http://myblog.com/api/v0/posts/2/save_and_add_to_list'
post.save('/save_and_add_to_list')
# Will make a PUT to 'http://myblog.com/api/v0/posts/save_and_add_to_list'
```
---
### 2.4) Saving using HTTP Patch verb
```ruby
class Page < MyBlogResource
options update_http_verb: 'patch'
# OR
#smooth_operator_options update_http_verb: 'patch'
end
page = Page.find(2)
page.body = 'editing my second page'
page.save # will make a http PATCH call to 'http://myblog.com/api/v0/pages/2'
# with `{ page: { body: 'editing my second page' } }`
```
---
### 2.5) Retrieving remote objects - 'index' REST action
```ruby
remote_call = Page.find(:all) # Will make a GET call to 'http://myblog.com/api/v0/pages'
# and will return a SmoothOperator::RemoteCall instance
pages = remote_call.data
# If the server response is positive (http code between 200 and 299, or 304):
remote_call.ok? # true
remote_call.not_processed? # false
remote_call.error? # false
remote_call.status # true
pages = remote_call.data # array of Page instances
remote_call.http_status # server_response code
# If the server response is unprocessed entity (http code 422):
remote_call.ok? # false
remote_call.not_processed? # true
remote_call.error? # false
remote_call.status # false
remote_call.http_status # server_response code
# If the server response is client error (http code between 400..499, except 422):
remote_call.ok? # false
remote_call.not_processed? # false
remote_call.error? # true
remote_call.status # nil
remote_call.http_status # server_response code
# If the server response is server error (http code between 500 and 599), or the connection broke:
remote_call.ok? # false
remote_call.not_processed? # false
remote_call.error? # true
remote_call.status # nil
remote_call.http_status # server_response code or 0 if connection broke
```
---
### 2.6) Retrieving remote objects - 'show' REST action
```ruby
remote_call = Page.find(2) # Will make a GET call to 'http://myblog.com/api/v0/pages/2'
# and will return a SmoothOperator::RemoteCall instance
service_down = remote_call.error?
page = remote_call.data
```
---
### 2.7) Retrieving remote objects - custom query
```ruby
remote_call = Page.find('my_pages', { q: body_contains: 'link' }, { endpoint_user: 'admin', endpoint_pass: 'new_password' })
# will make a GET call to 'http://myblog.com/api/v0/pages/my_pages?q={body_contains="link"}'
# and will change the HTTP BASIC AUTH credentials to user: 'admin' and pass: 'new_password' for this connection only.
@service_down = remote_call.error?
# If the server json response is an Array [{ id: 1 }, { id: 2 }]
@pages = remote.data # will return an array with 2 Page's instances
@pages[0].id # 1
@pages[1].id # 2
# If the server json response is a Hash { id: 3 }
@page = remote.data # will return a single Page instance
@page.id # 3
# If the server json response is Hash with a key called 'pages' { current_page: 1, total_pages: 3, limit_value: 10, pages: [{ id: 4 }, { id: 5 }] }
@pages = remote.data # will return a single ArrayWithMetaData instance, that will allow you to access to both the Page's instances array and the metadata.
# @pages is now a valid object to work with kaminari
@pages.total_pages # 3
@pages.current_page # 1
@pages.limit_value # 10
@pages[0].id # 4
@pages[1].id # 5
```
### 2.8) Keeping your session alive - custom HTTP Headers
Controllers
ApplicationController
```ruby
```
Models
SmoothResource
```ruby
class SmoothResource < SmoothOperator::Rails
options headers: :custom_headers
def self.custom_headers
{
cookie: current_user.blog_cookie,
"X_CSRF_TOKEN" => current_user.blog_auth_token
}
end
protected ############## PROTECTED #################
def self.current_user
User.current_user
end
end
```
---
## 3) Methods
---
### 3.1) Persistence methods
Methods | Behaviour | Arguments | Return
------- | --------- | ------ | ---------
.create | Generates a new instance of the class with *attributes and calls #save with the rest of its arguments| Hash attributes = nil, String relative_path = nil, Hash data = {}, Hash options = {} | Class instance
#new_record? | Returns @new_record if defined, else populates it with true if #id is present or false if blank. | - | Boolean
#destroyed?| Returns @destroyed if defined, else populates it with false. | - | Boolean
#persisted?| Returns true if both #new_record? and #destroyed? return false, else returns false. | - | Boolean
#save | if #new_record? makes a HTTP POST, else a PUT call. If !#new_record? and relative_path is blank, sets relative_path = id.to_s. If the server POST response is positive, sets @new_record = false. See 4.2) for more behaviour info. | String relative_path = nil, Hash data = {}, Hash options = {} | Boolean or Nil
#save! | Executes the same behaviour as #save, but will raise RecordNotSaved if the returning value is not true | String relative_path = nil, Hash data = {}, Hash options = {} | Boolean or Nil
#destroy | Does nothing if !persisted? else makes a HTTP DELETE call. If server response it positive, sets @destroyed = true. If relative_path is blank, sets relative_path = id.to_s. See 4.2) for more behaviour info. | String relative_path = nil, Hash data = {}, Hash options = {} | Boolean or Nil
---
### 3.2) Finder methods
Methods | Behaviour | Arguments | Return
------- | --------- | ------ | ---------
.find | If relative_path == :all, sets relative_path = ''. Makes a Get call and initiates Class objects with the server's response data. See 4.3) and 4.4) for more behaviour info. | String relative_path, Hash data = {}, Hash options = {} | Class instance, Array of Class instances or an ArrayWithMetaData instance
---
### 3.3) Operator methods
...
---
### 3.3) Remote call methods
...
---
## 4) Behaviours
---
### 4.1) Delegation behaviour
...
---
### 4.2) Persistent operator behaviour
...
---
### 4.3) Operator behaviour
...
---
### 4.4) Remote call behaviour
...
---
## 4) TODO
1. Finish "Methods" and "Behaviours" documentation;
2. ModelSchema specs;
3. Cache.