29 Oct

Custom configuration settings made easy

October 29th, 2007 — 7:00 pm Chris

I’m currently working on extracting some of the business rules and options from slate into a separate configuration block. My first stab at this involved creating a custom ConfigurationBuilder class and using instance_eval to get some fancy DSL working. But then I thought of a much simpler solution: a custom Hash class that supported method-based access to keys. That is, hash.some_key would be the same as hash[:some_key].

Turns out, this really wasn’t too complicated. The slightly tricky part was handling nesting: I wanted my configuration options to be grouped, such as passwords.min_length. All I had to do for this to work was override the default method for my custom hash class.

The end result is as follows:

require 'active_support'
class Configuration
  cattr_accessor :settings

  # creates a new configuration object if
  # necessary
  def self.settings # !> redefine settings
    @@settings ||= ConfigurationHash.new
  end

  # returns the current configuration
  # by passing a block you can easily edit the
  # configuration values
  def self.config
    block_given? ? yield(self.settings) : self.settings
  end
end

# specialized hash for storing configuration settings
class ConfigurationHash < Hash
  # ensure that default entries always produce 
  # instances of the ConfigurationHash class
  def default(key=nil)
    include?(key) ? self[key] : self[key] = self.class.new
  end

  # provides member-based access to keys
  # i.e. params.id === params[:id]
  # note: all keys are converted to symbols
  def method_missing(name, *args)
    if name.to_s.ends_with? '=' 
      send :[]=, name.to_s.chomp('=').to_sym, *args
    else
      send(:[], name.to_sym)
    end    
  end
end

Here’s an example of how it works (say, in the environment.rb file):

Configuration.config do |config|
  config.passwords.min_length   = 8
  config.passwords.ttl          = 60
end

Configuration.config.passwords.min_length # => 8
Configuration.config.passwords.ttl # => 60

# some alternate ways to access the settings
Configuration.config[:passwords][:min_length] # => 8
Configuration.config.passwords[:ttl] # => 60

Just a note that the method chain can technically go on as long as you want:

Configuration.config.passwords.length.minimum = 8

This works because a new ConfigurationHash is created when you access a key that doesn’t exist.

There you have it – custom configuration settings made easy. One caveat: since this is all dynamic, if you spell a key wrong or provide an incorrect path, you won’t be getting the configuration setting you want. However, the trade-off is the ability to define new options instantly.

#1: Ryan said on Oct 29 at 9:26 pm

Nice. Did you consider @OpenStruct@ for the method-based access to the keys of a hash?

#2: Chris said on Oct 29 at 10:03 pm

That would make sense, wouldn’t it? Honestly, though, that part (the method_missing) was easy. I’m sure it could be done either way. I’ll likely switch to a real class-based configuration with lots of @attr_accessor@ in the future simply for the benefit of easily documenting each of the properties. But this definitely works for now.

#3: dave said on Oct 29 at 9:38 pm

Congrats on the 100th comment ryan :)

#4: John Nunemaker said on Oct 31 at 12:54 am

Looks cool. One api suggestion: Configuration.config.passwords.min_length seems repetitive. Was there a purpose the .config. in there? Seems like it could be removed and then you have less key strokes. I’m lazy ;)

#5: Ryan said on Oct 31 at 11:20 am

I thought about that, too. I almost mentioned (inquired?) about it the other day, but it’s typically safe to assume Chris is handling it in the slickest way possible. However, here’s my rough thought:

Using OpenStruct, could you do something like this?

config = OpenStruct.new(
  :pwd_min_length => Configuration.config.passwords.min_length,
  :pwd_ttl                 => Configuration.config.passwords.ttl
)

Then somehow make that available everywhere so you could refer to the Configuration as config.pwd_ttl or something.

For the grouping, you could probably come up with some sort of nested ostruct loop or something. But I haven’t really thought about that.

Just wondering what your thoughts were, and I if you’re actually using conditions like:

if params[:password].length < Configuration.config.passwords.min_length

But that might not even be how it works.

#6: Chris said on Oct 31 at 11:48 am

Ok, I’ll make everyone happy – add this to the Configuration class:

    def self.method_missing(name, *args, &block)
      self.config.send(name, *args, &block)
    end

Now, Configuration.users is the same as Configuration.config.users.

#7: Chris said on Oct 31 at 2:15 pm

Just a note – I wasn’t being “snarky” (Dave said I was) :-)

Anyway, I have a new addition to ConfigurationHash:

    # retrieves the specified key and yields it
    # if a block is provided
    def [](key, &block)
      block_given? ? yield(super(key)) : super(key)
    end

Now, you can do things like this:

Configuration.cbscharf do |cbscharf|
  cbscharf.first_name = 'Chris'
  cbscharf.last_name  = 'Scharf'

  cbscharf.address do |address|
    address.city = 'Morgantown'
    address.state = 'WV'
  end
end

This is getting crazy!

#8: Rob said on Feb 13 at 7:18 am

When I first tried this it wasn’t working – the block was being ignored. Took my brain a couple of minutes to realise that the method_missing in ConfigurationHash needed updating to include an &block parameter:



def method_missing(name, *args, &block)
    if name.to_s.ends_with? '='
      send(:[]=, name.to_s.chomp('=').to_sym, *args)
    else
      send(:[], name.to_sym, &block)
    end
  end


Other than that, thanks for putting this together!

#9: Erik P said on Dec 9 at 12:02 pm

Very nice writeup. Thank you! I am looking into re-factoring much of my configuration and your article is definitely helpful.

#10: Anton said on Jan 12 at 5:39 pm

Of course this is all dynamic, which is good and bad.
Some things you might want to be more permanent.

How about the ability load from and save to a .yml file?

Add comment

You are adding a new comment