Using ActiveResource to consume web-services

Posted by Jamis Monday, September 03, 2007 04:26:00 GMT

Today I’m reviewing Joe Van Dyk’s monkeycharger application, which is a web-service for storing and charging credit cards. I loved looking at this app, because its only interface is a RESTful web service: there is no HTML involved. (If you’ve never written an app that only exposes a web-service UI, you ought to. It’s a blast.)

In general, Joe has done a fantastic job with keeping the controllers slim and moving logic to models. The only significant gripe I had with the application is that it is not ActiveResource compatible.

For those of you that are late to the party, ActiveResource is the newest addition to the Rails family. It lets you declare and consume web-services using an ActiveRecord-like interface…BUT. It is opinionated software, just like the rest of Rails, and makes certain assumptions about the web-services being consumed.

  1. The service must understand Rails-style REST URLs. (e.g. “POST /credit_cards.xml” to create a credit card, etc.)
  2. The service must respond with a single XML-serialized object (Rails-style).
  3. The service must make appropriate use of HTTP status codes (404 if the requested record cannot be found, 422 if any validations fail, etc.).

It’s really not much to ask, and working with ActiveResource (or “ares” as we affectively call it) is a real joy.

However, monkeycharger tends to do things like the following:

1
2
3
4
5
6
7
8
9
10
class AuthorizationsController < ApplicationController
  def create
    @credit_card   = Authorizer.prepare_credit_card_for_authorization(params)
    transaction_id = Authorizer::authorize!(:amount => params[:amount], :credit_card => @credit_card)
    response.headers['X-AuthorizationSuccess'] = true
    render :text => transaction_id
  rescue AuthorizationError => e
    render :text => e.message
  end
end

Three things: the request is not representing an “authorization” object, the response is not XML, and errors are not employing HTTP status codes to indicate failure.

Fortunately, this is all really, really easy to fix. First, you need (for this specific example) an Authorization model (to encapsulate both the the XML serialization and the actual authorization).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Authorization
  attr_reader :attributes

  def initialize(attributes)
    @attributes = attributes
  end

  def credit_card
    @credit_card ||= Authorizer.prepare_credit_card_for_authorization(attributes)
  end

  def authorize!
    @transaction_id = Authorizer.authorize!(:amount => attributes[:amount],
      :credit_card => credit_card)
  end

  def to_xml
    { :transaction_id => @transaction_id }.to_xml(:root => "authorization")
  end
end

Then, we rework the AuthorizationsController to use the model:

1
2
3
4
5
6
7
8
class AuthorizationsController < ApplicationController
  def create
    authorization = Authorization.new(params[:authorization])
    authorization.authorize!
    render :xml => authorization.to_xml, :status => :created
  rescue AuthorizationError => e
    render :xml => "<errors><error>#{e.message}</error></errors>", :status => :unprocessable_entity
  end

(Note the use of the “created” status, which is HTTP status code 201. Other verbs just use “ok”, status code 200, to indicate success. Also, with an error, we return an “unprocessable_entity” status, which is HTTP status code 422. ActiveResource will treat that as a failed validation.)

With that change, you could now use ActiveResource to authorize a credit card transaction:

1
2
3
4
5
6
7
8
9
10
11
12
class Authorization < ActiveResource::Base
  self.site = "http://my.monkeycharger.site"
end

auth = Authorization.new(:amount => 15, :credit_card_id => 1234,
  :remote_key => remote_key_for_card)

if auth.save
  puts "success: #{auth.transaction_id}"
else
  puts "error: #{auth.errors.full_messages.to_sentence}"
end

It should be mentioned, too, that making an app ActiveResource-compatible does nothing to harm compatibility with non-ActiveResource clients. Everything is XML, both ways, with HTTP status codes being used to report whether a request succeeded or not. Win-win!

Obviously, real, working code trumps theoretical whiteboard sketches every time, and Joe is to be congratulated on what’s done. Even though ActiveResource-compatibility can buy you a lot, you should always evaluate whether you really need it and implement accordingly.

Comments

