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
[...] 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. [...]