#95
Mar 02, 2008

More on ActiveResource

See how to handle authentication and custom actions using ActiveResource in this episode.
Download (18.5 MB, 8:15)
alternative download for iPod & Apple TV (12.8 MB, 8:15)

Railscasts is turning 1 year old on March 5th! To celebrate I will be putting on a contest in episode #100. If you are interested in sponsoring or donating prizes to the contest please send an email to ryan(at)railscasts(dot)com.

Resources

# models/product.rb (in blog app)
class Product < ActiveResource::Base
  self.site = "http://admin:secret@localhost:3000"
end

# script/console (in blog app)
Product.find(:all) # GET products.xml
Product.find(:all, :params => { :search => 'table' }) # GET products.xml?search=table
p = Product.create(:name => 'foo') # POST products.xml
p.name = 'bar'
p.save # PUT products/11.xml
p.destroy # DELETE products/11.xml
Product.get(:recent) # GET products/recent.xml
p = Product.find(1) # GET products/1.xml
p.put(:discontinue) # PUT products/1/discontinue.xml

# routes.rb (in store app)
map.resources :products, :collection => { :recent => :get },
                         :member     => { :discontinue => :put }

# products_controller.rb (in store app)
before_filter :authorize

protected

def authorize
  authenticate_or_request_with_http_basic do |username, password|
    username == "admin" && password == "secret"
  end
end

RSS Feed for Episode Comments 22 comments

1. Nuno Job Mar 03, 2008 at 00:59

Was waiting for this screencast :) I had no more railscast to see and I am almost finishing the design patterns on ruby book :)

ActiveResource seems like a very powerful backend to data models and I think we got a keeper here.

Other frameworks will copy this in no time :)


2. Ryan Townsend Mar 03, 2008 at 02:06

If your storing the user and pass in the site within the model, how would you dynamically change that based upon what user is logged into to the app?

So for example being able to do:

self.site = 'http://' + un + ':' + pw + '@localhost:3000/'


3. nicolash Mar 03, 2008 at 02:22

Thank you for the insights...
If it is not to advanced it would be nice if you show us how to communicate with apis that don't follow the best-practise of REST.
First example that comes into my mind is flickr. The first stumbling block is that the url you need to generate doesn't include the ".xml", or the resource (nor the id where appropriate). Basically what you need is a fixed url with everything! else in the querystring...
I guess you would have to overwrite the url generation for ActiveResource but i don't even know where to start. Beside i guess it is just the beginning of the problems as i guess the next step would be to parse the response xml by hand... Do you intend to go into these kinds of problems or do you consider this as to advanced/specialized problems?
(BTW,Put the champagne into the fridge you deserve to treat yourself and be proud for the work you've done so far for the community.. we are looking forward for an other 1000;-) screencasts once you finalized with the first 100. THANK YOU so far)


4. Arya Mar 03, 2008 at 07:49

@Ryan Townsend

The best way I can think of doing that is having a method that simply redefines the site variable for every username/password. For example in your active resource model, you could have a method:

def assign_to_user(username, password)
self.class.site = "http://#{username}:#{password}..."
end

and something similar as a class method too. As of now, when site is assigned, it immediately sends it to the URI class to parse which unfortunately means having something like a proc that gets called at the time of the actual request would require quite a few changes.

I hope I was helpful. :)

Thanks so much Ryan Bates! Your screen casts are very helpful, I particularly liked the refactoring screen cast.


5. Ted Mar 03, 2008 at 09:52

Congratz on the year anniversary! Your screencasts trump every Ruby/Rails book I've yet read... and for FREE no less! Thanks!


6. Ryan Bates Mar 03, 2008 at 20:07

@Ryan, Arya's suggestion should work. Alternatively you can try overriding the "site" class method:

def self.site
  "http://#{name}:#{pass}@example.com"
end

I don't know if that is supported, so you'll want to thoroughly test it.

@nicolash, great question! From my understanding ActiveResource isn't really designed to work with other APIs which don't follow the same REST interface. I believe it will take too much customization to the point you lose most of the benefits of ActiveResource. You're probably better off using a plugin designed specifically for that API or writing one yourself. Someone please correct me if I'm wrong.


7. andy Mar 04, 2008 at 06:13

Ryan - how would you use ARes to access a nested resource, like:

/admin/users #namespace route
/users/posts #nested route

Thanks for these screencasts!!!


8. Ryan Bates Mar 04, 2008 at 21:39

@andy, good question. I usually don't use nested resources, but I know many do so I looked into it while planning this episode. Unfortunately I wasn't able to find a solution. There are some examples where you set it in the "site" string as shown here:
http://whynotwiki.com/ActiveResource

But I was not able to get this to work. If anyone knows the solution please reply.


9. Bryce Mar 04, 2008 at 23:27

Great screencasts - could you please do one on making drag and drop widgets?


