configtoolkit 1.2.0 → 2.0.0

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