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.
- data/FAQ.txt +113 -25
- data/Hash.txt +128 -0
- data/History.txt +45 -2
- data/KeyValue.txt +235 -0
- data/Manifest.txt +66 -4
- data/README.txt +666 -202
- data/Rakefile +3 -5
- data/Ruby.txt +172 -0
- data/YAML.txt +188 -0
- data/examples/hash_example.rb +71 -0
- data/examples/key_value_example.dump.cfg +5 -0
- data/examples/key_value_example.normal1.cfg +20 -0
- data/examples/key_value_example.normal2.cfg +15 -0
- data/examples/key_value_example.rb +72 -0
- data/examples/key_value_example.wacky.cfg +13 -0
- data/examples/load_example.rb +32 -0
- data/examples/load_example.yaml +34 -0
- data/examples/load_group_example.rb +28 -0
- data/examples/load_group_example.yaml +33 -0
- data/examples/machineconfig.rb +77 -0
- data/examples/ruby_example.rb +32 -0
- data/examples/ruby_example.rcfg +52 -0
- data/examples/usage_example.rb +93 -0
- data/examples/yaml_example.dump.yaml +12 -0
- data/examples/yaml_example.rb +48 -0
- data/examples/yaml_example.yaml +59 -0
- data/lib/configtoolkit.rb +1 -1
- data/lib/configtoolkit/baseconfig.rb +522 -418
- data/lib/configtoolkit/hasharrayvisitor.rb +242 -0
- data/lib/configtoolkit/hashreader.rb +41 -0
- data/lib/configtoolkit/hashwriter.rb +45 -0
- data/lib/configtoolkit/keyvalueconfig.rb +105 -0
- data/lib/configtoolkit/keyvaluereader.rb +597 -0
- data/lib/configtoolkit/keyvaluewriter.rb +157 -0
- data/lib/configtoolkit/prettyprintwriter.rb +167 -0
- data/lib/configtoolkit/reader.rb +62 -0
- data/lib/configtoolkit/rubyreader.rb +270 -0
- data/lib/configtoolkit/types.rb +42 -26
- data/lib/configtoolkit/writer.rb +116 -0
- data/lib/configtoolkit/yamlreader.rb +10 -6
- data/lib/configtoolkit/yamlwriter.rb +113 -71
- data/test/bad_array_index.rcfg +1 -0
- data/test/bad_containing_object_assignment.rcfg +2 -0
- data/test/bad_directive.cfg +1 -0
- data/test/bad_include1.cfg +2 -0
- data/test/bad_include2.cfg +3 -0
- data/test/bad_parameter_reference.rcfg +1 -0
- data/test/contained_sample.cfg +10 -0
- data/test/contained_sample.pretty_print +30 -0
- data/test/contained_sample.rcfg +22 -0
- data/test/contained_sample.yaml +22 -21
- data/test/containers.cfg +6 -0
- data/test/exta_string_after_container.cfg +2 -0
- data/test/extra_container_closing.cfg +2 -0
- data/test/extra_string_after_container.cfg +2 -0
- data/test/extra_string_after_nested_container.cfg +1 -0
- data/test/missing_array_closing.cfg +2 -0
- data/test/missing_array_element.cfg +2 -0
- data/test/missing_directive.cfg +1 -0
- data/test/missing_hash_closing.cfg +2 -0
- data/test/missing_hash_element.cfg +2 -0
- data/test/missing_hash_value.cfg +1 -0
- data/test/missing_include_argument.cfg +1 -0
- data/test/missing_key_value_delimiter.cfg +1 -0
- data/test/readerwritertest.rb +28 -7
- data/test/sample.cfg +10 -0
- data/test/sample.pretty_print +30 -0
- data/test/sample.rcfg +26 -0
- data/test/test_baseconfig.rb +152 -38
- data/test/test_hash.rb +82 -0
- data/test/test_keyvalue.rb +185 -0
- data/test/test_prettyprint.rb +28 -0
- data/test/test_ruby.rb +50 -0
- data/test/test_yaml.rb +33 -26
- data/test/wacky_sample1.cfg +16 -0
- data/test/wacky_sample2.cfg +5 -0
- data/test/wacky_sample3.cfg +4 -0
- data/test/wacky_sample4.cfg +1 -0
- metadata +101 -10
- data/lib/configtoolkit/toolkit.rb +0 -20
- 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
|