Giter VIP home page Giter VIP logo

her's Introduction

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.

Gitter chat


Installation

In your Gemfile, add:

gem "her"

That’s it!

Usage

For a complete reference of all the methods you can use, check out the documentation.

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.rb
Her::API.setup url: "https://api.example.com" do |c|
  # Request
  c.use Faraday::Request::UrlEncoded

  # Response
  c.use Her::Middleware::DefaultParseJSON

  # Adapter
  c.use Faraday::Adapter::NetHttp
end

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

ActiveRecord-like methods

These are the basic ActiveRecord-like methods you can use with your models:

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 # returns false if it fails, errors in user.response_errors array
# PUT "/users/1" with `fullname=Lindsay+Fünke`

user.update_attributes(fullname: "Maeby Fünke")
# PUT "/users/1" with `fullname=Maeby+Fünke`

# => PUT /users/1 { "id": 1, "name": "new new name" }
# 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 Her::Errors::ResourceInvalid if it fails
# POST "/users" with `fullname=Maeby+Fünke`

You can look into the her-example repository for a sample application using Her.

Middleware

Since Her 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

Her 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_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 |c|
  # Request
  c.use MyTokenAuthentication
  c.use Faraday::Request::UrlEncoded

  # Response
  c.use Her::Middleware::DefaultParseJSON

  # Adapter
  c.use Faraday::Adapter::NetHttp
end

Now, each HTTP request made by Her will have the X-API-Token header.

Basic Http Authentication

Her can use basic http auth by adding a line to your initializer

# config/initializers/her.rb
Her::API.setup url: "https://api.example.com" do |c|
  # Request
  c.use Faraday::Request::BasicAuthentication, 'myusername', 'mypassword'
  c.use Faraday::Request::UrlEncoded

  # Response
  c.use Her::Middleware::DefaultParseJSON

  # Adapter
  c.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:

gem "her"
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: ""
}

Her::API.setup url: "https://api.twitter.com/1/" do |c|
  # Request
  c.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS

  # Response
  c.use Her::Middleware::DefaultParseJSON

  # Adapter
  c.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.

// 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:

# 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 |c|
  # Response
  c.use MyCustomParser

  # Adapter
  c.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:

gem "her"
gem "faraday_middleware"
gem "memcached"

In your Ruby code:

Her::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 Her::Middleware::DefaultParseJSON

  # Adapter
  c.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

Examples use this code:

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

Fetching data

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, 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, Her makes a 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 Her::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 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 throw an exception like this one:

Her::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 Her::Model
  belongs_to :owns, class_name: "Organization"
end

class Organization
  include Her::Model
  has_many :owners, class_name: "User"
end

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.

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" with `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.

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" 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 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 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_save
  • before_create
  • before_update
  • before_destroy
  • after_save
  • after_create
  • after_update
  • after_destroy
  • after_find
  • after_initialize

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.

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 "/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 Her::Model
  parse_root_in_json true
end

class Article
  include Her::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 Her as follows:

class User
  include Her::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 Her::JsonApi::Model

  # defaults to demodulized, pluralized class name, e.g. contributors
  type :developers
end

Finally, you'll need to use the included JsonApiParser Her middleware:

Her::API.setup url: 'https://my_awesome_json_api_service' do |c|
  # Request
  c.use FaradayMiddleware::EncodeJson

  # Response
  c.use Her::Middleware::JsonApiParser

  # Adapter
  c.use Faraday::Adapter::NetHttp
end

Custom requests

You can easily define custom requests for your models using custom_get, custom_post, etc.

class User
  include Her::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 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>

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 Her::Model

  def self.total
    get_raw(:stats) do |parsed_data, response|
      parsed_data[:data][:total_users]
    end
  end
end

User.total
# GET "/users/stats"
# => 42

You can also use full request paths (with strings instead of symbols).

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:

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:

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:

class User
  include Her::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 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.).

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.

class User
  include Her::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 Her::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 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 |c|
  # Response
  c.use Her::Middleware::DefaultParseJSON

  # Adapter
  c.use Faraday::Adapter::NetHttp
end

OTHER_API = Her::API.new
OTHER_API.setup url: "https://other-api.example.com" do |c|
  # Response
  c.use Her::Middleware::DefaultParseJSON

  # Adapter
  c.use Faraday::Adapter::NetHttp
end

You can then define which API a model will use:

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, 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_options do |c|
  # Response
  c.use Her::Middleware::DefaultParseJSON

  # Adapter
  c.use Faraday::Adapter::NetHttp
end

Testing

Suppose we have these two models bound to your API:

# 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, 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 = 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 |c|
        c.use Her::Middleware::FirstLevelParseJSON
        c.adapter(:test) { |s| yield(s) }
      end
    end
  end)
end

Then, 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
end

We 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
end

Upgrade

See the UPGRADE.md for backward compatibility issues.

Her IRL

Most projects I know that use Her are internal or private projects but here’s a list of public ones:

History

I told myself a few months ago that it would be great to build a gem to replace Rails’ ActiveResource 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 concepts were written on a Saturday morning of April 2012 (first commit at 7am!).

Maintainers

The gem is currently maintained by @zacharywelch and @edtjones.

Contribute

Yes please! Feel free to contribute and submit issues/pull requests on GitHub. There’s no such thing as a bad pull request — even if it’s for a typo, a small improvement to the code or the documentation!

See CONTRIBUTING.md for best practices.

Contributors

These fine folks helped with Her:

License

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

her's People

Contributors

alfius avatar blazingkin avatar chewi avatar dturnerts avatar ferpetrelli avatar hubert avatar ikuto0608 avatar jmlacroix avatar jonkarna avatar kindrowboat avatar lambcr avatar luxflux avatar marshall-lee avatar mesozoic avatar minktom avatar pencil avatar prognostikos avatar rafaelss avatar remi avatar simonprev avatar ssuttner avatar teamon avatar thomsbg avatar ticktricktrack avatar vaihtovirta avatar valo avatar vic avatar yozlet avatar zacharywelch avatar zoltankiss avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

her's Issues

Multipart HTTP Requests with Her

Someone asked me a question via email in regard to Her’s ability to perform multipart HTTP requests, so I though I’d answer here too in case someone else has the same question.

Since Her is using the awesome Faraday library to make HTTP requests and since Faraday supports multipart requests… Her supports them too!

Here’s an example of how to use them:

Her::API.setup :url => 'http://api.example.com' do |connection|
  connection.use Faraday::Request::Multipart
  connection.use Her::Middleware::DefaultParseJSON
  connection.use Faraday::Adapter::NetHttp
end

class User
  include Her::Model
end

User.post_raw("/users",
  :foo => "bar",
  :image => Faraday::UploadIO.new('/Users/remi/Desktop/foo.jpg', 'image/jpeg'),
  :text => Faraday::UploadIO.new('/Users/remi/Desktop/foo.txt', 'text/plain')
) # User.post would work too but woul expect the API to return a resource Hash

