# Her [](http://travis-ci.org/remiprev/her) [](https://gemnasium.com/remiprev/her) Her is an ORM (Object Relational Mapper) that maps REST resources to Ruby objects. It is designed to build applications that are powered by a RESTful API instead of a database. ## Installation In your Gemfile, add: ```ruby gem "her" ``` That’s it! ## Upgrade Please see the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) file for backward compability issues. ## Usage First, you have to define which API your models will be bound to. For example, with Rails, you would create a new `config/initializers/her.rb` file with these lines: ```ruby # config/initializers/her.rb Her::API.setup :url => "https://api.example.com" do |connection| connection.use Faraday::Request::UrlEncoded connection.use Her::Middleware::DefaultParseJSON connection.use Faraday::Adapter::NetHttp end ``` And then to add the ORM behavior to a class, you just have to include `Her::Model` in it: ```ruby class User include Her::Model end ``` After that, using Her is very similar to many ActiveModel-like ORMs: ```ruby User.all # GET https://api.example.com/users and return an array of User objects User.find(1) # GET https://api.example.com/users/1 and return a User object @user = User.create(:fullname => "Tobias Fünke") # POST "https://api.example.com/users" with the data and return a User object @user = User.new(:fullname => "Tobias Fünke") @user.occupation = "actor" @user.save # POST https://api.example.com/users with the data and return a User object @user = User.find(1) @user.fullname = "Lindsay Fünke" @user.save # PUT https://api.example.com/users/1 with the data and return+update the User object ``` You can look into the `examples` directory for sample applications using Her. ## Middleware Since Her relies on [Faraday](https://github.com/technoweenie/faraday) to send HTTP requests, you can choose the middleware used to handle requests and responses. Using the block in the `setup` call, you have access to Faraday’s `connection` object and are able to customize the middleware stack used on each request and response. ### Authentication Her doesn’t support authentication by default. However, it’s easy to implement one with request middleware. Using the `connection` block, we can add it to the middleware stack. For example, to add a API token header to your requests in a Rails application, you would do something like this: ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base around_filter :do_with_authenticated_user def as_authenticated_user Thread.current[:my_api_token] = session[:my_api_token] begin yield ensure Thread.current[:my_access_token] = nil end end end # lib/my_token_authentication.rb class MyTokenAuthentication < Faraday::Middleware def initialize(app, options={}) @app = app end def call(env) env[:request_headers]["X-API-Token"] = Thread.current[:my_api_token] if Thread.current[:my_api_token].present? @app.call(env) end end # config/initializers/her.rb require "lib/my_token_authentication" Her::API.setup :url => "https://api.example.com" do |connection| connection.use MyTokenAuthentication connection.use Faraday::Request::UrlEncoded connection.use Her::Middleware::DefaultParseJSON connection.use Faraday::Adapter::NetHttp end ``` Now, each HTTP request made by Her will have the `X-API-Token` header. ### Parsing JSON data By default, Her handles JSON data. It expects the resource/collection data to be returned at the first level. ```javascript // The response of GET /users/1 { "id" : 1, "name" : "Tobias Fünke" } // The response of GET /users [{ "id" : 1, "name" : "Tobias Fünke" }] ``` However, you can define your own parsing method using a response middleware. The middleware should set `env[:body]` to a hash with three keys: `data`, `errors` and `metadata`. The following code uses a custom middleware to parse the JSON data: ```ruby # Expects responses like: # # { # "result": { # "id": 1, # "name": "Tobias Fünke" # }, # "errors" => [] # } # class MyCustomParser < Faraday::Response::Middleware def on_complete(env) json = MultiJson.load(env[:body], :symbolize_keys => true) env[:body] = { :data => json[:result], :errors => json[:errors], :metadata => json[:metadata] } end end Her::API.setup :url => "https://api.example.com" do |connection| connection.use Faraday::Request::UrlEncoded connection.use MyCustomParser connection.use Faraday::Adapter::NetHttp end ``` ### OAuth Using the `faraday_middleware` and `simple_oauth` gems, it’s fairly easy to use OAuth authentication with Her. In your Gemfile: ```ruby gem "her" gem "faraday_middleware" gem "simple_oauth" ``` In your Ruby code: ```ruby # Create an application on `https://dev.twitter.com/apps` to set these values TWITTER_CREDENTIALS = { :consumer_key => "", :consumer_secret => "", :token => "", :token_secret => "" } Her::API.setup :url => "https://api.twitter.com/1/" do |connection| connection.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS connection.use Faraday::Request::UrlEncoded connection.use Her::Middleware::DefaultParseJSON connection.use Faraday::Adapter::NetHttp end class Tweet include Her::Model end @tweets = Tweet.get("/statuses/home_timeline.json") ``` See the *Authentication* middleware section for an example of how to pass different credentials based on the current user. ### Caching Again, using the `faraday_middleware` makes it very easy to cache requests and responses: In your Gemfile: ```ruby gem "her" gem "faraday_middleware" ``` In your Ruby code: ```ruby class MyCache < Hash def read(key) if cached = self[key] Marshal.load(cached) end end def write(key, data) self[key] = Marshal.dump(data) end def fetch(key) read(key) || yield.tap { |data| write(key, data) } end end # A cache system must respond to `#write`, `#read` and `#fetch`. # We should be probably using something like Memcached here, not a global object $cache = MyCache.new Her::API.setup :url => "https://api.example.com" do |connection| connection.use Faraday::Request::UrlEncoded connection.use FaradayMiddleware::Caching, $cache connection.use Her::Middleware::DefaultParseJSON connection.use Faraday::Adapter::NetHttp end class User include Her::Model end @user = User.find(1) # GET /users/1 @user = User.find(1) # This request will be fetched from the cache ``` ## Relationships You can define `has_many`, `has_one` and `belongs_to` relationships in your models. The relationship data is handled in two different ways. 1. If Her finds relationship data when parsing a resource, that data will be used to create the associated model objects on the resource. 2. If no relationship data was included when parsing a resource, calling a method with the same name as the relationship will fetch the data (providing there’s an HTTP request available for it in the API). For example: ```ruby class User include Her::Model has_many :comments has_one :role belongs_to :organization end class Comment include Her::Model end class Role include Her::Model end class Organization include Her::Model end ``` If there’s relationship data in the resource, no extra HTTP request is made when calling the `#comments` method and an array of resources is returned: ```ruby @user = User.find(1) # { # :data => { # :id => 1, # :name => "George Michael Bluth", # :comments => [ # { :id => 1, :text => "Foo" }, # { :id => 2, :text => "Bar" } # ], # :role => { :id => 1, :name => "Admin" }, # :organization => { :id => 2, :name => "Bluth Company" } # } # } @user.comments # [#<Comment id=1 text="Foo">, #<Comment id=2 text="Bar">] @user.role # #<Role id=1 name="Admin"> @user.organization # #<Organization id=2 name="Bluth Company"> ``` If there’s no relationship data in the resource, Her makes a HTTP request to retrieve the data. ```ruby @user = User.find(1) # { :data => { :id => 1, :name => "George Michael Bluth", :organization_id => 2 }} # has_many relationship: @user.comments # GET /users/1/comments # [#<Comment id=1>, #<Comment id=2>] # has_one relationship: @user.role # GET /users/1/role # #<Role id=1> # belongs_to relationship: @user.organization # (the organization id comes from :organization_id, by default) # GET /organizations/2 # #<Organization id=2> ``` Subsequent calls to `#comments`, `#role` and `#organization` will not trigger extra HTTP requests and will return the cached objects. ## Hooks You can add *before* and *after* hooks to your models that are triggered on specific actions (`save`, `update`, `create`, `destroy`): ```ruby class User include Her::Model before_save :set_internal_id def set_internal_id self.internal_id = 42 # Will be passed in the HTTP request end end @user = User.create(:fullname => "Tobias Fünke") # POST /users&fullname=Tobias+Fünke&internal_id=42 ``` ## Custom requests You can easily define custom requests for your models using `custom_get`, `custom_post`, etc. ```ruby class User include Her::Model custom_get :popular, :unpopular custom_post :from_default end User.popular # GET /users/popular # [#<User id=1>, #<User id=2>] User.unpopular # GET /users/unpopular # [#<User id=3>, #<User id=4>] User.from_default(:name => "Maeby Fünke") # POST /users/from_default?name=Maeby+Fünke # #<User id=5 name="Maeby Fünke"> ``` You can also use `get`, `post`, `put` or `delete` (which maps the returned data to either a collection or a resource). ```ruby class User include Her::Model end User.get(:popular) # GET /users/popular # [#<User id=1>, #<User id=2>] User.get(:single_best) # GET /users/single_best # #<User id=1> ``` Also, `get_collection` (which maps the returned data to a collection of resources), `get_resource` (which maps the returned data to a single resource) or `get_raw` (which yields the parsed data return from the HTTP request) can also be used. Other HTTP methods are supported (`post_raw`, `put_resource`, etc.). ```ruby class User include Her::Model def self.popular get_collection(:popular) end def self.total get_raw(:stats) do |parsed_data| parsed_data[:data][:total_users] end end end User.popular # GET /users/popular # [#<User id=1>, #<User id=2>] User.total # GET /users/stats # => 42 ``` You can also use full request paths (with strings instead of symbols). ```ruby class User include Her::Model end User.get("/users/popular") # GET /users/popular # [#<User id=1>, #<User id=2>] ``` ## Custom paths You can define custom HTTP paths for your models: ```ruby class User include Her::Model collection_path "/hello_users/:id" end @user = User.find(1) # GET /hello_users/1 ``` You can also include custom variables in your paths: ```ruby class User include Her::Model collection_path "/organizations/:organization_id/users" end @user = User.find(1, :_organization_id => 2) # GET /organizations/2/users/1 @user = User.all(:_organization_id => 2) # GET /organizations/2/users @user = User.new(:fullname => "Tobias Fünke", :organization_id => 2) @user.save # POST /organizations/2/users ``` ## Multiple APIs It is possible to use different APIs for different models. Instead of calling `Her::API.setup`, you can create instances of `Her::API`: ```ruby # config/initializers/her.rb $my_api = Her::API.new $my_api.setup :url => "https://my_api.example.com" do |connection| connection.use Faraday::Request::UrlEncoded connection.use Her::Middleware::DefaultParseJSON connection.use Faraday::Adapter::NetHttp end $other_api = Her::API.new $other_api.setup :url => "https://other_api.example.com" do |connection| connection.use Faraday::Request::UrlEncoded connection.use Her::Middleware::DefaultParseJSON connection.use Faraday::Adapter::NetHttp end ``` You can then define which API a model will use: ```ruby class User include Her::Model uses_api $my_api end class Category include Her::Model uses_api $other_api end User.all # GET https://my_api.example.com/users Category.all # GET https://other_api.example.com/categories ``` ## SSL When initializing `Her::API`, you can pass any parameter supported by `Faraday.new`. So [to use HTTPS](https://github.com/technoweenie/faraday/wiki/Setting-up-SSL-certificates), you can use Faraday’s `:ssl` option. ```ruby ssl_options = { :ca_path => "/usr/lib/ssl/certs" } Her::API.setup :url => "https://api.example.com", :ssl => ssl_options do |connection| connection.use Faraday::Request::UrlEncoded connection.use Her::Middleware::DefaultParseJSON connection.use Faraday::Adapter::NetHttp end ``` ## Testing Using Faraday stubbing feature, it’s very easy to write tests for our models. For example, using [RSpec](https://github.com/rspec/rspec-core): ```ruby # app/models/post.rb class Post include Her::Model custom_get :popular end # spec/models/post.rb describe Post do before do Her::API.setup :url => "http://api.example.com" do |connection| connection.use Her::Middleware::FirstLevelParseJSON connection.use Faraday::Request::UrlEncoded connection.adapter :test do |stub| stub.get("/users/popular") { |env| [200, {}, [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }].to_json] } end end end describe ".popular" do it "should fetch all popular posts" do @posts = Post.popular @posts.length.should == 2 end end end ``` ## Things to be done * Better error handling * Better API documentation (using YARD) ## Contribute Yes please! Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues). ### How to contribute * Fork the repository * Implement your feature or fix * Add examples that describe it (in the `spec` directory) * Make sure `bundle exec rake spec` passes after your modifications * Commit (bonus points for doing it in a `feature-*` branch) * Send a pull request! ### Contributors These fine folks helped with Her: * [@jfcixmedia](https://github.com/jfcixmedia) * [@EtienneLem](https://github.com/EtienneLem) * [@rafaelss](https://github.com/rafaelss) * [@tysontate](https://github.com/tysontate) * [@nfo](https://github.com/nfo) * [@simonprevost](https://github.com/simonprevost) * [@jmlacroix](https://github.com/jmlacroix) * [@thomsbg](https://github.com/thomsbg) ## License Her is © 2012 [Rémi Prévost](http://exomel.com) and may be freely distributed under the [MIT license](https://github.com/remiprev/her/blob/master/LICENSE). See the `LICENSE` file.