kronk 1.3.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +28 -0
- data/Manifest.txt +4 -0
- data/README.rdoc +46 -18
- data/Rakefile +0 -3
- data/bin/kronk +2 -1
- data/lib/kronk.rb +19 -98
- data/lib/kronk/cmd.rb +22 -12
- data/lib/kronk/data_set.rb +55 -228
- data/lib/kronk/diff.rb +65 -4
- data/lib/kronk/path.rb +427 -0
- data/lib/kronk/path/transaction.rb +204 -0
- data/lib/kronk/request.rb +66 -5
- data/lib/kronk/response.rb +1 -1
- data/lib/kronk/test/assertions.rb +4 -8
- data/lib/kronk/test/core_ext.rb +57 -9
- data/lib/kronk/xml_parser.rb +41 -7
- data/script/kronk_completion +3 -2
- data/test/test_core_ext.rb +2 -2
- data/test/test_data_set.rb +4 -297
- data/test/test_diff.rb +12 -12
- data/test/test_kronk.rb +2 -0
- data/test/test_path.rb +370 -0
- data/test/test_request.rb +0 -10
- data/test/test_transaction.rb +330 -0
- metadata +31 -63
- data/.gemtest +0 -0
data/lib/kronk/data_set.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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
|
-
|
94
|
-
|
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
|
-
|
100
|
-
|
121
|
+
[*data_paths].each do |data_path|
|
122
|
+
opts = Path.parse_regex_opts! data_path
|
123
|
+
data_path << "/.." if affect_parent
|
101
124
|
|
102
|
-
|
125
|
+
Path.find data_path, @data, opts do |obj, k, path|
|
126
|
+
next unless obj.respond_to? :[]
|
103
127
|
|
104
|
-
|
105
|
-
|
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
|
-
|
210
|
-
|
211
|
-
|
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
|
data/lib/kronk/diff.rb
CHANGED
@@ -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
|
-
|
30
|
-
|
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.
|
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
|
291
|
+
item.each(&block)
|
231
292
|
end
|
232
293
|
end
|
233
294
|
|
data/lib/kronk/path.rb
ADDED
@@ -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
|