Can Her support the concept of scoped API objects? #25

Closed
smarterclayton opened this Issue Aug 15, 2012 · 10 comments

Comments

Projects
None yet
5 participants

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?

Collaborator

pencil commented Feb 19, 2013

Ran into a similar problem when we wanted to use the same model with two different endpoints (depending on a user selection). Did some nasty things with class unloading to make this work. It would be nice to have an official solution.

Owner

remiprev commented Feb 20, 2013

Did some nasty things with class unloading to make this work.

Can you share this particular solution? I just want to be sure of what you’re talking about.

Collaborator

pencil commented Feb 21, 2013

I have something like this in my Rails controller:

before_filter :initialize_foo_models

# ...

# FooPlugin.setup does the Her::API.new and .setup(...) things
def initialize_foo_models
  if foo?
    FooPlugin.setup configs[:foo]
    reload_foo_model_classes
  elsif bar?
    FooPlugin.setup configs[:bar]
    reload_foo_model_classes
  end
end

def reload_foo_model_classes
  foo_lib_path = Gem::Specification.find_by_name('foo_plugin').gem_dir + '/lib/foo_plugin'
  [:User, :Address].each do |class_name|
    if FooPlugin.const_defined?(class_name)
      FooPlugin.send(:remove_const, class_name)
      load "#{foo_lib_path}/#{class_name.downcase}.rb"
    end
  end
end
Owner

remiprev commented Apr 22, 2013

I’m gonna close this issue now for lack of activity. If you guys want to submit a pull request for this feature, you’re more than welcome!

@remiprev remiprev closed this Apr 22, 2013

Contributor

alfonsocora commented Aug 7, 2013

I'm in the need of something like this now. I would like to be able to do:

admin_api = Her::API.new do |c|
  c.use FaradayMiddleware::OAuth2, @admin_token
end

user_api = Her::API.new do |c|
  c.use FaradayMiddleware::OAuth2, @user_token
end

User.scoped_api(admin_api).all.first  # uses the admin token
User.scoped_api(user_api).all.first  # uses the user token

# it should also support relationships:
User.scoped_api(user_api).find(1).profiles.first  # /profiles should be retrieved using the scoped api

I've been trying to make this change in Her but apparently this might require a major refactor that separates the models from the apis. Are there any plans to add this feature?

Contributor

alfonsocora commented Aug 7, 2013

Another option might be to introduce the idea of a Connection, untying the Faraday options from the Api object. A Connection could then be specified on the same way as my previous comment. Note that this will require an important refactor as well.

Collaborator

pencil commented Aug 7, 2013

As a workaround you could use the newly introduced Proc support of use_api (#166)

Contributor

alfonsocora commented Aug 13, 2013

@pencil thanks! That helps, although the app I'm dealing with makes the two types of requests (admin and current user) and I can't find a way to send that information down to the proc :\

Collaborator

pencil commented Aug 17, 2013

@alfonsocora You would want to have some kind of class attribute on the model or a global variable. In your Proc you would check this to decide what API to use. It's an ugly workaround, but it could work 😁

Contributor

m3talsmith commented Aug 18, 2013

That's almost the precise example for using a proc - the need to
dynamically choose an API for use_api.
On Aug 17, 2013 2:28 AM, "Nils Caspar" notifications@github.com wrote:

@alfonsocora https://github.com/alfonsocora You would want to have some
kind of class attribute on the model or a global variable. In your Proc you
would check this to decide what API to use. It's an ugly workaround, but it
could work [image: 😁]


Reply to this email directly or view it on GitHubhttps://github.com/remiprev/her/issues/25#issuecomment-22808610
.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment