Transforming YAML config files into Ruby objects with helpers

Have you ever found yourself juggling configuration data in your Ruby or Rails applications? YAML files are great for storing configuration, but accessing that data through plain hashes can get messy. What if you could transform that static YAML data into proper Ruby objects with custom methods? Today I'll show you exactly how to do that!

Why YAML for configuration?

YAML (YAML Ain't Markup Language) has become a standard for configuration files in Ruby applications, especially in Rails. Its human-readable format makes it perfect for storing structured data without the verbosity of XML or the strictness of JSON.

But the default way we use YAML in Ruby - parsing it into nested hashes - often leads to code like this:

config = YAML.load_file('config/admins.yml')
puts config['production']['john']['email']

This approach has several drawbacks:

  • No validation for your configuration structure
  • No methods for transforming or combining configuration values
  • No type checking
  • String keys instead of symbols (or vice versa) causing subtle bugs

Wouldn't it be nicer to write Admin.find(:john).email instead?

Creating a model to represent your configuration

Let's start by examining a sample YAML configuration file for admins:

# config/admins.yml
production:
  john:
    name: John Smith
    email: [email protected]
    role: admin
  jane:
    name: Jane Doe
    email: [email protected]
    role: editor

development:
  john:
    name: John Smith
    email: [email protected]
    role: admin
  jane:
    name: Jane Doe
    email: [email protected]
    role: developer

Now, we'll create a Ruby class that will represent each admin entry:

# app/models/admin.rb
class Admin
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :id, :string
  attribute :name, :string
  attribute :email, :string
  attribute :role, :string

  class << self
    def all
      @all ||=
        Rails.application.config_for(:admins).map do |key, attrs|
          new(attrs.merge(id: key.to_s))
        end
    end

    def find(key)
      all.find { |admin| admin.id == key.to_s }
    end
  end
end

Let's break down what's happening here:

  1. We're using ActiveModel::Model which gives us validations and other model-like behaviors without a database
  2. ActiveModel::Attributes gives us typed attributes with potential for custom types
  3. We define the attributes we expect for each admin
  4. The class methods (self.all and self.find) provide an interface to access our configuration data

How it works

The magic happens through Rails' config_for method, which loads the YAML file corresponding to the current environment. So in production, it loads the production section, and in development, it loads the development section.
For each admin entry in the YAML, we create a new Admin object, merging in the id (the key from the YAML) with the other attributes.
The result is cached in the @all class variable, so we only load and parse the YAML once.

Using your configuration objects

Now, instead of accessing your configuration through nested hashes, you can do:

# Get all configured admins
Admin.all

# Find a specific admin
john = Admin.find(:john) # => #<Admin ...>
puts john.name     # => "John Smith"
puts john.email    # => "[email protected]" (in development)

# What if the admin doesn't exist?
Admin.find(:guest)  # => nil

Extending with custom methods

The real power comes when you add domain-specific methods to your model:

class Admin
  # ... our current existing code ...

  def admin?
    role == 'admin'
  end

  def display_name
    "#{name} <#{email}>"
  end

  def domain
    email.split('@').last
  end

  def gravatar_url
    email_digest = Digest::MD5.hexdigest(email.downcase)
    "https://www.gravatar.com/avatar/#{email_digest}"
  end
end

Now you can use these methods in your application:

admin = Admin.find(:john)
puts admin.display_name  # => "John Smith <[email protected]>"
puts admin.domain        # => "example.com"
puts admin.admin?        # => true
puts admin.gravatar_url  # => "https://www.gravatar.com/avatar/b28fab587c86a2e20a97b29da731121d"

Summary

Converting YAML configuration files into Ruby objects gives you:

  1. A more intuitive API for accessing configuration
  2. Type checking and validation for configuration values
  3. The ability to add domain-specific methods to transform or combine configuration values
  4. Better organization of your configuration code

This pattern is especially useful for configurations that are referenced in multiple places in your application, as it centralizes the logic for accessing and manipulating the configuration data.

Happy YAML-ing!