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.
- 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
|