Living on the Edge of Rails, Part 1

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!

Fatal error: Call to undefined function aa_author_bios() in /opt/apache/dms/b2b/linux-mag.com/site/www/htdocs/wp-content/themes/linuxmag/single.php on line 62