Making “as_string” Attribute Readers for ActiveRecord
Occasionally, I need to transform boolean model attributes like “active” to display “active” or “inactive” instead of “true” or “false” when making reports or views. A lot of times this means writing some kind of helper method like this:
def active_or_inactive(object, true_message, false_message) object.active ? true_message : false_message end
and calling it like this:
<%= active_or_inactive(@project, "Active", "Inactive" %>
That’s not a bad approach, and it helps keep the views slightly cleaner by keeping the logic out, but it ends up being more characters than simply using a ternary operator in the view. I’ve used a slightly different approach in some of my more recent projects and I thought I should share it with you.
Move It To The Model
That’s right, I’m advocating pushing that helper into the model itself. I can hear you now, yelling something about “this guy doesn’t know what he’s talking about! How dare he put display logic in his models!” But before you close your browser, allow me to explain.
It just so happens that I need this logic not only in my views, but in my text-based reports that I run outside of the web server. I could mix the module with the helpers in when I needed it, but there’s also something un-object-oriented that bugs me about helpers. They remind me of PHP a bit. I feel like I should be calling object.active_as_string("Active", "Inactive") instead. So that’s what I’m going to do.
First, a unit test, because we’re all good professionals that write tests first. I want to call a method called active_as_string which takes two parameters – the string to print when it’s true and the string to print when it’s
false. Here are my tests:
require 'test_helper' class ProjectTest < ActiveSupport::TestCase test "should display 'Active' if active" do p = Project.new(:active => true) assert_equal p.active_as_string("Active", "Inactive"), "Active" end test "should display 'Inactive' if not active" do p = Project.new(:active => false) assert_equal p.active_as_string("Active", "Inactive"), "Inactive" end end
Tests help me design the method’s use up front. With two failing tests as my guide, I can now take my first stab at making the method work:
class Project < ActiveRecord::Base def active_as_string(true_message, false_message) self.active ? true_message : false_message end end
With that implemented, my tests pass. However, I also have a “closed” boolean I need to handle, and it would also be nice if I could display “No description” if a project’s description was blank. I could write my own _as_string methods like I’ve done already, but instead, I’ll do a little metaprogramming to generate what I need.
Let’s add four more test cases – to test the “closed” and the “description” fields.
test "should display 'Closed' if closed" do p = Project.new(:closed => true) assert_equal p.closed_as_string("Closed", "Open"), "Closed" end test "should display 'Open ' if not closed" do p = Project.new(:active => false) assert_equal p.closed_as_string("Closed", "Open"), "Open" end test "should display 'No Description' if description is nil" do p = Project.new(:description => nil) assert_equal p.description_as_string("No Description"), "No Description" end test "should display the description if it exists" do p = Project.new(:description => "Hi there!") assert_equal p.description_as_string("No Description"), "Hi there!" end
Now, let’s build some methods!
ActiveRecord::Base.columns
Every ActiveRecord class has a class method called columns that returns a collection of column objects. The Column object describes each database column and lets you determine its type and its name. We can use that and class_eval to generate a whole bunch of methods at runtime.
class Project < ActiveRecord::Base self.columns.each do |column| if column.type == :boolean class_eval <<-EOF def #{column.name}_as_string(t,f) value = self.#{column.name} value ? t : f end EOF end end end
In this example, we’re creating the _as_string method for each boolean column. It takes two parameters and is basically the same code we already used in our original method earlier. Notice how class_eval can do string interpolation using Ruby’s #{} syntax. That makes it easy to build up the method names.
We can use that same concept to do the same for any other methods – we’ll just cast them to strings and check to see if they are blank.
class_eval <<-EOF def #{column.name}_as_string(default_value) value = self.#{column.name}.to_s value.blank? ? default_value : value end EOF
We throw that into the else block and our whole example looks like this:
class Project < ActiveRecord::Base self.columns.each do |column| if column.type == :boolean class_eval <<-EOF def #{column.name}_as_string(t,f) value = self.#{column.name} value ? t : f end EOF else class_eval <<-EOF def #{column.name}_as_string(default_value) value = self.#{column.name}.to_s value.blank? ? default_value : value end EOF end end end
If you run your tests now, they all pass. But our work isn’t done – this isn’t very DRY. We may want to use this in another class too.
Modules!
Create a new module and mix the behavior into your models. Create the file lib/active_record/as_string_reader_methods.rb (create the active_recordfolder if it doesn’t exist already) and put this code in the file:
module ActiveRecord module AsStringReaderMethods def self.included(base) create_string_readers(base) end def self.create_string_readers(base) base.columns.each do |column| if column.type == :boolean class_eval <<-EOF def #{column.name}_as_string(t,f) value = self.#{column.name} value ? t : f end EOF else class_eval <<-EOF def #{column.name}_as_string(default_value) value = self.#{column.name}.to_s value.blank? ? default_value : value end EOF end end end end end
It’s mostly the same code we had before, but in this case we’re using the self.included method to trigger the method creation on the model that includes the module.
Now, remove the code from your Project mode and replace it with
include AsStringReaderMethodsRun your tests, and everything should pass. You now have a module you can drop into your projects and you’ll have this functionality yourself. Now it’s up to you to expand upon this, and use this pattern in your own work if you find it useful.
Good luck!
Seem overkill to me, why would i do all this crazy meta-madness only to go from x.active? ? ‘x’ : ‘y’ to x.active_as_string(‘x’,'y’), first one is easier to type, understand and maintain (e.g. search for active? and replace it, instead of having to know that there can be some active_as_xxx)
Thanks for this elegant solution Brian! And @grosser, why would you ever think that complex view logic that you have to maintain with find and replace is better than having a single place to define your behavior that is easily testable?