Rails: Mocking YAML arrays as ActiveRecord objects

I’ve been finding myself using YAML a lot for silly enums — maybe because I’m lazy, but mostly because there is no need to manage them via a database table. Here is just a simple example — I need to associate a region and division to a client. These will not change much and are simple enough to keep as a Yaml array until more logic can be brought to the table. Here is how that YAML file looks:

# RAILS_ROOT/config/app.yml

regions:
  - 'northeast'
  - 'southeast'
  - 'west'
  - 'central'
  - 'emerging east'
  - 'emerging west'
  - 'opportunity east'
  - 'opportunity west'
  - 'opportunity national east'
  - 'opportunity national west'

divisions:
  <%= (1..71).map{ |i| "Region #{i}" }.to_yaml %>

Loading it up using an initializer:

# RAILS_ROOT/config/initializers/load_app_config.rb

fdata =File.open(File.join(RAILS_ROOT, "config", "app.yml")).read
APP = YAML::load(ERB.new(fdata).result(binding)).symbolize_keys

Now we have our APP hash chock full o’ YAML goodness. My only problem with this is that I might want to eventually put this data set into a table and it is a pain to hunt and search for where the application references APP[:regions] and so on sooo: time to objectize.

I decided that since they are stupid simple objects they can all have the same parent: YamlArrayObject:

# RAILS_ROOT/app/models/yaml_array_object.rb
# or in lib

class YamlArrayObject
  attr_reader :name
  def id
    0
  end
  def initialize(name)
    @name = name
  end
  def to_s
    name
  end
  def display_name
    to_s.titleize
  end
  def <=>(b)
    name <=> b.name
  end
end

Simple enough. Now we just inherit and overload as appropriate.

# RAILS_ROOT/app/models/region.rb
class Region < YamlArrayObject
  def self.all
    APP[:regions].map{ |r| Region.new(r) }
  end
end

# RAILS_ROOT/app/models/division.rb
class Division < YamlArrayObject
  def <=>(b)
    name.to_i <=> b.name.to_i
  end
  def self.all
    APP[:divisions].map{ |d| Division.new(d) }
  end
end

This is all Jim Dandy right now since we can populate select boxes using cleaner code (bonus: won’t break if you decide to add Divisions to a database table)

# i like
<%= f.select :division, Division.all.sort.map{ |d| [d.display_name, d] } -%>
# better than
<%= f.select :division, APP[:divisions].sort{|a,b| a.to_i <=> b.to_i}.map{ |d| [d.titleize, d] } -%>

The only real issue is that calling Client.first.region still returns a string, which is bad since we want to keep our code as ambiguous as possible. The last step is to override the Client class’ methods for region and division (and whatever else we want to use):

# RAILS_ROOT/app/models/client.rb

def division
  Division.new(@attributes['division']) unless @attributes['division'].blank?
end
def region
  Region.new(@attributes['region']) unless @attributes['region'].blank?
end

Now in our views (client/show) we can simply call @client.region.display_name or whatever and not have to change the code when we finally get around to DBing those object.

(There might be better ways to do this that I’m missing, but this seems pretty okay for my needs right now.)

You might also be able to define the simple objects within YAML itself — but than can get cumbersome


Comments

No comments yet.

Add Yours

  • Author Avatar

    YOU


Comment Arrow



About Author

Rob Hurring

Ruby, Rails, PHP, bash... oh my!