Writing a Ruby Client for a RESTful Web Service using Bricklayer

So. I spent this evening tuning what was formerly AutoApi to get it ready for release. The result is Bricklayer, available for your consumption on GitHub.

The premise is this: you're writing or reading about (insert web 2.0 service here)'s new developer API, and you want to either test it, or quickly consume it without waiting for someone to write an official client for it. Huzzah! Bricklayer to the rescue. In just a few lines, you can be pulling the info you need.

So let's step through this. First, yoink the latest source from the GitHub project, and do a rake install. We're ready to roll.

The Venerable Twitter

I'm going to be using Twitter as a sample service. Naturally, there are many Ruby-based Twitter clients to choose from, but since their service goes a long way in providing a plethora of options, I decided it's a good place to cut our teeth.

First, let's look at the API documentation. I'm going to stick to the statuses portion of the API, but there's a ton there to implement if you want the practice.

You get the magic of Bricklayer by extending it, so let's start there.

                    require 'bricklayer'
                  
                    class TwitterBrick < Bricklayer::Base
                    end
                  

ZOMGZ WOW! You now have a Ruby client for Twitter! It doesn't do anything just yet, but you can just... feel the power hiding under the hood.

public_timeline is listed first. Let's begin there. I see the URL is http://twitter.com/statuses/public_timeline.format, where format is a token representing the response type I want back. Let's plop that in TwitterBrick.

                    require 'bricklayer'
                  
                    class TwitterBrick < Bricklayer::Base
                      service_url "http://twitter.com/statuses/public_timeline.{format}"
                    end
                  

A service_url is just what it sounds like, the URL needed to access a service. You must define one of these before defining any remote methods, which we'll do next.

                    require 'bricklayer'
                  
                    class TwitterBrick < Bricklayer::Base
                      service_url "http://twitter.com/statuses/public_timeline.{format}"
                      remote_method :public_timeline
                    end
                  

We can now do something like this:

                    require 'pp'
                    client = TwitterBrick.new
                    pp client.public_timeline(:format => "xml")
                  

If you ran that, you'd see a massive stream of XML scroll by. Pretty neat! But it seems like it would be a bit of a pain to specify :format every time you called it. Let's specify a sensible default. I rather like JSON.

                  require 'bricklayer'
                  
                  class TwitterBrick < Bricklayer::Base
                    service_url "http://twitter.com/statuses/public_timeline.{format}"
                    remote_method :public_timeline, :default_parameters => {:format => "json"}
                  end
                  
                  require 'pp'
                  client = TwitterBrick.new
                  pp client.public_timeline
                  

With that, you have a public_timeline method that returns a string of JSON (more on that in a bit). Let's try our next method, friends_timeline.

                    require 'bricklayer'
                  
                    class TwitterBrick < Bricklayer::Base
                      service_url "http://twitter.com/statuses/public_timeline.{format}"
                      remote_method :public_timeline, :default_parameters => {:format => "json"}
                  
                      service_url "http://twitter.com/statuses/friends_timeline.{format}"
                      remote_method :friends_timeline, :default_parameters => {:format => "json"}
                  
                    end
                  

Technically, this would work, but there's a bit of repetition going on here. We don't like that. First of all, we see that the two service URLs differ only slightly. We can do this:

                    require 'bricklayer'
                  
                    class TwitterBrick < Bricklayer::Base
                      service_url "http://twitter.com/statuses/{action}.{format}"
                      remote_method :public_timeline, :default_parameters => {:format => "json", :action => "public_timeline"}
                      remote_method :friends_timeline, :default_parameters => {:format => "json", :action => "friends_timeline"}
                    end
                  

The first service_url call will continue to apply until a new one is defined, which means here it will be used for both methods. {action} will be replaced with either public_timeline or friends_timeline depending on the method called.

One minor hitch. friends_timeline requires authentication. Let's do that.

                    require 'bricklayer'
                  
                    class TwitterBrick < Bricklayer::Base
                      authenticate YOUR_USERNAME, YOUR_PASSWORD
                      service_url "http://twitter.com/statuses/{action}.{format}"
                      remote_method :public_timeline, :default_parameters => {:format => "json", :action => "public_timeline"}
                      remote_method :friends_timeline, :default_parameters => {:format => "json", :action => "friends_timeline"}
                    end
                  

