advertisement

Print

Shoes Meets Merb: Driving a GUI App through Web Services in Ruby
Pages: 1, 2, 3, 4, 5, 6

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.

Pages: 1, 2, 3, 4, 5, 6

Next Pagearrow