configtoolkit 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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