Replace YOUR_USERNAME and YOUR_PASSWORD with strings containing your username and password. These are used to authenticate your request using Basic HTTP Authentication. Still, there is a bit of repetition there. Bricklayer has a concept of "cascading parameters" (to a certain degree), so we can simplify this as:

                    require 'bricklayer'
                  
                    class TwitterBrick < Bricklayer::Base
                      authenticate YOUR_USERNAME, YOUR_PASSWORD
                      service_url "http://twitter.com/statuses/{action}.{format}", :default_parameters => {:format => "json"}
                      remote_method :public_timeline, :default_parameters => {:action => "public_timeline"}
                      remote_method :friends_timeline, :default_parameters => {:action => "friends_timeline"}
                    end
                  

You can now call client.public_timeline and client.friends_timeline and get what you'd expect. But here's something interesting... what if you called client.public_timeline(:action => "friends_timeline")? Well, just as you'd expect, it would return the results for friends_timeline. We can prevent this type of behavior using an :override_parameters option:

                    require 'bricklayer'
                  
                    class TwitterBrick < Bricklayer::Base
                      authenticate YOUR_USERNAME, YOUR_PASSWORD
                      service_url "http://twitter.com/statuses/{action}.{format}", :default_parameters => {:format => "json"}
                      remote_method :public_timeline, :override_parameters => {:action => "public_timeline"}
                      remote_method :friends_timeline, :override_parameters => {:action => "friends_timeline"}
                    end
                  

Now, whatever you specify during client.public_timeline(:action => "whateveryouwant"), it will be replaced with "public_timeline" before the call is made.

So far, we have an object that has two methods for getting back Twitter data as a JSON string, but that's not really useful. Let's add some response processing!

                    require 'bricklayer'
                    require 'json'
                  
                    class TwitterBrick < Bricklayer::Base
                      authenticate YOUR_USERNAME, YOUR_PASSWORD
                      service_url "http://twitter.com/statuses/{action}.{format}", :default_parameters => {:format => "json"}
                      remote_method :public_timeline, :override_parameters => {:action => "public_timeline"} do |json_response|
                        JSON.parse(json_response)
                      end
                      remote_method :friends_timeline, :override_parameters => {:action => "friends_timeline"} do |json_response|
                        JSON.parse(json_response)
                      end
                    end
                  

Now, calling either of these methods would return the deserialized JSON string, i.e., native Ruby objects (arrays and hashes). Again, using a bit of cascading goodness, we can do:

                    require 'bricklayer'
                    require 'json'
                  
                    class TwitterBrick < Bricklayer::Base
                      authenticate YOUR_USERNAME, YOUR_PASSWORD
                      service_url "http://twitter.com/statuses/{action}.json" do |json_response|
                        JSON.parse(json_response)
                      end
                      remote_method :public_timeline, :override_parameters => {:action => "public_timeline"} 
                      remote_method :friends_timeline, :override_parameters => {:action => "friends_timeline"} 
                    end
                  

Since this service URL now expects the response type to be JSON, I've removed the {format} token and replaced it with the static "json".

Oofta. Quite a few changes there. The next one is simple:

                    require 'bricklayer'
                    require 'json'
                  
                    class TwitterBrick < Bricklayer::Base
                      authenticate YOUR_USERNAME, YOUR_PASSWORD
                      service_url "http://twitter.com/statuses/{action}.json" do |json_response|
                        JSON.parse(json_response)
                      end
                      remote_method :public_timeline, :override_parameters => {:action => "public_timeline"} 
                      remote_method :friends_timeline, :override_parameters => {:action => "friends_timeline"} 
                      remote_method :user_timeline, :override_parameters => {:action => "user_timeline"}
                    end
                  

The show method has a slightly different URL, so we'll specify that:

                    require 'bricklayer'
                    require 'json'
                  
                    class TwitterBrick < Bricklayer::Base
                      authenticate YOUR_USERNAME, YOUR_PASSWORD
                      service_url "http://twitter.com/statuses/{action}.json" do |json_response|
                        JSON.parse(json_response)
                      end
                      remote_method :public_timeline, :override_parameters => {:action => "public_timeline"} 
                      remote_method :friends_timeline, :override_parameters => {:action => "friends_timeline"} 
                      remote_method :user_timeline, :override_parameters => {:action => "user_timeline"}
                  
                      service_url "http://twitter.com/statuses/show/{id}.json" do |json_response|
                        JSON.parse(json_response)
                      end
                      remote_method :show
                  
                    end
                  

