What's new in Rails? Lots and lots. Over the next week, peek into Edge Rails and look forward as Rails 3 chugs to release.
One of the reasons I enjoy working with Rails is its dynamism. Everything about the platform bristles with energy: The community is helpful, charitable, and gregarious; the Ruby language is chock-full of tricks; and the tools and libraries available to developers seemingly leap forward every day. Indeed, it takes little time before some laborious task is encapsulated in a handy gem or reduced to shorthand in the Rails core. For example, I wrote previously about named scopes, application templates, and enhanced finders, all incremental improvements to technique, but nonetheless helpful.
Recently, another batch of valuable and time-saving enhancements was added to “Edge,” the leading-edge version of Rails that’s something of a proving ground for new features, and some merit a sneak peek and early adoption. Let’s quickly set up an Edge Rails environment and give the new bells and whistles a go for the rest of the week. Today, let’s look at conveniences for validation and the integration of a state machine into ActiveRecord.
Walking on the Edge
Edge Rails is surprisingly easy to establish in its own sandbox, tucked safely away from your other projects. The whole process requires three commands:
$ rails playground
$ cd playground
$ rake rails:freeze:edge
cd vendor
Downloading Rails from http://dev.rubyonrails.org/archives/rails_edge.zip
Unpacking Rails
rm -rf rails
rm -f rails.zip
rm -f rails/Rakefile
rm -f rails/cleanlogs.sh
rm -f rails/pushgems.rb
rm -f rails/release.rb
touch rails/REVISION_ef935240582ef6a7d47a9716e8269db817c91503
cd -
Updating current scripts, javascripts, and configuration settings
Assuming you have a recent version of Rails on your machine (the system used for this article was Mac OS X Leopard and Rails 2.3.3), the commands shown create a new Rails application, place a standalone copy of Edge in vendor/rails, and update the application accordingly, leaving the application, (here, playground) based on Edge. Since Edge is a moving target, you can update simply by re-running the latter rake command.
Better Validations
Validations are a fundamental part of a typical Rails application; hence it’s befitting that a number of recent enhancements bolster the features. One of the best is the new validates_with. You can now validate a model a separate class. Here’s a (somewhat contrived) example.
class Wheels < ActiveRecord::Validator
def validate
number_of_wheels = options[ :number_of_wheels ] || 4
if record.number_of_wheels != number_of_wheels
record.errors[ :number_of_wheels ] << "Your #{record.class} won't run"
end
end
end
class Engine < ActiveRecord::Validator
def validate
# Valid?
end
end
class Weight < ActiveRecord::Validator
def validate
# Valid?
end
end
class Vehicle < ActiveRecord::Base
attr_accessor :number_of_wheels
end
class Car < Vehicle
validates_with Wheels, :number_of_wheels => 4
validates_with Engine, Weight
end
class Motorcycle < Vehicle
validates_with Wheels, :number_of_wheels => 2
end
validates_with can list one or more subclasses of ActiveRecord::Validator and each is called in turn to substantiate an instance of a model. Within each validator, record is the model and options contains the parameters of validates_with. You set errors as you would in a model’s own validate method.
To test this code, drop into the Rails console.
$ ./script/console
Loading development environment (Rails 3.0.pre)
>> c = Car.new
=> #<Car id: nil, created_at: nil, updated_at: nil>
>> c.number_of_wheels = 4
=> 4
>> c.valid?
=> true
>> c.number_of_wheels = 2
=> 2
>> c.valid?
=> false
>> c.errors
=> {:number_of_wheels=>["Your Car won't run"]}
validates_with encapsulates rules and makes those rules reusable akin to any other class. I can imagine modules and gems full of validator classes for email, telephone numbers, and common formats that are best written once and shared among many applications. Thankfully, I was able to reuse some code — Wheels — even in this limited example.
Much like other validation rules, validates_with respects :on to specify when to validate in the instance lifecycle, and accepts :if and :unless to validate conditionally.
Oddly, the generate script does not yet create validators, meaning there is no convention (yet) to store the classes. For the moment, I create mine in app/models/validators and load the classes from environment.rb with config.load_paths += %W( #{RAILS_ROOT}/app/model/validators ). You may choose to keep your validators in lib.
One shortfall of validates_format_of was addressed recently, too. Up untikl last week, the rule checked for conformity of a string with the aptly named :with, but if you wanted to assert non-conformity, you typically had to write your own code using either validates_each or a plain old validate. For example, to exclude email addresses from the domain example.com, I might write:
class EmailAddress < ActiveRecord::Base
attr_accessor :domain
validate :acceptable_domain
def acceptable_domain
errors.add_to_base( "Invalid domain") unless
self.domain.match(/example\.(org|com|net|biz)$/).nil?
end
end
Yuck. (By the way, the attr_accessor :domain and the similar code used in the previous example are shortcuts to add a property in the models without defining actual fields in the migrations.) However, with a recent patch, the task becomes much more succinct.
class EmailAddress < ActiveRecord::Base
attr_accessor :domain
validates_format_of :domain, :without => /example\.(org|com|net|biz)$/
end
Mates of State
Many problems can be represented by a state machine. For example, in an online store, an order can transition between many states as it winds it way to a customer, where each transition requires some processing. The order might start as unpaid, a kind of limbo. Next, the order is deemed paid, which generates a pick list for the warehouse. Once all the items are collected, the order might be marked complete, which generates a shipping label, and so on.
Several plugins provide state machines for Rails, but the feature is so fundamental, the best features of available solutions were integrated into the core to create ActiveRecord::StateMachine. To add a state machine to any class, you simply include it and create a state string field in your table. Here’s an example of a state machine to implement some fictional order processing rules.
class Order < ActiveRecord::Base
include ActiveRecord::StateMachine
state_machine do
state :placed # In limbo
state :paid
state :assembled
state :packaged
state :shipped
state :received
state :exception # Uh oh!
event :advance_order do
transitions :to => :paid, :from => [ :placed ],
:on_transition => :pick_list
transitions :to => :assembled,:from => [ :paid ],
:on_transition => :shipping_label
transitions :to => :packaged, :from => [ :assembled ],
:on_transition => :ups_pickup
transitions :to => :shipped, :from => [ :packaged ],
:on_transition => :send_tracking_code
transitions :to => :received, :from => [ :shipped ],
:on_transition => :book_income
end
event :exception do
transitions :to => :exception,
:from => [ :paid, :assembled, :packaged, :shipped ],
:on_transition => :flag
end
end
def pick_list
puts "Go get it"
end
def shipping_label
puts "Send it here"
end
def ups_pickup
puts "Put it on the truck"
end
def send_tracking_code
puts "Watch it go"
end
def book_income
puts "Kaching!"
end
def flag
puts "Uh oh!"
end
end
After running this model’s migration to create the orders table, you can drop into the console again to test the code.
$ ./script/console
Loading development environment (Rails 3.0.pre)
>> o = Order.create
=> #
Nifty! In addition to states and transitions, you can also query if the model is in a particular state with the boolean method state?, where state is any of the states you defined.
More To Come
This only scratches the surface of Edge. Tomorrow, we'll look at database seeding made easy and more. Until then, happy tinkering!
Comments on "Living on the Edge of Rails, Part 1"
The problem with Rails is that it is an overbearing framework ( just like EVERY java framework ). A framework is supposed to be just that ( think of the frame of a house ).
If you want to try a ( IMO ) true, thin, framework, try Ramaze. (Sinatra is a similar framework ).