configtoolkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.txt ADDED
@@ -0,0 +1,3 @@
1
+ === 1.0.0 / 2008-05-12
2
+ Initial release of the ConfigToolkit. This release includes readers and
3
+ writers for YAML config files. The package is unit tested fully.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Designing Patterns
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ 'Software'), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.txt ADDED
@@ -0,0 +1,67 @@
1
+ = configtoolkit
2
+
3
+ * http://configtoolkit.rubyforge.org/
4
+
5
+ == DESCRIPTION:
6
+
7
+ This package makes sourcing information from configuration files robust
8
+ and easy! It:
9
+ * Allows programmers to specify the type of data that should be returned
10
+ from a configuration file. The toolkit automatically will validate
11
+ the file's data against this specification when loading the file, ensuring
12
+ that the specifications always are met and saving the programmer the tedious
13
+ chore of writing validation code.
14
+ * Allows programmers to easily programatically create and dump new
15
+ configuration files.
16
+ * Provides classes that can load from and dump to different kinds of
17
+ configuration files, including YAML configuration files.
18
+
19
+ == FEATURES/PROBLEMS:
20
+
21
+ None (known).
22
+
23
+ == SYNOPSIS:
24
+
25
+ class MachineConfig < ConfigToolkit::BaseConfig
26
+ add_required_param(:name, String)
27
+
28
+ add_required_param(:architecture, String)
29
+
30
+ add_required_param(:os, String)
31
+
32
+ add_required_param(:num_cpus, Integer) do |value|
33
+ if(value <= 0)
34
+ raise_error("num_cpus must be greater than zero")
35
+ end
36
+ end
37
+
38
+ add_optional_param(:behind_firewall, ConfigToolkit::Boolean, true)
39
+
40
+ add_required_param(:contains_sensitive_data, ConfigToolkit::Boolean)
41
+
42
+ add_required_param(:addresses, ConfigToolkit::ConstrainedArray.new(URI, 2, 3))
43
+
44
+ def validate_all_values
45
+ if(contains_sensitive_data && !behind_firewall)
46
+ raise_error("a machine cannot contain sensitive data and not be behind the firewall")
47
+ end
48
+ end
49
+ end
50
+
51
+ This code creates a configuration class for a company's servers. The
52
+ programmer specifies each of the parameters (the parameter's type, whether
53
+ the parameter is required, and a default value if the parameter is optional).
54
+
55
+ The programmer also specifies a method (validate_all_valus) that enforces
56
+ an invariant between multiple parameters.
57
+ == REQUIREMENTS:
58
+
59
+ Hoe is required, but only for running the tests.
60
+
61
+ == INSTALL:
62
+
63
+ sudo gem install configtoolkit
64
+
65
+ == LICENSE:
66
+
67
+ See LICENSE, at the root of the distribution.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # -*- ruby -*-
2
+
3
+ $LOAD_PATH.unshift("lib")
4
+
5
+ require 'rubygems'
6
+ require 'hoe'
7
+ require 'configtoolkit.rb'
8
+
9
+ $stderr = STDERR
10
+
11
+ Hoe.new('configtoolkit', ConfigToolkit::VERSION) do |p|
12
+ ENV[VERSION] = ConfigToolkit::VERSION
13
+ p.developer('DesigningPatterns', 'technical.inquiries@designingpatterns.com')
14
+ end
15
+
16
+ # vim: syntax=Ruby
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ruby
2
+ require 'configtoolkit/toolkit'
@@ -0,0 +1,555 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ require 'set'
5
+ require 'uri'
6
+
7
+ #
8
+ # Tomorrow:
9
+ # 7.) Ruby gems
10
+ # 11.) Write lots of readers + writers
11
+ # 13.) Clean up cast?
12
+ # 14.) Production comments
13
+ #
14
+ module ConfigToolkit
15
+
16
+ class BaseConfig
17
+ class ParamSpec
18
+ attr_reader :name
19
+ attr_reader :value_class
20
+ attr_reader :default_value # Only meaningful if is_required?
21
+
22
+ def initialize(name, value_class, is_required, default_value)
23
+ @name = name
24
+ @value_class = value_class
25
+ @is_required = is_required
26
+
27
+ if(!is_required)
28
+ @default_value = default_value
29
+ end
30
+ end
31
+
32
+ def is_required?
33
+ return @is_required
34
+ end
35
+ end
36
+
37
+ #
38
+ # The indentation used to indicate nesting when printing out
39
+ # a BaseConfig.
40
+ #
41
+ NESTING_INDENT = " " * 2
42
+
43
+ #
44
+ # Create accessors for some class instance variables;
45
+ # these variables will contain the structure of each
46
+ # BaseConfig child class (which parameters are supported,
47
+ # which are required, etc.).
48
+ #
49
+ # The config parameters are stored in a list (@param_list) for ordered
50
+ # access (we want to print them out in the order in which they
51
+ # were added to the config) and in a hash (@param_hash), for fast lookups
52
+ # (as we want to check whether a parameter that we read from a file
53
+ # is supported by the class).
54
+ #
55
+ class << self
56
+ attr_reader :param_spec_list
57
+ attr_reader :param_spec_lookup_table
58
+ attr_reader :longest_param_name_length
59
+ attr_reader :required_params
60
+ end
61
+
62
+ def self.inherited(child_class)
63
+ #
64
+ # Initialize the class instance variables.
65
+ #
66
+ child_class.class_eval do
67
+ @param_spec_list = []
68
+ @param_spec_lookup_table = {}
69
+ @longest_param_name_length = 0
70
+ @required_params = Set.new()
71
+ end
72
+ end
73
+
74
+ def self.add_param(name,
75
+ value_class,
76
+ is_required,
77
+ default_value,
78
+ &validate_value_block)
79
+ param_spec = ParamSpec.new(name, value_class, is_required, default_value)
80
+ @param_spec_list.push(param_spec)
81
+ @param_spec_lookup_table[name] = param_spec
82
+
83
+ #
84
+ # Define the setter method with a string passed to class_eval rather
85
+ # than a block, so as to avoid the need to access the reflectoin APIs
86
+ # when the setter actually is called.
87
+ #
88
+ # Note that the setter will call load_value_impl() on any value
89
+ # that it is passed; this ensures that data specified programatically
90
+ # gets processed the same way as that sourced from files.
91
+ #
92
+ # There is a bit of hackiness to get the specified value class; the
93
+ # value class is retrieved from a lookup table rather
94
+ # than being sourced from value_class. The reason for this is that the
95
+ # value_class argument could be an anonymous class without a valid name
96
+ # and so cannot safely be expanded in a string. Instead, we'll access a
97
+ # variable storing this class from the param_spec_lookup_table.
98
+ #
99
+ methods = String.new()
100
+ methods << "attr_reader :#{name}\n"
101
+ methods << "\n"
102
+ methods << "def #{name}=(value)\n"
103
+ methods << " value_class = self.class.param_spec_lookup_table[:#{name}].value_class\n"
104
+ methods << " loaded_value = load_value_impl(\"#{name}\", value_class, value)\n"
105
+ if(validate_value_block != nil)
106
+ validate_value_method = "validate_#{name}_value".to_sym()
107
+ methods << " begin\n"
108
+ methods << " #{validate_value_method}(value)\n"
109
+ methods << " rescue Error => e\n"
110
+ methods << " raise Error, construct_error_message(\"#{name}\", value, e.message), e.backtrace()\n"
111
+ methods << " end\n"
112
+ end
113
+
114
+ methods << " @#{name} = loaded_value\n"
115
+ methods << " @params_with_values.add(:#{name})\n"
116
+ methods << "end\n"
117
+
118
+ class_eval(methods)
119
+
120
+ if(validate_value_block != nil)
121
+ define_method(validate_value_method, &validate_value_block)
122
+ end
123
+
124
+ name_str = name.to_s
125
+ if(name_str.length > @longest_param_name_length)
126
+ @longest_param_name_length = name_str.length
127
+ end
128
+
129
+ if(is_required)
130
+ @required_params.add(name)
131
+ end
132
+ end
133
+ private_class_method :add_param
134
+
135
+ def self.add_required_param(name,
136
+ value_class,
137
+ &validate_value_block)
138
+ add_param(name, value_class, true, nil, &validate_value_block)
139
+ end
140
+ private_class_method :add_required_param
141
+
142
+ def self.add_optional_param(name,
143
+ value_class,
144
+ default_value,
145
+ &validate_value_block)
146
+ add_param(name, value_class, false, default_value, &validate_value_block)
147
+ end
148
+ private_class_method :add_optional_param
149
+
150
+ attr_reader :containing_object_name
151
+ def initialize
152
+ @containing_object_name = ""
153
+ @params_with_values = Set.new()
154
+
155
+ self.class.param_spec_list.each do |param_spec|
156
+ instance_variable_set("@#{param_spec.name}", nil)
157
+ end
158
+ end
159
+
160
+ def enforce_specs(operation_name)
161
+ #
162
+ # Iterate through the parameters without values. If any are
163
+ # required parameters, then throw (after completing the
164
+ # iteration in order to get the full list of missing
165
+ # parameters). Otherwise, set all optional, missing
166
+ # parameters to their default values.
167
+ #
168
+ missing_params = []
169
+
170
+ self.class.param_spec_list.each do |param_spec|
171
+ if(!@params_with_values.include?(param_spec.name))
172
+ if(param_spec.is_required?)
173
+ missing_params.push(param_spec.name)
174
+ else
175
+ send("#{param_spec.name}=", param_spec.default_value)
176
+ end
177
+ end
178
+ end
179
+
180
+ if(!missing_params.empty?)
181
+ missing_param_spec_list = missing_params.map do |param_name|
182
+ if(@containing_object_name.empty?)
183
+ next "#{param_name}"
184
+ else
185
+ next "#{@containing_object_name}.#{param_name}"
186
+ end
187
+ end
188
+
189
+ raise Error, "missing parameter(s): #{missing_param_spec_list.join(", ")}"
190
+ end
191
+
192
+ if(respond_to?(:validate_all_values))
193
+ begin
194
+ validate_all_values()
195
+ rescue Error => e
196
+ message = "#{self.class}#validate_all_values #{operation_name} error"
197
+
198
+ if(!@containing_object_name.empty?)
199
+ message << " for #{@containing_object_name}"
200
+ end
201
+
202
+ message << ": #{e.message}"
203
+ raise Error, message, e.backtrace()
204
+ end
205
+ end
206
+ end
207
+ private :enforce_specs
208
+
209
+ #
210
+ # Because the to_s in Ruby 1.8 for the Array and Hash classes
211
+ # are horrible...
212
+ #
213
+ def construct_value_str(value)
214
+ if(value.is_a?(Array))
215
+ str = "["
216
+
217
+ value.each_with_index do |element, index|
218
+ str << construct_value_str(element)
219
+
220
+ if(index != (value.size - 1))
221
+ str << ", "
222
+ end
223
+ end
224
+
225
+ str << "]"
226
+ return str
227
+ elsif(value.is_a?(Hash))
228
+ str = "{"
229
+
230
+ value.each_with_index do |key_value, index|
231
+ str << "#{construct_value_str(key_value[0])}=>#{construct_value_str(key_value[1])}"
232
+
233
+ if(index != (value.size - 1))
234
+ str << ", "
235
+ end
236
+ end
237
+
238
+ str << "}"
239
+ return str
240
+ else
241
+ return value.to_s
242
+ end
243
+ end
244
+ private :construct_value_str
245
+
246
+ def construct_error_message(param_name, value, message)
247
+ if(@containing_object_name.empty?)
248
+ full_param_name = ""
249
+ else
250
+ full_param_name = "#{@containing_object_name}."
251
+ end
252
+
253
+ full_param_name += param_name
254
+
255
+ return "error setting parameter #{full_param_name} with value #{construct_value_str(value)}: #{message}."
256
+ end
257
+ private :construct_error_message
258
+
259
+ def raise_error(message)
260
+ raise Error, message, caller()
261
+ end
262
+
263
+ def ==(rhs)
264
+ if(rhs == nil)
265
+ return false
266
+ end
267
+
268
+ self.class.param_spec_list.each do |param_spec|
269
+ if(send(param_spec.name) != rhs.send(param_spec.name))
270
+ return false
271
+ end
272
+ end
273
+
274
+ return true
275
+ end
276
+
277
+ def dump_value_impl(value_class, value)
278
+ if(value_class < BaseConfig)
279
+ return value.dump_impl({})
280
+ elsif(value_class < ConstrainedArray)
281
+ dumped_array = []
282
+ value.each do |element|
283
+ dumped_array.push(dump_value_impl(value_class.element_class, element))
284
+ end
285
+ return dumped_array
286
+ else
287
+ return value
288
+ end
289
+ end
290
+ private :dump_value_impl
291
+
292
+ def dump_impl(containing_object_hash)
293
+ #
294
+ # Configuration never will be dumped without the
295
+ # specifications first having been enforced.
296
+ #
297
+ enforce_specs("dump")
298
+
299
+ self.class.param_spec_list.each do |param_spec|
300
+ containing_object_hash[param_spec.name.to_s] = dump_value_impl(param_spec.value_class, send(param_spec.name))
301
+ end
302
+
303
+ return containing_object_hash
304
+ end
305
+ protected :dump_impl
306
+
307
+ def dump(writer)
308
+ dump_hash = {}
309
+ containing_object_hash = dump_hash
310
+
311
+ @containing_object_name.split(".").each do |object_name|
312
+ object_hash = {}
313
+ containing_object_hash[object_name] = object_hash
314
+ containing_object_hash = object_hash
315
+ end
316
+
317
+ dump_impl(containing_object_hash)
318
+ writer.write(dump_hash)
319
+ end
320
+
321
+ def load_value_impl(param_name, value_class, value)
322
+ if(value.class <= value_class)
323
+ #
324
+ # If the ConfigReader has returned a value of the same class as
325
+ # was specified for that parameter (or a child class of the specified
326
+ # class), then just return the value.
327
+ #
328
+ return value
329
+ elsif((value_class < BaseConfig) && (value.class == Hash))
330
+ #
331
+ # If we're looking for a BaseConfig child and have a hash, then
332
+ # construct the child with the hash.
333
+ #
334
+ child_config = value_class.new()
335
+
336
+ if(@containing_object_name.empty?)
337
+ nested_containing_object_name = param_name
338
+ else
339
+ nested_containing_object_name = "#{@containing_object_name}.#{param_name}"
340
+ end
341
+
342
+ child_config.load_impl(value, nested_containing_object_name)
343
+ return child_config
344
+ elsif((value_class < ConstrainedArray) && (value.class == Array))
345
+ #
346
+ # If we're looking for a ConstrainedArray and have a Ruby Array, then
347
+ # iterate over each element of the Ruby Array and recursively
348
+ # load it.
349
+ #
350
+ value.each_with_index do |element, index|
351
+ value[index] = load_value_impl("#{param_name}[#{index}]", value_class.element_class, element)
352
+ end
353
+
354
+ if((value_class.min_num_elements != nil) &&
355
+ (value.length < value_class.min_num_elements))
356
+ message = "the number of elements (#{value.length}) is less than the specified "
357
+ message << "minimum number of elements (#{value_class.min_num_elements})"
358
+ raise Error, construct_error_message(param_name, value, message)
359
+ end
360
+
361
+ if((value_class.max_num_elements != nil) &&
362
+ (value.length > value_class.max_num_elements))
363
+ message = "the number of elements (#{value.length}) is greater than the specified "
364
+ message << "maximum number of elements (#{value_class.max_num_elements})"
365
+ raise Error, construct_error_message(param_name, value, message)
366
+ end
367
+
368
+ return value
369
+ elsif((value_class == Boolean) &&
370
+ ((value.class == TrueClass) || (value.class == FalseClass)))
371
+ #
372
+ # If we're looking for a Boolean, then we just can return
373
+ # the ConfigReader's value if it's true or false.
374
+ #
375
+ return value
376
+ elsif(value.class == String)
377
+ #
378
+ # Some ConfigReaders only may return strings and leave it up to
379
+ # the caller to figure out the proper types for the parameters.
380
+ # In our case, we'll levarage Ruby's built-in conversion functions
381
+ # to support the most common parameter types. Should this be
382
+ # made extensible?
383
+ #
384
+ case
385
+ when (value_class <= Integer)
386
+ return Integer(value)
387
+ when (value_class <= Float)
388
+ return Float(value)
389
+ when (value_class <= Pathname)
390
+ return Pathname(value)
391
+ when (value_class <= Symbol)
392
+ return value.to_sym()
393
+ when (value_class <= URI)
394
+ return URI(value)
395
+ when (value_class == Boolean)
396
+ #
397
+ # Support both yes/no and true/false boolean values.
398
+ #
399
+ if((value.casecmp("yes") == 0) || (value.casecmp("true") == 0))
400
+ return true
401
+ elsif((value.casecmp("no") == 0)|| (value.casecmp("false") == 0))
402
+ return false
403
+ end
404
+ end
405
+ end
406
+
407
+ message = "cannot convert from value class #{value.class.name} to specified class #{value_class.name}"
408
+ raise Error, construct_error_message(param_name, value, message)
409
+ end
410
+ private :load_value_impl
411
+
412
+ def load_impl(containing_object_hash, containing_object_name)
413
+ @containing_object_name = containing_object_name
414
+ @params_with_values.clear()
415
+
416
+ if(containing_object_hash != nil)
417
+ containing_object_hash.each do |name, value|
418
+ #
419
+ # Lookup the spec by name. If the parameter is not found, the
420
+ # config file must have extra parameters in it, which is not
421
+ # an error (should it be?); we just skip the unsupported
422
+ # parameter.
423
+ #
424
+ param_spec = self.class.param_spec_lookup_table.fetch(name.to_sym(), nil)
425
+
426
+ if(param_spec == nil)
427
+ next
428
+ end
429
+
430
+ #
431
+ # Invoke the parameter's setter.
432
+ #
433
+ send("#{param_spec.name}=", value)
434
+ end
435
+ end
436
+
437
+ enforce_specs("load")
438
+ end
439
+ protected :load_impl
440
+
441
+ def load(reader, containing_object_name = "")
442
+ param_hash = reader.read
443
+
444
+ if(param_hash.class != Hash)
445
+ message = "#{reader.class}#read returned #{param_hash.class} "
446
+ message << "rather than Hash"
447
+ raise Error, message
448
+ end
449
+
450
+ #
451
+ # Work through the param_hash until containing_object_name is found by
452
+ # splitting up containing_object_name into an object list and
453
+ # hashing for each. For example, production.webserver.tokyo would
454
+ # result in a lookup for production, the results of which them would be
455
+ # searched for webserver, the results of which finally would be search
456
+ # for tokyo. Not being able to find one of the objects is not
457
+ # (necessarily) an error; that just means that none of the parameters will
458
+ # be set from param_hash, which is allowable if all of the parameters
459
+ # are optional (this might happen, for instance, if the config file
460
+ # is empty). On the other hand, if an object is found but it is not
461
+ # really an object (a Hash), then raise an exception.
462
+ #
463
+ containing_object_hash = param_hash
464
+ containing_object_name.split(".").each do |object_name|
465
+ containing_object_hash = containing_object_hash.fetch(object_name, nil)
466
+
467
+ if(containing_object_hash == nil)
468
+ break
469
+ elsif(containing_object_hash.class != Hash)
470
+ message = "error: #{self.class}#load found "
471
+ message << "#{containing_object_hash.class} "
472
+ message << "rather than Hash when reading the "
473
+ message << "#{containing_object_name} containing object"
474
+ raise Error, message
475
+ end
476
+ end
477
+
478
+ load_impl(containing_object_hash, containing_object_name)
479
+ end
480
+
481
+ def self.load(reader, containing_object_name = "")
482
+ instance = new()
483
+ instance.load(reader, containing_object_name)
484
+ return instance
485
+ end
486
+
487
+ def write_value_impl(str, indent, value_class, value)
488
+ nesting_indent = indent + NESTING_INDENT
489
+ if(value != nil)
490
+ if(value_class < BaseConfig)
491
+ #
492
+ # Writing a nested config requires calling its write(),
493
+ # just at one further indentation level.
494
+ #
495
+ str << "{\n"
496
+ value.write_impl(str, nesting_indent)
497
+ str << indent << "}"
498
+ elsif(value_class < ConstrainedArray)
499
+ #
500
+ # Writing an array requires going through each element and
501
+ # calling write_value_impl, just at one further indentation
502
+ # level.
503
+ #
504
+ str << "[\n"
505
+
506
+ value.each_with_index do |element, index|
507
+ str << nesting_indent
508
+ write_value_impl(str, nesting_indent, value_class.element_class, element)
509
+
510
+ if(index != (value.length - 1))
511
+ str << ","
512
+ end
513
+
514
+ str << "\n"
515
+ end
516
+
517
+ str << indent << "]"
518
+ else
519
+ str << "#{value}"
520
+ end
521
+ end
522
+ end
523
+ private :write_value_impl
524
+
525
+ def write_impl(str, indent)
526
+ self.class.param_spec_list.each do |param_spec|
527
+ name_field_length = self.class.longest_param_name_length
528
+ str << indent << param_spec.name.to_s.ljust(name_field_length) << ": "
529
+ value = send(param_spec.name)
530
+ write_value_impl(str, indent, param_spec.value_class, value)
531
+ str << "\n"
532
+ end
533
+ end
534
+ protected :write_impl
535
+
536
+ def to_s
537
+ #
538
+ # Print out config file name and the containing object_name and then
539
+ # call write.
540
+ #
541
+ str = String.new()
542
+
543
+ if(!@containing_object_name.empty?)
544
+ str << "#{@containing_object_name}: "
545
+ end
546
+
547
+ str << "{\n"
548
+ write_impl(str, NESTING_INDENT)
549
+ str << "}\n"
550
+
551
+ return str
552
+ end
553
+ end
554
+
555
+ end