We can clean up a little repetition by not having to rewrite the JSON parser for each service_url:

                    require 'bricklayer'
                    require 'json'
                  
                    class TwitterBrick < Bricklayer::Base
                      JSON_PARSER = Proc.new{|json_response| JSON.parse(json_response)}
                      authenticate YOUR_USERNAME, YOUR_PASSWORD
                      service_url "http://twitter.com/statuses/{action}.json", &JSON_PARSER
                  
                      remote_method :public_timeline, :override_parameters => {:action => "public_timeline"} 
                      remote_method :friends_timeline, :override_parameters => {:action => "friends_timeline"} 
                      remote_method :user_timeline, :override_parameters => {:action => "user_timeline"}
                  
                      service_url "http://twitter.com/statuses/show/{id}.json", &JSON_PARSER
                      remote_method :show
                  
                    end
                  

Lookin' good! But what happens when someone calls client.show without specifying :id => [some_note_id]? Well, it'll fail on the request end. Let's tell the user that TwitterBrick#show requires the :id param:

                    class TwitterBrick < Bricklayer::Base
                      JSON_PARSER = Proc.new{|json_response| JSON.parse(json_response)}
                      authenticate YOUR_USERNAME, YOUR_PASSWORD
                      service_url "http://twitter.com/statuses/{action}.json", &JSON_PARSER
                  
                      remote_method :public_timeline, :override_parameters => {:action => "public_timeline"} 
                      remote_method :friends_timeline, :override_parameters => {:action => "friends_timeline"} 
                      remote_method :user_timeline, :override_parameters => {:action => "user_timeline"}
                  
                      service_url "http://twitter.com/statuses/show/{id}.json", &JSON_PARSER
                      remote_method :show, :required_parameters => [:id]
                  
                    end
                  

Now, calling it without :id will raise an ArgumentError. So sweet and juicy!

The next method, update, uses the same service URL as the first three, so we'll plop it up there. There's one caveat, however. This method requires you send the data as a POST request. Additionally, this requires a :status parameter. Easy enough:

                    require 'bricklayer'
                    require 'json'
                  
                    class TwitterBrick < Bricklayer::Base
                      JSON_PARSER = Proc.new{|json_response| JSON.parse(json_response)}
                      authenticate YOUR_USERNAME, YOUR_PASSWORD
                      service_url "http://twitter.com/statuses/{action}.json", &JSON_PARSER
                  
                      remote_method :public_timeline, :override_parameters => {:action => "public_timeline"} 
                      remote_method :friends_timeline, :override_parameters => {:action => "friends_timeline"} 
                      remote_method :user_timeline, :override_parameters => {:action => "user_timeline"}
                      remote_method :update, :override_parameters => {:action => "update"}, :request_method => :post,
                        :required_parameters => [:status]
                  
                      service_url "http://twitter.com/statuses/show/{id}.json", &JSON_PARSER
                      remote_method :show, :required_parameters => [:id]
                  
                    end
                  

You can now call client.update(:status => "Your status message"). How exciting. And the last two!

                    require 'bricklayer'
                    require 'json'
                  
                    class TwitterBrick < Bricklayer::Base
                      JSON_PARSER = Proc.new{|json_response| JSON.parse(json_response)}
                      authenticate YOUR_USERNAME, YOUR_PASSWORD
                      service_url "http://twitter.com/statuses/{action}.json", &JSON_PARSER
                  
                      remote_method :public_timeline, :override_parameters => {:action => "public_timeline"} 
                      remote_method :friends_timeline, :override_parameters => {:action => "friends_timeline"} 
                      remote_method :user_timeline, :override_parameters => {:action => "user_timeline"}
                      remote_method :update, :override_parameters => {:action => "update"}, :request_method => :post,
                        :required_parameters => [:status]
                      remote_method :replies, :override_parameters => {:action => "replies"}
                      remote_method :destroy, :override_parameters => {:action => "destroy"}, :request_method => :delete,
                        :required_parameters => [:id]
                  
                  
                      service_url "http://twitter.com/statuses/show/{id}.json", &JSON_PARSER
                      remote_method :show, :required_parameters => [:id]
                  
                    end
                  

And voila! We have just implemented all of the status message methods for the Twitter API. I plan to add namespacing in the near future, so you can group and call methods like client.statuses.public_timeline or perhaps client[:statuses].public_timeline (probably both). Stay tuned!