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
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
end2.1) Creating a .new 'Post' and #save it
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 NoMethodError2.2) Editing an existing record
post = Post.find(2)
post.body = 'editing my second page'
post.save2.3) Customize #save 'url', 'params' and 'options'
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
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
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 broke2.6) Retrieving remote objects - 'show' REST action
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.data2.7) Retrieving remote objects - custom query
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 # 52.8) Keeping your session alive - custom HTTP Headers
Controllers ApplicationController
Models SmoothResource
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
end3) 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
- Finish "Methods" and "Behaviours" documentation;
- ModelSchema specs;
- Cache.