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