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