O'Reilly    
 Published on O'Reilly (http://oreilly.com/)
 See this if you're having trouble printing code examples


Shoes Meets Merb: Driving a GUI App through Web Services in Ruby

by Gregory Brown and Brad Ediger
01/08/2008

Ruby on Rails has helped launch the Ruby programming language into stardom, and for good reason. Rails opened many eyes to the power of Ruby and made web programming that much easier. But one of the unfortunate aspects of Rails is that it tends to color Ruby as a language primarily for database-backed web applications. Some software just doesn't work well in that mold. Additionally, the extreme popularity of Rails has left some Rubyists in the corner wondering what happened to the other great software written in their language. It hasn't gone away; on the contrary, there are a tremendous number of open-source Ruby projects under development. We are going to look at two of them here.

The Merb web framework, written by Ezra Zygmuntowicz, was first popularized as a lightweight way to handle file uploads for Rails applications. It has since grown to become an excellent framework in its own right for creating web applications. It is simpler and seems to be faster than Rails, and it is more flexible in some ways. While Rails is deliberately "opinionated software," Merb acknowledges that there are different options for object-relational mapping systems and web template engines, and does not try to pick one over the other.

If Merb is a paragon of professionalism and class, Shoes is a monkey on LSD. Shoes, by why the lucky stiff, is an incredibly compact cross-platform GUI toolkit for Ruby, but it looks nothing like the other cross-platform toolkits out there. For one thing, it is lightweight. Shoes lets you build GUIs in Ruby whose code actually looks like Ruby, not XML or Java. Shoes is under heavy development right now, but it will eventually form the basis for the new Hackety Hack, _why's programming environment for kids.

So, what are a web framework and a GUI framework doing together, you might ask? We are going to build a pastebin as a repository for our own code snippets and pieces of text we want to save. We'll build a GUI frontend using Shoes, and connect it to a Merb backend that will handle the database. We could just as easily slap on a web interface to the Merb application as well, but we will use the Shoes GUI to demonstrate the ease with which we can connect the two components using Ruby. In fact, the basic proof of concept took the two of us about an hour to get working, and it took another hour to finish.

Without further ado, we present our pastebin application, using Shoes and Merb, Shmerboes.

Creating a Simple YAML-Based Web Service with Merb

Though a lot can be said about Merb being a potential Rails-killer, we're not going to attempt to be so dramatic here. Instead, we'll let Merb speak for itself as we create the server side component of Shmerboes.

Configuring Our Merb Application

We'll start by creating our application skeleton and putting together a simple model that has the bare minimum fields we'll need.

$ merb merb_paste
      create  
      create  app/controllers
      create  app/models
      create  app/helpers
      # ...
      create  script/stop_merb
      create  script/generate
      create  script/destroy

Before we can generate our models, we need to specify the ORM we wish to use. For familiarity and simplicity, we're going to use ActiveRecord, so we need to uncomment the following line in config/dependencies.rb:

  use_orm :activerecord

If you don't already have it installed, you'll need the Merb ActiveRecord adapter, which you can grab via:

  gem install merb_activerecord -y

You may have noticed when you looked in the dependencies file that Merb offers you some other ORM choices, in fact, Sequel and DataMapper are supported out of the box, so long as you install the appropriate adapters. If you're tired of the "Railsy" ORM feel, you might give one of these a try, but for now we'll stick with the conventional approach.

Once we have our ORM specified, we can set up our database configuration. By running the merb executable, a database.sample.yml file will be created. This serves as a template for your for your database.yml configuration. To keep things nice and easy configuration-wise, we skip over the default MySQL configuration and use SQLite:

  ---
  :development: &defaults
    :adapter: sqlite3
    :database: db/merb_paste_development.sqlite3

  :test:
    <<: *defaults
    :database: db/merb_paste_test.sqlite3

  :production:
    <<: *defaults
    :database: db/merb_paste_production.sqlite3

The YAML here is actually pretty tidy, using the settings for the development environment as defaults and just overriding as needed for the other environments.

Because Merb doesn't autogenerate a db folder for us, we'll need to create one manually:

  $ mkdir db

A Simple Paste Model

Once we have all our configuration stuff set up, we can generate our model files:

  $ script/generate model paste
  ** Ruby version is not up-to-date; loading cgi_multipart_eof_fix
  Started merb_init.rb ...
  Connecting to database...
  Mon, 07 Jan 2008 01:45:20 GMT: loading gem 'merb_activerecord' from config/dependencies.rb:16 ...
  Loading Application...
  Compiling routes..
  Loaded DEVELOPMENT Environment...
        exists  app/models
        create  app/models/paste.rb
    dependency  merb_model_test
        exists    spec/models
        create    spec/models/paste_spec.rb
    dependency  migration
        create    schema/migrations
        create    schema/migrations/001_add_model_pastes.rb

