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.
In your Gemfile, add:
gem"her"That’s it!
Please see the UPGRADE.md file for backward compability issues.
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:
# config/initializers/her.rbHer::API.setup:url=>"https://api.example.com"do |connection|
connection.useFaraday::Request::UrlEncodedconnection.useHer::Middleware::DefaultParseJSONconnection.useFaraday::Adapter::NetHttpendAnd then to add the ORM behavior to a class, you just have to include Her::Model in it:
classUserincludeHer::ModelendAfter that, using Her is very similar to many ActiveModel-like ORMs:
User.all# GET https://api.example.com/users and return an array of User objectsUser.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 objectYou can look into the examples directory for sample applications using Her.
Since Her relies on Faraday to send HTTP requests, you can add additional middleware 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.
Her doesn’t support any kind of authentication. However, it’s very easy to implement one with a request middleware. Using the connection block, we add it to the default list of middleware.
classMyAuthentication < Faraday::Middlewaredefinitialize(app,options={})@options=optionsenddefcall(env)env[:request_headers]["X-API-Token"]=@options[:token]if@options.include?(:token)@app.call(env)endendHer::API.setup:url=>"https://api.example.com"do |connection|
# This token could be stored in the client sessionconnection.useMyAuthentication,:token=>"bb2b2dd75413d32c1ac421d39e95b978d1819ff611f68fc2fdd5c8b9c7331192"connection.useFaraday::Request::UrlEncodedconnection.useHer::Middleware::DefaultParseJSONconnection.useFaraday::Adapter::NetHttpendNow, each HTTP request made by Her will have the X-API-Token header.
By default, Her handles JSON data. It expects the resource/collection data to be returned at the first level.
// 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 is expected to set env[:body] to a hash with three keys: data, errors and metadata. The following code enables parsing JSON data and treating the result as first-level properties. Using the connection block, we then replace the default parser with our custom parser.
classMyCustomParser < Faraday::Response::Middlewaredefon_complete(env)json=MultiJson.load(env[:body],:symbolize_keys=>true)env[:body]={:data=>json[:result],:errors=>json[:errors],:metadata=>json[:metadata]}endendHer::API.setup:url=>"https://api.example.com"do |connection|
connection.useFaraday::Request::UrlEncodedconnection.useMyCustomParserconnection.useFaraday::Adapter::NetHttpend# User.find(1) will now expect "https://api.example.com/users/1" to return something like '{ "result" => { "id": 1, "name": "Tobias Fünke" }, "errors" => [] }'Using the faraday_middleware and simple_oauth gems, it’s fairly easy to use OAuth authentication with Her.
In your Gemfile:
gem"her"gem"faraday_middleware"gem"simple_oauth"In your Ruby code:
# Create an application on `https://dev.twitter.com/apps` to set these valuesTWITTER_CREDENTIALS={:consumer_key=>"",:consumer_secret=>"",:token=>"",:token_secret=>""}Her::API.setup:url=>"https://api.twitter.com/1/"do |connection|
connection.useFaradayMiddleware::OAuth,TWITTER_CREDENTIALSconnection.useFaraday::Request::UrlEncodedconnection.useHer::Middleware::DefaultParseJSONconnection.useFaraday::Adapter::NetHttpendclassTweetincludeHer::Modelend@tweets=Tweet.get("/statuses/home_timeline.json")Again, using the faraday_middleware makes it very easy to cache requests and responses:
In your Gemfile:
gem"her"gem"faraday_middleware"In your Ruby code:
classMyCache < Hashdefread(key)ifcached=self[key]Marshal.load(cached)endenddefwrite(key,data)self[key]=Marshal.dump(data)enddeffetch(key)read(key) || yield.tap{ |data| write(key,data)}endend# 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.newHer::API.setup:url=>"https://api.example.com"do |connection|
connection.useFaraday::Request::UrlEncodedconnection.useFaradayMiddleware::Caching, $cache
connection.useHer::Middleware::DefaultParseJSONconnection.useFaraday::Adapter::NetHttpendclassUserincludeHer::Modelend@user=User.find(1)# GET /users/1@user=User.find(1)# This request will be fetched from the cacheYou can define has_many, has_one and belongs_to relationships in your models. The relationship data is handled in two different ways. If there’s relationship data when parsing a resource, it will be used to create new Ruby objects.
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, with this setup:
classUserincludeHer::Modelhas_many:commentshas_one:rolebelongs_to:organizationendclassCommentincludeHer::ModelendclassRoleincludeHer::ModelendclassOrganizationincludeHer::ModelendIf 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:
@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>, #<Comment id=2>] fetched directly from @user@user.role# => #<Role id=1> fetched directly from @user@user.organization# => #<Organization id=2> fetched directly from @userIf there’s no relationship data in the resource, an extra HTTP request (to GET /users/1/comments) is made when calling the #comments method:
@user=User.find(1)# { :data => { :id => 1, :name => "George Michael Bluth" }}@user.comments# => [#<Comment id=1>, #<Comment id=2>] fetched from /users/1/commentsFor has_one relationships, an extra HTTP request (to GET /users/1/role) is made when calling the #role method:
@user=User.find(1)# { :data => { :id => 1, :name => "George Michael Bluth" }}@user.role# => #<Role id=1> fetched from /users/1/roleFor belongs_to relationships, an extra HTTP request (to GET /organizations/2) is made when calling the #organization method:
@user=User.find(1)# { :data => { :id => 1, :name => "George Michael Bluth", :organization_id => 2 }}@user.organization# => #<Organization id=2> fetched from /organizations/2However, subsequent calls to #comments, #role and #organization will not trigger extra HTTP requests as the data has already been fetched.
You can add before and after hooks to your models that are triggered on specific actions (save, update, create, destroy):
classUserincludeHer::Modelbefore_save:set_internal_iddefset_internal_idself.internal_id=42# Will be passed in the HTTP requestendend@user=User.create(:fullname=>"Tobias Fünke")# POST /users&fullname=Tobias+Fünke&internal_id=42You can easily define custom requests for your models using custom_get, custom_post, etc.
classUserincludeHer::Modelcustom_get:popular,:unpopularcustom_post:from_defaultendUser.popular# => [#<User id=1>, #<User id=2>]# GET /users/popularUser.unpopular# => [#<User id=3>, #<User id=4>]# GET /users/unpopularUser.from_default(:name=>"Maeby Fünke")# => #<User id=5># POST /users/from_default?name=Maeby+FünkeYou can also use get, post, put or delete (which maps the returned data to either a collection or a resource).
classUserincludeHer::ModelendUser.get(:popular)# => [#<User id=1>, #<User id=2>]# GET /users/popularUser.get(:single_best)# => #<User id=1># GET /users/single_bestAlso, 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.).
classUserincludeHer::Modeldefself.popularget_collection(:popular)enddefself.totalget_raw(:stats)do |parsed_data|
parsed_data[:data][:total_users]endendendUser.popular# => [#<User id=1>, #<User id=2>]User.total# => 42You can also use full request paths (with strings instead of symbols).
classUserincludeHer::ModelendUser.get("/users/popular")# => [#<User id=1>, #<User id=2>]# GET /users/popularYou can define custom HTTP paths for your models:
classUserincludeHer::Modelcollection_path"/hello_users/:id"end@user=User.find(1)# GET /hello_users/1You can also include custom variables in your paths:
classUserincludeHer::Modelcollection_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/usersIt is possible to use different APIs for different models. Instead of calling Her::API.setup, you can create instances of Her::API:
# config/initializers/her.rb
$my_api =Her::API.new
$my_api.setup:url=>"https://my_api.example.com"do |connection|
connection.useFaraday::Request::UrlEncodedconnection.useHer::Middleware::DefaultParseJSONconnection.useFaraday::Adapter::NetHttpend
$other_api =Her::API.new
$other_api.setup:url=>"https://other_api.example.com"do |connection|
connection.useFaraday::Request::UrlEncodedconnection.useHer::Middleware::DefaultParseJSONconnection.useFaraday::Adapter::NetHttpendYou can then define which API a model will use:
classUserincludeHer::Modeluses_api $my_api
endclassCategoryincludeHer::Modeluses_api $other_api
endUser.all# GET https://my_api.example.com/usersCategory.all# GET https://other_api.example.com/categoriesWhen initializing Her::API, you can pass any parameter supported by Faraday.new. So to use HTTPS, you can use Faraday’s :ssl option.
ssl_options={:ca_path=>"/usr/lib/ssl/certs"}Her::API.setup:url=>"https://api.example.com",:ssl=>ssl_optionsdo |connection|
connection.useFaraday::Request::UrlEncodedconnection.useHer::Middleware::DefaultParseJSONconnection.useFaraday::Adapter::NetHttpendUsing Faraday stubbing feature, it’s very easy to write tests for our models. For example, using RSpec:
# app/models/post.rbclassPostincludeHer::Modelcustom_get:popularend# spec/models/post.rbdescribePostdobeforedoHer::API.setup:url=>"http://api.example.com"do |connection|
connection.useHer::Middleware::FirstLevelParseJSONconnection.useFaraday::Request::UrlEncodedconnection.adapter:testdo |stub|
stub.get("/users/popular"){ |env| [200,{},[{:id=>1,:name=>"Tobias Fünke"},{:id=>2,:name=>"Lindsay Fünke"}].to_json]}endendenddescribe".popular"doit"should fetch all popular posts"do@posts=Post.popular@posts.length.should == 2endendend- Better error handling
- Better API documentation (using YARD)
Yes please! Feel free to contribute and submit issues/pull requests on GitHub.
- Fork the repository
- Implement your feature or fix
- Add examples that describe it (in the
specdirectory) - Make sure
bundle exec rake specpasses after your modifications - Commit (bonus points for doing it in a
feature-*branch) - Send a pull request!
These fine folks helped with Her:
Her is © 2012 Rémi Prévost and may be freely distributed under the MIT license. See the LICENSE file.