Hope that helps someone out there! 😄

undefined method `delete' for true:TrueClass when executing destroy

This is what I get when I execute 'destroy' on a resource. The resource itself is deleted afterwards.

>> j = PostgresRecoverJob.find("6e3421a41aacd3354c9fb9e7f1d0c854")
=> #<PostgresRecoverJob(recover_jobs/6e3421a41aacd3354c9fb9e7f1d0c854) status="queued" time="2013-04-02 22:04:03 +0200" message="" pct_complete=0 server="sauspiel" target="localhost" backup_id="20130402T171257" completed_at="" id="6e3421a41aacd3354c9fb9e7f1d0c854">
>> j.destroy
NoMethodError: undefined method `delete' for true:TrueClass
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/her-0.5.2/lib/her/middleware/first_level_parse_json.rb:11:in `parse'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/her-0.5.2/lib/her/middleware/first_level_parse_json.rb:29:in `on_complete'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/faraday-0.8.6/lib/faraday/response.rb:9:in `block in call'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/faraday-0.8.6/lib/faraday/response.rb:63:in `on_complete'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/faraday-0.8.6/lib/faraday/response.rb:8:in `call'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/faraday-0.8.6/lib/faraday/request/url_encoded.rb:14:in `call'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/faraday-0.8.6/lib/faraday/connection.rb:247:in `run_request'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/faraday-0.8.6/lib/faraday/connection.rb:100:in `delete'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/her-0.5.2/lib/her/api.rb:88:in `request'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/her-0.5.2/lib/her/model/http.rb:24:in `request'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/her-0.5.2/lib/her/model/orm.rb:177:in `block in destroy'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/activesupport-3.2.13/lib/active_support/callbacks.rb:403:in `_run__1606273922648791830__destroy__1484856305865657010__callbacks'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/activesupport-3.2.13/lib/active_support/callbacks.rb:405:in `__run_callback'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/activesupport-3.2.13/lib/active_support/callbacks.rb:385:in `_run_destroy_callbacks'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/activesupport-3.2.13/lib/active_support/callbacks.rb:81:in `run_callbacks'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/her-0.5.2/lib/her/model/orm.rb:176:in `destroy'
    from (irb):2
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/railties-3.2.13/lib/rails/commands/console.rb:47:in `start'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/railties-3.2.13/lib/rails/commands/console.rb:8:in `start'
    from /Users/hamann/workspace/sauspiel/sauspiel-rails/vendor/bundle/ruby/1.9.1/gems/railties-3.2.13/lib/rails/commands.rb:41:in `<top (required)>'
    from script/rails:6:in `require'
    from script/rails:6:in `<main>

Her model should handle 404 / 500 statuses

When I use a Her::Model and my request get a 404 response it's not treated as it should.

class Article
  include Her::Model
end

Article.find('nothing')
#=> #<Article(articles) error="404 Not Found">

Maybe some sort of exception should be raised like ActiveRecord::RecordNotFound when an AR.find fails.

EDIT:

When an API returns no body on 404 error, the JSON parser raises a MultiJson::DecodeError exception.

Faraday connections can store default headers/params

You don't necessarily need middleware like this:

class MyAuthentication < Faraday::Middleware
  def call(env)
    env[:request_headers]["X-API-Token"] = "foobar"
    @app.call(env)
  end
end

With Faraday, you'd do this:

http = Faraday.new :url => 'http://api.foo.com', :headers => {'X-API-Token' => 'foo'}

# change it after initialization
http.headers['X-API-Token'] = 'foo'

Just general feedback, go ahead and close it if you want :)

Follow redirects vs DefaultParseJSON

In the rails world, you normally respond with a redirect after a successful POST/PUT to the actual resource (POST /users would redirect to /users/1). There is FaradayMiddleware::FollowRedirects which would allow following redirects. But this fails because Her::Middleware::DefaultParseJSON tries to parse the (empty) body of the response.

I think there are two possible solutions: DefaultParseJSON should respect the Content-Type and/or DefaultParseJSON should not try to parse empty body. What would be the preferred way? I'd like to fix this and submit a pull request.

Relationships do not work correctly

I'm a bit confused, maybe you can help me

if have two classes

class PostgresBackupServer
  include Her::Model
  collection_path "servers"
  has_many :targets, :class_name => "PostgresBackupTarget"
end

class PostgresBackupTarget
  include Her::Model
  collection_path "servers/:postgres_backup_server/targets"
  attributes :path
  belongs_to :server, :class_name => "PostgresBackupServer"
end

this works so far

>> PostgresBackupServer.all[0].targets[0]
=> #<PostgresBackupTarget(servers/#<PostgresBackupServer:0x007f8eefce51c8>/targets/localhost) id="localhost" postgres_backup_server=#<PostgresBackupServer(servers/sauspiel) id="sauspiel" targets=[#<PostgresBackupTarget(servers/#<PostgresBackupServer:0x007f8eefce51c8>/targets/localhost) id="localhost" postgres_backup_server=#<PostgresBackupServer(servers/sauspiel) id="sauspiel" targets=[...]>>, #<PostgresBackupTarget(servers/#<PostgresBackupServer:0x007f8eefce51c8>/targets/ssh_localhost) id="ssh_localhost" postgres_backup_server=#<PostgresBackupServer(servers/sauspiel) id="sauspiel" targets=[...]>>]>>

but this doesn't work

>> PostgresBackupTarget.all(:_postgres_backup_server => "sauspiel")
Her::Errors::PathError: Missing :_postgres_backup_server parameter to build the request path. Path is `servers/:postgres_backup_server/targets/:id`. Parameters are `{:id=>"localhost"}`.
    from /Users/hamann/workspace/tmp/railstest/vendor/bundle/ruby/1.9.1/gems/her-0.5.2/lib/her/model/paths.rb:70:in `block in build_request_path'
    from /Users/hamann/workspace/tmp/railstest/vendor/bundle/ruby/1.9.1/gems/her-0.5.2/lib/her/model/paths.rb:68:in `gsub'
    from /Users/hamann/workspace/tmp/railstest/vendor/bundle/ruby/1.9.1/gems/her-0.5.2/lib/her/model/paths.rb:68:in `build_request_path'
    from /Users/hamann/workspace/tmp/railstest/vendor/bundle/ruby/1.9.1/gems/her-0.5.2/lib/her/model/paths.rb:15:in `request_path'
    from /Users/hamann/workspace/tmp/railstest/vendor/bundle/ruby/1.9.1/gems/her-0.5.2/lib/her/model/introspection.rb:15:in `inspect'
    from /Users/hamann/workspace/tmp/railstest/vendor/bundle/ruby/1.9.1/gems/railties-3.2.13/lib/rails/commands/console.rb:47:in `start'
    from /Users/hamann/workspace/tmp/railstest/vendor/bundle/ruby/1.9.1/gems/railties-3.2.13/lib/rails/commands/console.rb:8:in `start'
    from /Users/hamann/workspace/tmp/railstest/vendor/bundle/ruby/1.9.1/gems/railties-3.2.13/lib/rails/commands.rb:41:in `<top (required)>'
    from script/rails:6:in `require'
    from script/rails:6:in `<main>'

What's the problem here?

Ability to access HTTP headers via Model

Thank you for the great library.

I believe this is a feature request unless there is a clean way to pass the needed information to a faraday middleware layer that I'm not seeing.

I'm hoping for the ability to set/read request/response headers from the model. The specific use case is to embed pagination information in the request headers rather than sending it via query params and more importantly, avoid having to chunk up the response JSON to embed metadata needed for pagination such as the total number of items.

I've taken a look over the code and at first glance it seems a lot of changes would need to be made to pass header information up/down the stack. I'm happy to take a crack at it if some guidance could be offered in terms of preferred style.

non-nested models

Perhaps I'm dense, but I can't work out how to get Her to create relationships where the URLs are not nested - ie, User has_many Books, but the URL should be /books/:book_id, not /users/:id/books/:book_id. It looks like straight string concatenation in relationships.rb - am i missing the point somehow?

Create an example application using Her

I’m going to create an example application (probably using the Twitter API) to show how easy it is to create REST API-backed Web applications with Her.

parse "include root in json"?

When building an API with rails/active model serializers the root name is picked from the name of the model. How can I, in a middleware automatically set data to json[:model_name] ? Alternatively, is there any other way to do it?

Restrict operations

What

Restrict a Her::Model to only supporting certain operations.

Why

In the case where you want to use all the features of the Her ORM, but you only have read-only access to a given object.

Proposed DSL

class Profile
   include Her::Model

   # Whitelist options
   supports :read, :create, :update, :delete

   # Blacklist actions
   restricts :update, :delete
end

I like the idea of whitelisting with an :all over blacklisting, thoughts?

Metadata usage?

I was trying to use metadata with my Her model. While the middlware segregates the response into data, errors and metadata, but on model initialization the parameters of metadata being send-in are parsed_data[:data] instead of parsed_data[:metadata].
Saw your comment
#16 (comment)

Was curious to know if it is something that has been done intentionally or is it some feature in-dev?

Problems with the example

Hi,

I am trying to use the 'her' gem as it seems like a really good gem. However I can't even get the simplest thing to work. Here is the error I am getting

1.9.2p320 :001 > s = Fileflow::Student.find(1)
URI::InvalidURIError: query conflicts with opaque
    from /Users/ceicke/.rvm/rubies/ruby-1.9.2-p320/lib/ruby/1.9.1/uri/generic.rb:521:in `check_query'
    from /Users/ceicke/.rvm/rubies/ruby-1.9.2-p320/lib/ruby/1.9.1/uri/generic.rb:540:in `query='
    from /Users/ceicke/.rvm/gems/ruby-1.9.2-p320@intranet/gems/faraday-0.8.4/lib/faraday/connection.rb:277:in `build_exclusive_url'
    from /Users/ceicke/.rvm/gems/ruby-1.9.2-p320@intranet/gems/faraday-0.8.4/lib/faraday/request.rb:94:in `to_env'
    from /Users/ceicke/.rvm/gems/ruby-1.9.2-p320@intranet/gems/faraday-0.8.4/lib/faraday/connection.rb:225:in `run_request'
    from /Users/ceicke/.rvm/gems/ruby-1.9.2-p320@intranet/gems/faraday-0.8.4/lib/faraday/connection.rb:87:in `get'
    from /Users/ceicke/.rvm/gems/ruby-1.9.2-p320@intranet/gems/her-0.2/lib/her/api.rb:67:in `request'
    from /Users/ceicke/.rvm/gems/ruby-1.9.2-p320@intranet/gems/her-0.2/lib/her/model/http.rb:37:in `request'
    from /Users/ceicke/.rvm/gems/ruby-1.9.2-p320@intranet/gems/her-0.2/lib/her/model/orm.rb:53:in `find'
    from (irb):1
    from /Users/ceicke/.rvm/gems/ruby-1.9.2-p320@intranet/gems/railties-3.0.10/lib/rails/commands/console.rb:44:in `start'
    from /Users/ceicke/.rvm/gems/ruby-1.9.2-p320@intranet/gems/railties-3.0.10/lib/rails/commands/console.rb:8:in `start'
    from /Users/ceicke/.rvm/gems/ruby-1.9.2-p320@intranet/gems/railties-3.0.10/lib/rails/commands.rb:23:in `<top (required)>'
    from script/rails:6:in `require'
    from script/rails:6:in `<main>'

My setup is pretty default:

# config/initializers/her.rb
Her::API.setup :url => "http://127.0.0.1:3003/api/v1/" do |connection|
  connection.use Faraday::BasicAuthentication.basic_auth("user","pass")
  connection.use Faraday::Request::UrlEncoded
  connection.use Her::Middleware::DefaultParseJSON
  connection.use Faraday::Adapter::NetHttp
end
module Fileflow
  class Student
        include Her::Model
  end
end

I'm suspecting that the URL is not correctly built, but I have no idea where to hook into...

Any help would be appreciated,
Christoph

Typhoeus/Parallel request Compatability

Background

Hey, awesome gem! I've been working on something of an ORM myself, and I wanted to implement parallel requesting with Typhoeus. I might still implement this myself, but I really enjoy this implementation and figured this would be a good spot to add such functionality.

The Question

So, does anyone have any interest in cooperating to add parallel requesting with Typhoeus on here?

Initial glance

At a glance, just using:

Her::API.setup :url => "http://example-url.example.com" do |connection|
  # ...
  connection.use Faraday::Adapter::Typhoeus
end

There's an error upon request:

Ethon::Errors::InvalidOption: The option: disable_ssl_peer_verification is invalid.
from ...@broker/gems/ethon-0.5.7/lib/ethon/easy.rb:254:in `block in set_attributes'

