views:

103

answers:

5

This is more of a style question, I'm wondering what other people do.

Let's say I have a field in my database called "status" for a blog post. And I want it to have several possible values, like "draft", "awaiting review", and "posted", just as an example.

Obviously we don't want to "hard code" in these magic values each time, that wouldn't be DRY.

So what I sometimes do is something like this:

class Post
  STATUS = {
    :draft => "draft",
    :awaiting_review => "awaiting review",
    :posted => "posted"
  }

  ...

end

Then I can write code referring to it later as STATUS[:draft] or Post::STATUS[:draft] etc.

This works ok, but there are a few things I don't like about it.

  1. If you have a typo and call something like STATUS[:something_that_does_not_exist] it won't throw an error, it just returns nil, and may end up setting this in the database, etc before you ever notice a bug
  2. It doesn't look clean or ruby-ish to write stuff like if some_var == Post::STATUS[:draft] ...

I dunno, something tells me there is a better way, but just wanted to see what other people do. Thanks!

+2  A: 

You could use a class method to raise an exception on a missing key:

class Post
  def self.status(key)
    statuses = {
      :draft => "draft",
      :awaiting_review => "awaiting review",
      :posted => "posted"
    }
    raise StatusError unless statuses.has_key?(key)
    statuses[key]
  end
end

class StatusError < StandardError; end

Potentially, you could also use this method to store the statuses as integers in the database by changing your strings to integers (in the hash), converting your column types, and adding a getter and a setter.

Alex Reisner
+6  A: 

This is a common problem. Consider something like this:

class Post < ActiveRecord::Base
  validates_inclusion_of :status, :in => [:draft, :awaiting_review, :posted]
  def status
    read_attribute(:status).to_sym
  end
  def status= (value)
    write_attribute(:status, value.to_s)
  end
end

You can use a third-party ActiveRecord plugin called symbolize to make this even easier:

class Post < ActiveRecord::Base
  symbolize :status
end
John Feminella
Nice John, thanks!
Brian Armstrong
+1  A: 

I do it like this:

class Post
  DRAFT = "draft"
  AWAITING_REPLY = "awaiting reply"
  POSTED = "posted"
  STATUSES = [DRAFT, AWAITING_REPLY, POSTED]

  validates_inclusion_of :status, :in => STATUSES
  ...
end

This way you get errors if you misspell one. If I have multiple sets of constants, I might do something like DRAFT_STATUS to distinguish.

mckeed
I like it, except you then have to refer to it later as `STATUSES[0]` right? In that case it isn't quite as readable.
Brian Armstrong
+5  A: 

You can use Hash.new and give it a block argument which is called if a key is unknown.

class Post
  STATUS = Hash.new{ |hash, key| raise( "Key #{ key } is unknown" )}.update(
    :draft => "draft",
    :awaiting_review => "awaiting review",
   :posted => "posted" )
end

It's a bit messy but it works.

irb(main):007:0> Post::STATUS[ :draft ]
=> "draft"
irb(main):008:0> Post::STATUS[ :bogus ]
RuntimeError: Key bogus is unknown
    from (irb):2
    from (irb):8:in `call'
    from (irb):8:in `default'
    from (irb):8:in `[]'
    from (irb):8
Farrel
Interesting, thanks for posting it!
Brian Armstrong
I like this method. No external dependancies.
Myrddin Emrys
+1  A: 

Take a look at the attribute_mapper gem.

There's a related article that shows how you can handle the problem declaratively, like this (borrowed from the article):

class Post < ActiveRecord::Base
  include AttributeMapper

  map_attribute :status, :to => {
    :draft => 1,
    :reviewed => 2,
    :published => 3
  }
end

...which looks rather stylish.

Mike Woodhouse
Pretty cool. It's too bad it doesn't group them in case you have multiple attributes, like status[:draft] vs something_else[:draft] but overall this might be the best solution I've seen
Brian Armstrong