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!!