kronk 1.3.1 → 1.4.0

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