kronk 1.3.1 → 1.4.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.
@@ -1,6 +1,8 @@
1
1
  class Kronk
2
2
 
3
3
  ##
4
+ # THIS CLASS IS DEPRECATED AND WILL BE REMOVED IN KRONK-1.5.x
5
+ #
4
6
  # Wraps a complex data structure to provide a search-driven interface.
5
7
 
6
8
  class DataSet
@@ -23,17 +25,20 @@ class Kronk
23
25
  # Modify the data object by passing inclusive or exclusive data paths.
24
26
  # Supports the following options:
25
27
  # :only_data:: String/Array - keep data that matches the paths
26
- # :only_data_with:: String/Array - keep data with a matched child
27
28
  # :ignore_data:: String/Array - remove data that matches the paths
29
+ #
30
+ # Deprecated options:
31
+ # :only_data_with:: String/Array - keep data with a matched child
28
32
  # :ignore_data_with:: String/Array - remove data with a matched child
29
33
  #
30
34
  # Note: the data is processed in the following order:
31
- # * only_data_with
32
- # * ignore_data_with
33
35
  # * only_data
34
36
  # * ignore_data
35
37
 
36
38
  def modify options
39
+ warn_path_deprecation! if options[:ignore_data_with] ||
40
+ options[:only_data_with]
41
+
37
42
  collect_data_points options[:only_data_with], true if
38
43
  options[:only_data_with]
39
44
 
@@ -48,31 +53,51 @@ class Kronk
48
53
  end
49
54
 
50
55
 
56
+ ##
57
+ # New implementation of DataSet#modify
58
+
59
+ def fetch options
60
+ warn_path_deprecation! if options[:ignore_data_with] ||
61
+ options[:only_data_with]
62
+
63
+ options[:only_data] = [*options[:only_data]].compact
64
+ options[:ignore_data] = [*options[:ignore_data]].compact
65
+
66
+ options[:only_data].concat(
67
+ [*options[:only_data_with]].map!{|path| path << "/.."}
68
+ ) if options[:only_data_with]
69
+
70
+ options[:ignore_data].concat(
71
+ [*options[:ignore_data_with]].map!{|path| path << "/.."}
72
+ ) if options[:ignore_data_with]
73
+
74
+ Path::Transaction.run @data, options do |t|
75
+ t.select(*options[:only_data])
76
+ t.delete(*options[:ignore_data])
77
+ end
78
+ end
79
+
80
+
51
81
  ##
52
82
  # Keep only specific data points from the data structure.
53
83
 
54
84
  def collect_data_points data_paths, affect_parent=false
85
+ Kronk::Cmd.warn "DataSet#collect_data_points deprecated. "+
86
+ "Use Path::Transaction"
87
+
55
88
  new_data = @data.class.new
56
89
 
57
90
  [*data_paths].each do |data_path|
58
- find_data data_path do |obj, k, path|
91
+ opts = Path.parse_regex_opts! data_path
92
+ data_path << "/.." if affect_parent
59
93
 
94
+ Path.find data_path, @data, opts do |data, k, path|
60
95
  curr_data = @data
61
96
  new_curr_data = new_data
62
97
 
63
98
  path.each_with_index do |key, i|
64
-
65
- if i == path.length - 1 && !affect_parent
66
- new_curr_data[key] = curr_data[key]
67
-
68
- elsif i == path.length - 2 && affect_parent
69
- new_curr_data[key] = curr_data[key]
70
- break
71
-
72
- elsif path.length == 1 && affect_parent
73
- new_data = curr_data
74
- break
75
-
99
+ if i == path.length - 1
100
+ new_curr_data[key] = curr_data[key]
76
101
  else
77
102
  new_curr_data[key] ||= curr_data[key].class.new
78
103
  new_curr_data = new_curr_data[key]
@@ -82,7 +107,7 @@ class Kronk
82
107
  end
83
108
  end
84
109
 
85
- @data = new_data
110
+ @data.replace new_data
86
111
  end
87
112
 
88
113
 
@@ -90,21 +115,18 @@ class Kronk
90
115
  # Remove specific data points from the data structure.
91
116
 
