Subscribe to RSS

Writing A Web Application In Merb Part II

More Thoughts on Our Design

As good as it is that we’ve started to play around with the model behind our web application, I feel I was flailing a little bit too much in the original article—jumping all over the place from idea to implementation. Reading over it again we get a good idea of what we want the application to do, but it doesn’t translate clearly into how we’re going to accomplish it. So let’s take a step back.

Our application will—for the moment—consist of two pages. The landing page will provide the choices for a user to enter in their own Hacker Key. This could be done with a series of drop down menus and checkbox / radio lists, like so:

Software Hacking Category

[ 9 - I'm Bill Joy, Eric Raymond or JWZ. ]   [ ] C++  [ ] Perl [X] Python ...   
| 8 - I am an uberhacker; I wrote my ... |   [ ] Java [X] Ruby [ ] Prolog ...   

( ) I do this activity for a living.
(*) I don't know much about this activity.

A design that contains all this information up front probably means that it could be pulled from a database without much trouble. But how are we going to go about doing that? Very carefully.

Our Models

We want our design to be robust and last for everything we want it to do. The model we created before, HackerKeyCategory, is a ‘pure’ representation of the Hacker Key Categories - that is, what the user will select from when they first create their Key. The fields above will, in fact, consist of three separate models: A Category, that category’s Ratings, and that category’s Suffixes. This will lead to a table design in our database that looks something like this:

HackerKeyCategory

ID Name Abbreviation
1 Software Hacking sw
2 Hardware Hacking hw

HackerKeyRating

ID category_id Rating Description
1 1 9 I’m Bill Joy, Eric Raymond or JWZ.

HackerKeySuffix

ID category_id Abbreviation Description
1 1 C C++
2 1 R Ruby

Everything nice and ordered. This, however, means that the code we wrote before, however awesome it is, is going to have to be refactored. This is hardly a bad thing, especially now that we have a direction to take it. All that model needs to do now is accept a valid Name and Abbreviation. This is cake. Test first! :-)

spec/models/hacker_key_category_spec.rb

require File.join( File.dirname(__FILE__), "..", "spec_helper" )
 
describe HackerKeyCategory do
 
  it "should accept a name and an abbreviation" do
    sw = HackerKeyCategory.new
    sw.attributes = { :name => 'Software Hacking', :abbrev => 'sw' }
    lambda { sw.save }.call.should be_true
    sw.name.should == 'Software Hacking'
    sw.abbrev.should == 'sw'
  end
 
  it "should not accept a null name and abbreviation" do
    xx = HackerKeyCategory.new
    xx.attributes = { :abbrev => nil, :name => nil }
    lambda { xx.save }.call.should_not be_true
    xx.errors.errors.should == { :name => ["Name must not be blank"], :abbrev => ["Abbrev must not be blank"] }
  end
 
end

Since we know these attributes are going to go straight into the database, we need to tell our migrator about them, instead of creating them as variables of the class. Since we’re using DataMapper, we don’t have to leave our model file to do it—they get defined right there.

app/models/hacker_key_category.rb

class HackerKeyCategory < DataMapper::Base
 
  property :name,   :string, :nullable => false
  property :abbrev, :string, :nullable => false
 
  has_many :ratings,  :class => 'HackerKeyRating', :foreign_key => 'category_id'
  has_many :suffixes, :class => 'HackerKeySuffix', :foreign_key => 'category_id'
 
end

Completing The Model Logic

For now, on to the other parts of our logic.

$ script/generate model HackerKeyRating
$ script/generate model HackerKeySuffix

app/models/hacker_key_rating.rb

class HackerKeyRating < DataMapper::Base
 
  property :rating,      :integer
  property :description, :string
 
  belongs_to :category, :class => 'HackerKeyCategory', :foreign_key => 'category_id'
 
end

app/models/hacker_key_suffix.rb

class HackerKeySuffix < DataMapper::Base
 
  property :abbrev,      :string
  property :description, :string
 
  belongs_to :category, :class => 'HackerKeyCategory', :foreign_key => 'category_id'
 
end

Pretty cut-and-dry. You’ll notice we have the option to override what the foreign key constraint is called, since category_id is a lot cleaner than hacker_key_category_id.

Now, all of our preliminary model logic is defined. This is a good segue into the next part of our application: the view and controllers. It also gives me a chance to highlight a useful feature of Merb and other Railikes: custom rake tasks.

Custom Rake Tasks

It’s good that we’re on the right track, but a question has risen: Since this models are the ‘pure’ versions of our Hacker Key information, how are we going to get that information into them? We could use a fixture like apparatus, but we can keep it much, much simpler, and use DataMapper to do the (well, most of the) work for us. Merb looks for custom rake tasks in lib/tasks/*. This is where we’ll create a task to prepare the databases for us.

namespace :data do
  desc 'Fill the database tables with Hacker Key specifications.'
  task :prep do
 
    raise "unknown environment" unless %w{ development test production }.include? Merb.environment
 
    Dir["#{Merb.root}/app/models/*"].each { |m| require m }
 
    HackerKeyCategory.delete_all
    HackerKeyRating.delete_all
    HackerKeySuffix.delete_all
 
    # Categories
    {
      'Software Hacking' => 'sw',
      'Hardware Hacking' => 'hw'
      # ...
    }.each do | name, abbrev |
      c = HackerKeyCategory.new :name => name, :abbrev => abbrev
      c.save
    end
 
    # Ratings
    {
      'sw' => [
        nil,
        "I'm a manager and/or work at IBM",
        "I'm not even a programmer, much less a hacker!",
        "I don't hack software at all. I'm a structured programmer!",
        # ...
        ]
 
      # ...
 
    }.each do | category, ratings |
      ratings.each_index do | rating |
        c = HackerKeyCategory.first(:abbrev => category)
        c.ratings.create :rating => rating, :description => ratings[rating]
      end
    end
 
    # Suffixes
    {
      'sw' => {
        'C(++)'            => 'C',
        '(Visual) Basic'   => 'B',
        'Lisp'             => 'L'
        # ...
      }
    }.each do | category, suffixes |
      suffixes.each do | description, abbrev |
        c = HackerKeyCategory.first(:abbrev => category)
        c.suffixes.create :description => description, :abbrev => abbrev
      end
    end
 
  end
end

And we run our rake task like so, substituting XXX for one of the environments: development, test, or production.

$ MERB_ENV=XXX rake data:prep

Is this code testable? Perhaps :-) I’ll leave it up to you to decide how you might test our first rake task.

We’ve covered a lot of ground now, and we haven’t even seen a single page of our web application! Next time I’ll improve our method of prefilling our Hacker Key information into the database, and we’ll start taking a look at what our landing page is going to look like.

One Trackback

  1. By Ardekantur / A Redaction of Sorts on March 25, 2008 at 9:32 pm

    [...] A while ago I wrote that Merb reads in custom rake tasks in lib/tasks/*.rake. This doesn’t appear to be the case anymore, at least not the last few times I tried. The latest commits of Merb core have a Rakefile with the friendly reminder to put custom rake tasks at the bottom of it. I’m not sure if this was in the interests of lightness, feature creep reduction, or what. Additionally, feel free to correct me if I’m doing it wrong or forgetting some setting or something. Post a comment — Trackback URI RSS 2.0 feed for these comments This entry (permalink) was posted on Tuesday, March 25, 2008, at 9:32 pm by Ardekantur. Filed in Merb and tagged custom rake tasks, Merb, rake. [...]

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*