Specifying Options in Rails
Posted: March 8th, 2008 | Author: Daniel Higginbotham | Filed under: Rails | 3 Comments »The code below offers a way to solidify the “options” concept which is prevalent in Rails and Rails plugins. Its advantages are that an error is thrown when you try to specify an illegal option, rather than when the options are read; and that you can use expressions for options, consolidating option-specific code and tying it explicitly to your options.
When you want to pass your Options object to a method, like ActiveRecord’s find, use options.to_hash.
I would have liked to just pass the Options object itself, but that would require lazy evaluation, and I’m not sure how to do that with Ruby.
class Options
def self.option_list(*options)
attr_accessor(*options)
define_method(:options){ options }
end
def to_hash
hash = {}
options.each do |option|
hash[option] = self.send(option) unless self.send(option).nil?
end
hash
end
end
class ActiveRecordOptions < Options
option_list :conditions, :order, :group, :limit, :offset, :joins, :include, :select, :joins, :page, :per_page
attr_accessor :condition_text, :condition_values, :finder
def initialize
@condition_text = []
@condition_values = []
end
def conditions
text = @condition_text.collect{|ct| "(#{ct})"}.join(" AND ")
return unless text && !text.blank?
[text, @condition_values].flatten.compact
end
end
In the example below, a condition is built up in each if params[:search] block. I’ve found myself doing this a lot. With the ActiveRecordOptions class I can DRY up my code by specifying in one place how to take the conditions components I’ve built up and generate one value from them.
options = ActiveRecordOptions.new
options.order = "created_at DESC"
if params[:search]
if params[:search]['start_date'] && !(params[:search]['start_date'] =~ /From/i)
start_date = Date.parse(params[:search]['start_date'])
options.condition_text << "created_at >= ?"
options.condition_values << start_date
end
if params[:search]['end_date'] && !(params[:search]['end_date'] =~ /To/i)
end_date = Date.parse(params[:search]['end_date']) >> 1
options.condition_text << "created_at <= ?"
options.condition_values << end_date
end
if params[:search]['user_id'] && params[:search]['user_id'].strip =~ /^\d+$/
options.condition_text << "user_id = ?"
options.condition_values << params[:search]['user_id'].strip
end
end
@bulk_uploads = BulkUpload::Source.find(:all, options.to_hash)
# options.to_hash will yield something like
# {
# :order => "created_at DESC",
# :conditions => ["(created_at >= ?) AND (user_id = ?)", "2008-03-10", 80]
# }

seems like you are reinventing the Struct, (but adding to_hash).
That does look very much like a Struct :) I started out with something different, and in changing it I didn’t realize the similarity.
One of the useful parts of my code is the
conditionsmethod in ActiveRecordOptions , which then gets called byto_hash. Can Structs do this or something similar by default? That would be helpful.Using the Options class might still be a better choice because its purpose is more explicit. So perhaps it is a reinvention, but so far I find it a useful reinvention :)
On the other hand, it might be confusing to have a new
Optionsclass because perceptive people like yourself will look at it and think the same thing you did, and wonder what the point is. In either case, I’ve found that using something a non-primitive object like this is useful when you’re building up options over multiple methods.Doing so helps keep things DRY, reduces failure distance, and communicates more explicitly.