Making “as_string” Attribute Readers for ActiveRecord

Posted by Brian in Howto, Metaprogramming, Rails, tips (March 15th, 2010)

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 AsStringReaderMethods

Run 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!

2 Responses to ' Making “as_string” Attribute Readers for ActiveRecord '

Subscribe to comments with RSS or TrackBack to ' Making “as_string” Attribute Readers for ActiveRecord '.

  1. grosser said,
    on March 22nd, 2010 at 1:34 pm

    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)

  2. Jon said,
    on March 25th, 2010 at 10:58 am

    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?

Leave a reply

:mrgreen: :neutral: :twisted: :shock: :smile: :???: :cool: :evil: :grin: :oops: :razz: :roll: :wink: :cry: :eek: :lol: :mad: :sad: