Saturday, September 22, 2012

Forget Struct and OpenStruct: Flexible Well-defined Ruby Configuration Made Easy

Looking for a way to provide easy configuration for your Gem, or some other component in Ruby/Rails?

The following works especially well if you have a module that most of our classes fall under, such as in a Gem.

Thanks to Jesús Gabriel y Galán and Calvin B. that helped me out in a post to come up with using class_eval in the configure method and how to specify variables and methods in the block with "@" or "self.":

module MyModule
  class << self
    attr_accessor :debug, :proc, :some_mapping
    def configure(&blk); class_eval(&blk); end
  end
end

To add defaults, you can either set self.varname = ... in the main part of the module under the block injection into the module class (either "@" or "self." are ok):

module MyModule
  class << self
    attr_accessor :debug, :proc, :some_mapping
    def configure(&blk); class_eval(&blk); end
  end

  self.debug = true
  self.proc = lambda{|a,b| puts "a=#{a} b=#{b}"}
  self.some_mapping = {:key => 'value}
end

or you can put them down below just as your users would (either "@" or "self." are ok):

module MyModule
  class << self
    attr_accessor :debug, :proc, :some_mapping
    def configure(&blk); class_eval(&blk); end
  end
end

MyModule.configure do
  @debug = true
  @proc = lambda{|a,b| puts "a=#{a} b=#{b}"}
  @some_mapping = {:key => 'value}
end

Note: you can configure here or in the application that includes the Gem either by:

MyModule.debug = true
MyModule.proc = lambda{|a,b| puts "a=#{a} b=#{b}"}

Or by (either "@" or "self." are ok):

MyModule.configure do
  self.debug = true
  self.proc = lambda{|a,b| puts "a=#{a} b=#{b}"}
end

And you can define debug or proc or whatever as methods in the do block:

module MyModule
  class << self
    attr_accessor :foo, :time
    def configure(&blk); class_eval(&blk); end
  end
end

MyModule.configure do
  @foo = false
  def self.time; Time.now; end
end

One advantage over OpenStruct is that you still can have well-defined names to avoid misspelling problems with variable names.

Ok, now for the loose ends! It is a good idea to put the extension of the module for configuration into a file in your gem like lib\config.rb, because you'll need to require it at the top in some files and may not want to require everything.

And, maybe you'll want to add question mark methods and make the option array(s) to be contained in constant(s):

module MyModule
  OPTIONS = [
    :a,
    :b,
    :c
  ]
  
  class << self
    OPTIONS.each{|o|attr_accessor o; define_method("#{o}?".to_sym){!!send("#{o}")}}
    def configure(&blk); class_eval(&blk); end
  end
end
Note: even though this is a great way to handle config in a gem, if you are doing this sort of things not in a gem but in your Rails app, be sure that the config part of the module is defined in your_module_name.rb in the top of one of the autoloaded paths (like app/controllers) and note that it will try to redefine the OPTIONS constant sometimes, resulting in errors like "warning: already initialized constant OPTIONS".

No comments: