1 ## 2 # 3 # A scriptable configuration file is a configuration file that is 4 # actually a Ruby script. Or maybe it's a Ruby script that's really a 5 # configuration file in disguise. Either way, this is a powerful 6 # combination. 7 # 8 # The ScriptableConfig class is used to evaluate a Ruby script and 9 # generate a data structure that holds the configuration information 10 # contained in the Ruby script. The format for such a Ruby script looks 11 # like this: 12 # 13 # <tt> 14 # section_name { 15 # option_name parameter 16 # another_var_name another_param 17 # } 18 # </tt> 19 # 20 # All configuration elements are set through method calls (in the above 21 # example, section_name and option_name are actually method calls -- 22 # remember that parens are optional in Ruby). Options are set through 23 # methods that take parameters; sections are created through methods 24 # that take a block as a parameter. 25 # 26 # Every ScriptableConfig object is created with a template that 27 # describes which configuration options and sections are valid. This 28 # template is a Hash that maps the name of the option/section to a 29 # factory that creates configuration elements. 30 # 31 # There are a few basic types of standard elements: Option, Section, and 32 # SectionTable. Read the class-specific documentation for more 33 # information about each. It is possible to create user-defined element 34 # types; see the source for an example of how to do this. 35 # 36 # Example: 37 # 38 # <tt> 39 # module ConfigTemplate 40 # extend ScriptableConfig::Template 41 # Tmpl = { 42 # :foo => Option(), 43 # :bar => Option(RequireType(String)), 44 # :point => SectionTable ({ 45 # :x => Option(RequireType(Numeric)), 46 # :y => Option(RequireType(Numberic)) 47 # }) 48 # } 49 # end 50 # c = Config.new(ConfigTemplate::Tmpl) 51 # c.read_file("config.rb") 52 # c.validate() 53 # </tt> 54 # 55 class ScriptableConfig 56 public 57 ## 58 # The Template module holds the standard configuration templates. 59 # 60 module Template 61 ## 62 # Specifies a configuration option. Configuration options are 63 # standard Ruby objects, and are set in the configuration file by 64 # calling a method, e.g.: 65 # 66 # <tt> 67 # magic_number 42 68 # </tt> 69 # 70 # Produces a key-value pair: 71 # 72 # <tt> 73 # :magic_number => 42 74 # </tt> 75 # 76 # (this key-value pair will be part of a Hash or other structure 77 # when the configuration file is read) 78 # 79 # The validator is a proc that does a sanity check on the object. 80 # The default is nil, which means no validation. 81 # 82 def Option(validator=nil) 83 return OptionFactory.new(validator) 84 end 85 module_function :Option 86 87 ## 88 # RequireType returns a proc that checks an object to make sure it 89 # is the given type. It can be used to generate a validator for an 90 # option. 91 # 92 def RequireType(type) 93 return proc { |obj| 94 if not obj.kind_of?(type) then 95 raise TypeError, "Expecting #{type}; got #{obj.class}" 96 end 97 } 98 end 99 100 ## 101 # Specifies a configuration section. Configuration sections are 102 # Hashes mapping an option name to a configuration element. They 103 # are set in the configuration file by passing a block to a method, 104 # e.g.: 105 # 106 # <tt> 107 # server_info { 108 # name "httpd" 109 # listen_port 80 110 # listen_address Socket::INADDR_ANY 111 # } 112 # </tt> 113 # 114 # Produces a key-value pair where the value is a Hash: 115 # 116 # <tt> 117 # :server_info => { 118 # :name => "http", 119 # :listen_port => 80, 120 # :listen_address => Socket::INADDR_INY 121 # } 122 # 123 def Section(template) 124 return SectionFactory.new(template) 125 end 126 module_function :Section 127 128 ## 129 # Specifies a table of configuration sections. A section table is a 130 # Hash of configuration sections. For example: 131 # 132 # <tt> 133 # interface("eth0") { 134 # address "192.168.1.1" 135 # firewall_enabled false 136 # } 137 # interface("eth1") { 138 # address "10.0.0.0" 139 # firewall_enabled true 140 # } 141 # </tt> 142 # 143 # Produces: 144 # 145 # <tt> 146 # :interface => { 147 # :eth0 => { 148 # :address => "192.168.1.1", 149 # :firewall_enabeld => false 150 # }, 151 # eth1 => { 152 # :address => "10.0.0.0", 153 # :firewall_enabled => true 154 # } 155 # } 156 # </tt> 157 # 158 def SectionTable(template) 159 return SectionTableFactory.new(template) 160 end 161 module_function :SectionTable 162 end 163 164 class AlreadyExists < StandardError; end 165 class InvalidOption < StandardError; end 166 class OptionUnset < StandardError; end 167 168 ## 169 # Create a new ScriptableConfig, using the provided template. 170 # 171 def initialize(template) 172 @template = template 173 @reader = SectionReader.new(template) 174 end 175 176 ## 177 # Read the specified configuration file. The resulting configuration 178 # is a Hash, mapping names to a configuration elements, and can be 179 # accessed with the config() method. The 180 # 181 def read_file(filename) 182 File.open(filename) do |input| 183 read_str(input.read, filename) 184 end 185 end 186 187 ## 188 # Read str as if it were a configuration file. 189 # 190 def read_str(str, filename=nil) 191 filename ,= caller[0].split(':') if not filename 192 @reader.instance_eval(str, filename) 193 end 194 195 ## 196 # Return the configuration read by read_file() or read_str(). 197 # 198 def config 199 return @reader.members 200 end 201 202 ## 203 # Return a string representation of the current configuration. 204 # 205 def to_s 206 s = "#{self.class.name} {\n" 207 @template.each do |k, v| 208 s << INDENT_STR << k.to_s << " => " 209 s << v.stringify(@reader.members[k], 2) << "\n" 210 end 211 s << "}\n" 212 return s 213 end 214 215 ## 216 # 217 # Returns true if a configuration is valid (all required fields are 218 # set), or raises an exception if the configuration is invalid. 219 # 220 # Most errors will be caught when the configuration file is read, but 221 # those errors that are not will be caught here. 222 # 223 def validate 224 @template.each do |k, v| 225 obj = @reader.members.fetch(k) do 226 raise OptionUnset, "Unset: #{k} (#{v.class})" 227 end 228 v.validate(obj, k) 229 end 230 return true 231 end 232 233 private 234 ## 235 # An OptionFactory creates options. An option can be any Ruby object. 236 # 237 class OptionFactory 238 def initialize(validator) 239 @validator = validator 240 end 241 242 def create(orig, *args, &block) 243 raise AlreadyExists, "Option already set" if orig 244 raise ArgumentError, "Cannot pass block to option" if block 245 @validator.call(*args) if @validator 246 return args.size == 1 ? args[0] : args 247 end 248 249 def stringify(obj, indent) 250 return obj.inspect 251 end 252 253 def validate(obj, name) 254 return true 255 end 256 end 257 258 259 ## 260 # A SectionReader is a helper class that is used to read a section. 261 # 262 class SectionReader 263 attr_reader :members 264 265 def initialize(template) 266 @template = template 267 @members = {} 268 end 269 270 def method_missing(m, *args, &block) 271 if @template[m] then 272 @members[m] = @template[m].create(@members[m], *args, &block) 273 else 274 raise InvalidOption, "No such configuration option #{m}" 275 end 276 end 277 end 278 279 ## 280 # A SectionFactory creates sections (which, presently, are Hashes). 281 # 282 class SectionFactory 283 def initialize(template) 284 @template = template 285 end 286 287 def create(orig, &block) 288 raise AlreadyExists, "Section already created" if orig 289 section_reader = SectionReader.new(@template) 290 section_reader.instance_eval(&block) 291 return section_reader.members 292 end 293 294 def stringify(section, indent) 295 s = "{\n" 296 section.each do |k, v| 297 s << INDENT_STR * indent << k.to_s << " => " 298 s << @template[k].stringify(v, indent + 1) << "\n" 299 end 300 s << INDENT_STR * (indent - 1) << "}" 301 return s 302 end 303 304 def validate(section, name) 305 @template.each do |k, v| 306 obj = section.fetch(k) do 307 raise OptionUnset, "Unset: #{name}:#{k} (#{v.class})" 308 end 309 v.validate(obj, "#{name}:#{k}") 310 end 311 return true 312 end 313 end 314 315 ## 316 # A SectionTableFactory creates tables (hashes) of sections. 317 # 318 class SectionTableFactory 319 def initialize(template) 320 @template = template 321 @section_factory = SectionFactory.new(template) 322 end 323 324 def create(orig, item, &block) 325 table = orig || Hash.new 326 section = @section_factory.create(nil, &block) 327 table[item] = section 328 return table 329 end 330 331 def stringify(table, indent) 332 s = "{\n" 333 table.each do |section_id, section| 334 s << INDENT_STR * indent << section_id.to_s << " => " 335 s << @section_factory.stringify(section, indent + 1) << "\n" 336 end 337 s << INDENT_STR * (indent - 1) << "}" 338 return s 339 end 340 341 def validate(table, name) 342 table.each do |section_id, section| 343 @section_factory.validate(section, "#{name}(#{section_id})") 344 end 345 return true 346 end 347 end 348 349 INDENT_STR = " " 350 end 351 352 if __FILE__ == $0 then 353 module ConfigTemplate 354 extend ScriptableConfig::Template 355 Tmpl = { 356 :foo => Option(), 357 :bar => Option(RequireType(String)), 358 :point => SectionTable ({ 359 :x => Option(RequireType(Fixnum)), 360 :y => Option(RequireType(Fixnum)) 361 }) 362 } 363 end 364 c = ScriptableConfig.new(ConfigTemplate::Tmpl) 365 c.read_file("config.rb") 366 c.validate() 367 puts c 368 end