ruby-path 1.0.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/.autotest +23 -0
- data/.gemtest +0 -0
- data/History.rdoc +6 -0
- data/Manifest.txt +14 -0
- data/README.rdoc +96 -0
- data/Rakefile +14 -0
- data/lib/path.rb +340 -0
- data/lib/path/core_ext.rb +106 -0
- data/lib/path/match.rb +130 -0
- data/lib/path/matcher.rb +193 -0
- data/lib/path/transaction.rb +341 -0
- data/test/test_core_ext.rb +73 -0
- data/test/test_helper.rb +3 -0
- data/test/test_path.rb +316 -0
- data/test/test_path_match.rb +105 -0
- data/test/test_path_matcher.rb +371 -0
- metadata +103 -0
data/.autotest
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'autotest/restart'
|
4
|
+
|
5
|
+
# Autotest.add_hook :initialize do |at|
|
6
|
+
# at.extra_files << "../some/external/dependency.rb"
|
7
|
+
#
|
8
|
+
# at.libs << ":../some/external"
|
9
|
+
#
|
10
|
+
# at.add_exception 'vendor'
|
11
|
+
#
|
12
|
+
# at.add_mapping(/dependency.rb/) do |f, _|
|
13
|
+
# at.files_matching(/test_.*rb$/)
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# %w(TestA TestB).each do |klass|
|
17
|
+
# at.extra_class_map[klass] = "test/test_misc.rb"
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
|
21
|
+
# Autotest.add_hook :run_command do |at|
|
22
|
+
# system "rake build"
|
23
|
+
# end
|
data/.gemtest
ADDED
File without changes
|
data/History.rdoc
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
.autotest
|
2
|
+
History.rdoc
|
3
|
+
Manifest.txt
|
4
|
+
README.rdoc
|
5
|
+
Rakefile
|
6
|
+
lib/path.rb
|
7
|
+
lib/path/core_ext.rb
|
8
|
+
lib/path/match.rb
|
9
|
+
lib/path/matcher.rb
|
10
|
+
lib/path/transaction.rb
|
11
|
+
test/test_path.rb
|
12
|
+
test/test_core_ext.rb
|
13
|
+
test/test_path_match.rb
|
14
|
+
test/test_path_matcher.rb
|
data/README.rdoc
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
= ruby-path
|
2
|
+
|
3
|
+
* https://github.com/yaksnrainbows/ruby-path
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Simple path-based search and lookup for Ruby.
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* Search recursively through hashes and arrays.
|
12
|
+
|
13
|
+
* Modify or delete found items in place.
|
14
|
+
|
15
|
+
== SYNOPSIS:
|
16
|
+
|
17
|
+
=== Find Data
|
18
|
+
|
19
|
+
data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
|
20
|
+
data.find_data "**/foo" do |parent, key, path|
|
21
|
+
p path
|
22
|
+
p parent[key]
|
23
|
+
puts "---"
|
24
|
+
end
|
25
|
+
|
26
|
+
# [:foo]
|
27
|
+
# "bar"
|
28
|
+
# ---
|
29
|
+
# [:foobar, 2, :foo]
|
30
|
+
# "other bar"
|
31
|
+
# ---
|
32
|
+
|
33
|
+
=== Replace Data:
|
34
|
+
|
35
|
+
data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
|
36
|
+
data.replace_at_path "**=*bar", "BAR"
|
37
|
+
#=> true
|
38
|
+
|
39
|
+
data
|
40
|
+
#=> {:foo => "BAR", :foobar => [:a, :b, {:foo => "BAR"}, :c]}
|
41
|
+
|
42
|
+
=== Delete Data:
|
43
|
+
|
44
|
+
data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
|
45
|
+
data.replace_at_path "**=*bar", "BAR"
|
46
|
+
#=> {[:foo] => "bar", [:foobar, 2, :foo] => "other bar"}
|
47
|
+
|
48
|
+
=== Existance of Data:
|
49
|
+
|
50
|
+
data = {:foo => "bar", :foobar => [:a, :b, {:foo => "other bar"}, :c]}
|
51
|
+
|
52
|
+
data.has_path? "foobar/*/foo"
|
53
|
+
#=> true
|
54
|
+
|
55
|
+
data.has_path? "foobar/0/foo"
|
56
|
+
#=> false
|
57
|
+
|
58
|
+
== INSTALL:
|
59
|
+
|
60
|
+
* gem install ruby-path
|
61
|
+
|
62
|
+
* require 'path'
|
63
|
+
|
64
|
+
== DEVELOPERS:
|
65
|
+
|
66
|
+
After checking out the source, run:
|
67
|
+
|
68
|
+
$ rake newb
|
69
|
+
|
70
|
+
This task will install any missing dependencies, run the tests/specs,
|
71
|
+
and generate the RDoc.
|
72
|
+
|
73
|
+
== LICENSE:
|
74
|
+
|
75
|
+
(The MIT License)
|
76
|
+
|
77
|
+
Copyright (c) 2012 Jeremie Castagna
|
78
|
+
|
79
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
80
|
+
a copy of this software and associated documentation files (the
|
81
|
+
'Software'), to deal in the Software without restriction, including
|
82
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
83
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
84
|
+
permit persons to whom the Software is furnished to do so, subject to
|
85
|
+
the following conditions:
|
86
|
+
|
87
|
+
The above copyright notice and this permission notice shall be
|
88
|
+
included in all copies or substantial portions of the Software.
|
89
|
+
|
90
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
91
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
92
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
93
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
94
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
95
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
96
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
|
6
|
+
Hoe.spec 'ruby-path' do
|
7
|
+
developer('Jeremie Castagna', 'yaksnrainbows@gmail.com')
|
8
|
+
|
9
|
+
self.readme_file = "README.rdoc"
|
10
|
+
self.history_file = "History.rdoc"
|
11
|
+
self.extra_rdoc_files = FileList['*.rdoc']
|
12
|
+
end
|
13
|
+
|
14
|
+
# vim: syntax=ruby
|
data/lib/path.rb
ADDED
@@ -0,0 +1,340 @@
|
|
1
|
+
##
|
2
|
+
# Finds specific data points from a nested Hash or Array data structure
|
3
|
+
# through the use of a file-glob-like path selector.
|
4
|
+
#
|
5
|
+
# Special characters are: "/ * ? = | \ . , \ ( )"
|
6
|
+
# and are interpreted as follows:
|
7
|
+
#
|
8
|
+
# :foo/ - walk down tree by one level from key "foo"
|
9
|
+
# :*/foo - walk down tree from any parent with key "foo" as a child
|
10
|
+
# :foo1|foo2 - return elements with key value of "foo1" or "foo2"
|
11
|
+
# :foo(1|2) - same behavior as above
|
12
|
+
# :foo=val - return elements where key has a value of val
|
13
|
+
# :foo\* - return root-level element with key "foo*" ('*' char is escaped)
|
14
|
+
# :**/foo - recursively search for key "foo"
|
15
|
+
# :foo? - return keys that match /\Afoo.?\Z/
|
16
|
+
# :2..10 - match any integer from 2 to 10
|
17
|
+
# :2...10 - match any integer from 2 to 9
|
18
|
+
# :2,5 - match any integer from 2 to 7
|
19
|
+
#
|
20
|
+
# Examples:
|
21
|
+
#
|
22
|
+
# # Recursively look for elements with value "val" under top element "root"
|
23
|
+
# Path.find "root/**=val", data
|
24
|
+
#
|
25
|
+
# # Find child elements of "root" that have a key of "foo" or "bar"
|
26
|
+
# Path.find "root/foo|bar", data
|
27
|
+
#
|
28
|
+
# # Recursively find child elements of root whose value is 1, 2, or 3.
|
29
|
+
# Path.find "root/**=1..3", data
|
30
|
+
#
|
31
|
+
# # Recursively find child elements of root of literal value "1..3"
|
32
|
+
# Path.find "root/**=\\1..3", data
|
33
|
+
|
34
|
+
class Path
|
35
|
+
VERSION = '1.0.0'
|
36
|
+
|
37
|
+
|
38
|
+
# Used as path instruction to go up one path level.
|
39
|
+
module PARENT; end
|
40
|
+
|
41
|
+
# Mapping of letters to Regexp options.
|
42
|
+
REGEX_OPTS = {
|
43
|
+
"i" => Regexp::IGNORECASE,
|
44
|
+
"m" => Regexp::MULTILINE,
|
45
|
+
"u" => (Regexp::FIXEDENCODING if defined?(Regexp::FIXEDENCODING)),
|
46
|
+
"x" => Regexp::EXTENDED
|
47
|
+
}
|
48
|
+
|
49
|
+
# The path item delimiter character "/"
|
50
|
+
DCH = "/"
|
51
|
+
|
52
|
+
# The replacement character "%" for path mapping
|
53
|
+
RCH = "%"
|
54
|
+
|
55
|
+
# The path character to assign value "="
|
56
|
+
VCH = "="
|
57
|
+
|
58
|
+
# The escape character to use any PATH_CHARS as its literal.
|
59
|
+
ECH = "\\"
|
60
|
+
|
61
|
+
# The Regexp escaped version of ECH.
|
62
|
+
RECH = Regexp.escape ECH
|
63
|
+
|
64
|
+
# The EndOfPath delimiter after which regex opt chars may be specified.
|
65
|
+
EOP = DCH + DCH
|
66
|
+
|
67
|
+
# The key string that represents PARENT.
|
68
|
+
PARENT_KEY = ".."
|
69
|
+
|
70
|
+
# The key string that indicates recursive lookup.
|
71
|
+
RECUR_KEY = "**"
|
72
|
+
|
73
|
+
|
74
|
+
require 'path/match'
|
75
|
+
require 'path/matcher'
|
76
|
+
require 'path/transaction'
|
77
|
+
require 'path/core_ext'
|
78
|
+
|
79
|
+
|
80
|
+
|
81
|
+
##
|
82
|
+
# Instantiate a Path object with a String data path.
|
83
|
+
# Path.new "/path/**/to/*=bar/../../**/last"
|
84
|
+
|
85
|
+
def initialize path_str, regex_opts=nil, &block
|
86
|
+
path_str = path_str.dup
|
87
|
+
@path = self.class.parse_path_str path_str, regex_opts, &block
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
##
|
92
|
+
# Finds the current path in the given data structure.
|
93
|
+
# Returns a Hash of path_ary => data pairs for each match.
|
94
|
+
#
|
95
|
+
# If a block is given, yields the parent data object matched,
|
96
|
+
# the key, and the path array.
|
97
|
+
#
|
98
|
+
# data = {:path => {:foo => :bar, :sub => {:foo => :bar2}}, :other => nil}
|
99
|
+
# path = Path.new "path/**/foo"
|
100
|
+
#
|
101
|
+
# all_args = []
|
102
|
+
#
|
103
|
+
# path.find_in data |*args|
|
104
|
+
# all_args << args
|
105
|
+
# end
|
106
|
+
# #=> {[:path, :foo] => :bar, [:path, :sub, :foo] => :bar2}
|
107
|
+
#
|
108
|
+
# all_args
|
109
|
+
# #=> [
|
110
|
+
# #=> [{:foo => :bar, :sub => {:foo => :bar2}}, :foo, [:path, :foo]],
|
111
|
+
# #=> [{:foo => :bar2}, :foo, [:path, :sub, :foo]]
|
112
|
+
# #=> ]
|
113
|
+
|
114
|
+
def find_in data
|
115
|
+
matches = {[] => data}
|
116
|
+
|
117
|
+
@path.each_with_index do |matcher, i|
|
118
|
+
last_item = i == @path.length - 1
|
119
|
+
|
120
|
+
self.class.assign_find(matches, data, matcher) do |sdata, key, spath|
|
121
|
+
yield sdata, key, spath if last_item && block_given?
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
matches
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
##
|
130
|
+
# Returns a path-keyed data hash. Be careful of mixed key types in hashes
|
131
|
+
# as Symbols and Strings both use #to_s.
|
132
|
+
#
|
133
|
+
# Path.pathed {'foo' => %w{thing bar}, 'fizz' => {'buzz' => 123}}
|
134
|
+
# #=> {
|
135
|
+
# # '/foo/0' => 'thing',
|
136
|
+
# # '/foo/1' => 'bar',
|
137
|
+
# # '/fizz/buzz' => 123
|
138
|
+
# # }
|
139
|
+
|
140
|
+
def self.pathed data, escape=true
|
141
|
+
new_data = {}
|
142
|
+
|
143
|
+
find "**", data do |subdata, key, path|
|
144
|
+
next if Array === subdata[key] || Hash === subdata[key]
|
145
|
+
path_str = "#{DCH}#{join(path, escape)}"
|
146
|
+
new_data[path_str] = subdata[key]
|
147
|
+
end
|
148
|
+
|
149
|
+
new_data
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
SPECIAL_CHARS = "*?()|/."
|
154
|
+
R_SPECIAL_CHARS = /[\0#{Regexp.escape SPECIAL_CHARS}]/u
|
155
|
+
|
156
|
+
##
|
157
|
+
# Joins an Array into a path String.
|
158
|
+
|
159
|
+
def self.join path_arr, escape=true
|
160
|
+
path_str = path_arr.join("\0")
|
161
|
+
if escape
|
162
|
+
path_str.gsub!(R_SPECIAL_CHARS){|c| c == "\0" ? DCH : "\\#{c}"}
|
163
|
+
else
|
164
|
+
path_str.gsub! "\0", DCH
|
165
|
+
end
|
166
|
+
path_str
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
##
|
171
|
+
# Fully streamed version of:
|
172
|
+
# Path.new(str_path).find_in data
|
173
|
+
#
|
174
|
+
# See Path#find_in for usage.
|
175
|
+
|
176
|
+
def self.find path_str, data, regex_opts=nil, &block
|
177
|
+
matches = {[] => data}
|
178
|
+
|
179
|
+
parse_path_str path_str, regex_opts do |matcher, last_item|
|
180
|
+
assign_find matches, data, matcher do |sdata, key, spath|
|
181
|
+
yield sdata, key, spath if last_item && block_given?
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
matches
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
##
|
190
|
+
# Common find functionality that assigns to the matches hash.
|
191
|
+
|
192
|
+
def self.assign_find matches, data, matcher
|
193
|
+
matches.keys.each do |path|
|
194
|
+
pdata = matches.delete path
|
195
|
+
|
196
|
+
if matcher.key == PARENT
|
197
|
+
path = path[0..-2]
|
198
|
+
subdata = data_at_path path[0..-2], data
|
199
|
+
|
200
|
+
#!! Avoid yielding parent more than once
|
201
|
+
next if matches[path]
|
202
|
+
|
203
|
+
yield subdata, path.last, path if block_given?
|
204
|
+
matches[path] = subdata[path.last]
|
205
|
+
next
|
206
|
+
end
|
207
|
+
|
208
|
+
matcher.find_in pdata, path do |sdata, key, spath|
|
209
|
+
yield sdata, key, spath if block_given?
|
210
|
+
matches[spath] = sdata[key]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
|
216
|
+
##
|
217
|
+
# Returns the data object found at the given path array.
|
218
|
+
# Returns nil if not found.
|
219
|
+
|
220
|
+
def self.data_at_path path_arr, data
|
221
|
+
c_data = data
|
222
|
+
|
223
|
+
path_arr.each do |key|
|
224
|
+
c_data = c_data[key]
|
225
|
+
end
|
226
|
+
|
227
|
+
c_data
|
228
|
+
|
229
|
+
rescue NoMethodError, TypeError
|
230
|
+
nil
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
##
|
235
|
+
# Parses a path String into an Array of arrays containing
|
236
|
+
# matchers for key, value, and any special modifiers
|
237
|
+
# such as recursion.
|
238
|
+
#
|
239
|
+
# Path.parse_path_str "/path/**/to/*=bar/../../**/last"
|
240
|
+
# #=> [["path",ANY_VALUE,false],["to",ANY_VALUE,true],[/.*/,"bar",false],
|
241
|
+
# # [PARENT,ANY_VALUE,false],[PARENT,ANY_VALUE,false],
|
242
|
+
# # ["last",ANY_VALUE,true]]
|
243
|
+
#
|
244
|
+
# Note: Path.parse_path_str will slice the original path string
|
245
|
+
# until it is empty.
|
246
|
+
|
247
|
+
def self.parse_path_str path, regex_opts=nil
|
248
|
+
path = path.dup
|
249
|
+
regex_opts = parse_regex_opts! path, regex_opts
|
250
|
+
|
251
|
+
parsed = []
|
252
|
+
|
253
|
+
escaped = false
|
254
|
+
key = ""
|
255
|
+
value = nil
|
256
|
+
recur = false
|
257
|
+
next_item = false
|
258
|
+
|
259
|
+
until path.empty?
|
260
|
+
char = path.slice!(0..0)
|
261
|
+
|
262
|
+
case char
|
263
|
+
when DCH
|
264
|
+
next_item = true
|
265
|
+
char = ""
|
266
|
+
|
267
|
+
when VCH
|
268
|
+
value = ""
|
269
|
+
next
|
270
|
+
|
271
|
+
when ECH
|
272
|
+
escaped = true
|
273
|
+
next
|
274
|
+
end unless escaped
|
275
|
+
|
276
|
+
char = "#{ECH}#{char}" if escaped
|
277
|
+
|
278
|
+
if value
|
279
|
+
value << char
|
280
|
+
else
|
281
|
+
key << char
|
282
|
+
end
|
283
|
+
|
284
|
+
next_item = true if path.empty?
|
285
|
+
|
286
|
+
if next_item
|
287
|
+
next_item = false
|
288
|
+
|
289
|
+
if key == RECUR_KEY
|
290
|
+
key = "*"
|
291
|
+
recur = true
|
292
|
+
key = "" and next unless value || path.empty?
|
293
|
+
|
294
|
+
elsif key == PARENT_KEY
|
295
|
+
key = PARENT
|
296
|
+
|
297
|
+
if recur
|
298
|
+
key = "" and next unless value
|
299
|
+
key = "*"
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Make sure we're not trying to access /./thing
|
304
|
+
unless key =~ /^\.?$/ && !value
|
305
|
+
matcher = Matcher.new :key => key,
|
306
|
+
:value => value,
|
307
|
+
:recursive => recur,
|
308
|
+
:regex_opts => regex_opts
|
309
|
+
|
310
|
+
parsed << matcher
|
311
|
+
yield matcher, path.empty? if block_given?
|
312
|
+
end
|
313
|
+
|
314
|
+
key = ""
|
315
|
+
value = nil
|
316
|
+
recur = false
|
317
|
+
end
|
318
|
+
|
319
|
+
escaped = false
|
320
|
+
end
|
321
|
+
|
322
|
+
parsed
|
323
|
+
end
|
324
|
+
|
325
|
+
|
326
|
+
##
|
327
|
+
# Parses the tail end of a path String to determine regexp matching flags.
|
328
|
+
|
329
|
+
def self.parse_regex_opts! path, default=nil
|
330
|
+
opts = default || 0
|
331
|
+
|
332
|
+
return default unless
|
333
|
+
path =~ %r{[^#{RECH}]#{EOP}[#{REGEX_OPTS.keys.join}]+\Z}
|
334
|
+
|
335
|
+
path.slice!(%r{#{EOP}[#{REGEX_OPTS.keys.join}]+\Z}).to_s.
|
336
|
+
each_char{|c| opts |= REGEX_OPTS[c] || 0}
|
337
|
+
|
338
|
+
opts if opts > 0
|
339
|
+
end
|
340
|
+
end
|