92
117
  def delete_data_points data_paths, affect_parent=false
93
- [*data_paths].each do |data_path|
94
- find_data data_path do |obj, k, path|
95
-
96
- if affect_parent && data_at_path?(path)
97
- @data = @data.class.new and return if path.length == 1
118
+ Kronk::Cmd.warn "DataSet#delete_data_points deprecated. "+
119
+ "Use Path::Transaction"
98
120
 
99
- parent_data = data_at_path path[0..-3]
100
- del_method = Array === parent_data ? :delete_at : :delete
121
+ [*data_paths].each do |data_path|
122
+ opts = Path.parse_regex_opts! data_path
123
+ data_path << "/.." if affect_parent
101
124
 
102
- parent_data.send del_method, path[-2]
125
+ Path.find data_path, @data, opts do |obj, k, path|
126
+ next unless obj.respond_to? :[]
103
127
 
104
- else
105
- del_method = Array === obj ? :delete_at : :delete
106
- obj.send del_method, k
107
- end
128
+ del_method = Array === obj ? :delete_at : :delete
129
+ obj.send del_method, k
108
130
  end
109
131
  end
110
132
 
@@ -112,206 +134,11 @@ class Kronk
112
134
  end
113
135
 
114
136
 
115
- ##
116
- # Find specific data points from a nested hash or array data structure.
117
- # If a block is given, will pass it any matched parent data object,
118
- # key, and full path.
119
- #
120
- # Data points must be an Array or String with a glob-like format.
121
- # Special characters are: / * = | \ and are interpreted as follows:
122
- # :key/ - walk down tree by one level from key
123
- # :*/key - walk down tree from any parent with key as a child
124
- # :key1|key2 - return elements with key value of key1 or key2
125
- # :key=val - return elements where key has a value of val
126
- # :key\* - return root-level element with key "key*"
127
- #
128
- # Other examples:
129
- # find_data data, root/**=invalid|
130
- # # Returns an Array of grand-children key/value pairs
131
- # # where the value is 'invalid' or blank
132
-
133
- def find_data data_path, curr_path=nil, data=nil, &block
134
- curr_path ||= []
135
- data ||= @data
136
-
137
- key, value, rec, data_path = parse_data_path data_path
138
-
139
- yield_data_points data, key, value, rec, curr_path do |d, k, p|
140
-
141
- if data_path
142
- find_data data_path, p, d[k], &block
143
- else
144
- yield d, k, p
145
- end
146
- end
147
- end
148
-
149
-
150
- ##
151
- # Checks if data is available at the given path.
152
-
153
- def data_at_path? path
154
- data_at_path path
155
- true
156
-
157
- rescue NoMethodError, TypeError
158
- false
159
- end
160
-
161
-
162
- ##
163
- # Retrieve the data at the given path array location.
164
-
165
- def data_at_path path
166
- curr = @data
167
- path.each do |p|
168
- raise TypeError, "Expected instance of Array or Hash" unless
169
- Array === curr || Hash === curr
170
- curr = curr[p]
171
- end
172
-
173
- curr
174
- end
175
-
176
-
177
- ##
178
- # Parses a given data point and returns an array with the following:
179
- # - Key to match
180
- # - Value to match
181
- # - Recursive matching
182
- # - New data path value
183
-
184
- def parse_data_path data_path
185
- data_path = data_path.dup
186
- key = nil
187
- value = nil
188
- recursive = false
189
-
190
- until key && key != "**" || value || data_path.empty? do
191
- value = data_path.slice!(%r{((.*?[^\\])+?/)})
192
- (value ||= data_path).sub!(/\/$/, '')
193
-
194
- data_path = nil if value == data_path
195
-
196
- key = value.slice! %r{((.*?[^\\])+?=)}
197
- key, value = value, nil if key.nil?
198
- key.sub!(/\=$/, '')
199
-
200
- value = parse_path_item value if value
201
-
202
- if key =~ /^\*{2,}$/
203
- if data_path && !value
204
- key, value, rec, data_path = parse_data_path(data_path)
205
- else
206
- key = /.*/
207
- end
137
+ private
208
138
 
