Custom configuration settings made easy
October 29th, 2007 — 7:00 pm —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.
About slate
slate is a content management system (CMS) developed using Ruby on Rails focused on rapid production of traditional websites created by WVU Web Services. Read more about why we created slate and a longer list of features of slate. You can also check out a list of sites using slate. If you have questions or comments let us know but if it's a question about open sourcing slate have a look at this article first.Archives
Recent articles
- No Wonder Rails Is Default in Mac OS X Server...
- Good News: An Open Sourced slate Is Coming
- The WVU Open Source License
- Implementation idea: .do templates
- Keeping slate Humming - Part 1
- Campfire for Design & Keeping Your Tag Cloud Running
- Custom configuration settings made easy
- HOW-TO: Add a Gallery to Your Site
- JSONRequest.post Example
- Miscellaneous
Articles
Nice. Did you consider @OpenStruct@ for the method-based access to the keys of a hash?
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.Congrats on the 100th comment ryan :)
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 ;)
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?
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:
But that might not even be how it works.
Ok, I’ll make everyone happy – add this to the
Configurationclass:def self.method_missing(name, *args, &block) self.config.send(name, *args, &block) endNow,
Configuration.usersis the same asConfiguration.config.users.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) endNow, 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 endThis is getting crazy!
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 endOther than that, thanks for putting this together!
Very nice writeup. Thank you! I am looking into re-factoring much of my configuration and your article is definitely helpful.
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