You'll notice that, like Rails, Merb generates test files for your models. However, the defaults are a bit different. Merb will use RSpec by default instead of Test::Unit and it also does not automatically generate fixtures. You can of course tweak these defaults if you'd like, but avoiding fixtures and using RSpec are two things that'd generally make a lot of Rubyists happy, so they tend to be decent defaults. At any rate, we won't be focusing on testing in this article, so we leave further tweaking with this as an exercise to the reader.

Now that our Paste model is generated, we need to populate our migration with the appropriate field definitions. As you can see above, this file was generated for us as schema/migrations/001_add_model_pastes.rb. Below is the migration we're actually using:

  class AddModelPastes < ActiveRecord::Migration
    def self.up
      create_table :pastes do |t|
        t.string :title   
        t.string :text
        t.timestamps
      end    
    end

    def self.down
      drop_table :pastes
    end
  end

Luckily, pastebins are incredibly simple at their core, and our migration reflects this. We can now run the migration and check to see that everything is wired up correctly.

  $ rake db:migrate
  (in /Users/sandal/merb_paste)
  ** Ruby version is not up-to-date; loading cgi_multipart_eof_fix
  Connecting to database...
  Mon, 07 Jan 2008 02:09:30 GMT: loading gem 'merb_activerecord' from config/dependencies.rb:16 ...
  == 1 AddModelPastes: migrating ================================================
  -- create_table(:pastes)
     -> 0.0027s
  == 1 AddModelPastes: migrated (0.0030s) =======================================

With the migrations running fine, we'll just do a quick sanity check in the Merb console:

 $ merb -i
  ** Ruby version is not up-to-date; loading cgi_multipart_eof_fix
  # startup info truncated
  >> Paste
  => Paste(id: integer, title: string, text: string, created_at: datetime, updated_at: datetime)

Great! This tells us we can access our model and that it is wired up to the database. We can now begin on the real work, which is implementing a controller that exposes a simple web API that our shoes client will interact with.

Serving Up YAML

If we were building a traditional web application, we might start thinking about creating some pages to implement the CRUD functionality for pastes. However, since Shmerboes will have a GUI frontend, we get to take some shortcuts here.

Web services use several different interchange formats, the most common of course being XML. Though it would be quite easy to serve XML through Merb, we decided to use YAML as our means of communicating between the client and server.

As a quick refresher, or a glimpse for those not familiar with YAML, take a look at this simple example:

 
  >> require "yaml" 
  => true
  >> puts [{:a => "foo", :b => "bar"}, {:a => "baz", :b => "quux"}].to_yaml
  --- 
  - :a: foo
    :b: bar
  - :a: baz
    :b: quux
  => nil
  >> YAML.load([{:a => "foo", :b => "bar"}, {:a => "baz", :b => "quux"}].to_yaml)
  => [{:a=>"foo", :b=>"bar"}, {:a=>"baz", :b=>"quux"}]

We can easily envision our index for pastes looking something like this:

  [ { :id => 1, :title => "Test Paste 1" },
    { :id => 2, :title => "Test Paste 2" } ]

Though slightly simplified, the above is sufficient for creating links to individual paste records, and is quite close to what we actually use in Shmerboes.

Now that we've talked a bit about the way our service will work, let's look at the whole controller to get a bird's-eye view, and then walk through how to create it step by step:

  class Pastes < Application
    provides :yaml 

    before :retrieve_paste, :only => [:show,:update,:destroy]

    def index      
      @pastes = Paste.find(:all)
      render
    end

    def show
      render 
    end     

    def create               
      p = Paste.create(:title => params[:title], :text => params[:text])  
      redirect url(:paste, p)
    end    

    def update
      @paste.update_attributes(:title => params[:title], :text => params[:text])
      redirect url(:paste, @paste)
    end   

    def destroy
      @paste.destroy
      redirect url(:pastes) 
    end   

    protected                     

    def retrieve_paste
      @paste = Paste.find(params[:id]) 
    end

  end

As you can see, the implementation is quite lean. Though you don't need to know exactly how it works after just looking at it, you might already have a good sense of what's going on here, especially if you've done RESTful development in Rails before.

Of course, this was actually written in tight little iterations, not as one big lump of code, so let's go back through it in smaller chunks.

First, we need to actually generate the controller file:

  script/generate controller pastes

