configtoolkit 1.2.0 → 2.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.
Files changed (81) hide show
  1. data/FAQ.txt +113 -25
  2. data/Hash.txt +128 -0
  3. data/History.txt +45 -2
  4. data/KeyValue.txt +235 -0
  5. data/Manifest.txt +66 -4
  6. data/README.txt +666 -202
  7. data/Rakefile +3 -5
  8. data/Ruby.txt +172 -0
  9. data/YAML.txt +188 -0
  10. data/examples/hash_example.rb +71 -0
  11. data/examples/key_value_example.dump.cfg +5 -0
  12. data/examples/key_value_example.normal1.cfg +20 -0
  13. data/examples/key_value_example.normal2.cfg +15 -0
  14. data/examples/key_value_example.rb +72 -0
  15. data/examples/key_value_example.wacky.cfg +13 -0
  16. data/examples/load_example.rb +32 -0
  17. data/examples/load_example.yaml +34 -0
  18. data/examples/load_group_example.rb +28 -0
  19. data/examples/load_group_example.yaml +33 -0
  20. data/examples/machineconfig.rb +77 -0
  21. data/examples/ruby_example.rb +32 -0
  22. data/examples/ruby_example.rcfg +52 -0
  23. data/examples/usage_example.rb +93 -0
  24. data/examples/yaml_example.dump.yaml +12 -0
  25. data/examples/yaml_example.rb +48 -0
  26. data/examples/yaml_example.yaml +59 -0
  27. data/lib/configtoolkit.rb +1 -1
  28. data/lib/configtoolkit/baseconfig.rb +522 -418
  29. data/lib/configtoolkit/hasharrayvisitor.rb +242 -0
  30. data/lib/configtoolkit/hashreader.rb +41 -0
  31. data/lib/configtoolkit/hashwriter.rb +45 -0
  32. data/lib/configtoolkit/keyvalueconfig.rb +105 -0
  33. data/lib/configtoolkit/keyvaluereader.rb +597 -0
  34. data/lib/configtoolkit/keyvaluewriter.rb +157 -0
  35. data/lib/configtoolkit/prettyprintwriter.rb +167 -0
  36. data/lib/configtoolkit/reader.rb +62 -0
  37. data/lib/configtoolkit/rubyreader.rb +270 -0
  38. data/lib/configtoolkit/types.rb +42 -26
  39. data/lib/configtoolkit/writer.rb +116 -0
  40. data/lib/configtoolkit/yamlreader.rb +10 -6
  41. data/lib/configtoolkit/yamlwriter.rb +113 -71
  42. data/test/bad_array_index.rcfg +1 -0
  43. data/test/bad_containing_object_assignment.rcfg +2 -0
  44. data/test/bad_directive.cfg +1 -0
  45. data/test/bad_include1.cfg +2 -0
  46. data/test/bad_include2.cfg +3 -0
  47. data/test/bad_parameter_reference.rcfg +1 -0
  48. data/test/contained_sample.cfg +10 -0
  49. data/test/contained_sample.pretty_print +30 -0
  50. data/test/contained_sample.rcfg +22 -0
  51. data/test/contained_sample.yaml +22 -21
  52. data/test/containers.cfg +6 -0
  53. data/test/exta_string_after_container.cfg +2 -0
  54. data/test/extra_container_closing.cfg +2 -0
  55. data/test/extra_string_after_container.cfg +2 -0
  56. data/test/extra_string_after_nested_container.cfg +1 -0
  57. data/test/missing_array_closing.cfg +2 -0
  58. data/test/missing_array_element.cfg +2 -0
  59. data/test/missing_directive.cfg +1 -0
  60. data/test/missing_hash_closing.cfg +2 -0
  61. data/test/missing_hash_element.cfg +2 -0
  62. data/test/missing_hash_value.cfg +1 -0
  63. data/test/missing_include_argument.cfg +1 -0
  64. data/test/missing_key_value_delimiter.cfg +1 -0
  65. data/test/readerwritertest.rb +28 -7
  66. data/test/sample.cfg +10 -0
  67. data/test/sample.pretty_print +30 -0
  68. data/test/sample.rcfg +26 -0
  69. data/test/test_baseconfig.rb +152 -38
  70. data/test/test_hash.rb +82 -0
  71. data/test/test_keyvalue.rb +185 -0
  72. data/test/test_prettyprint.rb +28 -0
  73. data/test/test_ruby.rb +50 -0
  74. data/test/test_yaml.rb +33 -26
  75. data/test/wacky_sample1.cfg +16 -0
  76. data/test/wacky_sample2.cfg +5 -0
  77. data/test/wacky_sample3.cfg +4 -0
  78. data/test/wacky_sample4.cfg +1 -0
  79. metadata +101 -10
  80. data/lib/configtoolkit/toolkit.rb +0 -20
  81. data/test/common.rb +0 -5
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module ConfigToolkit
4
+
5
+ #
6
+ # This class allows each node of an Array or Hash containing
7
+ # arbitrarily deeply nested Arrays or Hashes to be visited. Users extend
8
+ # this class for a particular task and override some or all of the
9
+ # +visit_*+, +enter_*+, and +leave_*+ methods to accomplish the task. They
10
+ # then call the +visit+ method in order to visit the nodes of a structure.
11
+ #
12
+ # This class was written in order to factor out (fairly complicated) iteration
13
+ # logic from methods that need to process each element of one of these
14
+ # structures. Such methods almost invariably require a stack to store
15
+ # information for all levels above the level currently being processed.
16
+ # Previously, when these methods recursively iterated over these structures,
17
+ # they used the call stack for this purpose. The HashArrayVisitor, however,
18
+ # provides its own stack. All of the +enter_*+ methods are expected to
19
+ # return a new stack frame for the container being entered, which will be
20
+ # accessible via the stack_top method while processing elements of the
21
+ # container. When the +leave_*+ method is called for the container, the
22
+ # associated stack frame will be popped from the top of the stack and
23
+ # passed to the +leave_*+ method. An initial stack frame can be specified
24
+ # in new, which often is handy as it ensures that there always will
25
+ # be at least one element in the stack. The HashArrayVisitor base class
26
+ # has no knowledge of the contents of a stack frame, and so the stack frames
27
+ # can be of any type (even +nil+ if no stack is required).
28
+ #
29
+ # PrettyPrintWriter, YAMLWriter, and KeyValueWriter all use this class.
30
+ #
31
+ class HashArrayVisitor
32
+ #
33
+ # ====Description:
34
+ # This constructs the visitor with initial stack frame
35
+ # +initial_stack_frame+.
36
+ #
37
+ # ====Parameters:
38
+ # [initial_stack_frame]
39
+ # The initial stack frame; if not specified, then the stack
40
+ # initially will be empty. Specifying an initial stack frame
41
+ # ensures that the stack never will be empty while visiting
42
+ # a structure (this frame never will be popped).
43
+ #
44
+ def initialize(initial_stack_frame = nil)
45
+ @stack = []
46
+
47
+ if(initial_stack_frame != nil)
48
+ @stack.push(initial_stack_frame)
49
+ end
50
+ end
51
+
52
+ #
53
+ # ====Returns:
54
+ # +true+ if and only if value is a container
55
+ #
56
+ def container_value?(value)
57
+ return (value.is_a?(Hash) || value.is_a?(Array))
58
+ end
59
+ private :container_value?
60
+
61
+ #
62
+ # ====Description:
63
+ # This method visits the nodes of +structure+, calling the appropriate
64
+ # method for each node.
65
+ #
66
+ # ====Parameters:
67
+ # [structure]
68
+ # The Hash or Array whose nodes are to be visited (if +structure+'s
69
+ # nodes themselves are Arrays or Hashes, the nodes of these
70
+ # nested structures also will be visited).
71
+ #
72
+ def visit(structure)
73
+ if(structure.is_a?(Hash))
74
+ @stack.push(enter_hash(structure))
75
+
76
+ structure.each_with_index do |key_value, index|
77
+ key = key_value[0]
78
+ value = key_value[1]
79
+
80
+ is_container_value = container_value?(value)
81
+ visit_hash_element(key, value, index, is_container_value)
82
+
83
+ if(is_container_value)
84
+ visit(value)
85
+ end
86
+ end
87
+
88
+ leave_hash(@stack.pop())
89
+ elsif(structure.is_a?(Array))
90
+ @stack.push(enter_array(structure))
91
+
92
+ structure.each_with_index do |element, index|
93
+ is_container_value = container_value?(element)
94
+ visit_array_element(element, index, is_container_value)
95
+
96
+ if(is_container_value)
97
+ visit(element)
98
+ end
99
+ end
100
+
101
+ leave_array(@stack.pop())
102
+ else
103
+ message = "The argument to HashArrayVisitor::visit must be a "
104
+ message << "Hash or Array; instead, a #{structure.class} "
105
+ message << "(#{structure}) was passed!"
106
+ raise ArgumentError, message
107
+ end
108
+ end
109
+
110
+ #
111
+ # ====Returns:
112
+ # The top stack frame
113
+ #
114
+ def stack_top
115
+ return @stack[-1]
116
+ end
117
+
118
+ #
119
+ # ====Returns:
120
+ # True if and only if the stack is empty
121
+ #
122
+ def stack_empty?
123
+ return @stack.empty?()
124
+ end
125
+
126
+ #
127
+ # ====Description:
128
+ # This method is called when a Hash is visited for the first time. It
129
+ # must return the new stack frame for the Hash.
130
+ #
131
+ # This can be overridden; the default implementation is a no-op
132
+ # and returns +nil+.
133
+ #
134
+ # ====Parameters:
135
+ # [hash]
136
+ # The Hash being visited
137
+ #
138
+ # ====Returns:
139
+ # The new stack frame for the Hash
140
+ #
141
+ def enter_hash(hash)
142
+ return nil
143
+ end
144
+
145
+ #
146
+ # ====Description:
147
+ # This method is called when finished visiting all elements of a Hash.
148
+ # The stack will be popped before the call (so stack_top will
149
+ # return the frame for the Hash's parent), but the popped stack
150
+ # frame is passed as +popped_stack_frame+.
151
+ #
152
+ # This can be overridden; the default implementation is a no-op.
153
+ #
154
+ # ====Parameters:
155
+ # [popped_stack_frame]
156
+ # The stack frame associated with the Hash, which has been
157
+ # popped from the stack.
158
+ #
159
+ def leave_hash(popped_stack_frame)
160
+ end
161
+
162
+ #
163
+ # ====Description:
164
+ # This method is called when visiting a Hash element
165
+ # mapping +key+ to +value+.
166
+ #
167
+ # This can be overridden; the default implementation is a no-op.
168
+ #
169
+ # ====Parameters:
170
+ # [key]
171
+ # The key in the Hash element
172
+ # [value]
173
+ # The value in the Hash element
174
+ # [index]
175
+ # The index of key=>value in the Hash (the index that is returned by
176
+ # +each_with_index+)
177
+ # [is_container_value]
178
+ # Whether or not +value+ is a container (in
179
+ # which case, it will be visited next with a call to
180
+ # +enter_array+ or +enter_hash+)
181
+ #
182
+ def visit_hash_element(key, value, index, is_container_value)
183
+ end
184
+
185
+ #
186
+ # ====Description:
187
+ # This method is called when an Array is visited for the first time. It
188
+ # must return the new stack frame for the Array.
189
+ #
190
+ # This can be overridden; the default implementation is a no-op
191
+ # and returns +nil+.
192
+ #
193
+ # ====Parameters:
194
+ # [array]
195
+ # The Array being visited
196
+ #
197
+ # ====Returns:
198
+ # The new stack frame for the Array
199
+ #
200
+ def enter_array(array)
201
+ return nil
202
+ end
203
+
204
+ #
205
+ # ====Description:
206
+ # This method is called when finished visiting all elements of an Array.
207
+ # The stack will be popped before the call (so stack_top will
208
+ # return the frame for the Array's parent), but the popped stack
209
+ # frame is passed as +popped_stack_frame+.
210
+ #
211
+ # This can be overridden; the default implementation is a no-op.
212
+ #
213
+ # ====Parameters:
214
+ # [popped_stack_frame]
215
+ # The stack frame associated with the Array, which has been
216
+ # popped from the stack.
217
+ #
218
+ def leave_array(popped_stack_frame)
219
+ end
220
+
221
+ #
222
+ # ====Description:
223
+ # This method is called when visiting an Array element +value+ at
224
+ # +index+ in the Array.
225
+ #
226
+ # This can be overridden; the default implementation is a no-op.
227
+ #
228
+ # ====Parameters:
229
+ # [value]
230
+ # The Array element
231
+ # [index]
232
+ # The index of +value+ in the Array
233
+ # [is_container_value]
234
+ # Whether or not +value+ is a container (in
235
+ # which case, it will be visited next with a call to
236
+ # +enter_array+ or +enter_hash+)
237
+ #
238
+ def visit_array_element(value, index, is_container_value)
239
+ end
240
+ end
241
+
242
+ end
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'configtoolkit/reader'
4
+
5
+ module ConfigToolkit
6
+
7
+ #
8
+ # This class implements a no-op Reader interface. A Hash
9
+ # is specified in its constructor and is passed through to
10
+ # requesting BaseConfig instances in its read method.
11
+ #
12
+ # This allows BaseConfig instances to be programatically loaded from
13
+ # Hashes. For instance,
14
+ # config = SimpleConfig.load(ConfigToolkit::HashReader.new(:param1 => 1))
15
+ #
16
+ # See Hash.txt for more details.
17
+ #
18
+ class HashReader < Reader
19
+ #
20
+ # ====Description:
21
+ # This constructs a HashReader instance that will
22
+ # return +config_hash+ in read.
23
+ #
24
+ # ====Parameters:
25
+ # [config_hash]
26
+ # A Hash to be returned by read
27
+ #
28
+ def initialize(config_hash)
29
+ @config_hash = config_hash
30
+ end
31
+
32
+ #
33
+ # ====Returns:
34
+ # The Hash specified in the constructor
35
+ #
36
+ def read
37
+ return @config_hash
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'configtoolkit/writer'
4
+
5
+ module ConfigToolkit
6
+
7
+ #
8
+ # This class implements a no-op Writer interface. An instance
9
+ # simply stores the last Hash passed to its write method.
10
+ #
11
+ # See Hash.txt for more details.
12
+ #
13
+ class HashWriter < Writer
14
+ #
15
+ # This is set by the argument to write.
16
+ #
17
+ attr_reader :config_hash
18
+
19
+ #
20
+ # ====Returns:
21
+ # Returns +true+, since the HashWriter requires Symbol parameter names in the
22
+ # Hash passed to write.
23
+ #
24
+ def require_symbol_parameter_names?
25
+ return true
26
+ end
27
+
28
+ #
29
+ # ====Description:
30
+ # This method sets the +config_hash+ attribute to the +config_hash+ argument,
31
+ # modified to reflect a +containing_object_name+ containing object.
32
+ #
33
+ # ====Parameters:
34
+ # [config_hash]
35
+ # The new value of the +config_hash+ attribute, modified to reflect
36
+ # a +containing_object_name+ containing object
37
+ # [containing_object_name]
38
+ # The configuration's containing object name
39
+ #
40
+ def write(config_hash, containing_object_name)
41
+ @config_hash = create_containing_object_hash(config_hash, containing_object_name)
42
+ end
43
+ end
44
+
45
+ end
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'configtoolkit/baseconfig'
4
+
5
+ module ConfigToolkit
6
+
7
+ #
8
+ # This class configures instances of the KeyValueReader and KeyValueWriter
9
+ # classes. It essentially describes a key-value file format
10
+ # (what delimiter is used to separate keys and values, what string
11
+ # prefixes comments, etc).
12
+ #
13
+ # See KeyValue.txt for more details.
14
+ #
15
+ class KeyValueConfig < BaseConfig
16
+ #
17
+ # The String that separates keys and values (the default value is "=")
18
+ #
19
+ add_optional_param(:key_value_delimiter, String, "=")
20
+
21
+ #
22
+ # The String that prefixes comment lines, which the KeyValueReader
23
+ # ignores (the default value is "#")
24
+ #
25
+ add_optional_param(:comment_line_prefix, String, "#")
26
+
27
+ #
28
+ # The String that prefixes directive lines (only include directives
29
+ # currently are supported; the default value is "*")
30
+ #
31
+ add_optional_param(:directive_line_prefix, String, "*")
32
+
33
+ #
34
+ # The String used to separate object names in containing object names,
35
+ # which prefix keys in the key-value format (the default value is ".")
36
+ #
37
+ add_optional_param(:object_delimiter, String, ".")
38
+
39
+ #
40
+ # Whether or not this format supports array and hash values (the default
41
+ # value is +true+)
42
+ #
43
+ add_optional_param(:allow_containers, ConfigToolkit::Boolean, true)
44
+
45
+
46
+ #
47
+ # The character used to separate elements of a container (hash or array
48
+ # values; the default value is ",")
49
+ #
50
+ add_optional_param(:container_element_delimiter, String, ",") do |new_value|
51
+ if(new_value.size() > 1)
52
+ raise_error("container_element_delimiter only can be a single character")
53
+ end
54
+ end
55
+
56
+
57
+ #
58
+ # The character that begins an array (the default value is "[")
59
+ #
60
+ add_optional_param(:array_opening, String, "[") do |new_value|
61
+ if(new_value.size() > 1)
62
+ raise_error("array_opening only can be a single character")
63
+ end
64
+ end
65
+
66
+ #
67
+ # The character that ends an array (the default value is "]")
68
+ #
69
+ add_optional_param(:array_closing, String, "]") do |new_value|
70
+ if(new_value.size() > 1)
71
+ raise_error("array_closing only can be a single character")
72
+ end
73
+ end
74
+
75
+ #
76
+ # The character that begins a hash (the default value is "{")
77
+ #
78
+ add_optional_param(:hash_opening, String, "{") do |new_value|
79
+ if(new_value.size() > 1)
80
+ raise_error("hash_opening only can be a single character")
81
+ end
82
+ end
83
+
84
+ #
85
+ # The character that ends a hash (the default value is "}")
86
+ #
87
+ add_optional_param(:hash_closing, String, "}") do |new_value|
88
+ if(new_value.size() > 1)
89
+ raise_error("hash_closing only can be a single character")
90
+ end
91
+ end
92
+
93
+ #
94
+ # The String that separates keys and values within a hash (the default value
95
+ # is "=>")
96
+ #
97
+ add_optional_param(:hash_key_value_delimiter, String, "=>")
98
+
99
+ #
100
+ # Just use the default values for all of the parameters.
101
+ #
102
+ DEFAULT_CONFIG = self.new(){}
103
+ end
104
+
105
+ end
@@ -0,0 +1,597 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'configtoolkit/keyvalueconfig'
4
+ require 'configtoolkit/reader'
5
+ require 'configtoolkit/types'
6
+
7
+ require 'pathname'
8
+
9
+ module ConfigToolkit
10
+
11
+ #
12
+ # This class implements the Reader interface for
13
+ # key-value configuration files.
14
+ #
15
+ # See KeyValue.txt for more details about the key-value configuration format.
16
+ #
17
+ class KeyValueReader < Reader
18
+ #
19
+ # An internal error class that never is exposed to users of
20
+ # the KeyValueReader. Its main purpose is to be caught and
21
+ # repackaged with a more informative error message into an
22
+ # Error.
23
+ #
24
+ class ParsingError < RuntimeError # :nodoc:
25
+ end
26
+
27
+ #
28
+ # ====Description:
29
+ # This constructs a KeyValueReader instance for a key-value
30
+ # format described by +config+ for +stream+,
31
+ # where stream is either a file name (a String)
32
+ # or an IO object.
33
+ #
34
+ # ====Parameters:
35
+ # [stream]
36
+ # A file name (String) or an IO object. If
37
+ # +stream+ is a file name, then the KeyValueReader
38
+ # will open the associated file.
39
+ # [config]
40
+ # A KeyValueConfig that defines the format of the key-value
41
+ # input that this instance can read
42
+ #
43
+ def initialize(stream, config = KeyValueConfig::DEFAULT_CONFIG)
44
+ if(stream.class == String)
45
+ @stream = File.open(stream, "r")
46
+ else
47
+ @stream = stream
48
+ end
49
+
50
+ @stream_path = Reader.stream_path(stream)
51
+ @config = config
52
+ end
53
+
54
+ #
55
+ # ====Description:
56
+ # This method processes directive with arguments, modifying param_hash
57
+ # with the result.
58
+ #
59
+ # ====Parameters:
60
+ # [param_hash]
61
+ # The parameter hash that should be modified (Hash)
62
+ # [directive]
63
+ # The directive to process (String)
64
+ # [arguments]
65
+ # The directive's arguments (String)
66
+ # [stream_path]
67
+ # The path of the file currently being processed
68
+ #
69
+ def handle_directive(param_hash, directive, arguments, stream_path)
70
+ case(directive)
71
+ when "include"
72
+ if(arguments.empty?())
73
+ raise ParsingError, "missing filename for include directive"
74
+ end
75
+
76
+ #
77
+ # Relative include file paths should be made relative to the
78
+ # path of the file currently being parsed, *not* relative to
79
+ # the current working directory.
80
+ #
81
+ include_file_path = Pathname.new(arguments)
82
+ if(include_file_path.relative?())
83
+ if(stream_path == nil)
84
+ raise ParsingError, "only can process relative include directives in File streams"
85
+ end
86
+
87
+ include_file_path = stream_path.dirname() + include_file_path
88
+ end
89
+
90
+ included_stream = File.open(include_file_path, "r")
91
+ read_impl(param_hash, included_stream, include_file_path)
92
+ else
93
+ raise ParsingError, "unknown directive '#{directive}'"
94
+ end
95
+ end
96
+ private :handle_directive
97
+
98
+ #
99
+ # This class is *not* meant to be accessed by KeyValueReader
100
+ # users; it solely is to be used internally.
101
+ #
102
+ # This class represents a hash entry while the entry is being parsed.
103
+ #
104
+ class HashEntry #:nodoc:
105
+ attr_reader :key
106
+ attr_accessor :value
107
+
108
+ #
109
+ # ====Description:
110
+ # This initializes an empty instance (empty key and empty value).
111
+ # key_value_delimiter is the String that the instance will search
112
+ # for in order to know when to start appending characters to the
113
+ # value rather than to the key.
114
+ #
115
+ # ====Parameters:
116
+ # [key_value_delimiter]
117
+ # The String that will separate the key from the value
118
+ #
119
+ def initialize(key_value_delimiter)
120
+ @key = ""
121
+ @value = ""
122
+ @key_value_delimiter = key_value_delimiter
123
+ @curr_delimiter = ""
124
+ @finished_parsing_key = false
125
+ end
126
+
127
+ #
128
+ # ====Description:
129
+ # This method appends char to the hash entry. This method will
130
+ # figure out whether char should be appended to the key or the to value,
131
+ # depending on whether the key-value delimiter has been appended yet.
132
+ #
133
+ # ====Parameters:
134
+ # [char]
135
+ # The character (String) to append
136
+ #
137
+ def <<(char)
138
+ #
139
+ # If still parsing the key, check whether the new character matches
140
+ # the next unmatched character in the delimiter. If it does, then
141
+ # do not append the character to the key (yet), because the character
142
+ # may be part of the delimiter. If the all characters in the delimiter
143
+ # have been matched, then switch to parsing the value; otherwise,
144
+ # return in order to get the next character.
145
+ #
146
+ # If the new character does not match the next character in
147
+ # the delimiter, then append any prior characters that did match to the
148
+ # key since they now have been proven not to be part of the delimiter.
149
+ #
150
+ if(!@finished_parsing_key)
151
+ if(char == @key_value_delimiter[@curr_delimiter.size(), 1])
152
+ @curr_delimiter << char
153
+
154
+ if(@curr_delimiter.size() == @key_value_delimiter.size())
155
+ @finished_parsing_key = true
156
+ end
157
+ else
158
+ if(!@curr_delimiter.empty?())
159
+ @key << @curr_delimiter
160
+ @curr_delimiter = ""
161
+ end
162
+
163
+ @key << char
164
+ end
165
+ else
166
+ @value << char
167
+ end
168
+ end
169
+ end
170
+
171
+ #
172
+ # This class is *not* meant to be accessed by KeyValueReader
173
+ # users; it solely is to be used internally.
174
+ #
175
+ # This class represents a nesting level while parsing a container.
176
+ # When a container is encountered (perhaps within another container),
177
+ # one of these frames is pushed onto a stack. After the container has
178
+ # been completed, this frame is popped off the stack.
179
+ #
180
+ # A lot of logic of the form:
181
+ # if the contaier is an Array
182
+ # do something
183
+ # else if the container is a Hash
184
+ # do something else
185
+ # ...
186
+ # lives within this class, and so it is messy. If more containers
187
+ # are added, some kind of interface over a container could be developed
188
+ # to replace the conditionals with polymorphism.
189
+ #
190
+ # Right now, the following containers are supported:
191
+ # * Array (elements can be Strings or other containers)
192
+ # * Hash (values are Strings or other containers; the curr_element
193
+ # attribute is a HashEntry).
194
+ #
195
+ class ContainerParsingFrame #:nodoc:
196
+ #
197
+ # The container being parsed.
198
+ #
199
+ attr_reader :container
200
+
201
+ #
202
+ # The current container element being parsed. A setter actually
203
+ # exists for this, but it is implemented by hand in order to
204
+ # take into account the differences in assigning to Array and Hash
205
+ # elements.
206
+ #
207
+ attr_reader :curr_element
208
+
209
+ #
210
+ # ====Description:
211
+ # This initializes a frame for parsing container, given
212
+ # key-value config config.
213
+ #
214
+ # ====Parameters:
215
+ # [container]
216
+ # The container to parse
217
+ # [config]
218
+ # The KeyValueConfig for the configuration being parsed
219
+ #
220
+ def initialize(container, config)
221
+ @container = container
222
+ @config = config
223
+ @curr_element = nil
224
+ end
225
+
226
+ #
227
+ # ====Description:
228
+ # This method should be called when done parsing the current
229
+ # container element; it causes the current element to be added
230
+ # to the container and readies the frame to parse a new
231
+ # element.
232
+ #
233
+ def finish_curr_element
234
+ if(@container.is_a?(Array))
235
+ if(@curr_element.is_a?(String))
236
+ @curr_element.strip!()
237
+ end
238
+
239
+ @container.push(@curr_element)
240
+ else # @container.is_a?(Hash)
241
+ @curr_element.key.strip!()
242
+
243
+ if(@curr_element.value.is_a?(String))
244
+ @curr_element.value.strip!()
245
+
246
+ #
247
+ # String values that are empty are not allowed.
248
+ #
249
+ if(@curr_element.value.empty?())
250
+ raise ParsingError, "hash key '#{@curr_element.key}' is missing a value"
251
+ end
252
+ end
253
+
254
+ @container[@curr_element.key.to_sym()] = @curr_element.value
255
+ end
256
+
257
+ @curr_element = nil
258
+ end
259
+
260
+ #
261
+ # ====Description:
262
+ # This method appends char to the container element currently being parsed.
263
+ #
264
+ # ====Parameters:
265
+ # [char]
266
+ # The character to be appended to the container element currently
267
+ # being parsed.
268
+ #
269
+ def append_char_to_curr_element(char)
270
+ #
271
+ # Create an element of the appropriate type if a new
272
+ # element is being parsed.
273
+ #
274
+ if(@curr_element == nil)
275
+ if(@container.is_a?(Array))
276
+ @curr_element = ""
277
+ else @container.is_a?(Hash)
278
+ @curr_element = HashEntry.new(@config.hash_key_value_delimiter)
279
+ end
280
+ end
281
+
282
+ #
283
+ # It only makes sense to append characters to String values.
284
+ #
285
+ if(@curr_element.is_a?(String) ||
286
+ (@curr_element.is_a?(HashEntry) && @curr_element.value.is_a?(String)))
287
+ @curr_element << char
288
+ else
289
+ raise ParsingError, "extraneous character '#{char}' after container"
290
+ end
291
+ end
292
+
293
+ #
294
+ # ====Description:
295
+ # This is a setter for @curr_element.
296
+ #
297
+ # ====Parameters:
298
+ # [value]
299
+ # The new value of @curr_element.
300
+ #
301
+ def curr_element=(value)
302
+ if(@curr_element.is_a?(HashEntry))
303
+ @curr_element.value = value
304
+ else
305
+ @curr_element = value
306
+ end
307
+ end
308
+
309
+ #
310
+ # ====Returns:
311
+ # Returns true if and only if the currently parsed element is empty,
312
+ # which will be the case if no non-whitespace characters have been appended
313
+ # yet.
314
+ #
315
+ def curr_element_empty?
316
+ if(@curr_element == nil)
317
+ return true
318
+ end
319
+
320
+ if(@curr_element.is_a?(String))
321
+ return @curr_element.match(/^\s*$/)
322
+ elsif(@curr_element.is_a?(HashEntry))
323
+ return @curr_element.key.match(/^\s*$/)
324
+ else
325
+ return false
326
+ end
327
+ end
328
+ end
329
+
330
+ #
331
+ # ====Description:
332
+ # This method parses a container from raw_value. This *only* should be
333
+ # called if raw_value is a container.
334
+ #
335
+ # ====Parameters:
336
+ # [raw_value]
337
+ # The String to parse
338
+ #
339
+ # ====Returns:
340
+ # The container represented by raw_value
341
+ #
342
+ def parse_container_value(raw_value)
343
+ #
344
+ # Welcome to the jungle! This method is pretty messy...
345
+ # It parses raw_value with the aid of a stack. Each time a nested
346
+ # container is encountered, a new frame is pushed onto the stack.
347
+ # When the method finishes parsing a container, a frame is popped off
348
+ # the stack.
349
+ #
350
+ parsing_stack = []
351
+ top_frame = nil
352
+ container_value = nil
353
+
354
+ #
355
+ # raw_value.split("").each <=> raw_value.each_char
356
+ # in Ruby 1.9.
357
+ #
358
+ raw_value.split("").each_with_index do |char, index|
359
+ #
360
+ # If there are no frames on the stack, and this is not the
361
+ # first iteration, then something must have horribly gone wrong.
362
+ # Either there are too many array or hash closing characters (each
363
+ # one of which pops the stack) or there is gunk after the container.
364
+ # Abort!
365
+ #
366
+ if(parsing_stack.empty?() && (index != 0))
367
+ if((char == @config.array_closing) ||
368
+ (char == @config.hash_closing))
369
+ raise ParsingError, "unmatched '#{char}'"
370
+ else
371
+ raise ParsingError, "extraneous string '#{raw_value[index, raw_value.size() - index + 1]}' after container"
372
+ end
373
+ end
374
+
375
+ #
376
+ # The character can:
377
+ # 1.) Begin a new container.
378
+ # 2.) End a container element.
379
+ # 3.) End a container.
380
+ # 4.) Be part of a container element.
381
+ #
382
+ if((char == @config.array_opening) ||
383
+ (char == @config.hash_opening))
384
+ #
385
+ # A nested container has been encountered. Push a frame onto the
386
+ # stack and parse the nested container.
387
+ #
388
+ if(char == @config.array_opening)
389
+ parsing_stack.push(ContainerParsingFrame.new([], @config))
390
+ else # char == @config.hash_opening
391
+ parsing_stack.push(ContainerParsingFrame.new({}, @config))
392
+ end
393
+
394
+ top_frame = parsing_stack[-1]
395
+ elsif(char == @config.container_element_delimiter)
396
+ #
397
+ # We're done parsing a container element. If the current
398
+ # container element is empty, however, then the user
399
+ # must have made a syntax error.
400
+ #
401
+ if(!top_frame.curr_element_empty?())
402
+ top_frame.finish_curr_element()
403
+ else
404
+ raise ParsingError, "missing container element before '#{@config.container_element_delimiter}'"
405
+ end
406
+ elsif((char == @config.array_closing) ||
407
+ (char == @config.hash_closing))
408
+ #
409
+ # We're done parsing a container. Finalize the element currently
410
+ # being processed and pop the stack.
411
+ # If the container is nested within a parent
412
+ # container, then set the element being processed in the parent
413
+ # container to the container that we just parsed. Otherwise, we
414
+ # must be finished and so set the return value (container_value) to
415
+ # the container that we just parsed (we could return right here, but
416
+ # allow the code to flow to the top of the loop in order to ensure
417
+ # that there are no more characters to be processed, which would be
418
+ # an error in the configuration file).
419
+ #
420
+ if(!top_frame.curr_element_empty?())
421
+ top_frame.finish_curr_element()
422
+ end
423
+
424
+ prev_top_frame = parsing_stack.pop()
425
+
426
+ if(!parsing_stack.empty?())
427
+ top_frame = parsing_stack[-1]
428
+ top_frame.curr_element = prev_top_frame.container
429
+ else
430
+ container_value = prev_top_frame.container
431
+ end
432
+ else
433
+ top_frame.append_char_to_curr_element(char)
434
+ end
435
+ end
436
+
437
+ if(!parsing_stack.empty?())
438
+ if(top_frame.container.is_a?(Array))
439
+ raise ParsingError, "missing array closing '#{@config.array_closing}'"
440
+ else # top_frame.container.is_a?(Hash)
441
+ raise ParsingError, "missing hash closing '#{@config.hash_closing}'"
442
+ end
443
+ end
444
+
445
+ return container_value
446
+ end
447
+ private :parse_container_value
448
+
449
+ #
450
+ # ====Description:
451
+ # This method parses a value from raw_value; the method
452
+ # can parse scalar and container values.
453
+ #
454
+ # ====Parameters:
455
+ # [raw_value]
456
+ # The String to parse
457
+ #
458
+ # ====Returns:
459
+ # The value represented by raw_value
460
+ #
461
+ def parse_parameter_value(raw_value)
462
+ if(@config.allow_containers &&
463
+ raw_value.size() > 1 &&
464
+ ((raw_value[0, 1] == @config.array_opening) ||
465
+ (raw_value[0, 1] == @config.hash_opening)))
466
+ return parse_container_value(raw_value)
467
+ else
468
+ return raw_value
469
+ end
470
+ end
471
+ private :parse_parameter_value
472
+
473
+ #
474
+ # ====Description:
475
+ # This method parses the contents of line and adds the contents to
476
+ # param_hash.
477
+ #
478
+ # ====Parameters:
479
+ # [param_hash]
480
+ # The parameter hash that should be modified (Hash)
481
+ # [line]
482
+ # The line to be parsed (String)
483
+ # [stream_path]
484
+ # The path of the file currently being parsed (Pathname or nil)
485
+ #
486
+ def parse_line(param_hash, line, stream_path)
487
+ if(line.empty?())
488
+ return
489
+ elsif(line[0, @config.comment_line_prefix.length()] == @config.comment_line_prefix)
490
+ return
491
+ elsif(line[0, @config.directive_line_prefix.length()] == @config.directive_line_prefix)
492
+ directive_and_args = line[@config.directive_line_prefix.length(), line.length()].split(" ", 2)
493
+
494
+ if(directive_and_args.empty?())
495
+ raise ParsingError, "missing directive on line starting with directive prefix '#{@config.directive_line_prefix}'"
496
+ end
497
+
498
+ directive = directive_and_args[0].strip()
499
+ if(directive_and_args.size() > 1)
500
+ arguments = directive_and_args[1].strip()
501
+ else
502
+ arguments = ""
503
+ end
504
+
505
+ handle_directive(param_hash, directive, arguments, stream_path)
506
+ else
507
+ key_and_value = line.split(@config.key_value_delimiter, 2)
508
+
509
+ if(key_and_value.length() != 2)
510
+ raise ParsingError, "missing key-value delimiter '#{@config.key_value_delimiter}'"
511
+ end
512
+
513
+ key = key_and_value[0].strip()
514
+ value = key_and_value[1].strip()
515
+
516
+ #
517
+ # In order to allow containing objects to be represented compactly (and
518
+ # not via hashes, which are ugly in key-value format files), they can be
519
+ # repesented like:
520
+ # containing_object.param = blah
521
+ #
522
+ # So, split the key on the object delimiter and, for each object found,
523
+ # hash into param_hash.
524
+ #
525
+ object_names = key.split(@config.object_delimiter).map do |object_name|
526
+ object_name.to_sym() # Convert the object names from Strings to Symbols
527
+ end
528
+
529
+ #
530
+ # The last object name actually is the parameter name, which must
531
+ # map to whatever is parsed from the value portion of the line.
532
+ # So, use upto rather than each here, since each would iterate over
533
+ # all of the object names.
534
+ #
535
+ object_hash = param_hash
536
+ 0.upto(object_names.size() - 2) do |index|
537
+ object_hash[object_names[index]] ||= {}
538
+ object_hash = object_hash[object_names[index]]
539
+ end
540
+
541
+ object_hash[object_names[-1]] = parse_parameter_value(value)
542
+ end
543
+ end
544
+ private :parse_line
545
+
546
+ #
547
+ # ====Description:
548
+ # This method reads and parses each line of stream.
549
+ # Each line is parsed into a key and a value, which then are
550
+ # added to param_hash.
551
+ #
552
+ # ====Parameters:
553
+ # [param_hash]
554
+ # The parameter hash that should be modified (Hash)
555
+ # [stream]
556
+ # The stream containing the configuration information
557
+ # [stream_path]
558
+ # The path of stream (Pathname or nil)
559
+ #
560
+ def read_impl(param_hash, stream, stream_path)
561
+ line_num = 1
562
+
563
+ stream.each_line do |line|
564
+ line.strip!()
565
+
566
+ begin
567
+ parse_line(param_hash, line, stream_path)
568
+ rescue ParsingError => e
569
+ if(stream_path == nil)
570
+ stream_name = "<unknown>"
571
+ else
572
+ stream_name = stream_path.to_s()
573
+ end
574
+
575
+ message = "Error parsing line ##{line_num} of #{stream_name}: #{e.message}!"
576
+ raise Error, message, e.backtrace()
577
+ end
578
+
579
+ line_num += 1
580
+ end
581
+ end
582
+ private :read_impl
583
+
584
+ #
585
+ # ====Returns:
586
+ # The contents of the key-value configuration file
587
+ #
588
+ def read
589
+ param_hash = {}
590
+
591
+ read_impl(param_hash, @stream, @stream_path)
592
+
593
+ return param_hash
594
+ end
595
+ end
596
+
597
+ end