Leave a response

  1. FooSeptember 03, 2007 @ 06:52 AM

    So It’s a blast to write webservices nowadays but where is the effort to make it a blast to do blazing frontends nowadays? Answer: Nowhere. Everyone is busy writing services. For example walmart, which were thinking about writing just a backend and establish some app on facebook because nobody would stickaround at walmarts site anyway.

  2. Luigi MontanezSeptember 03, 2007 @ 04:50 PM

    Thanks for this great write-up Jamis.

    @Foo, you may want to check out Dave Thomas’ post:

    http://pragdave.pragprog.com/pragdave/2007/03/the_radar_archi.html

    Inspired by this, I’ve started to implement a CMS with ActiveResource capability built-in. I’m calling it Resource and Content Management, or RACM (pronounced rack ‘em). It’s your standard CMS, but instead of extending it with Plugins (like Wordpress/Mephisto) or Modules (like Drupal), an admin can simply specify a URL for a Resource, give it a name, and use it in her templates. That’s the idea anyway, as I’m developing it right now.

  3. Chuck VoseSeptember 03, 2007 @ 10:22 PM

    @Luigi, thanks for the link. Dave Thomas really hits the mark sometimes.

    @Foo, I think that we’re going to start seeing front-end come to more prominence very soon. There are two reasons for the separation though: Building a really nice service makes external developers more likely to use it (free development), and testing a service in isolation is a hell of a lot easier than when combined with the front end.

    My point being that once we have a lot of really well tested, easy to use services floating around, developers that could never quite grok that side but can grok the design side will start showing up.

    Already I think we’re seeing this in the Prototype/Scriptaculous arena and the AIR/Gears arena. On the one hand P/S is starting to make html fun again, on the other AIR/Gears is trying to just throw out the whole browser thing and just have happy programs hitting happy services.

    Where there’s a frustration there’s soon to be a solution. As long as you’re having the same frustrations as a lot of other people :)

  4. Joe Van DykSeptember 04, 2007 @ 05:06 PM

    Thanks for the write-up! I’ll work on making those changes—they make sense to me.

    One question: how do people test ActiveResource clients / services locally? Do you have something like this in config/environments/test.rb and config/environments/development.rb:

    class Authorization < ActiveResource::Base self.site = “http://localhost:4000” end

    And then set the site variable to the real one in production.rb?

  5. JamisSeptember 04, 2007 @ 06:15 PM

    @Joe, In dev mode, yeah, I’ll set up a local version running on some port and hit it that way. In test mode, I usually just mock it all up, and script fake responses to requests.

  6. MorganSeptember 04, 2007 @ 07:52 PM

    Greetings, Interesting write-up. I am just finishing up writing a webservice internally at my work, and I made heavy use of http status codes, and had a real issue with ‘422’. It’s a WebDAV code, not a standard code, and so I didn’t want to use it for error returns. I use 400 instead, in general, and most error returns do NOT return XML.

    I’m curious what you think should be returned for errors? Sometimes there is nothing to tell, and the status code says it all (404 especially). Sometimes there are errors (usually validation or illegal combinations). It’s not clear whether to return XML, or let the status code do the talking. So far I’ve opted for the latter, providing errors when it’s reasonable, but no consumer yet relies on the format of error-returns.

    So. Do you really think 422 is the right code to use for data errors?

    Is XML-returned data expected even when an error status is returned?

    I’m not coding for ActiveResource mind you, as (afaik) it’s only edge rails, and this is production code, but I’d love to be able to transition to it in the future.

    — Morgan

  7. Joe Van DykSeptember 04, 2007 @ 10:25 PM

    I’ve active-resourced monkeycharger. Check it: http://monkeycharger.googlecode.com/svn/trunk/app/controllers/authorizations_controller.rb

  8. JamisSeptember 09, 2007 @ 01:24 AM

    @Morgan, is the “real issue” you had with the 422 code simply that you didn’t want to use it, since it originated with WebDAV? I do really think it is the right code to use for data errors, since no other HTTP status code specifically covers that case.

    And in the case of validation errors, how do you report which attributes failed validation if you don’t return data with the error status?

  9. Daniel ChoiSeptember 10, 2007 @ 03:15 PM

    Can you POST and PUT binary data (e.g. images or swf files) with ActiveResource? Could you give an example? Would much appreciate it.

  10. TedSeptember 13, 2007 @ 01:22 AM

    Thanks for the great writeup.

    Do you know when the ActiveResource development is going to settle down and the Rails version number will be bumped up? It still seems to be in a bit of flux…

  11. CarolineSeptember 13, 2007 @ 07:38 AM

    Hey there,

    This is Caroline from SocialRank.

    I am trying to get in touch with you but couldn’t find your email address.

    We’re launching a new Web 2.0 site dedicated to Ruby On Rails and we have started indexing your blog posts as part of our

    content filter.

    I’d like to send you an invite to a beta preview. Can you get back to me with your email address.

    Mine is caroline@rubygalore.com

    Kind regards,

    Caroline

    www.SocialRank.com