labs

AwesomeResource

I’ve had it. I’ve had the misfortune to need ActiveResource (an http client library for giving you an ActiveRecord-like API for interacting with restful services) off and on for several years now. Even when it’s worked, it’s never worked well.

Let’s start with the way you configure it. You know how you can use a database.yml file to specify your ActiveRecord connections for different environments? Wouldn’t you expect ActiveResource to work similarly? I mean, it seems unlikely that you’d actually connect to the exact same server endpoint in test, development, and production, right? Too bad. ActiveResource gives you a single way to set a model’s “site”: an attr_writer on the model’s singleton:


class MyModel < ActiveResource::Base
  self.site = ‘http://some-server.com’
end

That global state isn’t a good sign. Even after you hack in your own environment-specific connection code, do you think your model will be thread-safe? Hell no they won’t. If you try to use your models in a threaded environment (e.g., in threaded background worker systems like Sidekiq), you’ll eventually run into a race condition on the model’s singleton “connection” attribute. And your code will raise an exception. Fun.

Let’s talk about JSON. Everyone loves JSON, right? ActiveResource is an old library; when it was originally written, XML was in vogue. The ActiveResource XML support is very mature. It’s JSON support? Broken. That’s right, it’s broken. Has been for years. Sending nested attributes over JSON does the wrong thing. There’s a fix that was merged in a year ago that will be released with Rails 4. In the meantime you can use my “activeresource_json_patch” gem.

Let’s look at the ActiveResource code. It’s a great example of Stunt Programming. Once, when attempting to determine how I might monkey-patch ActiveResource to allow me to set a lambda as the “site”, I stumbled into this method:


      def prefix_source
        prefix
        prefix_source
      end

Um, what? How could that possibly work? It’s INFINITELY RECURSIVE ISN’T IT? Let’s look in the prefix method:


      def prefix(options={})
        default = site.path
        default << '/' unless default[-1..-1] == '/'
        # generate the actual method based on the current site path
        self.prefix = default
        prefix(options)
      end

Wait, the prefix method calls itself too! It’s also INFINITELY RECURSIVE TOO!!! What’s going on? Let’s look one level deeper: the `prefix=` method:


def prefix=(value = '/')
  # snip...
  silence_warnings do
    # Redefine the new methods.
    instance_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
      def prefix_source() "#{value}" end
      def prefix(options={}) "#{prefix_call}" end
    RUBY_EVAL
  end

rescue Exception => e
  logger.error "Couldn't set prefix: #{e}n  #{code}" if logger
  raise
end

And there you have it. The prefix= method redefines the prefix_source and prefix methods. Thereby avoiding the infinite recursion. FACEPALM

All right, enough complaining. Taken at face value, ActiveResource isn’t actually all that bad. If your needs are incredibly simple, it will likely do the job. And I’ve actually tried to improve the ActiveResource ecosystem over the years. I released a gem that dealt with the nested-attributes-over-JSON bug during the interim until the bug fix is released. I created another gem that made environment-specific site configurations possible. But in the end, I’ve just had it. The code’s a mess. The library is half-forgotten. It’s time for a reboot.

While I was at home recovering from the latest round of the never-ending biological warfare called being a parent, I started a new project on Github: AwesomeResource (http://github.com/moonmaster9000/awesome_resource). Some goals:

  • ActiveRecord-like API
  • Thread-safety
  • Environment-specific configuration
  • Dynamic site, proxy, and password configuration (instead of forcing them to be statically configured in the code)
  • First-class JSON support
  • An integration test suite that would verify that something as critical as JSON support will actually work against a live Rails app
  • A spec that exposes the JSON format the AwesomeResource expects from a server

I’ve got a single feature for SomeModel.create passing. It’s a start.