209
- recursive = true
210
- else
211
- key = parse_path_item key
212
- end
213
- end
214
-
215
- data_path = nil if data_path && data_path.empty?
216
-
217
- [key, value, recursive, data_path]
218
- end
219
-
220
-
221
- ##
222
- # Decide whether to make path item a regex, range, array, or string.
223
-
224
- def parse_path_item str
225
- if str =~ /(^|[^\\])([\*\?\|])/
226
- str.gsub!(/(^|[^\\])(\*|\?)/, '\1.\2')
227
- /^(#{str})$/i
228
-
229
- elsif str =~ %r{^(\-?\d+)(\.{2,3})(\-?\d+)$}
230
- Range.new $1.to_i, $3.to_i, ($2 == "...")
231
-
232
- elsif str =~ %r{^(\-?\d+),(\-?\d+)$}
233
- Range.new $1.to_i, ($1.to_i + $2.to_i), true
234
-
235
- else
236
- str.gsub "\\", ""
237
- end
238
- end
239
-
240
-
241
- ##
242
- # Yield data object and key, if a specific key or value matches
243
- # the given data.
244
-
245
- def yield_data_points data, mkey, mvalue=nil,
246
- recursive=false, path=nil, &block
247
-
248
- return unless Hash === data || Array === data
249
- path ||= []
250
-
251
- each_data_item data do |key, value|
252
- curr_path = path.dup << key
253
-
254
- found = match_data_item(mkey, key) &&
255
- match_data_item(mvalue, value)
256
-
257
- yield data, key, curr_path if found
258
- yield_data_points data[key], mkey, mvalue, true, curr_path, &block if
259
- recursive
260
- end
261
- end
262
-
263
-
264
- ##
265
- # Check if data key or value is a match for nested data searches.
266
-
267
- def match_data_item item1, item2
268
- return if !item1.nil? && (Array === item2 || Hash === item2)
269
-
270
- if item1.class == item2.class
271
- item1 == item2
272
-
273
- elsif Regexp === item1
274
- item2.to_s =~ item1
275
-
276
- elsif Range === item1
277
- item1.include? item2.to_i
278
-
279
- elsif item1.nil?
280
- true
281
-
282
- else
283
- item2.to_s.downcase == item1.to_s.downcase
284
- end
285
- end
286
-
287
-
288
- ##
289
- # Universal iterator for Hash and Array objects.
290
-
291
- def each_data_item data, &block
292
- case data
293
-
294
- when Hash
295
- data.each(&block)
296
-
297
- when Array
298
- i = 0
299
-
300
- # We need to iterate through the array this way
301
- # in case items in it get deleted.
302
-
303
- while i < data.length do
304
- index = i
305
- old_length = data.length
306
-
307
- block.call index, data[index]
308
-
309
- adj = old_length - data.length
310
- adj = 0 if adj < 0
311
-
312
- i = i.next - adj
313
- end
314
- end
139
+ def warn_path_deprecation!
140
+ Kronk::Cmd.warn "The :ignore_data_with and :only_data_with options "+
141
+ "are deprecated. Use the '/..' path notation."
315
142
  end
316
143
  end
317
144
  end
@@ -25,14 +25,17 @@ class Kronk
25
25
  when Hash
26
26
  output = "{\n"
27
27
 
28
+ sorted_keys = sort_any data.keys
29
+
28
30
  data_values =
29
- data.map do |key, value|
30
- pad = " " * indent
31
+ sorted_keys.map do |key|
32
+ value = data[key]
33
+ pad = " " * indent
31
34
  subdata = ordered_data_string value, struct_only, indent + 1
32
35
  "#{pad}#{key.inspect} => #{subdata}"
33
36
  end
34
37
 
35
- output << data_values.sort.join(",\n") << "\n" unless data_values.empty?
38
+ output << data_values.join(",\n") << "\n" unless data_values.empty?
36
39
  output << "#{" " * indent}}"
37
40
 
38
41
  when Array
@@ -55,6 +58,64 @@ class Kronk
55
58
  end
56
59
 
57
60
 
61
+ ##
62
+ # Sorts an array of any combination of string, integer, or symbols.
63
+
64
+ def self.sort_any arr
65
+ i = 1
66
+ until i >= arr.length
67
+ j = i-1
68
+ val = arr[i]
69
+ prev_val = arr[j]
70
+
71
+ loop do
72
+ if smaller?(val, arr[j])
73
+ arr[j+1] = arr[j]
74
+ j = j - 1
75
+ break if j < 0
76
+
77
+ else
78
+ break
79
+ end
80
+ end
81
+
82
+ arr[j+1] = val
83
+
84
+ i = i.next
85
+ end
86
+
87
+ arr
88
+ end
89
+
90
+
91
+ ##
92
+ # Compares Numerics, Strings, and Symbols and returns true if the left
93
+ # side is 'smaller' than the right side.
94
+
95
+ def self.smaller? left, right
96
+ case left
97
+ when Numeric
98
+ case right
99
+ when Numeric then right > left
100
+ else true
101
+ end
102
+
103
+ when Symbol
104
+ case right
105
+ when Numeric then false
106
+ when Symbol then right.to_s > left.to_s
107
+ else true
108
+ end
109
+
110
+ when String
111
+ case right
112
+ when String then right > left
113
+ else false
114
+ end
115
+ end
116
+ end
117
+
118
+
58
119
  ##
59
120
  # Adds line numbers to each lines of a String.
60
121
 
@@ -227,7 +288,7 @@ class Kronk
227
288
  while sequences.length > dist
228
289
  item = sequences.pop
229
290
  next unless item
230
- item.each &block
291
+ item.each(&block)
231
292
  end
232
293
  end
233
294
 
@@ -0,0 +1,427 @@
1
+ class Kronk
2
+
3
+ ##
4
+ # Finds specific data points from a nested Hash or Array data structure
5
+ # through the use of a file-glob-like path selector.
6
+ #
7
+ # Special characters are: "/ * ? = | \ . , \ ( )"
8
+ # and are interpreted as follows:
9
+ #
10
+ # :foo/ - walk down tree by one level from key "foo"
11
+ # :*/foo - walk down tree from any parent with key "foo" as a child
12
+ # :foo1|foo2 - return elements with key value of "foo1" or "foo2"
13
+ # :foo(1|2) - same behavior as above
14
+ # :foo=val - return elements where key has a value of val
15
+ # :foo\* - return root-level element with key "foo*" ('*' char is escaped)
16
+ # :**/foo - recursively search for key "foo"
17
+ # :foo? - return keys that match /\Afoo.?\Z/
18
+ # :2..10 - match any integer from 2 to 10
19
+ # :2...10 - match any integer from 2 to 9
20
+ # :2,5 - match any integer from 2 to 7
21
+ #
22
+ # Examples:
23
+ #
24
+ # # Recursively look for elements with value "val" under top element "root"
25
+ # Path.find "root/**=val", data
26
+ #
27
+ # # Find child elements of "root" that have a key of "foo" or "bar"
28
+ # Path.find "root/foo|bar", data
29
+ #
30
+ # # Recursively find child elements of root whose value is 1, 2, or 3.
31
+ # Path.find "root/**=1..3", data
32
+ #
33
+ # # Recursively find child elements of root of literal value "1..3"
34
+ # Path.find "root/**=\\1..3", data
35
+
36
+ class Path
37
+
38
+ # Used as path instruction to go up one path level.
39
+ module PARENT; end
40
+
41
+ # Used as path item value to match any key or value.
42
+ module ANY_VALUE; end
43
+
44
+ # Mapping of letters to Regexp options.
45
+ REGEX_OPTS = {
46
+ "i" => Regexp::IGNORECASE,
47
+ "m" => Regexp::MULTILINE,
48
+ "u" => (Regexp::FIXEDENCODING if defined?(Regexp::FIXEDENCODING)),
49
+ "x" => Regexp::EXTENDED
50
+ }
51
+
52
+ # Shortcut characters that require modification before being turned into
53
+ # a matcher.
54
+ SUFF_CHARS = Regexp.escape "*?"
55
+
56
+ # All special path characters.
57
+ PATH_CHARS = Regexp.escape("()|") << SUFF_CHARS
58
+
59
+ # Path chars that get regexp escaped.
60
+ RESC_CHARS = "*?()|/"
61
+
62
+ # The path item delimiter character "/"
63
+ DCH = "/"
64
+
65
+ # The path character to assign value "="
66
+ VCH = "="
67
+
68
+ # The escape character to use any PATH_CHARS as its literal.
69
+ ECH = "\\"
70
+
71
+ # The Regexp escaped version of ECH.
72
+ RECH = Regexp.escape ECH
73
+
74
+ # The EndOfPath delimiter after which regex opt chars may be specified.
75
+ EOP = DCH + DCH
76
+
77
+ # The key string that represents PARENT.
78
+ PARENT_KEY = ".."
79
+
80
+ # The key string that indicates recursive lookup.
81
+ RECUR_KEY = "**"
82
+
83
+ # Matcher for Range path item.
84
+ RANGE_MATCHER = %r{^(\-?\d+)(\.{2,3})(\-?\d+)$}
85
+
86
+ # Matcher for index,length path item.
87
+ ILEN_MATCHER = %r{^(\-?\d+),(\-?\d+)$}
88
+
89
+ # Matcher allowing any value to be matched.
90
+ ANYVAL_MATCHER = /^(\?*\*+\?*)*$/
91
+
92
+ # Matcher to assert if any unescaped special chars are in a path item.
93
+ PATH_CHAR_MATCHER = /(^|[^#{RECH}])([#{PATH_CHARS}])/
94
+
95
+
96
+ ##
97
+ # Instantiate a Path object with a String data path.
98
+ # Path.new "/path/**/to/*=bar/../../**/last"
99
+
100
+ def initialize path_str, regex_opts=nil
101
+ path_str = path_str.dup
102
+ @path = self.class.parse_path_str path_str, regex_opts
103
+ end
104
+
105
+
106
+ ##
107
+ # Finds the current path in the given data structure.
108
+ # Returns a Hash of path_ary => data pairs for each match.
109
+ #
110
+ # If a block is given, yields the parent data object matched,
111
+ # the key, and the path array.
112
+ #
113
+ # data = {:path => {:foo => :bar, :sub => {:foo => :bar2}}, :other => nil}
114
+ # path = Path.new "path/**/foo"
115
+ #
116
+ # all_args = []
117
+ #
118
+ # path.find_in data |*args|
119
+ # all_args << args
120
+ # end
121
+ # #=> {[:path, :foo] => :bar, [:path, :sub, :foo] => :bar2}
122
+ #
123
+ # all_args
124
+ # #=> [
125
+ # #=> [{:foo => :bar, :sub => {:foo => :bar2}}, :foo, [:path, :foo]],
126
+ # #=> [{:foo => :bar2}, :foo, [:path, :sub, :foo]]
127
+ # #=> ]
128
+
129
+ def find_in data
130
+ matches = {[] => data}
131
+
132
+ @path.each_with_index do |(mkey, mvalue, recur), i|
133
+ args = [matches, data, mkey, mvalue, recur]
134
+ last_item = i == @path.length - 1
135
+
136
+ self.class.assign_find(*args) do |sdata, key, spath|
137
+ yield sdata, key, spath if last_item && block_given?
138
+ end
139
+ end
140
+
141
+ matches
142
+ end
143
+
144
+
145
+ ##
146
+ # Fully streamed version of:
147
+ # Path.new(str_path).find_in data
148
+ #
149
+ # See Path#find_in for usage.
150
+
151
+ def self.find path_str, data, regex_opts=nil, &block
152
+ matches = {[] => data}
153
+
154
+ parse_path_str path_str, regex_opts do |mkey, mvalue, recur, last_item|
155
+ assign_find matches, data, mkey, mvalue, recur do |sdata, key, spath|
156
+ yield sdata, key, spath if last_item && block_given?
157
+ end
158
+ end
159
+
160
+ matches
161
+ end
162
+
163
+
164
+ ##
165
+ # Common find functionality that assigns to the matches hash.
166
+
167
+ def self.assign_find matches, data, mkey, mvalue, recur
168
+ matches.keys.each do |path|
169
+ pdata = matches.delete path
170
+
171
+ if mkey == PARENT
172
+ path = path[0..-2]
173
+ subdata = data_at_path path[0..-2], data
174
+
175
+ #!! Avoid yielding parent more than once
176
+ next if matches[path]
177
+
178
+ yield subdata, path.last, path if block_given?
179
+ matches[path] = subdata[path.last]
180
+ next
181
+ end
182
+
183
+ find_match pdata, mkey, mvalue, recur, path do |sdata, key, spath|
184
+ yield sdata, key, spath if block_given?
185
+ matches[spath] = sdata[key]
186
+ end
187
+ end
188
+ end
189
+
190
+
191
+ ##
192
+ # Returns the data object found at the given path array.
193
+ # Returns nil if not found.
194
+
195
+ def self.data_at_path path_arr, data
196
+ c_data = data
197
+
198
+ path_arr.each do |key|
199
+ c_data = c_data[key]
200
+ end
201
+
202
+ c_data
203
+
204
+ rescue NoMethodError, TypeError
205
+ nil
206
+ end
207
+
208
+
209
+ ##
210
+ # Universal iterator for Hash and Array like objects.
211
+ # The data argument must either respond to both :each_with_index
212
+ # and :length, or respond to :each yielding a key/value pair.
213
+
214
+ def self.each_data_item data, &block
215
+ if data.respond_to?(:has_key?) && data.respond_to?(:each)
216
+ data.each(&block)
217
+
218
+ elsif data.respond_to?(:each_with_index) && data.respond_to?(:length)
219
+ # We need to iterate through the array this way
220
+ # in case items in it get deleted.
221
+ (data.length - 1).downto(0) do |i|
222
+ block.call i, data[i]
223
+ end
224
+ end
225
+ end
226
+
227
+
228
+ ##
229
+ # Finds data with the given key and value matcher, optionally recursive.
230
+ # Yields data, key and path Array when block is given.
231
+ # Returns an Array of path arrays.
232
+
233
+ def self.find_match data, mkey, mvalue=ANY_VALUE,
234
+ recur=false, path=nil, &block
235
+
236
+ return [] unless Array === data || Hash === data
237
+
238
+ paths = []
239
+ path ||= []
240
+
241
+ each_data_item data do |key, value|
242
+ c_path = path.dup << key
243
+
244
+ if match_data_item(mkey, key) && match_data_item(mvalue, value)
245
+ yield data, key, c_path if block_given?
246
+ paths << c_path
247
+ end
248
+
249
+ paths.concat \
250
+ find_match(data[key], mkey, mvalue, true, c_path, &block) if recur
251
+ end
252
+
253
+ paths
254
+ end
255
+
256
+
257
+ ##
258
+ # Check if data key or value is a match for nested data searches.
259
+
260
+ def self.match_data_item item1, item2
261
+ return if ANY_VALUE != item1 && (Array === item2 || Hash === item2)
262
+
263
+ if item1.class == item2.class
264
+ item1 == item2
265
+
266
+ elsif Regexp === item1
267
+ item2.to_s =~ item1
268
+
269
+ elsif Range === item1
270
+ item1.include? item2.to_i
271
+
272
+ elsif ANY_VALUE == item1
273
+ true
274
+
275
+ else
276
+ item2.to_s.downcase == item1.to_s.downcase
277
+ end
278
+ end
279
+
280
+
281
+ ##
282
+ # Decide whether to make path item matcher a regex, range, array, or string.
283
+
284
+ def self.parse_path_item str, regex_opts=nil
285
+ case str
286
+ when nil, ANYVAL_MATCHER
287
+ ANY_VALUE
288
+
289
+ when RANGE_MATCHER
290
+ Range.new $1.to_i, $3.to_i, ($2 == "...")
291
+
292
+ when ILEN_MATCHER
293
+ Range.new $1.to_i, ($1.to_i + $2.to_i), true
294
+
295
+ else
296
+ if String === str && (regex_opts || str =~ PATH_CHAR_MATCHER)
297
+
298
+ # Remove extra suffix characters
299
+ str.gsub! %r{(^|[^#{RECH}])(\*+\?+|\?+\*+)}, '\1*'
300
+ str.gsub! %r{(^|[^#{RECH}])\*+}, '\1*'
301
+
302
+ str = Regexp.escape str
303
+
304
+ # Remove escaping from special path characters
305
+ str.gsub! %r{#{RECH}([#{PATH_CHARS}])}, '\1'
306
+ str.gsub! %r{#{RECH}([#{RESC_CHARS}])}, '\1'
307
+ str.gsub! %r{(^|[^#{RECH}])([#{SUFF_CHARS}])}, '\1.\2'
308
+
309
+ Regexp.new "\\A(#{str})\\Z", regex_opts
310
+
311
+ elsif String === str
312
+ str.gsub %r{#{RECH}([^#{RECH}]|$)}, '\1'
313
+
314
+ else
315
+ str
316
+ end
317
+ end
318
+ end
319
+
320
+
321
+ ##
322
+ # Parses a path String into an Array of arrays containing
323
+ # matchers for key, value, and any special modifiers
324
+ # such as recursion.
325
+ #
326
+ # Path.parse_path_str "/path/**/to/*=bar/../../**/last"
327
+ # #=> [["path",ANY_VALUE,false],["to",ANY_VALUE,true],[/.*/,"bar",false],
328
+ # # [PARENT,ANY_VALUE,false],[PARENT,ANY_VALUE,false],
329
+ # # ["last",ANY_VALUE,true]]
330
+ #
331
+ # Note: Path.parse_path_str will slice the original path string
332
+ # until it is empty.
333
+
334
+ def self.parse_path_str path, regex_opts=nil
335
+ path = path.dup
336
+ regex_opts = parse_regex_opts! path, regex_opts
337
+
338
+ parsed = []
339
+
340
+ escaped = false
341
+ key = ""
342
+ value = nil
343
+ recur = false
344
+ next_item = false
345
+
346
+ until path.empty?
347
+ char = path.slice!(0..0)
348
+
349
+ case char
350
+ when DCH
351
+ next_item = true
352
+ char = ""
353
+
354
+ when VCH
355
+ value = ""
356
+ next
357
+
358
+ when ECH
359
+ escaped = true
360
+ next
361
+ end unless escaped
362
+
363
+ char = "#{ECH}#{char}" if escaped
364
+
365
+ if value
366
+ value << char
367
+ else
368
+ key << char
369
+ end
370
+
371
+ next_item = true if path.empty?
372
+
373
+ if next_item
374
+ next_item = false
375
+
376
+ if key == RECUR_KEY
377
+ key = "*"
378
+ recur = true
379
+ key = "" and next unless value || path.empty?
380
+
381
+ elsif key == PARENT_KEY
382
+ key = PARENT
383
+
384
+ if recur
385
+ key = "" and next unless value
386
+ key = "*"
387
+ end
388
+ end
389
+
390
+ unless key =~ /^\.?$/ && !value
391
+ parsed << [ parse_path_item(key, regex_opts),
392
+ parse_path_item(value, regex_opts),
393
+ recur ]
394
+
395
+ yield_args = (parsed.last.dup << path.empty?)
396
+
397
+ yield(*yield_args) if block_given?
398
+ end
399
+
400
+ key = ""
401
+ value = nil
402
+ recur = false
403
+ end
404
+
405
+ escaped = false
406
+ end
407
+
408
+ parsed
409
+ end
410
+
411
+
412
+ ##
413
+ # Parses the tail end of a path String to determine regexp matching flags.
414
+
415
+ def self.parse_regex_opts! path, default=nil
416
+ opts = default || 0
417
+
418
+ return default unless
419
+ path =~ %r{[^#{RECH}]#{EOP}[#{REGEX_OPTS.keys.join}]+\Z}
420
+
421
+ path.slice!(%r{#{EOP}[#{REGEX_OPTS.keys.join}]+\Z}).to_s.
422
+ each_char{|c| opts |= REGEX_OPTS[c] || 0}
423
+
424
+ opts if opts > 0
425
+ end
426
+ end
427
+ end