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