tl;dr

Hoping to get some parallel request implementation, potentially on Typhoeus, what are your thoughts?

Model-specific headers

What

Set headers for a model to be a Hash, Proc or lambda.

Why

This is for the case when your using an OAuth-based API, like Facebook, where you need to use the specific user token for any request. Token is also an attribute on the object so it must be evaluated per-user on request.

Proposed Change Example

class User
  include Her::Model

  headers Proc.new do |obj|
    'Authorize' => "token #{obj.token}"
  end

What are your thoughts? I can try to take a pass at this unless there is a better way or somehow I missed the feature while surfing through the source code.

Not sending the correct parameters on create/update

I just started using her (so I'm probably missing something) but when I use the example code from the readme:

User.create :full_name => 'Nate'

The response is received as:

Started POST "/users" for 127.0.0.1 at 2012-10-31 11:39:32 -0400
Processing by UsersController#create as */*
  Parameters: {"full_name"=>"Nate"}
   (0.0ms)  begin transaction
  SQL (1.0ms)  INSERT INTO "users" ("created_at", "full_name", "updated_at") VALUES (?, ?, ?)  [["created_at", Wed, 31 Oct 2012 15:39:32 UTC +00:00], ["full_name", nil], ["updated_at", Wed, 31 Oct 2012 15:39:32 UTC +00:00]]

The create works (technically) but the full_name is blank because the parameter came across without being scoped under user. Is this supposed to be this way or am I missing something?

Can't destroy through association.

class Configuration
  include Her::Model
  has_many :publish_sets
end

class PublishSet
  include Her::Model
  belongs_to :configuration
end

This association works as expected for get requests:

Configuration.find(1234).publish_sets
GET .../configurations/1234/publish_sets

but for delete requests:

Configuration.find(1234).publish_sets.first.destroy
DELETE .../publish_sets/5678

I need to make it so that delete requests use the full nested url
DELETE .../configurations/1234/publish_sets/5678

I tried setting
collection_path("/configurations/:configuration_id/publish_sets")
but the publish_set objects i get back from the api don't have the configuration_id param, only a configuration object.

Is there anyway to do this kind of delete through the association instead of a delete_raw (how i'm doing it now)

Introspecting a model with a custom path throws an exception

Given the following model:

class User
  collection_path "/companies/:company_id/users"
end

If in the console I do the following it works:

> User.all({company_id: 1}).first.id
1

However the following throws an error:

> User.all({company_id: 1}).first.id
Her::Errors::PathError: Missing :_company_id parameter to build the request path (/companies/:company_id/users).

This is because #inspect calls #request_path again, but without the parameters.

ssl support

Would you consider to add ssl connection option at Farraday connection establishment in
#50 Her::Api setup

Even nice way would be, allowing to pass the same options to setup, as to Farraday.new

How to call a custom member action on a resource

Say you have a group resource within your api-server like this:

resources :groups do
  put :approve, on: :member
end

How to call this action from the Her Api?

I would expect something like Group.find(15).put(:approve) but that itsn't working. Could someone indicate if is possible?

Allow data wrapping when calling create/update

A Her model should provide an option to allow data wrapping when sending it to an API.
I'd like to see something like this:

class User
  include Her::Model
  params_wrapping true
end

When calling save or create or any other saving method, it would send the following:

{ user: { name: "user1", ... } }

Alternatively, a user could override the default wrapping key by providing it this way:

class User
  include Her::Model
  params_wrapping :person
end

It would send

{ person: { name: "user1", ... } }

(see also issue #41)

New Gem-Version

As you merged some stuff yesterday, it would be cool to have a new Version on RubyGems so we don't have to use your master branch :)

:after_find hook?

Do you have any plans to add an :after_find hook? Would you entertain a pull request with such a feature? ;)

Undefined Method '+' for nil:NilClass

Hopefully this is not a stupid question, or something obvious, but I can't get Her to work at all. I've tried to follow the readme to the letter but I must be missing something.

Tried with ruby-1.8.7-p370, ree-1.8.7-2012.02, ruby-1.9.3-p194, same result each time.
Using rails 3.2.3

I have a model User in app2 which I want to pull from into app1.

# app1/Gemfile
gem 'her'

# app1/config/initializers/her.rb
# http://local.portal is app2, and I can access it fine through my browser

Her::API.setup :url => "http://local.portal" do |connection|
  connection.use Faraday::Request::UrlEncoded
  connection.use Her::Middleware::DefaultParseJSON
  connection.use Faraday::Adapter::NetHttp
end

# app1/models/user.rb
class User
  include Her::Model
end

# rails c
Loading development environment (Rails 3.2.3)
1.8.7-p370 :001 > u = User.all
NoMethodError: undefined method `+' for nil:NilClass
    from /home/tom/.rvm/rubies/ruby-1.8.7-p370/lib/ruby/1.8/net/http.rb:1118:in `addr_port'
    from /home/tom/.rvm/rubies/ruby-1.8.7-p370/lib/ruby/1.8/net/http.rb:1078:in `begin_transport'
    from /home/tom/.rvm/rubies/ruby-1.8.7-p370/lib/ruby/1.8/net/http.rb:1048:in `request'
    from /home/tom/.rvm/rubies/ruby-1.8.7-p370/lib/ruby/1.8/net/http.rb:1037:in `request'
    from /home/tom/.rvm/rubies/ruby-1.8.7-p370/lib/ruby/1.8/net/http.rb:543:in `start'
    from /home/tom/.rvm/rubies/ruby-1.8.7-p370/lib/ruby/1.8/net/http.rb:1035:in `request'
    from /home/tom/.rvm/rubies/ruby-1.8.7-p370/lib/ruby/1.8/net/http.rb:772:in `get'
    from /home/tom/.rvm/gems/ruby-1.8.7-p370@io/gems/faraday-0.7.6/lib/faraday/adapter/net_http.rb:59:in `call'
    from /home/tom/.rvm/gems/ruby-1.8.7-p370@io/gems/faraday-0.7.6/lib/faraday/request/url_encoded.rb:14:in `call'
    from /home/tom/.rvm/gems/ruby-1.8.7-p370@io/gems/faraday-0.7.6/lib/faraday/response.rb:8:in `call'
    from /home/tom/.rvm/gems/ruby-1.8.7-p370@io/gems/faraday-0.7.6/lib/faraday/connection.rb:210:in `run_request'
    from /home/tom/.rvm/gems/ruby-1.8.7-p370@io/gems/faraday-0.7.6/lib/faraday/connection.rb:93:in `get'
    from /home/tom/.rvm/gems/ruby-1.8.7-p370@io/gems/her-0.2/lib/her/api.rb:67:in `send'
    from /home/tom/.rvm/gems/ruby-1.8.7-p370@io/gems/her-0.2/lib/her/api.rb:67:in `request'
    from /home/tom/.rvm/gems/ruby-1.8.7-p370@io/gems/her-0.2/lib/her/model/http.rb:37:in `request'
    from /home/tom/.rvm/gems/ruby-1.8.7-p370@io/gems/her-0.2/lib/her/model/orm.rb:64:in `all'
    from (irb):11.8.7-p370 :002 > 

I've been working at this for a couple hours now, getting nowhere. I must be missing something very simple, but for the life of me can't see it. If you have any suggestions (or if this is actually a bug and I also missed THAT in the tickets), I am all ears.

Thanks, and I am really looking forward to using Her!

her whitelists faraday options

Hi,
I'd like to ask the reasoning behind whitelisting faraday options instead of allowing them to be passed through. I recently had the need to specify a global timeout longer than the faraday default and had to utilize a round-about method (below) to apply that.

Thanks!

Whitelist line:
(https://github.com/remiprev/her/blob/master/lib/her/api.rb#L56)

Workaround:

my_api = Her::API.new
my_api.setup :url => 'foobar.com' do...
options = my_api.connection.instance_variable_get("@options") || {}
my_api.connection.instance_variable_set("@options", options.merge({:timeout => 60}))

test breakage under Faraday 0.9.x/1.0 (pre-release)

Hey, I've made some radical changes to the underlying builder/middleware system in Faraday. Her has just one test failure when run in legacy mode:

  1) Her::API initialization#setup takes custom options
     Failure/Error: @api.setup :foo => { :bar => "baz" }, :url => "https://api.example.com"
     NoMethodError:
       undefined method `foo=' for #<Faraday::ConnectionOptions:0x007ffabf31c538>
     # ./lib/her/api.rb:56:in `setup'
     # ./spec/api_spec.rb:34:in `block (4 levels) in <top (required)>'

Faraday 0.9 and up has replaced the nested hashes that Faraday::Connection.new takes in favor of Structs. The benefit is that misspelled options immediately raise exceptions, instead of subtly introducing bugs. The downside is that you can no longer pass custom options around.

Is Her relying on this functionality?

Examples don't jive with the actual middleware stack order

The documentation here (specifically the first example) implies that if I add my middleware with use that I can run my code during both the request and response cycles. However, this is not true. In the example, the "X-API-TOKEN" header wouldn't get set until after the request had been made, which of course makes no sense.

This is the order of middleware before MyAuthentication is added (the default order):

Her::Middleware::FirstLevelParseJSON
Faraday::Request::UrlEncoded
Faraday::Adapter::NetHttp

When you use builder.use, you are appending to the end of the stack, after NetHttp. However, NetHttp, when called, performs the actual request. So, MyAuthentication doesn't get called until after we get a response.

There's a few solutions here. The easiest would, of course, be just updating the docs to use builder.insert 0, ... However, it would be nice if we had a way to use middleware while ensuring that it comes before the middleware that makes the actual request (e.g. NetHttp).

I'm happy to implement this, but I wanted to see what your opinion was on how this would best be done.

Token Authentication Middleware returns nill for @app

I hope I'm not being dense on this one, but I am trying to set up an api for App1, and using Her to call it from App2. However, when I try to send the json request, App2 fails with 'undefined method `call' for nil:NilClass'.

My api for App1 is pretty standard, I think, and the code from the readme seems to be perfect for it how it is.

App2 is running:

Ruby 1.8.7 ree 358, rails 3.2.6, her 0.3.1, faraday (came with her) 0.8.4, rvm 1.14.10
#app2/config/initializers/her.rb
class TokenAuthentication < Faraday::Middleware
  def initialize(app, options={})
    @options = options
  end

  def call(env)
    env[:request_headers]["X-API-Token"] = @options[:token] if @options.include?(:token)
    @app.call(env)
  end
end

$app2 = Her::API.new
$app2.setup :url => 'http://local.app1' do |connection| #local.app1 is all setup
  connection.use TokenAuthentication, :token => "token_stuff"
  connection.use Faraday::Request::UrlEncoded
  connection.use Her::Middleware::DefaultParseJSON
  connection.use Faraday::Adapter::NetHttp
end
#employee.rb
class Employee
  include Her::Model
  uses_api $app2
end
# rails c
1.8.7 :001 > e = Employee.find 1
NoMethodError: undefined method `call' for nil:NilClass
    from /var/www/app2/config/initializers/her.rb:16:in `call' # This is where @app.call(env) is
    from /home/username/.rvm/gems/ree-1.8.7-2012.02@app2/gems/faraday-0.8.4/lib/faraday/connection.rb:226:in `run_request'
    from /home/username/.rvm/gems/ree-1.8.7-2012.02@app2/gems/faraday-0.8.4/lib/faraday/connection.rb:87:in `get'
    from /home/username/.rvm/gems/ree-1.8.7-2012.02@app2/gems/her-0.3.1/lib/her/api.rb:71:in `send'
    from /home/username/.rvm/gems/ree-1.8.7-2012.02@app2/gems/her-0.3.1/lib/her/api.rb:71:in `request'
    from /home/username/.rvm/gems/ree-1.8.7-2012.02@app2/gems/her-0.3.1/lib/her/model/http.rb:13:in `request'
    from /home/username/.rvm/gems/ree-1.8.7-2012.02@app2/gems/her-0.3.1/lib/her/model/orm.rb:79:in `find'
    from (irb):1

So, where am I messing up? I must admit I am not very familiar at all with middleware or faraday, and I don't quite see where @app is supposed to be coming from.

Any help is appreciated, thanks!

Authenticating to Rails API using authenticate_or_request_with_http_token

The sample code in the readme

def call(env)
  env[:request_headers]["X-API-Token"] = @options[:token] if @options.include?(:token)
  @app.call(env)
end

produces the following header: "X-API-TOKEN"=>"bb2b2dd75413d3"

Using the built-in Rails token authentication method (authenticate_or_request_with_http_token) it needs to come in like this: "HTTP_AUTHORIZATION"=>"Token token=\"bb2b2dd75413d3\"".

Am I missing anything or how can I change the sent header?

Posting new entities attached to existing ones?

I'd love to be able to run

Post.find(10).comments.build({ :comment => "lol fristpost"}) 
  • is this ActiveRecord style post interface something that's planned, not-implemented-but-welcome-to-write, or actively rejected?

undefined method `delete' for nil:NilClass

Hey All,

Running into an issue getting started with her. It's probably me, but wanted to run it by you.

I've got a simple model using Her:

class Course < ActiveRecord::Base
  include Her::Model
end

Here's my initilizer:

Her::API.setup :url => "http://localhost:3010" do |connection|
  connection.use Faraday::Request::UrlEncoded
  connection.use Her::Middleware::FirstLevelParseJSON
  connection.use Faraday::Adapter::NetHttp
end

I'm trying to do a basic find (/courses/1) in my client. The find is calling my api, which is correctly returning {"id":1,"name":"Alondra"}.

However, it looks like parsing that response is causing problems. Here's the full error I'm getting:

activerecord (3.2.8) lib/active_record/attribute_methods/write.rb:28:in write_attribute' activerecord (3.2.8) lib/active_record/attribute_methods/dirty.rb:67:inwrite_attribute'
activerecord (3.2.8) lib/active_record/attribute_methods/write.rb:14:in id=' her (0.3.6) lib/her/model/orm.rb:36:inblock in use_setter_methods'
her (0.3.6) lib/her/model/orm.rb:33:in each' her (0.3.6) lib/her/model/orm.rb:33:ininject'
her (0.3.6) lib/her/model/orm.rb:33:in use_setter_methods' her (0.3.6) lib/her/model/orm.rb:16:ininitialize'
her (0.3.6) lib/her/model/orm.rb:196:in new' her (0.3.6) lib/her/model/orm.rb:196:inblock (2 levels) in find'
her (0.3.6) lib/her/model/http.rb:22:in request' her (0.3.6) lib/her/model/orm.rb:195:inblock in find'
her (0.3.6) lib/her/model/orm.rb:194:in map' her (0.3.6) lib/her/model/orm.rb:194:infind'
app/controllers/course_s_controller.rb:7:in show' actionpack (3.2.8) lib/action_controller/metal/implicit_render.rb:4:insend_action'
actionpack (3.2.8) lib/abstract_controller/base.rb:167:in process_action' actionpack (3.2.8) lib/action_controller/metal/rendering.rb:10:inprocess_action'
actionpack (3.2.8) lib/abstract_controller/callbacks.rb:18:in block in process_action' activesupport (3.2.8) lib/active_support/callbacks.rb:425:in_run__2721729234568045003__process_action__79657235857638791__callbacks'
activesupport (3.2.8) lib/active_support/callbacks.rb:405:in __run_callback' activesupport (3.2.8) lib/active_support/callbacks.rb:385:in_run_process_action_callbacks'
activesupport (3.2.8) lib/active_support/callbacks.rb:81:in run_callbacks' actionpack (3.2.8) lib/abstract_controller/callbacks.rb:17:inprocess_action'
actionpack (3.2.8) lib/action_controller/metal/rescue.rb:29:in process_action' actionpack (3.2.8) lib/action_controller/metal/instrumentation.rb:30:inblock in process_action'
activesupport (3.2.8) lib/active_support/notifications.rb:123:in block in instrument' activesupport (3.2.8) lib/active_support/notifications/instrumenter.rb:20:ininstrument'
activesupport (3.2.8) lib/active_support/notifications.rb:123:in instrument' actionpack (3.2.8) lib/action_controller/metal/instrumentation.rb:29:inprocess_action'
actionpack (3.2.8) lib/action_controller/metal/params_wrapper.rb:207:in process_action' activerecord (3.2.8) lib/active_record/railties/controller_runtime.rb:18:inprocess_action'
actionpack (3.2.8) lib/abstract_controller/base.rb:121:in process' actionpack (3.2.8) lib/abstract_controller/rendering.rb:45:inprocess'
actionpack (3.2.8) lib/action_controller/metal.rb:203:in dispatch' actionpack (3.2.8) lib/action_controller/metal/rack_delegation.rb:14:indispatch'
actionpack (3.2.8) lib/action_controller/metal.rb:246:in block in action' actionpack (3.2.8) lib/action_dispatch/routing/route_set.rb:73:incall'
actionpack (3.2.8) lib/action_dispatch/routing/route_set.rb:73:in dispatch' actionpack (3.2.8) lib/action_dispatch/routing/route_set.rb:36:incall'
journey (1.0.4) lib/journey/router.rb:68:in block in call' journey (1.0.4) lib/journey/router.rb:56:ineach'
journey (1.0.4) lib/journey/router.rb:56:in call' actionpack (3.2.8) lib/action_dispatch/routing/route_set.rb:600:incall'
omniauth (1.1.1) lib/omniauth/strategy.rb:177:in call!' omniauth (1.1.1) lib/omniauth/strategy.rb:157:incall'
warden (1.2.1) lib/warden/manager.rb:35:in block in call' warden (1.2.1) lib/warden/manager.rb:34:incatch'
warden (1.2.1) lib/warden/manager.rb:34:in call' actionpack (3.2.8) lib/action_dispatch/middleware/best_standards_support.rb:17:incall'
rack (1.4.1) lib/rack/etag.rb:23:in call' rack (1.4.1) lib/rack/conditionalget.rb:25:incall'
actionpack (3.2.8) lib/action_dispatch/middleware/head.rb:14:in call' actionpack (3.2.8) lib/action_dispatch/middleware/params_parser.rb:21:incall'
actionpack (3.2.8) lib/action_dispatch/middleware/flash.rb:242:in call' rack (1.4.1) lib/rack/session/abstract/id.rb:205:incontext'
rack (1.4.1) lib/rack/session/abstract/id.rb:200:in call' actionpack (3.2.8) lib/action_dispatch/middleware/cookies.rb:339:incall'
activerecord (3.2.8) lib/active_record/query_cache.rb:64:in call' activerecord (3.2.8) lib/active_record/connection_adapters/abstract/connection_pool.rb:473:incall'
actionpack (3.2.8) lib/action_dispatch/middleware/callbacks.rb:28:in block in call' activesupport (3.2.8) lib/active_support/callbacks.rb:405:in_run__2801846045624969339__call__293598484128179998__callbacks'
activesupport (3.2.8) lib/active_support/callbacks.rb:405:in __run_callback' activesupport (3.2.8) lib/active_support/callbacks.rb:385:in_run_call_callbacks'
activesupport (3.2.8) lib/active_support/callbacks.rb:81:in run_callbacks' actionpack (3.2.8) lib/action_dispatch/middleware/callbacks.rb:27:incall'
actionpack (3.2.8) lib/action_dispatch/middleware/reloader.rb:65:in call' actionpack (3.2.8) lib/action_dispatch/middleware/remote_ip.rb:31:incall'
actionpack (3.2.8) lib/action_dispatch/middleware/debug_exceptions.rb:16:in call' actionpack (3.2.8) lib/action_dispatch/middleware/show_exceptions.rb:56:incall'
railties (3.2.8) lib/rails/rack/logger.rb:26:in call_app' railties (3.2.8) lib/rails/rack/logger.rb:16:incall'
disable_assets_logger (1.0.0) lib/disable_assets_logger/middleware.rb:11:in call' actionpack (3.2.8) lib/action_dispatch/middleware/request_id.rb:22:incall'
rack (1.4.1) lib/rack/methodoverride.rb:21:in call' rack (1.4.1) lib/rack/runtime.rb:17:incall'
activesupport (3.2.8) lib/active_support/cache/strategy/local_cache.rb:72:in call' rack (1.4.1) lib/rack/lock.rb:15:incall'
actionpack (3.2.8) lib/action_dispatch/middleware/static.rb:62:in call' railties (3.2.8) lib/rails/engine.rb:479:incall'
railties (3.2.8) lib/rails/application.rb:223:in call' rack (1.4.1) lib/rack/content_length.rb:14:incall'
railties (3.2.8) lib/rails/rack/log_tailer.rb:17:in call' thin (1.5.0) lib/thin/connection.rb:81:inblock in pre_process'
thin (1.5.0) lib/thin/connection.rb:79:in catch' thin (1.5.0) lib/thin/connection.rb:79:inpre_process'
thin (1.5.0) lib/thin/connection.rb:54:in process' thin (1.5.0) lib/thin/connection.rb:39:inreceive_data'
eventmachine (1.0.0) lib/eventmachine.rb:187:in run_machine' eventmachine (1.0.0) lib/eventmachine.rb:187:inrun'
thin (1.5.0) lib/thin/backends/base.rb:63:in start' thin (1.5.0) lib/thin/server.rb:159:instart'
rack (1.4.1) lib/rack/handler/thin.rb:13:in run' rack (1.4.1) lib/rack/server.rb:265:instart'
railties (3.2.8) lib/rails/commands/server.rb:70:in start' railties (3.2.8) lib/rails/commands.rb:55:inblock in <top (required)>'
railties (3.2.8) lib/rails/commands.rb:50:in tap' railties (3.2.8) lib/rails/commands.rb:50:in<top (required)>'
script/rails:6:in require' script/rails:6:in

'

Any thoughts?

thanks,
Charlie

Can Her support the concept of scoped API objects?

One of the challenges we face working with REST ORMs is that only a few have a built in concept of per user or per request authorization - authorization to the API tends to be global in scope. In our use cases, we authorize as the user to the remote API and so need to scope authorization on a per instance / request basis. One of the ways we've worked around this with ActiveResource is exposing the concept of an authorization object that is passed on object initialization:

Cartridge.new :name => 'foo', :as => current_user

and then making sure that the appropriate remote calls in ActiveResource introspect the model object for the presence of that "as" object in order to build HTTP headers for the response (either BASIC or API token, as necessary). The :as object in this case exposes either to_headers or :user, :password / :to_api_token depending on what makes sense. In ActiveResource this is a fair amount of pain.

As I was looking at Her I liked that most API requests passed through an API object that could be scoped, although in many cases the scope was at the class level. From a pure architectural level, are you opposed to the idea that a Her API object might have a local scope (on an instance) or a global scope (on a model object) and that the instance would delegate up the chain to locate that object before making a new request? One implication with that is that new() and find() methods would need to properly inject the correct API object to factory created methods, but that did not seem insurmountable. The goal would be to allow a model to parameterize the HTTP requests it makes to the remote endpoint - I was working from the assumption that the cleanest way to do that would be to allow a model object to add middleware to the parent API object (thus specializing the call).

I took a rough stab at a branch to demonstrate the concept - https://github.com/smarterclayton/her/compare/experimental_api_scoping, with a Gist demonstrating mixing in of the API (https://gist.github.com/3361943). Some of the changes in the gist would also need to be in Her but the idea would be that specialization of an api object would be possible through any Her descended model class or instance. In this case it's demonstrating a mixin that looks for :as on the instance and adds a custom faraday middleware to requests that come from that instance.

A usage example is:

api = Her::API.new
api.setup :base_uri => "http://localhost" do |builder|
  ...
  builder.use Faraday::Adapter::NetHttp
end

class Model
  include Her::Model
  include ConsoleModel::WithAuthorization
  authorizes :with => ConsoleModel::Middleware::Authorizes
  uses_api api
end

model = Model.new :as => User.new('bob', 'password'), .... # other attributes
model.save # the presence of :as triggers api specialization such that the middleware

# or alternatively, if you prefer specialization of the model class

model = Model.as(User.new('bob', 'password').new #other attributes
model.save

Authorization is one example where this sort of middleware approach might make sense - model level caching mixins is possibly another:

 model = Model.cached(:for => 5.min).find('by_id')

Would you be opposed to a pull request exploring the minimal changes necessary to support an inheritance chain for API objects (allowing an instance to delegate to the class, which could delegate to its parent class, etc) and the support for specializing those objects? Is model specialization via class/eigenclass/instance inheritance something you've previously considered for Her?

Issue with plural-named has_one element

hi all, first off, we've been looking for support like this since our UI will be accessing only REST services and I found this very easy to integrate (even tweaking Faraday with our client certs and custom headers) and is mostly working with one of our services. But I noticed the following issue. we have a person service and the person has one details, ie
class Person
include Her::Model
has_one :details
has_one :summary....
and I created a Details class but my test errors with
undefined method `new' for nil:NilClass
But if I changed details.rb to detail.rb (and classname Details to Detail), and leave the above "has_one: details" as-is, then it works. I also tried the opposite, "has_one :detail" but it failed too since the JSON had "details"
I noticed a couple instances of using pluralize in the Her source but not sure if related to this issue. Now, arguably, the server-side guys maybe should have gone with one detail instead of details since its not has_many details, but from a human readable perspective I can see their design.
Any idea why this is happening? My workaround above is fine for now but just inconsistent.
thanks for your time.

Her models should respond correctly to the Rails link helper

Right now trying to do link_to(foo_path(@foo)) or link_to(@foo) will result in the wrong thing or a routing error. It would be cool if Her responded to whatever methods link_to depends on to do its thing. It might be as simple as implementing to_param or something, but it seems like it should be easy enough. You might have to assume more than you want to about the model though (e.g. presence of an id attribute). Either way, something to consider.

Allow means to specify alternate id

Ideally there would be a means to specify the id (such as self.primary_key in AR) as opposed to assuming it will be id. Many legacy systems don't have this as the field.
The current impact is realized when fetching an existing record, applying an update then saving. The record appears to be new because the id field is not present in the data hash.

Please forgive me if there is a preferred approach to detailing the issue or proposing fixes etc. I've not contributed to other Gems before so this is a first for me.

What would you feel about making the following additions to lib/her/model.rb


module Model
    extend ActiveSupport::Concern

    # Instance methods
    include Her::Model::ORM
    include Her::Model::Introspection
    include Her::Model::Paths

    # Class methods
    included do
      extend Her::Model::Base
      extend Her::Model::HTTP
      extend Her::Model::Relationships
      extend Her::Model::Hooks

      # add the following to allow each Her::Model to define it's own primary_key ========================
      cattr_accessor :primary_key
      @@primary_key = :id


      # Define default settings
      base_path = self.name.split("::").last.underscore.pluralize
      collection_path "#{base_path}"
      resource_path "#{base_path}/:id"
      uses_api Her::API.default_api
    end
  end

With this in place, all references to id can be replaced by primary_key.


class Facade::Person
  include Her::Model

  self.primary_key = :person_id

end

ActiveModel Object

Her is really awesome, but it would be even more if it could returning ActiveModel object no?
Maybe with Rails 4? Is it something intentional?

Thanks a lot for your gem!

Relation collection has "belongs_to"-Object not set

Hi

We have the following two models:

class Domain < PluginHelpers::HerModel
  include Her::Model
  uses_api ::PdnsPlugin.api

  has_many :records
end

class Record < PluginHelpers::HerModel
  include Her::Model
  uses_api ::PdnsPlugin.api

  belongs_to :domain

  def name_with_domain
    "name.#{domain.name}"
  end
end

Now if I do the following, there will be a request like Domain.find(1) does for every record, means a lot of overhead for informations which already have been fetched.

domain = Domain.find(1)
domain.records.each do |record|
  puts record.name_with_domain
end

I think the problem is, that record.domain has not been set even though I accessed the Record-object coming from the Domain object.
Do you have any idea how to easily add this already fetched Domain object to the Record object?

NoMethodError: undefined method `bytesize' for {}:Hash on #destroy

When I call #destroy on a Her::Model I get this error (I have try with different middlewares configuration/order, still get it):

NoMethodError: undefined method `bytesize' for {}:Hash
  from /Users/Thibaud/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/net/http.rb:1932:in `send_request_with_body'
  from /Users/Thibaud/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/net/http.rb:1919:in `exec'
  from /Users/Thibaud/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/net/http.rb:1317:in `block in transport_request'
  from /Users/Thibaud/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/net/http.rb:1316:in `catch'
  from /Users/Thibaud/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/net/http.rb:1316:in `transport_request'
  from /Users/Thibaud/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/net/http.rb:1293:in `request'
  from /Users/Thibaud/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/net/http.rb:1286:in `block in request'
  from /Users/Thibaud/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/net/http.rb:745:in `start'
  from /Users/Thibaud/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/net/http.rb:1284:in `request'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/faraday-0.8.4/lib/faraday/adapter/net_http.rb:74:in `perform_request'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/faraday-0.8.4/lib/faraday/adapter/net_http.rb:37:in `call'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/faraday-0.8.4/lib/faraday/response.rb:8:in `call'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/faraday-0.8.4/lib/faraday/connection.rb:226:in `run_request'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/faraday-0.8.4/lib/faraday/connection.rb:87:in `delete'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/her-0.3.8/lib/her/api.rb:71:in `request'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/her-0.3.8/lib/her/model/http.rb:24:in `request'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/her-0.3.8/lib/her/model/orm.rb:173:in `block in destroy'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/her-0.3.8/lib/her/model/hooks.rb:65:in `wrap_in_hooks'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/her-0.3.8/lib/her/model/orm.rb:172:in `destroy'
  from (irb):2
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/railties-3.2.11/lib/rails/commands/console.rb:47:in `start'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/railties-3.2.11/lib/rails/commands/console.rb:8:in `start'
  from /Users/Thibaud/Codes/rails_app/vendor/ruby/1.9.1/gems/railties-3.2.11/lib/rails/commands.rb:41:in `<top (required)>'
  from script/rails:6:in `require'

Access Rails session from Faraday middleware

The README file has the following comment:

Her::API.setup :url => "https://api.example.com" do |builder|
  # This token could be stored in the client session
  builder.use MyAuthentication, :token => "..."
  # ...
end

Can you give me an example of how you could access the current Rails session from that code block? I've put the code in a Rails initialiser file.

Thanks

handle relationships with no data

Some of the data I get back from the server will have nil data. e.g.

class Token
  include Her::Model

  has_one :user
end
{ "id": "...", "user":null, ... }
% rails c          
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > t = Token.get("token", :site_key =>'site')
(Object doesn't support #inspect)
 =>  
1.9.3p194 :003 > t.inspect
NoMethodError: undefined method `include?' for nil:NilClass
    from [...]/gems/her-0.2/lib/her/model/introspection.rb:15:in `inspect'
    from [...]/gems/her-0.2/lib/her/model/introspection.rb:27:in `attribute_for_inspect'
    from [...]/gems/her-0.2/lib/her/model/introspection.rb:16:in `block in inspect'
    from [...]/gems/her-0.2/lib/her/model/introspection.rb:16:in `each'
    from [...]/gems/her-0.2/lib/her/model/introspection.rb:16:in `inject'
    from [...]/gems/her-0.2/lib/her/model/introspection.rb:16:in `inspect'
    from (irb):3
    from [...]/gems/railties-3.2.3/lib/rails/commands/console.rb:47:in `start'
    from [...]/gems/railties-3.2.3/lib/rails/commands/console.rb:8:in `start'
    from [...]/gems/railties-3.2.3/lib/rails/commands.rb:41:in `<top (required)>'
    from script/rails:6:in `require'
    from script/rails:6:in `<main>'

The problem is that it tries to set t.user to a new instance of User, but passes nil to the constructor. Introspection#inspect then raises NoMethodError when accessing @data.inspect? because @data is nil.

I have a simple fix which I can give you a pull request for:

diff --git a/lib/her/model/relationships.rb b/lib/her/model/relationships.rb
index 5f98cbc..0489b65 100644
--- a/lib/her/model/relationships.rb
+++ b/lib/her/model/relationships.rb
@@ -16,7 +16,7 @@ module Her
           relationships.each do |relationship|
             name = relationship[:name]
             class_name = relationship[:class_name]
-            next unless data.include?(name)
+            next if data[name].blank?
             data[name] = case type
               when :has_many
                 Her::Model::ORM.initialize_collection(class_name, data[name])

With that patch applied the inspect method works fine again, and t.user returns nil.

However, this breaks a lot of tests which expect a null instance of a relationship class to be instantiated even if there is no data. Let me know your thoughts.

Thanks for the library though, it's going to be extremely useful!

Testing relationships?

How do you guys go about testing Her relationships?

Is there a better way than this? Or maybe you don't test them?

it "belongs to a Some" do
  some_relationship = { 
    class_name: "Some", 
    name: :some, 
    foreign_key: "some_id", 
    path: "/somes/:id" 
  }
  described_class.relationships.fetch(:belongs_to).should include(some_relationship)
end

Thanks.

failed to parse root in json

Hi all,
I'd like to user Her to consume Rails based API. My application returns a json like

{
   "videos": [{id: 0, url: "http"}, .... ]
}

When in send a get request to /videos.json

but model like

class Video
   include Her::Model
   parse_root_in_json true
end

fails with can't convert Symbol int Integer.

Custom primary keys broke inheritance

class ApiBase
  include Her::Model
end

class User < ApiBase
  collection_path '/api/users'
end

We have models defined like this. In the ApiBase we have custom functionality that we want to provide to each model. Previously this worked fine, but the changes in a961557 broke this. Now when I try and get a User it makes the following request:

=> User.find(1)
GET /api/users/:

The issue seems to be that the primary key is set on the base class, and lost when it is inherited. We can avoid it by changing the base class to be like this, but it feels a bit nasty:

class ApiBase
  def self.inherited(base)
    base.send :include, Her::Model
  end
end

We haven't got around to it yet, but we have a few models that we need to consume using STI so I'm not sure how well that is going to behave.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.