config_frame.rb

Rubyでアプリケーションを書くとき、設定をロードする手法がまちまちだったので作ってみた。
method_missingを使う外法も考えたけどほんとのエラーのときがめんどいので却下。
リフレクションしまくりで外法じゃないつもりか、とか怒られそうでもあるけど :-)

  • 追記 - コピーライトとライセンスを追加。
  • 修正 - セキュリティ強化(?)
  • 追記 - ユースケースに付け足し
  • 追記 - RDoc書き足し、#description, #description= を追加。
#
#=config_frame.rb - Version 0.2.0
#
#Copyright (C) 2007 - , by arikui
#mailto:shionist@yahoo.co.jp
#
#distoributes under the modified BSD lisence.
#


# 
# CCFW - Config Class FrameWork.
# 
module CCFW #config class framework
  # 
  # A exception is raised by a invalid change of config
  # value.
  # 
  class ConfigError < RuntimeError ; end
  
  # 
  # Create config class object based on
  # this class object.
  # That means this class is superclass of
  # classes created by this framework.
  # 
  # That created class have the same methods.
  #
  class BasicConfigulation
    ItemList = []
    
    #set config item accessor.
    def self.def_items(*names)
      class_eval names.map{|name|
        <<-Eos
          def #{name}(arg=nil)
            unless arg
              @#{name}
            else
              @in_load_dsl or not_in_dsl(:#{name})
              @#{name} = arg
            end
          end
        Eos
      }.join("\n\n")
    end
    private_class_method :def_items
    
    #new() and call the same name instance method.
    def self.def_class_singleton_methods(*names)
      class_eval names.map{|name|
        <<-Eos
          def self.#{name}(*args)
            new().#{name}(*args)
          end
        Eos
      }.join("\n\n")
    end
    private_class_method :def_class_singleton_methods
    
    def_class_singleton_methods :parse, :import, :load
    
    
  private
    #to stack this method name to caller.
    def method_missing(*args)
      super
    end
    
    #banning call private method in DSL.
    def private_method_protecting
      if @in_load_dsl
        m = caller.first.match(/`(.*?)'/)
        name = m ? m[1].intern : :private_method_protecting
        method_missing(name)
      end
      yield
    end
    
    # 
    # :call-seq:
    #   new(){ ... }
    # 
    # Create new config object.
    # 
    # this method can receive DSL block.
    # 
    def initialize(desc=nil, &block)
      private_method_protecting do
        @description = desc
        ItemList.each{|name| instance_variable_set("@#{name}", nil) }
        block_given? and load_dsl(&block)
      end
    end
    
    def load_dsl(src=nil, &block)
      private_method_protecting do
        @in_load_dsl = true
        instance_eval(src) if src
        instance_eval(&block) if block_given?
        @in_load_dsl = false
        self
      end
    end
    
    def not_in_dsl(name)
      private_method_protecting do
        raise ConfigError, "value is must been set by DSL -- #{name}"
      end
    end
    
    
  public
    attr_accessor :description
    
    # loading String's content as DSL.
    def parse(str)
      load_dsl(str)
      self
    end
    
    # loading DSL from IO.
    def import(io)
      parse(io.read)
    end
    
    # loading DSL from file.
    def load(path)
      File.open(path){|f| import(f) }
    end
    
    def inspect #:nodoc:
      list = self.class::ItemList.map{|x| x.to_s }
      col = list.map{|x| x.length }.max
      list.map!{|x| [x, instance_variable_get("@#{x}")] }
      list.map!{|name, val|
        sprintf("\t%-#{col}s\t%s", name, val.inspect)
      }
      head = "#{description}:" || super.split(/\s+/).first + '>'
      head + "\n" + list.join("\n")
    end
  end
  
  # 
  # CCFW module function.
  # 
  # item_list is a collection of Symbols
  # represent names of items you want to your config class.
  # 
  def create_config_class(*item_list)
    c = Class.new(BasicConfigulation)
    c.class_eval{
      const_set(:ItemList, item_list.dup)
      def_items *self::ItemList
    }
    c
  end
  module_function :create_config_class
end

使い方はこんな感じ。

require 'config_frame'

module SomeApplication
  Config = CCFW.create_config_class(
    :encording,
    :home_directory,
    :bin_directory
  )
end

config = SomeApplication::Config.new("For Example") do
  encording      "UTF-8"
  home_directory "~/app/home"
  bin_directory  "#{home_directory}/bin"
end

p config.encording       #=> "UTF-8"
p config.home_directory  #=> "~/app/home"
p config.bin_directory   #=> "~/app/home/bin"

config.encording "Shift-JIS"  #Error!!