You'll notice Merb maps the name you give its generator directly to the class name of the controller. This means it's up to you to make sure your controller names don't clash with your models. Luckily, Merb gracefully catches these issues:

  $ script/generate controller paste
  ** Ruby version is not up-to-date; loading cgi_multipart_eof_fix
  # ...
  The name 'Paste' is reserved.
  Please choose an alternative and run this generator again.

As long as you're careful, this shouldn't be an issue, but may be something to get used to. Nevertheless, with our controller in place, we can begin working on it. A good start would be to let it know we want to work with YAML:

  class Pastes < Application
    provides :yaml 
  end

Merb is quite good at understanding different content types. We can quickly hack together something that works right away.

  class Pastes < Application
    provides :yaml  

    def index      
      @pastes = Paste.find(:all)
      render @pastes
    end
  end

If we point our browser at http://localhost:4000/pastes/index.yaml, we'll get a downloadable YAML file, which will look like this:

  --- []

Since an empty array isn't particularly interesting, let's populate the database with a few pastes and try again.

  $ merb -i 
  >> Paste.create(:title => "Paste 1", :text => "A first paste")
  >> Paste.create(:title => "Paste 2", :text => "A Second paste")

When we download the file this time, you can see it's full of more useful information:

  --- 
  - !ruby/object:Paste 
    attributes: 
      updated_at: 2008-01-06 23:34:49
      title: Paste 1
      text: A first paste
      id: "1" 
      created_at: 2008-01-06 23:34:49
    attributes_cache: {}

  - !ruby/object:Paste 
    attributes: 
      updated_at: 2008-01-06 23:35:01
      title: Paste 2
      text: A Second paste
      id: "2" 
      created_at: 2008-01-06 23:35:01
    attributes_cache: {}

This shows us that we can easily get at the data we're interested in. Still, although this is neat as a default behaviour, there is a whole lot of cruft in this YAML output we don't care much about.

The reason for this is because Merb is simply calling #to_yaml on the results of our ActiveRecord#find(). Since this is the output produced by ActiveRecord, it's not particularly general, and probably not quite what we need.

Though it means rolling up our sleeves, we can easily create our own custom ERB template that'll render something closer to what we want. We'll place it in app/views/pastes/index.yaml.erb:

---      
<% if @pastes.blank? %> 
 []
<% else %>
  <% @pastes.each do |p| %> 
  - :title: <%= p.title %>
    :created_at: <%= p.created_at %>
    :id: <%= p.id %>
  <% end %>   
<% end %>

Without going into too much detail, we need to create a default layout for YAML files. For our purposes, this is simply boilerplate. Here's our app/layouts/application.yaml.erb:

  <%= catch_content :layout %>

We also need to make a minor tweak to the controller to make sure it uses the view directly:

  def index      
    @pastes = Paste.find(:all)
    render
  end

Calling render() with no options does the trick, and we get a much cleaner YAML file:

  
---      
  - :title: Paste 1
    :created_at: Sun Jan 06 23:34:49 EST 2008
    :id: 1
  - :title: Paste 2
    :created_at: Sun Jan 06 23:35:01 EST 2008
    :id: 2

Let's take a look at this YAML string loaded into Ruby:

  >> YAML.load("---      
    - :title: Paste 1
      :created_at: Sun Jan 06 23:34:49 EST 2008
      :id: 1
    - :title: Paste 2
      :created_at: Sun Jan 06 23:35:01 EST 2008
      :id: 2  ")
  => [{:title=>"Paste 1", :created_at=>"Sun Jan 06 23:34:49 EST 2008", :id=>1}, 
      {:title=>"Paste 2", :created_at=>"Sun Jan 06 23:35:01 EST 2008", :id=>2}]

At this point, our index is complete. We can always add additional fields later when things get more complex, but this is more than enough to pass along to Shoes and create a meaningful paste listing from.

With the index in place, we'll want to add a way to get the details for a single paste. It turns out this isn't any more challenging than the index:

  class Pastes < Application
    provides :yaml 

    before :retrieve_paste, :only => [:show]

    # ...

    def show
      render 
    end

    protected                     

    def retrieve_paste
      @paste = Paste.find(params[:id]) 
    end

  end

The above should look quite familiar to anyone who's worked with Rails before. We are using a before filter to retrieve the paste record from the database for a given id. This means that when we point the browser at: http://localhost:4000/pastes/2.yaml, it will generate the YAML output that describes our paste with :id => 2. The benefit of using a before_filter is mostly that it saves typing for common operations.

With this simple controller action, we have a matching view at app/views/show.yaml.erb:

  ---
  :title: <%= @paste.title %>
  :created_at: <%= @paste.created_at %>
  :id: <%= @paste.id %>     
  :text: <%= @paste.text %>

Finally, to get all this RESTful stuff, we need to add a line to config/router.rb :

  r.resources :pastes

Now, let's check the output for http://localhost:4000/pastes/2.yaml:

---
:title: Paste 2
:created_at: Sun Jan 06 23:35:01 EST 2008
:id: 2     
:text: A Second paste

Believe it or not, for a simple paste server, that's all the output we'll need. We simply need to allow creating, updating, and deleting pastes, and we'll have a fully functional service.

Fleshing Out the Rest of Our API

The rest of our work is vanilla ActiveRecord stuff. We want to be able to create, update, and delete pastes in addition to viewing them. The following three controller methods allow for that:

               
  before :retrieve_paste, :only => [:show, :update, :destroy]

  # ...

  def create               
    p = Paste.create(:title => params[:title], :text => params[:text])  
    redirect url(:paste, p)
  end    

  def update
    @paste.update_attributes(:title => params[:title], :text => params[:text])
    redirect url(:paste, @paste)
  end   

  def destroy
    @paste.destroy
    redirect url(:pastes) 
  end

In both the create and update methods, we return the results of show() for the Paste object. The url() method generates the proper location for us to redirect to, which will be something like /pastes/2.

For the earlier bits of code, we were able to download the YAML files from the browser and inspect their output. Here, we're dealing with something a little more complicated, as data is actually being posted to the server. Though we could certainly verify that this code actually works through formal tests, instant gratification comes through curl.

If you haven't heard of curl before, it's an excellent tool that should be in every web developer's toolkit.

In the quick session below, we verify that given correct data, all of the features we've implemented so far work correctly.

         
  # See the index
  $ curl -H "Accept: text/yaml"  http://localhost:4000/pastes
  ---      
    - :title: Paste 1
      :created_at: Sun Jan 06 23:34:49 EST 2008
      :id: 1
    - :title: Paste 2
      :created_at: Sun Jan 06 23:35:01 EST 2008
      :id: 2

  # List details for Paste 2 
  $ curl -H "Accept: text/yaml"  http://localhost:4000/pastes/2
  ---
  :title: Paste 2
  :created_at: Sun Jan 06 23:35:01 EST 2008
  :id: 2     
  :text: A Second paste     

  # Delete paste 2 (redirects to index)

  $ curl -H "Accept: text/yaml" -L -d "_method=delete" http://localhost:4000/pastes/2 
  ---      
    - :title: Paste 1
      :created_at: Sun Jan 06 23:34:49 EST 2008
      :id: 1

  # Add a new paste

  $ curl -H "Accept: text/yaml" -L -d "title=Paste3&text=A new paste" http://localhost:4000/pastes   
  ---
  :title: Paste3
  :created_at: Mon Jan 07 00:45:58 EST 2008
  :id: 3     
  :text: A new paste

  # Edit a paste

  $ curl -H "Accept: text/yaml" -L -d "_method=put&title=The First Paste&text=booga booga" http://localhost:4000/pastes/1
  ---
  :title: The First Paste
  :created_at: Sun Jan 06 23:34:49 EST 2008
  :id: 1     
  :text: booga booga   

  # See the index again

  $ curl -H "Accept: text/yaml"  http://localhost:4000/pastes
  ---      
    - :title: The First Paste
      :created_at: Sun Jan 06 23:34:49 EST 2008
      :id: 1
    - :title: Paste3
      :created_at: Mon Jan 07 00:45:58 EST 2008
      :id: 3

From this, you can see that we can easily create, retrieve, update, and delete pastes, so our work is essentially done here. Let's take a look at some of the interesting bits of what's going on here before we move on though.

Take a look at the first example:

   $ curl -H "Accept: text/yaml"  http://localhost:4000/pastes

Notice here that we didn't specify pastes/index.yaml, but that the server fed us back YAML anyway. That's because of our earlier definition:

  provides :yaml

This allows you to either use file extensions or the Accept header to specify which format you're interested in. In the case of clients to web services, the Accept header is almost certainly more attractive, and you'll see it used by our Shoes client in just a moment.

The other interesting bit of this curl session was that we were able to use the same trick that Merb and Rails use under the hood for faking out PUT and DELETE requests. For our update, we used _method=put and for our destroy, we used _method=delete. This allows us to use a normal POST request to still take advantage of RESTful routing. Since many apps you'll build with Merb will tend to follow this pattern, it's worth making a note of.

From here, we've got everything we need to build a client side interface to Shmerboes. Our server is admittedly a little fragile, but it implements a clean, platform agnostic Web API that can easily interact with anything that speaks HTTP and YAML. Let's now take a look at how to bridge Shoes to Merb, and learn a bit more about the fun and quirky GUI framework along the way.

Creating a User Interface with Shoes

"Shoes" is a GUI toolkit modeled after the Web, with some additional widgets inspired by native UI toolkits. It is designed to be simple, intuitive, and clean; many cross-platform interface toolkits try to do so many things that they are difficult to use. Shoes has its limitations, but it is extremely fun to hack around with. It will serve us well for our pastebin application.

Running Shoes

Shoes can be obtained from the Download Shoes page on the Shoes wiki. There are binary builds available for Windows and OS X, both with and without the heavy video libraries (yes, Shoes has a video widget!). All other platforms need to compile Shoes from source, which may be a tedious process, depending on how many of its dependencies (in particular, Cairo and Pango) are already installed.

OS X users should avoid revision 371, which, as this is being written, is the latest version available for download. That revision has several severe bugs on OS X and is unusable. Revision 327 works with the code in this article; older versions can be downloaded from _why's download page.

Shoes can be run from the command line, taking as an argument the Ruby file to run. If the filename is not provided, Shoes will pop up a dialog prompting for a file to run. Under OS X, the full path to the Shoes binary inside the application bundle is required:

  $ /Applications/Shoes.app/Contents/MacOS/shoes /path/to/shoes_application.rb

You can add that directory to your PATH to be able to run shoes directly from the command line.

Let's try a sample application; this one comes from the Shoes homepage. It is nearly the simplest Shoes application possible, and is self-explanatory:

  Shoes.app {
    button("Press Me") { alert("You pressed me") }
  }

Save that code as button.rb. We can try to run this directly from our current directory using Shoes:

  $ shoes button.rb

However, this doesn't get us very far:

It seems that Shoes is looking in its own directory for button.rb. There is a simple solution, to fully qualify the path that we pass to shoes:

  $ shoes `pwd`/button.rb

This has the expected result:

Attaching Shoes to the Web

We are going to connect the Shoes user interface to the Merb backend using HTTP. To make this easier, we create a helper module, HttpToYaml, that will function as a super-simple generic web service wrapper. We can include this module into our Shoes application, and it provides four Ruby methods corresponding to the four primary HTTP methods (get, post, put, and delete). It requests YAML data using a custom Accept header, and decodes the response using YAML.load.

We could have avoided the need to write this wrapper by looking for a third-party library that accomplishes these functions for us. However, Shoes doesn't make it terribly easy to structure large applications or package up third-party libraries. Shoes loads the application's code by reading its source file and evaling it in the context of Shoes, so it's not really possible to reference files using paths relative to the source file. With all that in mind, we opted to write our own interface.

Here is the full code for the web service wrapper. Later, we will examine it in detail.

  require "uri" 
  require "net/http" 

  module HttpToYaml
    class TooManyRedirects < StandardError; end

    BACKEND = 'http://localhost:4000'

    protected

    def get(uri, redirection_limit = 10)
      uri = URI.parse("#{BACKEND}#{uri}")
      response = Net::HTTP.start(uri.host, uri.port) do |http|
        http.get(uri.path, {'Accept' => 'text/yaml'})
      end
      handle_or_decode_response(response, :redirection_limit => redirection_limit)
    end

    def post(uri, params = {})
      uri = URI.parse("#{BACKEND}#{uri}")
      response = Net::HTTP.start(uri.host, uri.port) do |http|
        request = Net::HTTP::Post.new(uri.path)
        request['Accept'] = 'text/yaml'
        request.set_form_data(params)
        http.request(request)
      end
      handle_or_decode_response(response)
    end

    def put(uri, params = {})
      post uri, params.merge(:_method => 'put')    
    end

    def delete(uri, params = {})
      post uri, params.merge(:_method => 'delete')
    end

    def handle_or_decode_response(response, options = {})
      case response
      when Net::HTTPSuccess
        YAML.load(response.body)
      when Net::HTTPRedirection
        limit = options[:redirection_limit] || 10
        raise TooManyRedirects if limit == 1
        get response['Location'], limit - 1
      end
    end
  end

This is not terribly fun code, but it serves our purpose of wrapping up Net::HTTP's ugly bits into a higher-level interface, somewhat reminiscent of integration test scripts in Rails. Let's take a look at it, piece by piece. First off, we need to pull in the URI and Net::HTTP libraries from Ruby's standard library.

  require "uri" 
  require "net/http"

Net::HTTP provides our basic HTTP interface, and the URI library breaks down URI strings (such as http://example.com:8080/index.rdf) into their constituent parts (scheme, host, port, and path).

The most basic HTTP method is GET, which we expose through our module's get method. Here it is again, along with some of its supporting code:

  module HttpToYaml
    class TooManyRedirects < StandardError; end

    BACKEND = 'http://localhost:4000'

    def get(uri, redirection_limit = 10)
      uri = URI.parse("#{BACKEND}#{uri}")
      response = Net::HTTP.start(uri.host, uri.port) do |http|
        http.get(uri.path, {'Accept' => 'text/yaml'})
      end
      handle_or_decode_response(response, :redirection_limit => redirection_limit)
    end

    def handle_or_decode_response(response, options = {})
      case response
      when Net::HTTPSuccess
        YAML.load(response.body)
      when Net::HTTPRedirection
        limit = options[:redirection_limit] || 10
        raise TooManyRedirects if limit == 1
        get response['Location'], limit - 1
      end
    end
  end

Note the BACKEND constant, which is the base URI of our server. Since we are speaking plain HTTP between the client and server, we could separate the client and server on a network or even the Internet. All we would need to do is make the Merb server accessible and specify its URI in the BACKEND constant.

When we perform the request, we need to pass a custom Accept header of text/yaml. This tells Merb, or any other server from which we request data, that the only response format we understand is YAML. Merb will use this information to decide among different representations of the same content. For example, we could augment the Merb application to have a Web interface as well, and it could coexist on the exact same URIs, differentiated only by the version the client requests in the Accept header.

Much of the complexity in these methods comes from our need to gracefully follow HTTP redirects; Net::HTTP makes us decide for ourselves how we want to handle a redirection response. When a paste is created, the Merb server will send a 302 redirection to the paste's URI. For our application, we want to automatically follow this redirect and return the body of the second response. The handle_or_decode_response method fills this need. If it encounters a redirection, it will issue another GET to the URI stored in the current response's Location header. Otherwise, on a successful request, the method will pipe the response body through YAML.load and return the resulting data structure.

We do need to take some care to ensure that we do not follow an infinite loop of redirects, and this is what the redirection_limit option and TooManyRedirects exception are for. On each redirect, we decrement the redirection_limit counter (which is essentially a time-to-live variable); if it would reach zero, we break the redirection loop by raising a TooManyRedirects exception.

The post method is similar to get, but it takes a second parameter: a hash of form parameters to be used as the POST data. These will be unescaped on the server side and will come out looking pretty much the same as the Merb params hash. This interface constrains us to posting form-like data, but it will be good enough for our purposes. Again, we request YAML data and expect YAML back. Here is the code:

  def post(uri, params = {})
    uri = URI.parse("#{BACKEND}#{uri}")
    response = Net::HTTP.start(uri.host, uri.port) do |http|
      request = Net::HTTP::Post.new(uri.path)
      request['Accept'] = 'text/yaml'
      request.set_form_data(params)
      http.request(request)
    end
    handle_or_decode_response(response)
  end

One thing that may not be immediately obvious from this code is that any redirection that a POST request experiences is followed using GET, not POST. This is technically incorrect behavior according to RFC 2616, but it is widespread enough that we will not worry about it (indeed, the RFC specifically mentions that most clients do exactly as we do here, and the server should use 303 or 307 response codes where there could be ambiguity about how to follow such a redirect).

The other two HTTP methods, PUT and DELETE, are very useful in conjunction with Merb's resource-based routing. Unfortunately, they are not as well known on the Web, and their use is not always robust. Some Web proxies do not understand PUT and DELETE. The only methods allowed on HTML forms are GET and POST, which has made the other HTTP verbs second-class citizens on the public Internet.

To ensure that these methods will still work even in the face of intermediaries that do not understand them, we will fake them by using the POST method with a special _method parameter, which is set to the actual method we are emulating. Merb understands this convention and treats these requests just as if they had been issued using the correct method.

  def put(uri, params = {})
    post uri, params.merge(:_method => 'put')    
  end

  def delete(uri, params = {})
    post uri, params.merge(:_method => 'delete')
  end

These two methods are short stubs that simply delegate to the post method, adding a _method parameter specifying the real HTTP method.

Testing the Web Service Bridge

We can fire up irb to test this interface. Just make sure the Merb server is running in the background by changing to its directory and running merb. Then we can play around with the web service client and test it interactively:

  >> require 'http_to_yaml' # wherever the HttpToYaml code is located
  >> include HttpToYaml
  => Object
  >> get '/pastes'
  => [{:created_at=>"Sun Jan 06 14:01:27 -0600 2008", :id=>1, :title=>"Foo"}]
  >> get '/pastes/1'
  => {:created_at=>"Sun Jan 06 14:01:27 -0600 2008", :text=>"the sweetest bar that ever barred", :id=>1, :title=>"Foo"}

Our library appears to be functional now, and we can see the exact data structures that will be returned when we use those method calls in the Shoes application.

Building a Shoes Frontend

Now we can get down to business and build the user interface for our pastebin. Documentation for Shoes is scant, but the best reference is the book Nobody Knows Shoes, by why the lucky stiff himself. The official API reference is available online as The Entirety of the Shoes Family.

The Shoes portion of the code is fairly simple, and should be easy to understand if you have experience programming for the Web, even if you have not used Shoes before. We will present it in its entirety, and then dissect and explain it. Because Shoes evals the code it is provided with, all of the code for a Shoes application should be in one file. Therefore, this code should follow the HttpToYaml code that we showed earlier:

  class ShoesPaste < Shoes
    include HttpToYaml

    url '/', :index
    url '/new', :new_paste
    url '/(\d+)', :show_paste

    def index
      pastes = get "/pastes" 

      stack :margin => 20 do    
        title "Shoes Pastebin" 

        pastes.sort_by{|p| p[:created_at]}.each do |paste|
          para link(paste[:title], :click => "/#{paste[:id]}")
        end

        para link('New paste', :click => "/new")
      end
    end

    def show_paste(id)
      paste = get "/pastes/#{id}" 
      stack :margin => 20 do
        title paste[:title]
        para paste[:text], :font => 'Monospace 12px'
        button "delete" do
          if confirm("Are you sure you want to delete this paste?")
            delete "/pastes/#{paste[:id]}" 
            visit "/" 
          end
        end
        view_all_link
      end
    end

    def new_paste
      stack :margin => 20 do
        title "New paste" 
        flow(:margin_top => 20) { caption "Title: "; @title = edit_line }
        @text = edit_box :margin_top => 20, :width => 400, :height => 200

        button("Paste!", :margin => 5) do
          paste = post '/pastes', :title => @title.text, :text => @text.text
          visit "/#{paste[:id]}" 
        end

        view_all_link
      end
    end

    protected

    def view_all_link
      para(link('View all pastes', :click => '/'), :margin_top => 20)
    end

  end

  Shoes.app :title => 'Shoes Pastebin', :width => 640, :height => 400

We inherit from Shoes to provide a clean environment in which to contain our application code. It is possible to contain the entire application in a block passed to the Shoes.app method, but it gets messy with larger applications. Our method allows us to keep our application code contained in one class.

After the initial include statement that pulls in our web service bridge, we set up the URLs for our application using the Shoes.url class method. This is a potential source of confusion because these URLs are completely different from those exposed by our Merb application. Because Shoes is modeled after the Web, it uses pseudo-URLs to reference different sections within an application.

  url '/', :index
  url '/new', :new_paste
  url '/(\d+)', :show_paste

Each url statement maps a regular expression to a method that is executed when a URL matching that regexp is "visited." (URLs can be triggered with a :click action on some elements, or with the visit method, as we will see later.) Any captures in the regular expression (such as (\d+)) will be passed as arguments to the method named.

We have three types of pages: the index, which shows a list of pastes, the "new paste" screen, and the screen to show an existing paste. Let's look at the index first, as it will introduce us to several aspects of the Shoes API.

  def index
    pastes = get "/pastes" 

    stack :margin => 20 do    
      title "Shoes Pastebin" 

      pastes.sort_by{|p| p[:created_at]}.each do |paste|
        para link(paste[:title], :click => "/#{paste[:id]}")
      end

      para link('New paste', :click => "/new")
    end
  end

The first line of the index method retrieves all of the current pastes from the /pastes URL on the server. As we saw before when experimenting with irb, this will be an array of hashes, with each hash containing attributes of one paste.

The stack method is unique to Shoes. For layout, Shoes uses the concept of stacks and flows, which are two types of boxes. Either type of box contains a series of elements. Stacks order their contents from top to bottom, while flows organize them from left to right (wrapping lines if necessary). We will use both stacks and flows to lay out different parts of our application. This method call also illustrates that most methods accept a hash of styles to fine-tune their positioning and look.

Within the stack, we first have a title, which is a simple text heading. Rather than HTML's unimaginative h1 through h6, Shoes uses creative heading names: banner, title, subtitle, tagline, caption, and inscription.

Next in the stack, we sort the pastes by their creation time, and create a paragraph (para) containing a link to that paste's show_paste method. (The "/#{paste[:id]}" URL will match the '/(\d+)' regexp.) Finally, we close out the stack by linking to the "new paste" URL.

When we run this with shoes /path/to/shoes_paste.rb, we see the following:

Now we probably want to be able to see some of the pasted code. So we will look at the show_paste function that lets us get at the pastes:

  def show_paste(id)
    paste = get "/pastes/#{id}" 
    stack :margin => 20 do
      title paste[:title]
      para paste[:text], :font => 'Monospace 12px'
      button "delete" do
        if confirm("Are you sure you want to delete this paste?")
          delete "/pastes/#{paste[:id]}" 
          visit "/" 
        end
      end
      view_all_link
    end
  end

  protected

  def view_all_link
    para(link('View all pastes', :click => '/'), :margin_top => 20)
  end

Because show_paste's mountpoint (its URL regular expression) has one capture, this function takes one argument: the ID of the paste. A URL of /123 will translate into a call to show_paste(123). Once we have that ID, we interpolate it into an actual URL on the Merb server, and retrieve the paste. The web service bridge does the translation from YAML to a hash, so we can work directly with the attributes of the paste.

We set up another stack for layout, and title the page based on the title of the paste. Below that comes the actual text of the paste, with a monospace font specified. Next comes a delete button, with a confirmation behind it. This will pop up a yes/no alert as a safeguard against accidental deletion. If the user agrees to continue with the deletion, we send a DELETE request to the paste's URL, and the Merb server deletes its record. We then "visit" the / pseudo-URL, which sends us back to the list of pastes.

At the bottom of the page is a "View all pastes" link that we can use to return to the list of pastes. We reuse this on the New Paste page, so we have factored it into a separate method.

In our running Shoes application, we can see the result of this page by clicking on a paste title from the main list.

The last page to show is the New Paste page, generated by the new_paste method:

  def new_paste
    stack :margin => 20 do
      title "New paste" 
      flow(:margin_top => 20) { caption "Title: "; @title = edit_line }
      @text = edit_box :margin_top => 20, :width => 400, :height => 200

      button("Paste!", :margin => 5) do
        paste = post '/pastes', :title => @title.text, :text => @text.text
        visit "/#{paste[:id]}" 
      end

      view_all_link
    end
  end

By now, most of this should be familiar. Here we see a flow used for the first time to flow elements horizontally; we use this to put the caption for the "Title" box to the left of the box itself.

We assign the edit_line and edit_box (similar to HTML's <input type="text"> and <textarea> controls, respectively) to instance variables. This allows us to read their values later, when the "Paste!" button is clicked. Many elements can be updated by holding references to them like this. (Sadly, button text cannot be updated in this manner; this prevented us from writing the tic-tac-toe game that we wanted to.)

When the Paste button is clicked, we send a POST request to the server, passing the paste's information as parameters. The server sends back a data structure, and we use that to build a Shoes pseudo-URL (which corresponds to the newly created paste's show page). Here is what the completed New Paste page looks like:

Upon submitting this paste, we are redirected to the resulting "show" page:

Conclusion

By walking through both the Merb and Shoes side of Shmerboes, you've seen a whole lot of Ruby code. The beauty of this application is not that it solves a particularly difficult or exciting problem, but that it shows something a little bit different than your average web application.

Merb is already showing lots of strengths in unexpected places, and though Rails may still be king for your more conventional applications, the flexibility and power of Merb can really come in handy for jobs that have custom needs. The fact that it is lightweight and fast is quite a nice bonus as well.

Shoes seems to show a ton of promise for Ruby's GUI future. Right now, it is admittedly a little too rough around the edges in terms of stability to feel safe with. Admittedly, like all things _why produces, the creativity and the bit of 'danger' surrounding the framework are exciting, and can even be useful for certain things. A year from now, Shoes will hopefully be as fun as it is now to work with, but polished enough to do stuff that smells a bit like work in.

What we hope you've gained from this article is a basic understanding of how Ruby can be used to interoperate between different environments, and that the possibility of a GUI frontend to a web service backend is possible writing nothing but Ruby code. If you'd like to poke around a bit more and pick up where we left off, the Shmerboes code is available under the public domain. Please enjoy, and happy hacking!

Gregory Brown is a New Haven, CT based Rubyist who spends most of his time on free software projects in Ruby. He is the original author of Ruby Reports.

Brad Ediger is a freelance programmer, specializing in Rails, who has used the framework since its release in 2004. He and his wife Kristen (a web designer) own Madriska Media Group, a web development firm.


Return to O'Reilly Ruby.

Copyright © 2009 O'Reilly Media, Inc.