10. Nicolás Sanguinetti Mar 05, 2008 at 00:04

First of all, happy birthday, I've been watching since day one (actually day 14, since I heard of you in RubyInside which posted the announcement on March 19th :)) and even after two years of using rails I learn a few little things here and there every week -- particularly those handy textmate shortcuts! :D

THANK YOU!

Second, and I'm just nitpicking, so don't mind me ;), the discontinue action should conceptually be a DELETE, not a PUT. By discontinuing the product you're "removing" it from the cloud, even if you aren't deleting it from the database :)

@Ryan Townsend:
In edge rails you don't have to override self.site anymore, you can use self.user and self.password :)

http://blog.codefront.net/2008/02/27/living-on-the-edge-of-rails-9-the-sleeper-edition/


11. Derano Mar 06, 2008 at 06:45

@andy
I think it's only possible to access one nest of resources in one model, i think it could get complicated when you're changing self.site at runtime. Anyway, for myself, I was able to run ActiveResources through nested resources, all I've done was:
class Project < ActiveResource::Base
  self.site = http://localhost:3000/users/:user_id
end
If you like to call a find method you have to do it like this:
Project.find(:all, :params => {:user_id => 5})
For sure your REST Service has to support this kind of request.


12. Albert Mar 06, 2008 at 09:49

Great screencast again! I really like the way you were using script/console that you can see the exact http request on the server side. Can you kindly tell me how to setup it?

Thanks again.


13. Mike Walker Mar 06, 2008 at 10:26

Thanks so much for all these screencasts. I've pretty much downloaded them all now, and refer to them a LOT whenever I forget things. This is an excellent resource!


14. JJ Mar 17, 2008 at 19:07

Ryan, just wondering how you call a method on the model on the remote site? For example, my host has a model called Post. The Post class has some methods called find_all_by_permalink(permalink) and find_all_by_date(year, month, day). If I try to call these methods from the Weblog controller on the client side, I get an error saying the method is undefined. How do I expose this method to my client? Or do I *have* to define a new action on the host side which does the call to Post.find_by_date for me? I've not been able to find anything on this anywhere. Thanks for your awesome casts - I hang out for them each week.


15. Dan Apr 04, 2008 at 16:14

Hey Ryan - love the railscasts - thank you so much- your pod-cast is like a reference guide to me!

+1 votes for drag and drop cast!


16. kino May 03, 2008 at 07:25

Is it possible to block such as the one showing fields?


17. Chris May 14, 2008 at 00:34

hi ryan,

i love your railscasts. i'm sure it helps pushing rails a lot.
my question:
is there a way to get ActiveResource objects when using custom methods like get instead of hash/array of hashes?

big up
chris


18. Matt Jun 05, 2008 at 16:09

I second Chris's question. It seems like if you do MyActiveResource.find(1).get(:custom_method), ActiveResource returns a hash instead of object that you can call with methods.


19. Matt Jun 05, 2008 at 16:39

Update: in the ActiveResource code, all the find methods are calling the private method instantiate_record, which essentially just does a Base.new(hash_returned_from_get). There's also a similar private method instantiate_collection.

Unfortunately, these are private methods - it would be great if they were protected, so we could call them from our subclasses. In the meantime, I just use the dirty dirty workaround:

       def get_record(path)
        MyActiveResource.new(get(path))
      end

Calling it with

MyActiveResource.find(1). get_record(:custom_method)


20. Jon Bracy Jun 29, 2008 at 22:24

@Ryan Townsend
@Ryan Bates
@Arya

I got around the authentication by doing the following:

class Pie < ActiveResource::Base
  self.site = "https://api.pie.com/"
  def self.for_user(user)
    self.site = "https://#{user}@api.pie.com/"
    return self
  end
end

rs = Pie.for_user(1).find(:all)
puts rs

One problem though is that you have to make sure after you use it to reset self.site to the correct url, otherwise there could be a security breach and you could access someone elses stuff elsewhere in your code. Something like this should really get built into ActiveResource


21. Mark Simoneau Jul 17, 2008 at 13:16

Am I off my rocker or is any solution that modifies self.site going to be un-thread-safe?

If you have 3 processes accessing 3 different sites (different authentications) then when you modify self.site for any one of them, you're either going to have to lock out other processes until you can "reset" it or you're going to allow each unique process to use a random authentication willy nilly.


22. Keith Rowell Jul 31, 2008 at 08:17

@John Bracy

I guess you could use a block to ensure the self.site thing gets set back to what it was before, without you having to remember to reset it.

something like:

class Pie < ActiveResource::Base
  self.site = "https://api.pie.com/"
  def self.for_user(user)
    old_site = self.site
    self.site = "https://#{user}@api.pie.com/"
    yield self
    self.site = old_site
  end
end

Pie.for_user(1) do |pie|
  rs = pie.find(:all)
  puts rs
end

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player