ruby-path 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
File without changes
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2012-03-27
2
+
3
+ * Major Enhancements:
4
+
5
+ * Birthday!
6
+
@@ -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
@@ -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.
@@ -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
@@ -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