Two Support Objects You May Have Missed
If you spend time daily in a large ruby project (such as a Rails app) that has ActiveSupport pulled in, you are likely relying on its string, time, hash, and other extensions. I’ve found two objects it provides prove useful, and having found them lesser known amongst my coding friends, figured they are worth sharing.
The first useful tidbit is the ActiveSupport::StringInquirer class. It’s a simple method missing call that lets you do prettier equality tests on strings. If you’ve ever done a Rails.env.development? check, it uses this implementation. Let’s go to the code:
Rails.env.development? # => true
Rails.env # => "development"
Rails.env.class # => ActiveSupport::StringInquirer
si = ActiveSupport::StringInquirer.new("foo")
si.foo? # => true
si.bar? # => false
I think this is great for two reasons. First, it’s a more expressive use of code, and secondly, it implies less coupling to a string outside of an object. Let’s take a simple example: a role for a User object. Imagine you start simple, where role is just a string. Now let’s say we’re using CanCan to add simple authorization to our app, with an ability class that looks like this:
class Ability
include CanCan::Ability
def initialize(user)
user = user || User.new
if user.role == "admin"
can :manage, :all
else
can :read, :all
end
end
end
Note we’re doing an #== for comparison on that role. This is a bit ugly and not as expressive as I’d like. Let’s get rid of ugly with the help of the StringInquirer.
class User < ActiveRecord::Base
def role
role = read_attribute(:role).to_s
ActiveSupport::StringInquirer.new(role)
end
end
class Ability
include CanCan::Ability
def initialize(user)
@user = user || User.new
if role.admin?
can :manage, :all
else
can :read, :all
end
end
private
def role
@user.role
end
end
I’ve redefined User#role to wrap the attribute in a StringInquirer, and updated the Ability class to call the role with the predicate #admin? method. Our end behavior for ability checking is the same, but I think we’ve got more readable code. There’s another win on this: we’ve decoupled from treating our role like a string which can pay out nicely in the future. Imagine for instance the day arrives when a user no longer has one role, but many. A simple user may be able to function as both a forum moderator or a comment moderator. You can shift to supporting many roles per user with a bitmask method and leave your external calls untouched. A simple application of define_method or method_missing on your role attribute wrapper is all you need to keep rolling. Now, you could also define #== on your role object for such string comparisons, but comparing to a string reads more like the caller knows too much of an implementation detail. I haven’t touched on the User#role= setter here; you may need some sanitizing and cleanup on it if you were assigning it the results from the getter method anywhere (and, ahem, possibly breaking encapsulation with your own string assignments, too). I’ll leave that as an exercise for the reader.
Our second friend is the ActiveSupport::SecureRandom interface. Actually, saying this is from ActiveSupport is a little misleading. If you are working on an older Rails 2 project, you’ll probably be using this by way of ActiveSupport. However for modern and future use, this is deprecated and delegated to Ruby 1.9.x stdlib’s SecureRandom. SecureRandom is great for generating random character strings on the fly that are useful as API keys, temporary passwords, tokens, etc. It’s simple to use, and can replace those naive calls to rand() you’ve been making for generating random strings. Don’t reinvent the wheel! I’ll leave you with a few examples:
SecureRandom.hex # => ace59c788b498fadcaa88216e45cf800
SecureRandom.base64 # => iJKR2NQ8Jk1wBdp0nU/fhA==
# Optionally pass 5 for 5 hex pairs
SecureRandom.hex(5) # => a5f8bf212f
—Oct 16, 2011