Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

A simple REST-based ORM. Her?

Fetching latest commit…

Octocat-spinner-32-eaf2f5

Cannot retrieve the latest commit at this time

Octocat-spinner-32 lib
Octocat-spinner-32 spec
Octocat-spinner-32 .gitignore
Octocat-spinner-32 .travis.yml
Octocat-spinner-32 Gemfile
Octocat-spinner-32 LICENSE
Octocat-spinner-32 README.md
Octocat-spinner-32 Rakefile
Octocat-spinner-32 her.gemspec
README.md

Her

Build Status

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 and no database.

Installation

In your Gemfile, add:

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 this line:

# config/initializers/her.rb
Her::API.setup :base_uri => "https://api.example.com"

And then to add the ORM behavior to a class, you just have to include Her::Model in it:

class User
  include Her::Model
end

After 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 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

Parsing data

By default, Her handles JSON data. It expects the data to be formatted in a certain structure. The default is this:

// The response of GET /users/1
{
  "data" : {
    "id" : 1,
    "name" : "Tobias Fünke"
  }
}

// The response of GET /users
{
  "data" : [
    {
      "id" : 1,
      "name" : "Tobias Fünke"
    },
    {
      "id" : 2,
      "name" : "Lindsay Fünke"
    }
  ],
  "metadata" : {
    "page" : 1,
    "per_page" : 10
  }
}

However, you can define your own parsing method, with Her::API.parse_with. The parse_with method takes a block which will be executed each time data from an HTTP response needs to be parsed. The block is expected to return a hash with three keys: data, errors and metadata. The following code enables parsing JSON data and treating this data as first-level properties:

Her::API.setup :base_uri => "https://api.example.com"
Her::API.parse_with |response|
  json = JSON.parse(response.body, :symbolize_names => true)
  errors = json.delete(:errors)
  {
    :data => json,
    :errors => errors || [],
    :metadata => {}
  }
end

# User.find(1) will now expect "https://api.example.com/users/1" to return something like '{ "id": 1, "name": "Tobias Fünke" }'

This feature is not stable and might change in the future, probably by using a middleware through Faraday.

Relationships

You can define has_many, has_one and belongs_to relationships in your models. The relationship data is handled in two different ways. When parsing a resource from JSON data, if there’s a relationship data included, 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:

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 are 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 @user

If 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/comments

For has_one relationship, 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/role

For belongs_to relationship, 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/2

However, subsequent calls to #comments or #role will not trigger the extra HTTP request.

Custom requests

You can easily add custom methods for your models. You can either use 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). Other HTTP methods are supported (post_raw, put_resource, etc.)

class User
  include Her::Model

  def self.popular
    get_collection("/users/popular")
  end

  def self.total
    get_raw("/users/stats") do |parsed_data|
      parsed_data[:data][:total_users]
    end
  end
end

User.popular  # => [#<User id=1>, #<User id=2>]
User.total    # => 42

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:

# config/initializers/her.rb
$my_api = Her::API.new
$my_api.setup :base_uri => "https://my_api.example.com"

$other_api = Her::API.new
$other_api.setup :base_uri => "https://other_api.example.com"

You can then define which API a model will use:

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

Things to be done

  • Deleting resources
  • Support for Faraday middleware to handle caching, alternative formats, etc.
  • Hooks before save, update, create, destroy, etc.
  • Better error handling
  • Better introspection for debug
  • Better documentation

Contributors

Feel free to contribute and submit issues/pull requests on GitHub.

Take a look at the spec folder before you do, and make sure bundle exec rake spec passes after your modifications :)

License

Her is © 2012 Rémi Prévost and may be freely distributed under the LITL license. See the LICENSE file.

Something went wrong with that request. Please try again.