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