November 8, 2014 · ruby

Configure Ruby Module

I think you are familiar with configure method, which a lot of gems provide for configuration needs. For example configuration of carrierwave:

CarrierWave.configure do |config|  
  config.storage = :file
  config.enable_processing = false
end  

So how to implement it?

Fast and dirty

Let's start from failing tests.

# configure.rb
require 'minitest/autorun'

class ConfigurationTest < MiniTest::Test  
  def test_configure_block
    MyModule.configure do |config|
      config.name = "TestName"
      config.per_page = 25
    end

    assert_equal "TestName", MyModule.config.name
    assert_equal 25, MyModule.config.per_page

    assert_equal "TestName", MyModule.config[:name]
    assert_equal 25, MyModule.config[:per_page]
  end
end  
➜  Projects  ruby configure.rb
Run options: --seed 25758

# Running:

E

Finished in 0.001166s, 857.6329 runs/s, 0.0000 assertions/s.

  1) Error:
ConfigurationTest#test_configure_block:  
NameError: uninitialized constant ConfigurationTest::MyModule  
    configure.rb:5:in `test_configure_block'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips  

Now we have failing tests and can start to implement functionality. First, define module with configure method.

module MyModule  
  def self.configure

  end
end  

Now we need a place to store our configuration. I think module's variables will suitable for this need.

module MyModule  
  def self.configure
    @config ||= {}
  end

  def self.config
    @config
  end
end  

Here there is an issue. We can’t store our config in a hash. For now, I will replace it with OpenStruct, which conform to our functional needs. After this, I already can yield block and pass config struct to it.

require 'minitest/autorun'  
require 'ostruct'

module MyModule  
  def self.configure
    @config ||= OpenStruct.new
    yield(@config) if block_given?
    @config
  end

  def self.config
    @config || configure
  end
end  

And now tests are passed.

➜  Projects  ruby configure.rb
Run options: --seed 8967

# Running:

.

Finished in 0.001607s, 622.2775 runs/s, 2489.1101 assertions/s.

1 runs, 4 assertions, 0 failures, 0 errors, 0 skips  

Refactoring

It’s time to refactor this solution. It has few problems:

Here’s tests, for being confident, that when we call not existing config attributes we will get an exception.

def test_set_not_exists_attribute  
  assert_raises NoMethodError do
    MyModule.configure do |config|
      config.unknown_attribute = "TestName"
    end
  end
end

def test_get_not_exists_attribute  
  assert_raises NoMethodError do
    MyModule.config.unknown_attribute
  end
end  

We have two ways to fix this. First we can use struct with whitelist of configurable attributes.

module MyModule  
  CONFIG_ATTRIBUTES = %i(name per_page)

  def self.configure
    @config ||= Struct.new(*CONFIG_ATTRIBUTES).new
    yield(@config) if block_given?
    @config
  end

  def self.config
    @config || configure
  end
end  

Everything looks OK. Tests pass. Code feels simple and readable. But I forget one more important detail. Default config values. Add one more test for it.

def test_default_values  
  MyModule.configure do |config|
    config.name = "TestName"
  end

  assert_equal 10, MyModule.config.per_page
end  

To avoid overriding config attributes in different tests we need to reset config before run each single test. I will provide reset method right inside test class, because it needs only for testing purposes.

module ::MyModule  
  def self.reset
    @config = nil
  end
end

def setup  
  MyModule.reset
end  

Simplest solution of the problem with default values will look something like this:

self.config ||= begin  
  config = Struct.new(*CONFIG_ATTRIBUTES).new
  config.per_page = 10
  config
end  

But code starts to smell. Default values may be much more complex. It will be difficult to maintain this code. I think we can do better. Let’s replace struct by class. In class, we can provide default values in initializer. It’s very easy to read and extend.

module MyModule  
  class Configuration
    attr_accessor :name, :per_page

    def initialize
      @per_page = 10
    end

    def [](value)
      self.public_send(value)
    end
  end

  def self.configure
    @config ||= Configuration.new
    yield(@config) if block_given?
    @config
  end

  def self.config
    @config || configure
  end
end  

I think this is my favourite solution. It’s still very simple and readable. It’s also flexible. We can provide complex default values and separate logic into methods if needed. We also give two ways to get config values: via methods and subscript.

That's all what I want to share with you today. Have a nice weekend!

Source code available here: http://goo.gl/feCwCC

UPD:

User of habr.ru @northbear suggested an alternative way where instead of Class uses Struct with initializer.

module Sample  
  DefaultConfig = Struct.new(:a, :b) do
    def initialize
      self.a = 10
      self.b = 'test'
    end
  end

  def self.configure
    @config = DefaultConfig.new
    yield(@config) if block_given?
    @config
  end

  def self.config
    @config || configure
  end
end  

In this case there is no needs to use public_send for implementing subscript. It makes code easier. Thanks, @northbear!