Him
Him 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.
The spiritual successor of Her, modernized for current Ruby/Rails.
Requirements:
- Ruby >= 3.1
- ActiveModel >= 6.1
- Faraday >= 2.0
Installation
In your Gemfile, add:
gem "him"That’s it! Him provides a Her = Him alias, so code using Her::Model will continue to work.
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/him.rb file with these lines:
# config/initializers/him.rb
Him::API.setup url: "https://api.example.com" do |c|
# Request
c.use Faraday::Request::UrlEncoded
# Response
c.use Him::Middleware::DefaultParseJSON
# Adapter
c.adapter :net_http
endAnd then to add the ORM behavior to a class, you just have to include Him::Model in it:
class User
include Him::Model
endAfter that, using Him is very similar to many ActiveRecord-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 `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=actor` 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 objectActiveRecord-like methods
These are the basic ActiveRecord-like methods you can use with your models:
class User
include Him::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 # returns false if it fails, errors in user.response_errors array
# PUT "/users/1" with `fullname=Lindsay+Fünke`
# Update a resource without fetching it
User.save_existing(1, fullname: "Lindsay Fünke")
# PUT "/users/1" with `fullname=Lindsay+Fünke`
# Destroy a fetched resource
user = User.find(1)
user.destroy
# DELETE "/users/1"
# Destroy a resource without fetching it
User.destroy_existing(1)
# DELETE "/users/1"
# Fetching a collection of resources
User.all
# GET "/users"
User.where(moderator: 1).all
# GET "/users?moderator=1"
# Create a new resource
User.create(fullname: "Maeby Fünke")
# POST "/users" with `fullname=Maeby+Fünke`
# Save a new resource
user = User.new(fullname: "Maeby Fünke")
user.save! # raises Him::Errors::ResourceInvalid if it fails
# POST "/users" with `fullname=Maeby+Fünke`Middleware
Since Him relies on 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
Him doesn’t support authentication by default. However, it’s easy to implement one with request middleware. Using the setup 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 gem like this:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :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/him.rb
require "lib/my_token_authentication"
Him::API.setup url: "https://api.example.com" do |c|
# Request
c.use MyTokenAuthentication
c.use Faraday::Request::UrlEncoded
# Response
c.use Him::Middleware::DefaultParseJSON
# Adapter
c.adapter :net_http
endNow, each HTTP request made by Him will have the X-API-Token header.
Basic HTTP Authentication
Him can use basic HTTP auth by adding a line to your initializer:
# config/initializers/him.rb
Him::API.setup url: "https://api.example.com" do |c|
# Request
c.request :authorization, :basic, 'myusername', 'mypassword'
c.use Faraday::Request::UrlEncoded
# Response
c.use Him::Middleware::DefaultParseJSON
# Adapter
c.adapter :net_http
endOAuth
Using the faraday_middleware and simple_oauth gems, it’s fairly easy to use OAuth authentication with Him.
In your Gemfile:
gem "him"
gem "faraday_middleware"
gem "simple_oauth"In your Ruby code:
# Create an application on `https://dev.twitter.com/apps` to set these values
TWITTER_CREDENTIALS = {
consumer_key: "",
consumer_secret: "",
token: "",
token_secret: ""
}
Him::API.setup url: "https://api.twitter.com/1/" do |c|
# Request
c.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
# Response
c.use Him::Middleware::DefaultParseJSON
# Adapter
c.adapter :net_http
end
class Tweet
include Him::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, Him 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, if you want Him 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:
# Expects responses like:
#
# {
# "result": { "id": 1, "name": "Tobias Fünke" },
# "errors": []
# }
#
class MyCustomParser < Faraday::Middleware
def on_complete(env)
json = JSON.parse(env[:body], symbolize_names: true)
env[:body] = {
data: json[:result],
errors: json[:errors],
metadata: json[:metadata]
}
end
end
Him::API.setup url: "https://api.example.com" do |c|
# Response
c.use MyCustomParser
# Adapter
c.adapter :net_http
endCaching
Again, using the faraday_middleware and memcached gems makes it very easy to cache requests and responses.
In your Gemfile:
gem "him"
gem "faraday_middleware"
gem "memcached"In your Ruby code:
Him::API.setup url: "https://api.example.com" do |c|
# Request
c.use FaradayMiddleware::Caching, Memcached::Rails.new('127.0.0.1:11211')
# Response
c.use Him::Middleware::DefaultParseJSON
# Adapter
c.adapter :net_http
end
class User
include Him::Model
end
@user = User.find(1)
# GET "/users/1"
@user = User.find(1)
# This request will be fetched from memcachedAdvanced Features
Here’s a list of several useful features available in Him.
Associations
Examples use this code:
class User
include Him::Model
has_many :comments
has_one :role
belongs_to :organization
end
class Comment
include Him::Model
end
class Role
include Him::Model
end
class Organization
include Him::Model
endFetching data
You can define has_many, has_one and belongs_to associations in your models. The association data is handled in two different ways.
- If Him finds association data when parsing a resource, that data will be used to create the associated model objects on the resource.
- 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, 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:
@user = User.find(1)
# GET "/users/1", response is:
# {
# "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 association data in the resource, Him makes an HTTP request to retrieve the data.
@user = User.find(1)
# GET "/users/1", response is { "id": 1, "name": "George Michael Bluth", "organization_id": 2 }
# has_many association:
@user.comments
# GET "/users/1/comments"
# => [#<Comment id=1>, #<Comment id=2>]
@user.comments.where(approved: 1)
# GET "/users/1/comments?approved=1"
# => [#<Comment id=1>]
# has_one association:
@user.role
# GET "/users/1/role"
# => #<Role id=1>
# belongs_to association:
@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.
Creating data
You can use the association methods to build new objects and save them.
@user = User.find(1)
@user.comments.build(body: "Just a draft")
# => [#<Comment body="Just a draft" user_id=1>]
@user.comments.create(body: "Hello world.", user_id: 1)
# POST "/comments" with `body=Hello+world.&user_id=1`
# => [#<Comment id=3 body="Hello world." user_id=1>]You can also explicitly request a new object via the API when using build. This is useful if you're dealing with default attributes.
class Comment
include Him::Model
request_new_object_on_build true
end
@user = User.find(1)
@user.comments.build(body: "Just a draft")
# GET "/users/1/comments/new" with `body=Just+a+draft.`
# => [#<Comment id=nil body="Just a draft" archived=false user_id=1>]Notes about paths
Resources must always have all the required attributes to build their complete path. For example, if you have these models:
class User
include Him::Model
collection_path "organizations/:organization_id/users"
end
class Organization
include Him::Model
has_many :users
endHim expects all User resources to have an :organization_id (or :_organization_id) attribute. Otherwise, calling mostly all methods, like User.all, will throw an exception like this one:
Him::Errors::PathError: Missing :_organization_id parameter to build the request path. Path is `organizations/:organization_id/users`. Parameters are `{ … }`.Associations with custom attributes
Associations can also be made using custom attributes:
class User
include Him::Model
belongs_to :owns, class_name: "Organization"
end
class Organization
include Him::Model
has_many :owners, class_name: "User"
endValidations
Him 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.
class User
include Him::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" with `fullname=Tobias+Fünke` will still be called, even if the user is not validDirty attributes
Him includes ActiveModel::Dirty so you can keep track of the attributes that have changed in an object.
class User
include Him::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" with `fullname=Tobias+Fünke`
@user.fullname_changed? # => false
@user.changes # => {}To update only the modified attributes specify :send_only_modified_attributes => true in the setup.
Callbacks
You can add before and after callbacks to your models that are triggered on specific actions. You can use symbols or blocks.
class User
include Him::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 Fünke")
# POST "/users" with `fullname=Tobias+Fünke&internal_id=42`
@user = User.find(1)
@user.fullname # => "TOBIAS FUNKE"The available callbacks are:
before_savebefore_createbefore_updatebefore_destroyafter_saveafter_createafter_updateafter_destroyafter_findafter_initialize
JSON attributes-wrapping
Him 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.
class User
include Him::Model
include_root_in_json true
end
class Article
include Him::Model
include_root_in_json :post
end
User.create(fullname: "Tobias Fünke")
# POST "/users" with `user[fullname]=Tobias+Fünke`
Article.create(title: "Hello world.")
# POST "/articles" with `post[title]=Hello+world`Parsing
If the API returns data wrapped in a root element based on the model name.
class User
include Him::Model
parse_root_in_json true
end
class Article
include Him::Model
parse_root_in_json :post
end
user = User.create(fullname: "Tobias Fünke")
# POST "/users" with `fullname=Tobias+Fünke`, response is { "user": { "fullname": "Tobias Fünke" } }
user.fullname # => "Tobias Fünke"
article = Article.create(title: "Hello world.")
# POST "/articles" with `title=Hello+world.`, response is { "post": { "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.
ActiveModel::Serializers support
If the API returns data in the default format used by the ActiveModel::Serializers project you need to configure Him as follows:
class User
include Him::Model
parse_root_in_json true, format: :active_model_serializers
end
user = Users.find(1)
# GET "/users/1", response is { "user": { "id": 1, "fullname": "Lindsay Fünke" } }
users = Users.all
# GET "/users", response is { "users": [{ "id": 1, "fullname": "Lindsay Fünke" }, { "id": 1, "fullname": "Tobias Fünke" }] }JSON API support
To consume a JSON API 1.0 compliant service, it must return data in accordance with the JSON API spec. The general format of the data is as follows:
{ "data": {
"type": "developers",
"id": "6ab79c8c-ec5a-4426-ad38-8763bbede5a7",
"attributes": {
"language": "ruby",
"name": "avdi grimm",
}
}Then to setup your models:
class Contributor
include Him::JsonApi::Model
# defaults to demodulized, pluralized class name, e.g. contributors
type :developers
endFinally, you'll need to use the included JsonApiParser Him middleware:
Him::API.setup url: 'https://my_awesome_json_api_service' do |c|
# Request
c.use FaradayMiddleware::EncodeJson
# Response
c.use Him::Middleware::JsonApiParser
# Adapter
c.adapter :net_http
endCustom requests
You can easily define custom requests for your models using custom_get, custom_post, etc.
class User
include Him::Model
custom_get :popular, :unpopular
custom_post :from_default, :activate
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" with `name=Maeby+Fünke`
# => #<User id=5 name="Maeby Fünke">
User.activate(id: 6)
# POST "/users/6/activate"
# => #<User id=6>You can also use get, post, put or delete (which maps the returned data to either a collection or a resource).
class User
include Him::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>You can also use get_raw which yields the parsed data and the raw response from the HTTP request. Other HTTP methods are supported (post_raw, put_raw, etc.).
class User
include Him::Model
def self.total
get_raw(:stats) do |parsed_data, response|
parsed_data[:data][:total_users]
end
end
end
User.total
# GET "/users/stats"
# => 42You can also use full request paths (with strings instead of symbols).
class User
include Him::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:
class User
include Him::Model
collection_path "/hello_users/:id"
end
@user = User.find(1)
# GET "/hello_users/1"You can also include custom variables in your paths:
class User
include Him::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:
class User
include Him::Model
primary_key :_id
end
user = User.find("4fd89a42ff204b03a905c535")
# GET "/users/4fd89a42ff204b03a905c535", response is { "_id": "4fd89a42ff204b03a905c535", "name": "Tobias" }
user.destroy
# DELETE "/users/4fd89a42ff204b03a905c535"Inheritance
If all your models share the same settings, you might want to make them children of a class and only include Him::Model in that class. However, there are a few settings that don’t get passed to the children classes:
root_element-
collection_pathandresource_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.).
module MyAPI
class Model
include Him::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.
class User
include Him::Model
scope :by_role, ->(role) { where(role: role) }
scope :admins, -> { by_role('admin') }
scope :active, -> { 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"A neat trick you can do with scopes is interact with complex paths.
class User
include Him::Model
collection_path "organizations/:organization_id/users"
scope :for_organization, ->(id) { where(organization_id: id) }
end
@user = User.for_organization(3).find(2)
# GET "/organizations/3/users/2"
@user = User.for_organization(3).create(fullname: "Tobias Fünke")
# POST "/organizations/3" with `fullname=Tobias+Fünke`Multiple APIs
It is possible to use different APIs for different models. Instead of calling Him::API.setup, you can create instances of Him::API:
# config/initializers/him.rb
MY_API = Him::API.new
MY_API.setup url: "https://my-api.example.com" do |c|
# Response
c.use Him::Middleware::DefaultParseJSON
# Adapter
c.adapter :net_http
end
OTHER_API = Him::API.new
OTHER_API.setup url: "https://other-api.example.com" do |c|
# Response
c.use Him::Middleware::DefaultParseJSON
# Adapter
c.adapter :net_http
endYou can then define which API a model will use:
class User
include Him::Model
use_api MY_API
end
class Category
include Him::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 Him::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" }
Him::API.setup url: "https://api.example.com", ssl: ssl_options do |c|
# Response
c.use Him::Middleware::DefaultParseJSON
# Adapter
c.adapter :net_http
endTesting
Suppose we have these two models bound to your API:
# app/models/user.rb
class User
include Him::Model
custom_get :popular
end
# app/models/post.rb
class Post
include Him::Model
custom_get :recent, :archived
endIn order to test them, we’ll have to stub the remote API requests. With RSpec, we can do this like so:
# spec/spec_helper.rb
RSpec.configure do |config|
config.include(Module.new do
def stub_api_for(klass)
klass.use_api (api = Him::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 |c|
c.use Him::Middleware::FirstLevelParseJSON
c.adapter(:test) { |s| yield(s) }
end
end
end)
endThen, in your tests, we can specify what (fake) HTTP requests will return:
# 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
endWe can redefine the API for a model as many times as we want, like for more complex tests:
# 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
endContribute
Feel free to contribute and submit issues/pull requests on GitHub.
License
Him is © 2026 Dale Stevens, based on Her © 2012-2021 Rémi Prévost. Freely distributed under the MIT license.