# Her [![Gem Version](https://badge.fury.io/rb/her.png)](https://rubygems.org/gems/her) [![Build Status](https://secure.travis-ci.org/remiprev/her.png?branch=master)](http://travis-ci.org/remiprev/her) [![Dependency Status](https://gemnasium.com/remiprev/her.png?travis)](https://gemnasium.com/remiprev/her) [![Code Climate](https://codeclimate.com/github/remiprev/her.png)](https://codeclimate.com/github/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! ## 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 ActiveRecord-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 `fullname=Tobias+Fünke` and return the saved User object @user = User.new(fullname: "Tobias Fünke") @user.occupation = "actor" @user.save # POST https://api.example.com/users with `fullname=Tobias+Fünke&occupation=actore` and return the saved User object @user = User.find(1) @user.fullname = "Lindsay Fünke" @user.save # PUT https://api.example.com/users/1 with `fullname=Lindsay+Fünke` and return the updated User object ``` ### ActiveRecord-like methods These are the basic ActiveRecord-like methods you can use with your models: ```ruby class User include Her::Model end # Update a fetched resource user = User.find(1) user.fullname = "Lindsay Fünke" # OR user.assign_attributes(fullname: "Lindsay Fünke") user.save # Update a resource without fetching it User.save_existing(1, fullname: "Lindsay Fünke") # Destroy a fetched resource user = User.find(1) user.destroy # Destroy a resource without fetching it User.destroy_existing(1) # Fetching a collection of resources User.all User.where(moderator: 1).all # Create a new resource User.create(fullname: "Maeby Fünke") # Save a new resource user = User.new(fullname: "Maeby Fünke") user.save ``` You can look into the [`her-example`](https://github.com/remiprev/her-example) repository for a sample application using Her. For a complete reference of all the methods you can use, check out [the documentation](http://rdoc.info/github/remiprev/her). ## Middleware Since Her relies on [Faraday](https://github.com/lostisland/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 token header to your API requests in a Rails application, you could use the excellent [`request_store`](https://rubygems.org/gems/request_store) gem like this: ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base before_filter :set_user_api_token protected def set_user_api_token RequestStore.store[:my_api_token] = current_user.api_token # or something similar based on `session` end end # lib/my_token_authentication.rb class MyTokenAuthentication < Faraday::Middleware def call(env) env[:request_headers]["X-API-Token"] = RequestStore.store[:my_api_token] @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. ### 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 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. ### 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, if you want Her to be able to parse the data from a single root element (usually based on the model name), you’ll have to use the `parse_root_in_json` method (See the **JSON attributes-wrapping** section). Also, you can define your own parsing method using a response middleware. The middleware should set `env[:body]` to a hash with three symbol 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 MyCustomParser connection.use Faraday::Adapter::NetHttp end ``` ### Caching Again, using the `faraday_middleware` and `memcached` gems makes it very easy to cache requests and responses. In your Gemfile: ```ruby gem "her" gem "faraday_middleware" gem "memcached" ``` In your Ruby code: ```ruby Her::API.setup url: "https://api.example.com" do |connection| connection.use FaradayMiddleware::Caching, Memcached::Rails.new('127.0.0.1:11211') 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 memcached ``` ## Advanced Features Here’s a list of several useful features available in Her. ### Associations You can define `has_many`, `has_one` and `belongs_to` associations in your models. The association data is handled in two different ways. 1. If Her finds association data when parsing a resource, that data will be used to create the associated model objects on the resource. 2. If no association data was included when parsing a resource, calling a method with the same name as the association 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 association 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) # { # "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 # [#, #] @user.role # # @user.organization # # ``` If there’s no association data in the resource, Her makes a HTTP request to retrieve the data. ```ruby @user = User.find(1) # { "id": 1, "name": "George Michael Bluth", "organization_id": 2 } # has_many association: @user.comments # GET /users/1/comments # [#, #] @user.comments.where(approved: 1) # GET /users/1/comments?approved=1 # [#] # has_one association: @user.role # GET /users/1/role # # # belongs_to association: @user.organization # (the organization id comes from :organization_id, by default) # GET /organizations/2 # # ``` Subsequent calls to `#comments`, `#role` and `#organization` will not trigger extra HTTP requests and will return the cached objects. #### Notes about paths Resources must always have all the required attributes to build their complete path. For example, if you have these models: ```ruby class User include Her::Model collection_path "organizations/:organization_id/users" end class Organization include Her::Model has_many :users end ``` Her expects all `User` resources to have an `:organization_id` (or `:_organization_id`) attribute. Otherwise, calling mostly all methods, like `User.all`, will thrown an exception like this one: ```ruby Her::Errors::PathError: Missing :_organization_id parameter to build the request path. Path is `organizations/:organization_id/users`. Parameters are `{ … }`. ``` ### Validations Her includes `ActiveModel::Validations` so you can declare validations the same way you do in Rails. However, validations must be triggered manually — they are not run, for example, when calling `#save` on an object, or `#create` on a model class. ```ruby class User include Her::Model attributes :fullname, :email validates :fullname, presence: true validates :email, presence: true end @user = User.new(fullname: "Tobias Fünke") @user.valid? # => false @user.save # POST /users&fullname=Tobias+Fünke will still be called, even if the user is not valid ``` ### Dirty attributes Her includes `ActiveModel::Dirty` so you can keep track of the attributes that have changed in an object. ```ruby class User include Her::Model attributes :fullname, :email end @user = User.new(fullname: "Tobias Fünke") @user.fullname_changed? # => true @user.changes # => { :fullname => [nil, "Tobias Fünke"] } @user.save # POST /users&fullname=Tobias+Fünke @user.fullname_changed? # => false @user.changes # => {} ``` ### Callbacks You can add *before* and *after* callbacks to your models that are triggered on specific actions. You can use symbols or blocks. ```ruby class User include Her::Model before_save :set_internal_id after_find { |u| u.fullname.upcase! } def set_internal_id self.internal_id = 42 # Will be passed in the HTTP request end end @user = User.create(fullname: "Tobias Funke") # POST /users&fullname=Tobias+Fünke&internal_id=42 @user = User.find(1) @user.fullname # => "TOBIAS FUNKE" ``` The available callbacks are: * `before_save` * `before_create` * `before_update` * `before_destroy` * `after_save` * `after_create` * `after_update` * `after_destroy` * `after_find` ### JSON attributes-wrapping Her supports *sending* and *parsing* JSON data wrapped in a root element (to be compatible with Rails’ `include_root_in_json` setting), like so: #### Sending If you want to send all data to your API wrapped in a *root* element based on the model name. ```ruby class User include Her::Model include_root_in_json true end class Article include Her::Model include_root_in_json :post end User.create(fullname: "Tobias Fünke") # POST { "user": { "fullname": "Tobias Fünke" } } to /users Article.create(title: "Hello world.") # POST { "post": { "title": "Hello world." } } to /articles ``` #### Parsing If the API returns data wrapped in a *root* element based on the model name. ```ruby class User include Her::Model parse_root_in_json true end class Article include Her::Model parse_root_in_json :post end # POST /users returns { "user": { "fullname": "Tobias Fünke" } } user = User.create(fullname: "Tobias Fünke") user.fullname # => "Tobias Fünke" # POST /articles returns { "post": { "title": "Hello world." } } article = Article.create(title: "Hello world.") article.title # => "Hello world." ``` Of course, you can use both `include_root_in_json` and `parse_root_in_json` at the same time. ### 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.unpopular # GET /users/unpopular # [#, #] User.from_default(name: "Maeby Fünke") # POST /users/from_default with `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.get(:single_best) # GET /users/single_best # # ``` 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 and the raw response 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, response| parsed_data[:data][:total_users] end end end User.popular # GET /users/popular # [#, #] 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 # [#, #] ``` ### 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 with `fullname=Tobias+Fünke` ``` ### Custom primary keys If your record uses an attribute other than `:id` to identify itself, specify it using the `primary_key` method: ```ruby class User include Her::Model primary_key :_id end user = User.find("4fd89a42ff204b03a905c535") # GET /users/1 returns { "_id": "4fd89a42ff204b03a905c535", "name": "Tobias" } user.save # PUT /users/4fd89a42ff204b03a905c535 ``` ### Inheritance If all your models share the same settings, you might want to make them children of a class and only include `Her::Model` in that class. However, there are a few settings that don’t get passed to the children classes: * `root_element` * `collection_path` and `resource_path` Those settings are based on the class name, so you don’t have to redefine them each time you create a new children class (but you still can). Every other setting is inherited from the parent (associations, scopes, JSON settings, etc.). ```ruby module MyAPI class Model include Her::Model parse_root_in_json true include_root_in_json true end end class User < MyAPI::Model end User.find(1) # GET /users/1 ``` ### Scopes Just like with ActiveRecord, you can define named scopes for your models. Scopes are chainable and can be used within other scopes. ```ruby class User include Her::Model scope :by_role, lambda { |role| where(role: role) } scope :admins, lambda { by_role('admin') } scope :active, lambda { where(active: 1) } end @admins = User.admins # GET /users?role=admin @moderators = User.by_role('moderator') # GET /users?role=moderator @active_admins = User.active.admins # @admins.active would have worked here too # GET /users?role=admin&active=1 ``` ### 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 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 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 use_api MY_API end class Category include Her::Model use_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/lostisland/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 Her::Middleware::DefaultParseJSON connection.use Faraday::Adapter::NetHttp end ``` ## Testing Suppose we have these two models bound to your API: ```ruby # app/models/user.rb class User include Her::Model custom_get :popular end # app/models/post.rb class Post include Her::Model custom_get :recent, :archived end ``` In order to test them, we’ll have to stub the remote API requests. With [RSpec](https://github.com/rspec/rspec-core), we can do this like so: ```ruby # spec/spec_helper.rb RSpec.configure do |config| config.include(Module.new do def stub_api_for(klass) klass.use_api (api = Her::API.new) # Here, you would customize this for your own API (URL, middleware, etc) # like you have done in your application’s initializer api.setup url: "http://api.example.com" do |connection| connection.use Her::Middleware::FirstLevelParseJSON connection.adapter(:test) { |s| yield(s) } end end end) end ``` Then, in your tests, we can specify what (fake) HTTP requests will return: ```ruby # spec/models/user.rb describe User do before do stub_api_for(User) do |stub| stub.get("/users/popular") { |env| [200, {}, [{ id: 1, name: "Tobias Fünke" }, { id: 2, name: "Lindsay Fünke" }].to_json] } end end describe :popular do subject { User.popular } its(:length) { should == 2 } its(:errors) { should be_empty } end end ``` We can redefine the API for a model as many times as we want, like for more complex tests: ```ruby # spec/models/user.rb describe Post do describe :recent do before do stub_api_for(Post) do |stub| stub.get("/posts/recent") { |env| [200, {}, [{ id: 1 }, { id: 2 }].to_json] } end end subject { Post.recent } its(:length) { should == 2 } its(:errors) { should be_empty } end describe :archived do before do stub_api_for(Post) do |stub| stub.get("/posts/archived") { |env| [200, {}, [{ id: 1 }, { id: 2 }].to_json] } end end subject { Post.archived } its(:length) { should == 2 } its(:errors) { should be_empty } end end ``` ## Upgrade See the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) for backward compability issues. ## Her IRL Most projects I know that use Her are internal or private projects but here’s a list of public ones: * [tumbz](https://github.com/remiprev/tumbz) * [crowdher](https://github.com/simonprev/crowdher) ## History I told myself a few months ago that it would be great to build a gem to replace Rails’ [ActiveResource](http://api.rubyonrails.org/classes/ActiveResource/Base.html) since it was barely maintained (and now removed from Rails 4.0), lacking features and hard to extend/customize. I had built a few of these REST-powered ORMs for client projects before but I decided I wanted to write one for myself that I could release as an open-source project. Most of Her’s core codebase was written on a Saturday morning of April 2012 ([first commit](https://github.com/remiprev/her/commit/689d8e88916dc2ad258e69a2a91a283f061cbef2) at 7am!). ## Contribute Yes please! Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues). See [CONTRIBUTING.md](https://github.com/remiprev/her/blob/master/CONTRIBUTING.md) for best practices. ### 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) * [@calmyournerves](https://github.com/calmyournerves) * [@luflux](https://github.com/luxflux) * [@simonc](https://github.com/simonc) * [@pencil](https://github.com/pencil) * [@joanniclaborde](https://github.com/joanniclaborde) * [@seanreads](https://github.com/seanreads) * [@jonkarna](https://github.com/jonkarna) * [@aclevy](https://github.com/aclevy) ## License Her is © 2012-2013 [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.