Loading Multiple Ruby Files and Finding Descendants of a Class
I was playing around with writing a little Ruby program to run simulations of the Prisoner’s Dilemma. I wanted to be able to program various strategies by creating multiple classes that descended from a common Player class:
class FirstPlayer < Player
...
endI put my player classes in little ruby files and wrote a “supervisor” program to load all my classes and then play them against each other, round-robin fashion. I run the supervisor like this:
ruby supervisor.rb player1.rb player2.rb- Load the player files specified on the command line, and
- Find all the player classes
I noticed that my command line looks a lot like what happens when you run e.g. rake test:units in a Rails project. This rake task loads all your unit tests and then finds all the test cases (i.e. the classes that are descendants of Test::Unit::TestCase).
The job of loading all the test files is handled by rake_test_loader.rb, which is part of the rake gem. The relevant code is trivial:
ARGV.each { |f| load f unless f =~ /^-/ }ARGV contains all the command line arguments (after supervisor.rb). The unless part rejects arguments that start with a dash, since those would presumably be option flags.
So I can use that code as-is to load my player classes.
In order to find my player classes, I needed to borrow a technique from Test::Unit to find all classes that are descendants from my base Player class.
The relevant code is in test/unit/autorunner.rb in your standard Ruby library directory. The Test::Unit is more complex than what I needed, but I was able to distill it down to this:
players = []
ObjectSpace.each_object(Class) do |klass|
players << klass if klass < Player
endObjectSpace is a handy Ruby gizmo that, among other things, lets you iterate over all the objects in your current process. By specifying Class in the call to each_object, we iterate over all classes. To find those that are descendants of my Player class, we use a handy < operator defined in Module. This operator returns true if the left-hand argument is a descendant of the right-hand argument.
Can It Be Done in One Line?
You may be wondering why I’m using an array here and then appending to the array as I go. Isn’t there some way to use collect()/select() to extract this data in one call?
The problem is that methods like select() are in module Enumerable, but ObjectSpace is not. However, Ruby provides a handy class called Enumerable::Enumerator that can turn any object with a method that yields to a block into an Enumerable object.
To use this for our class collector, you would write:
require 'enumerator'
players = Enumerator::Enumerable.new(ObjectSpace, :each_object, Class).select {|klass| klass < Player})I’ll let you decide which form is more readable.
Validating Mutually-Exclusive Attributes 5
Let’s say you have a model that holds a location, with a “state” and a “country”. Suppose that you want the user to enter either a state or a country, but not both.
Your first instinct might be to do something like this:
validates_presence_of :state, :if => lambda {|row| row.country.blank?}
validates_presence_of :country, :if => lambda {|row| row.state.blank?}That’s not quite right however. You won’t get an error if both a state and a country are entered. Furthermore, if both attributes are entered, which one should be considered “wrong”?
Here’s where the generic validate method can help. Rather than validating a specific attribute, validate works on the whole row. If you need to add error messages you use the ActiveRecord::Errors#add() and #add_to_base() methods.
The validation for our state/country example looks like this:
validate do |row|
if row.state.blank? ^ row.country.blank?
row.errors.add_to_base 'Either a state or a country is required (but not both)'
end
endNote how validate takes a block. The row being saved is passed to the block. The caret (^) is Ruby’s “exclusive-or” operator. It returns true only if exactly one of its operands is true, which fits our requirements perfectly.
The add_to_base method adds an error message to the list of errors for the row, but the message is not specific to any single attribute. In this case, the error relates to two attributes, so I chose to use add_to_base.