rbpath 0.2.3

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 31a72727369862168d205513377ef7f776ba1657
4
+ data.tar.gz: 8a67e07605cd5e5ef79f273fe31779e2eca9d6e6
5
+ SHA512:
6
+ metadata.gz: 6be6653406797068a4afb74d5d4adad456fd98f576929c4c337149290288f079a54ad78ac3e7d567e776147abc2b22d8c5cfda107bcac4b9a6cd67f6fcb7d419
7
+ data.tar.gz: 100d805624f2ef1b0e24f4fc39361d3358ee1c77cc4c760726f701d407db3712f6a79af35847b7c303dd0942debc184aa5685093e02746fec3481228430d465b
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jrel.gemspec
4
+ gemspec
5
+
6
+ gem 'pry'
7
+ gem 'growl'
8
+ gem 'guard'
9
+ gem 'hirb'
10
+ gem 'guard-minitest'
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ notification :growl
5
+
6
+ guard :minitest do
7
+ watch(%r{^test/test_(.*)\.rb})
8
+ watch(%r{^lib/(.*/)?([^/]+)\.rb}) { |m| "test/test_#{m[2]}.rb" }
9
+ watch(%r{^test/test_helper\.rb}) { 'test' }
10
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Alex Skryl
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,351 @@
1
+ # RbPath
2
+
3
+ RbPath is a small library for finding and retrieving data in large Ruby
4
+ collections (Arrays/Hashes) and object graphs, similar to XPath and CSS
5
+ selectors. You might use it over XPath or something similar because it's
6
+ super lightweight and may do exactly what you need without the complex
7
+ semantics of XPath or CSS selectors. It also makes operations such as regular
8
+ expression filtering much easier to use.
9
+
10
+
11
+ ## Table of contents
12
+
13
+ - [Installation](#installation)
14
+ - [Usage](#usage)
15
+ - [Queries](#queries)
16
+ - [Literals](#literals)
17
+ - [Wildcards](#wildcards)
18
+ - [Logic Expressions](#logic-expressions)
19
+ - [Regex Matching](#regex-matching)
20
+ - [Gotchas](#gotchas)
21
+ - [Working with JSON/YAML/XML](#working-with-jsonyamlxml)
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ gem 'rbpath'
28
+
29
+ And then execute:
30
+
31
+ $ bundle
32
+
33
+ Or install it yourself as:
34
+
35
+ $ gem install rbpath
36
+
37
+ ## Usage
38
+
39
+ ### Direct
40
+
41
+ You can use the query engine directly through the Query class.
42
+
43
+ ```ruby
44
+ require 'rbpath'
45
+
46
+ h = {...}
47
+
48
+ RbPath::Query.new(...).query(h)
49
+ ```
50
+
51
+ ### Object Mixin
52
+
53
+ You can add the query interface to an existing instance of a Hash or Array.
54
+
55
+ ```ruby
56
+ require 'rbpath'
57
+
58
+ h = {...}
59
+ h.extend RbPath
60
+
61
+ h.query(...)
62
+ ```
63
+
64
+ ### Class Mixin
65
+
66
+ You can make your own objects queryable by using the RbPath mixin.
67
+
68
+ ```ruby
69
+ require 'rbpath'
70
+
71
+ class Person < Struct.new(:first, :middle, :last, :age, :relatives)
72
+ include RbPath
73
+
74
+ rbpath :first, :middle, :last, :age, :relatives
75
+ end
76
+
77
+ p = Person.new('john', 'michael', 'doe', 21, [relative1,...])
78
+
79
+ p.query(...)
80
+ ```
81
+
82
+ Notice that the rbpath attributes must be explicitly listed.
83
+
84
+ ## Queries
85
+
86
+ Queries are similar to XPath expressions. They are used to navigate and find
87
+ information in tree-like data structures.
88
+
89
+ ```ruby
90
+ class Employee < Struct.new(:first, :last, :position)
91
+ include RbPath
92
+ rbpath :first, :last, :position
93
+ end
94
+
95
+ data = {
96
+ illinois: {
97
+ chicago: {
98
+ inventory: {
99
+ bakery: { white: 220, whole_wheat: 150, multigrain: 72, rye: 27 },
100
+ fish: { salmon: 110, tuna: 115, flounder: 22, catfish: 90, cod: 15 },
101
+ meat: { ribeye: 23, pork_chop: 19, pork_loin: 12, beef_brisket: 30 }},
102
+ employees: [
103
+ Employee.new("John", "Sansk", "General Manager"),
104
+ Employee.new("Gene", "Pollack", "Warehouse Manager"),
105
+ Employee.new("Luke", "Sanders", "Director")],
106
+ address: '101 Big St',
107
+ services: [:pharmacy, :bakery, :groceries, :kids_corner, :pet_grooming]
108
+ },
109
+ springfield: {
110
+ inventory: {
111
+ fish: { salmon: 101, trout: 97, snapper: 172, catfish: 17, cod: 93 },
112
+ meat: { ribeye: 13, chuck_roast: 82, flank_steak: 73, beef_brisket: 30 }},
113
+ employees: [
114
+ Employee.new("Kerry", "Adams", "General Manager"),
115
+ Employee.new("Sherry", "Nerst", "Warehouse Manager"),
116
+ Employee.new("Kate", "Holmes", "Director")],
117
+ address: '220 Small St',
118
+ services: [:groceries, :kids_corner]
119
+ }
120
+ }
121
+ }
122
+
123
+ data.extend RbPath
124
+ ```
125
+
126
+ The sample data above represents a chain of grocery stores and their employees.
127
+ We will see how RbPath can extract useful information from this set of data.
128
+
129
+
130
+ ### Literals
131
+
132
+ The result of a *query* call will always be a list values that satisfy it, or
133
+ an empty list if no matching values were found. There is also an analagous
134
+ *pquery* interface which, instead of returning the values themselves, will
135
+ return the paths to the values (or an empty list). Calling *path_values* on the
136
+ result of *pquery* is the same as calling *query* directly.
137
+
138
+ To make the examples more concise, only results from the *pquery* call will be
139
+ provided in later examples.
140
+
141
+ ```ruby
142
+ # Xpath: /illinois/chicago
143
+
144
+ > data.query("illinois chicago")
145
+ => [{inventory: {...}, employees: [...], address: "...", services: [...]}]
146
+
147
+ > data.pquery("illinois chicago")
148
+ => [['illinois','chicago']]
149
+
150
+ > data.path_values( data.pquery("illinois chicago") )
151
+ => [{inventory: {...}, employees: [...], address: "...", services: [...]}]
152
+
153
+ # Xpath: /california/san_francisco
154
+
155
+ > data.query("california san_francisco")
156
+ => []
157
+
158
+ > data.pquery("california san_francisco")
159
+ => []
160
+ ```
161
+
162
+ Notice that the elements are seperated by **spaces** instead of slashes, and rbpath
163
+ queries are **absolute** by default.
164
+
165
+ Because the access semantics for Ruby collections (Arrays vs Hashes vs Objects)
166
+ are inherently different, queries into Arrays will have numerical indices
167
+ while queries into Hashes and RbPath objects will usually have string
168
+ indices, much like in XPath.
169
+
170
+ ``` ruby
171
+ # Xpath: /illinois/chicago/services[1]
172
+
173
+ > data.pquery("illinois chicago services 0")
174
+ => [['illinois','chicago','services','0']]
175
+ ```
176
+
177
+ Results to absolute queries aren't very interesting though, since they only
178
+ return a single match. Other queries can return multiple matching paths.
179
+
180
+ ### Wildcards
181
+
182
+ The star in the query below represents a **wildcard** match. It allows us to
183
+ match more than one value at a particular depth in the tree.
184
+
185
+ ```ruby
186
+ # XPath: /illinois/*/employees
187
+
188
+ > data.pquery("illinois * employees")
189
+ => [['illinois','chicago','employees'],
190
+ ['illinois','springfield','employees']]
191
+ ```
192
+
193
+ Wildcards can also be span across multiple levels of the tree, in case you
194
+ don't know how deep your value lives. These **multi-level wildcards** will reach
195
+ across 0 or more depth levels.
196
+
197
+ ```ruby
198
+ # XPath: //illinois
199
+
200
+ > data.pquery("** illinois")
201
+ => [['illinois']]
202
+
203
+ # XPath: //employees
204
+
205
+ > data.pquery("** employees")
206
+ => [['illinois','employees'],
207
+ ['illinois','chicago','employees'],
208
+ ['illinois','springfield','employees']]
209
+
210
+ ```
211
+
212
+ Notice that paths of differnt lengths may be returned in the resulting set when
213
+ using multi-level wildcards.
214
+
215
+ ### Logic Expressions
216
+
217
+ In addiction to wildcards, there is another, more restrictive, way to match
218
+ several paths at once using **AND** and **NOR** expressions.
219
+
220
+ ```ruby
221
+ # XPath: /illinois/chicago/inventory/*/salmon | /illinois/chicago/inventory/*/pork_chop
222
+
223
+ > data.pquery("illinois chicago inventory * (salmon,pork_chop)")
224
+ => [['illinois','chicago','inventory','fish','salmon'],
225
+ ['illinois','chicago','inventory','meat','pork_chop']]
226
+ ```
227
+
228
+ The above query will select all the inventory paths in the chicago store that
229
+ match 'salmon' **AND** 'pork_chop' for any of the departments. We can also
230
+ achieve the opposite effect by using a **NOR** expression and specifying a
231
+ list of values to avoid matching.
232
+
233
+ ```ruby
234
+ # XPath: /illinois/chicago/inventory/fish[not(contains(salmon)) and not(contains(tuna))]
235
+
236
+ > data.pquery("illinois chicago inventory fish [salmon,tuna]")
237
+ => [['illinois','chicago','inventory','fish','flounder'],
238
+ ['illinois','chicago','inventory','fish','catfish'],
239
+ ['illinois','chicago','inventory','fish','cod']]
240
+ ```
241
+
242
+ This query gives us all the fish in the chicago store which **do not match**
243
+ 'salmon' or 'tuna'.
244
+
245
+ ### Regex Matching
246
+
247
+ It's not always enough to be able to filter on an exact field/key name.
248
+ Sometimes you only know part of the name or you want to match all the names
249
+ that match a certain pattern. This is where regular expressions come in handy.
250
+
251
+ You can include plain old ruby regexes in your quries by splitting the query up
252
+ into multiple arguments. In this case the query will will still be processed as
253
+ though it was continuous.
254
+
255
+ ```ruby
256
+ > data.pquery("illinois chicago inventory meat", /(pork.*|beef.*)/)
257
+ => [['illinois','chicago','inventory','meat','pork_chop'],
258
+ ['illinois','chicago','inventory','meat','pork_loin'],
259
+ ['illinois','chicago','inventory','meat','beef_brisket']]
260
+
261
+ > data.pquery("illinois", /(chi.*|spr.*)/, "inventory *")
262
+ => [['illinois','chicago','inventory','bakery'],
263
+ ['illinois','chicago','inventory','fish'],
264
+ ['illinois','chicago','inventory','meat'],
265
+ ['illinois','springfield','inventory','fish'],
266
+ ['illinois','springfield','inventory','meat']]
267
+ ```
268
+
269
+ XPath 1.0 doesn't actually support regular expressions, but it does provide some
270
+ [specialized functions](http://www.w3.org/TR/xpath/#function-starts-with) for
271
+ partially matching element names such as *starts-with()* and *contains()*.
272
+
273
+ ### Gotchas
274
+
275
+ Sometimes you may need to find strings with spaces or things which are not
276
+ strings at all. Here is how you do that.
277
+
278
+ Single quote your string or use a regex matcher when your filter string
279
+ contains spaces.
280
+
281
+ ```ruby
282
+ > data.pquery("* * employees * position 'General Manager'")
283
+ => [['illinois','chicago','employees','0','position','General Manager'],
284
+ ['illinois','chicago','employees','0','position','General Manager']]
285
+
286
+ > data.pquery("* * employees 0 position", /General Manager/)
287
+ => [['illinois','chicago','employees','0','position','General Manager'],
288
+ ['illinois','chicago','employees','0','position','General Manager']]
289
+ ```
290
+
291
+ Split up queries that use non-String filters into multiple arguments, just like
292
+ we did with regular expressions.
293
+
294
+ ```ruby
295
+ > data.pquery("* * inventory * *", 30)
296
+ => [['illinois','chicago','inventory','meat','beef_brisket',30],
297
+ ['illinois','springfield','inventory','meat','beef_brisket',30]]
298
+ ```
299
+
300
+ ## Working With JSON/YAML/XML
301
+
302
+ You can use the **rq** command (which is installed with the gem) from your
303
+ shell to query JSON, YAML and XML files using the RbPath engine, but you
304
+ will not be able to match regular expressions or non-string values due to the
305
+ limitations of the query parser.
306
+
307
+ ```bash
308
+ Usage: rq [OPTIONS] QUERY
309
+ -f, --file [FILE] File to parse
310
+ -t, --type [TYPE] File format
311
+ -p, --paths Paths only
312
+ -h, --help Show usage
313
+ ```
314
+
315
+ - If you don't supply the *file* option then data will be read from STDIN.
316
+ - The *paths* option will mimic the *pquery* interface shown in the examples.
317
+
318
+ ```bash
319
+ # read from a file
320
+ $ rq -f data.json '** john **'
321
+
322
+ # read from STDIN
323
+ $ curl http://myservice.com/api/1.json | rq -t json '** john **'
324
+ ```
325
+
326
+ ### XML
327
+
328
+ The [xml-simple](http://rubygems.org/gems/xml-simple) gem is used to convert
329
+ xml files to Ruby hashes prior to processing, so it must be installed if you
330
+ want to query XML files.
331
+
332
+ ```bash
333
+ gem install xml-simple
334
+ ```
335
+
336
+ ### Pretty Printer
337
+
338
+ Installing the [hirb](http://rubygems.org/gems/hirb) gem will tabularize
339
+ certain types of output, making it much easier to read.
340
+
341
+ ```bash
342
+ gem install hirb
343
+ ```
344
+
345
+ ## Contributing
346
+
347
+ 1. Fork it
348
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
349
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
350
+ 4. Push to the branch (`git push origin my-new-feature`)
351
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/TODO.md ADDED
@@ -0,0 +1,7 @@
1
+ # TODO
2
+ - explain leaf vs path value matching in readme
3
+ - paths should be returned with array indices as integers
4
+ - handle nil and false keys/values properly
5
+ - deep_stringify_all is too costly
6
+ - test all README examples
7
+ - add rq tests
data/bin/rq ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ GC.disable # short lived process doesn't need gc
4
+
5
+ lib = "#{File.expand_path(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__)}/../../lib"
6
+ $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
7
+
8
+ require 'optparse'
9
+ require 'rbpath'
10
+ require 'pp'
11
+
12
+ begin
13
+ require 'hirb'
14
+ rescue LoadError
15
+ end
16
+
17
+ options = {}
18
+ optparse = OptionParser.new do |opts|
19
+ opts.banner = "Usage: rq [OPTIONS] QUERY"
20
+
21
+ opts.on("-f", "--file [FILE]", "File to parse") { |opt| options[:file] = opt }
22
+ opts.on("-t", "--type [TYPE]", "File format") { |opt| options[:type] = opt }
23
+ opts.on("-p", "--paths", "Paths only") { |opt| options[:paths] = true}
24
+ opts.on("-h", "--help", "Show usage") { puts opts; exit }
25
+ end
26
+
27
+ begin
28
+ optparse.parse!(ARGV)
29
+ query = ARGV.shift.to_s
30
+ file, type = options.values_at(:file, :type)
31
+ content = (file && File.read(file)) || ARGF.read
32
+ extension = type || (file && File.extname(file)[1..-1]) || 'json'
33
+
34
+ raise OptionParser::MissingArgument unless query
35
+
36
+ data = \
37
+ case extension
38
+ when 'yml', 'yaml'
39
+ require 'yaml'
40
+ YAML.load(content)
41
+ when 'jsn', 'json'
42
+ require 'json'
43
+ JSON.parse(content)
44
+ when 'xml'
45
+ require 'xmlsimple'
46
+ XmlSimple.xml_in(content, 'ForceArray' => false)
47
+ end
48
+
49
+ rescue LoadError
50
+ puts "You are probably missing the 'xml-simple' gem, install it by running 'gem install xml-simple' and try again."
51
+ exit
52
+ rescue OptionParser::MissingArgument
53
+ puts optparse.help
54
+ exit
55
+ rescue Exception => e
56
+ puts "Error: #{e}"
57
+ exit
58
+ end
59
+
60
+ result = RbPath::Query.new(query).send(options[:paths] ? :pquery : :query, data)
61
+
62
+ if options[:paths]
63
+ defined?(Hirb)
64
+ puts(Hirb::Helpers::AutoTable.render(result))
65
+ else puts result.map(&:pretty_inspect)
66
+ end
data/lib/rbpath.rb ADDED
@@ -0,0 +1,17 @@
1
+ require "rbpath/version"
2
+
3
+ module RbPath
4
+ autoload :Query, 'rbpath/query'
5
+ autoload :Utils, 'rbpath/utils'
6
+ autoload :ObjectMixin, 'rbpath/object_mixin'
7
+ autoload :ClassMixin, 'rbpath/class_mixin'
8
+
9
+ def self.included(klass)
10
+ klass.send(:include, RbPath::ObjectMixin)
11
+ klass.extend RbPath::ClassMixin
12
+ end
13
+
14
+ def self.extended(obj)
15
+ obj.singleton_class.send(:include, RbPath::ObjectMixin)
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module RbPath::ClassMixin
2
+
3
+ def rbpath(*fields)
4
+ @_rbpath_fields_ = fields.map(&:to_s)
5
+ end
6
+
7
+ def rbpath_fields
8
+ @_rbpath_fields_
9
+ end
10
+
11
+ end
@@ -0,0 +1,26 @@
1
+ module RbPath::ObjectMixin
2
+
3
+ def query(*query)
4
+ RbPath::Query.new(*query).query(self)
5
+ end
6
+
7
+ def pquery(*query)
8
+ RbPath::Query.new(*query).pquery(self)
9
+ end
10
+
11
+ def path_values(paths)
12
+ RbPath::Query.new(*query).values_at(self, paths)
13
+ end
14
+
15
+ # The object's class may not have the ClassMixin if a singleton object
16
+ # was extended:
17
+ #
18
+ # h = { a: 1, b: 2}
19
+ # h.extend RbPath
20
+ #
21
+ def rbpath_fields
22
+ self.class.respond_to?(:rbpath_fields) ?
23
+ self.class.rbpath_fields : nil
24
+ end
25
+
26
+ end
@@ -0,0 +1,105 @@
1
+ class RbPath::Query
2
+ include RbPath::Utils
3
+
4
+ # takes a string query or a pre-parsed query list
5
+ #
6
+ def initialize(*query)
7
+ @query = parse_query_list(query)
8
+ end
9
+
10
+ # Parsing rules:
11
+ # - query keys are seperated by spaces, keys with spaces must be single quoted
12
+ # - brackets group keys into an NOR group
13
+ # - parens group keys into a OR group
14
+ # - valid keys names consist of [chars|nums|spaces|-|_|.], anything else can
15
+ # be used as a seperator inside the parens/brackets
16
+ #
17
+ def parse_string_query(query)
18
+ query.scan(/(\([^\)]+\)|\[[^\]]+\]|'[^']+'|[^\s]+)/)
19
+ .flatten
20
+ .map { |keys| { multi: /\*\*/ === keys[0..1],
21
+ neg: /[\[\*]/ === keys[0],
22
+ keys: keys.scan(/[\w\d\s\-\_\.]+/) }}
23
+ end
24
+
25
+ def parse_query_list(query)
26
+ query.flat_map do |part|
27
+ case part
28
+ when String, Symbol
29
+ parse_string_query(part.to_s)
30
+ when Regexp
31
+ {multi: false, neg: false, keys: [], regexp: part}
32
+ else {multi: false, neg: false, keys: [part]}
33
+ end
34
+ end
35
+ end
36
+
37
+ def query(data)
38
+ data = deep_stringify_all(data)
39
+ do_query(data, @query, [[]]).map(&:flatten)
40
+ .map { |path| get_value(data, path) }
41
+ end
42
+
43
+ def pquery(data)
44
+ do_query(deep_stringify_all(data), @query, [[]]).map(&:flatten)
45
+ end
46
+
47
+ def values_at(data, paths)
48
+ paths.map {|path| get_value(deep_stringify_all(data), path) }
49
+ end
50
+
51
+ private
52
+
53
+ def do_query(data, query, valid_paths)
54
+ matcher, *rest = *query
55
+
56
+ if query.empty? || valid_paths.empty?
57
+ valid_paths
58
+ elsif matcher[:multi]
59
+ do_query(data, rest, valid_paths) +
60
+ do_query(data, query, match(data, matcher, valid_paths))
61
+ else
62
+ do_query(data, rest, match(data, matcher, valid_paths))
63
+ end
64
+ end
65
+
66
+ def match(data, matcher, valid_paths)
67
+ neg, keys, rgx = matcher.values_at(:neg, :keys, :regexp)
68
+
69
+ valid_paths.flat_map { |path| children = if rgx
70
+ all_keys(data, path).grep(rgx)
71
+ elsif neg
72
+ (all_keys(data, path) - keys)
73
+ else keys; end
74
+ [path].product(children).map(&:flatten) } \
75
+ .select { |path| get_value(data, path) }
76
+ end
77
+
78
+ def all_keys(data, path)
79
+ value = get_value(data, path)
80
+
81
+ case value
82
+ when Hash then value.keys
83
+ when Array then (0...value.size).map(&:to_s)
84
+ when RbPath
85
+ value.rbpath_fields
86
+ else [value]
87
+ end
88
+ end
89
+
90
+ def get_value(data, path)
91
+ return data if path.empty?
92
+ key, *rest = *path
93
+
94
+ case data
95
+ when Hash
96
+ get_value(data[key], rest)
97
+ when Array
98
+ get_value(data[Integer(key)], rest) rescue nil
99
+ when RbPath
100
+ get_value(data.send(key), rest) if data.respond_to?(key)
101
+ when key
102
+ rest.empty? ? data : nil
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,20 @@
1
+ module RbPath::Utils
2
+ def deep_transform_all(obj, &block)
3
+ case obj
4
+ when Hash
5
+ Hash[ obj.map { |k,v| [deep_transform_all(k, &block),
6
+ deep_transform_all(v, &block)] }]
7
+ when Array
8
+ obj.map { |i| deep_transform_all(i, &block) }
9
+ else block[obj]
10
+ end
11
+ end
12
+
13
+ def deep_stringify_all(obj)
14
+ deep_transform_all(obj) { |obj| obj.is_a?(Symbol) ? obj.to_s : obj }
15
+ end
16
+
17
+ def deep_symbolize_all(obj)
18
+ deep_transform_all(obj) { |obj| obj.is_a?(String) ? obj.to_sym : obj }
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module RbPath
2
+ VERSION = "0.2.3"
3
+ end
data/rbpath.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rbpath/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rbpath"
8
+ spec.version = RbPath::VERSION
9
+ spec.authors = ["Alex Skryl"]
10
+ spec.email = ["rut216@gmail.com"]
11
+ spec.description = %q{A lightweight library for running XPath like queries on Ruby collections and object graphs.}
12
+ spec.summary = %q{A lightweight library for running XPath like queries on Ruby collections and object graphs}
13
+ spec.homepage = "http://github.com/skryl/rbpath"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ end
File without changes
File without changes
@@ -0,0 +1,48 @@
1
+ require_relative '../test_helper'
2
+
3
+ module TestData
4
+ class Employee < Struct.new(:first, :last, :position)
5
+ include RbPath
6
+ rbpath :first, :last, :position
7
+ end
8
+
9
+ STORE_DATA =
10
+ {
11
+ illinois: {
12
+ employees: [
13
+ Employee.new("Kerry", "Adams", "District Manager")],
14
+ chicago: {
15
+ inventory: {
16
+ apples: { granny_smith: 150, gala: 200, cameo: 150, honeycrisp: 75 },
17
+ bread: { white: 220, whole_wheat: 150, multigrain: 72, rye: 27 },
18
+ fish: { salmon: 110, tuna: 115, flounder: 22, catfish: 90, cod: 15 },
19
+ meat: { ribeye: 23, pork_chop: 19, pork_loin: 12, beef_brisket: 30 },
20
+ nuts: { brazil: 200, pecan: 173, almond: 37, cashew: 12, chestnut: 70 },
21
+ shrimp: { arctic: 120, fresh_water: 20, atlantic: 72 } },
22
+ employees: [
23
+ Employee.new("John", "Sansk", "General Manager"),
24
+ Employee.new("Sam", "Bogert", "Checkout Manager"),
25
+ Employee.new("Gene", "Pollack", "Warehouse Manager"),
26
+ Employee.new("Shane", "Leson", "Human Resources") ],
27
+ address: '101 Big St',
28
+ services: [:pharmacy, :groceries, :salon, :kids_corner, :pet_grooming]
29
+ },
30
+ springfield: {
31
+ inventory: {
32
+ apples: { golden_delicious: 220, fuji: 110, cameo: 101, honeycrisp: 75 },
33
+ bread: { white: 220, whole_wheat: 150, multigrain: 72, rye: 27 },
34
+ fish: { salmon: 101, trout: 97, snapper: 172, catfish: 17, cod: 93 },
35
+ meat: { ribeye: 13, chuck_roast: 82, flank_steak: 73, beef_brisket: 30 },
36
+ nuts: { mixed: 211, pistachio: 75, almond: 370, cashew: 121, trail_mix: 92},
37
+ shrimp: { uncooked: 252, fresh_water: 72, atlantic: 93 } },
38
+ employees: [
39
+ Employee.new("Kerry", "Adams", "General Manager"),
40
+ Employee.new("Jack", "Lenere", "Checkout Manager"),
41
+ Employee.new("Sherry", "Nerst", "Warehouse Manager"),
42
+ Employee.new("Ken", "Beson", "Human Resources") ],
43
+ address: '220 Small St',
44
+ services: [:groceries, :kids_corner]
45
+ }
46
+ }
47
+ }
48
+ end
@@ -0,0 +1,57 @@
1
+ module MiniTest::Assertions
2
+
3
+ class MatchArray
4
+ def initialize(expected, actual)
5
+ @expected = expected
6
+ @actual = actual
7
+ end
8
+
9
+ def match()
10
+ return result, message
11
+ end
12
+
13
+ def result()
14
+ return false unless @actual.respond_to? :to_ary
15
+ @extra_items = difference_between_arrays(@actual, @expected)
16
+ @missing_items = difference_between_arrays(@expected, @actual)
17
+ @extra_items.empty? & @missing_items.empty?
18
+ end
19
+
20
+ def message()
21
+ if @actual.respond_to? :to_ary
22
+ message = "expected collection contained: #{safe_sort(@expected).inspect}\n"
23
+ message += "actual collection contained: #{safe_sort(@actual).inspect}\n"
24
+ message += "the missing elements were: #{safe_sort(@missing_items).inspect}\n" unless @missing_items.empty?
25
+ message += "the extra elements were: #{safe_sort(@extra_items).inspect}\n" unless @extra_items.empty?
26
+ else
27
+ message = "expected an array, actual collection was #{@actual.inspect}"
28
+ end
29
+
30
+ message
31
+ end
32
+
33
+ private
34
+
35
+ def safe_sort(array)
36
+ array.sort rescue array
37
+ end
38
+
39
+ def difference_between_arrays(array_1, array_2)
40
+ difference = array_1.to_ary.dup
41
+ array_2.to_ary.each do |element|
42
+ if index = difference.index(element)
43
+ difference.delete_at(index)
44
+ end
45
+ end
46
+ difference
47
+ end
48
+ end # MatchArray
49
+
50
+ def assert_match_array(expected, actual)
51
+ result, message = MatchArray.new(expected, actual).match
52
+ assert result, message
53
+ end
54
+
55
+ end # MiniTest::Assertions
56
+
57
+ Array.infect_an_assertion :assert_match_array, :must_match_array
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ Bundler.require(:default)
4
+
5
+ require 'json'
6
+ require 'yaml'
7
+ require 'set'
8
+ require 'minitest/autorun'
9
+ require 'minitest/pride'
10
+
11
+ # extend minitest to support out of order array matches
12
+ #
13
+ require_relative 'extensions/match_array'
@@ -0,0 +1,360 @@
1
+ require_relative 'test_helper'
2
+ require_relative 'data/test_data'
3
+
4
+ describe RbPath::Query do
5
+ before do
6
+ @store_data = TestData::STORE_DATA
7
+ end
8
+
9
+ describe 'query parsing' do
10
+
11
+ describe "single string queries" do
12
+
13
+ it 'should identify a single key' do
14
+ RbPath::Query.new("some_key").instance_variable_get('@query').must_equal \
15
+ [{multi: false, neg: false, keys: ['some_key']}]
16
+ end
17
+
18
+ it 'should identify a list of keys' do
19
+ RbPath::Query.new("one two three").instance_variable_get('@query').must_equal \
20
+ [ {multi: false, neg: false, keys: ['one']},
21
+ {multi: false, neg: false, keys: ['two']},
22
+ {multi: false, neg: false, keys: ['three']} ]
23
+ end
24
+
25
+ it 'should identify list of keys with quoted spaces' do
26
+ RbPath::Query.new("one 'two and a half' three").instance_variable_get('@query').must_equal \
27
+ [ {multi: false, neg: false, keys: ['one']},
28
+ {multi: false, neg: false, keys: ['two and a half']},
29
+ {multi: false, neg: false, keys: ['three']} ]
30
+ end
31
+
32
+ it 'should identify ORed keys in a list' do
33
+ RbPath::Query.new("one (two,three,four) ('twenty two','forty three')").instance_variable_get('@query').must_equal \
34
+ [ {multi: false, neg: false, keys: ['one']},
35
+ {multi: false, neg: false, keys: ['two','three','four']},
36
+ {multi: false, neg: false, keys: ['twenty two', 'forty three']} ]
37
+ end
38
+
39
+ it 'should identify NORed keys in a list' do
40
+ RbPath::Query.new("one [two,three,four] ['fifty five','sixty six']").instance_variable_get('@query').must_equal \
41
+ [ {multi: false, neg: false, keys: ['one']},
42
+ {multi: false, neg: true, keys: ['two','three','four']},
43
+ {multi: false, neg: true, keys: ['fifty five', 'sixty six']} ]
44
+ end
45
+
46
+ it 'should identify splats' do
47
+ RbPath::Query.new("one [] (two,three) * four").instance_variable_get('@query').must_equal \
48
+ [ {multi: false, neg: false, keys: ['one']},
49
+ {multi: false, neg: true, keys: []},
50
+ {multi: false, neg: false, keys: ['two', 'three']},
51
+ {multi: false, neg: true, keys: []},
52
+ {multi: false, neg: false, keys: ['four']} ]
53
+ end
54
+
55
+ it 'should identify multi-splats' do
56
+ RbPath::Query.new("one [] (two,three) ** four").instance_variable_get('@query').must_equal \
57
+ [ {multi: false, neg: false, keys: ['one']},
58
+ {multi: false, neg: true, keys: []},
59
+ {multi: false, neg: false, keys: ['two', 'three']},
60
+ {multi: true, neg: true, keys: []},
61
+ {multi: false, neg: false, keys: ['four']} ]
62
+ end
63
+
64
+ it 'should just work' do
65
+ RbPath::Query.new("one [] ('twenty two',three) * () four [five,'sixty five'] 'seventy two' *").instance_variable_get('@query').must_equal \
66
+ [ {multi: false, neg: false, keys: ['one']},
67
+ {multi: false, neg: true, keys: []},
68
+ {multi: false, neg: false, keys: ['twenty two', 'three']},
69
+ {multi: false, neg: true, keys: []},
70
+ {multi: false, neg: false, keys: []},
71
+ {multi: false, neg: false, keys: ['four']},
72
+ {multi: false, neg: true, keys: ['five', 'sixty five']},
73
+ {multi: false, neg: false, keys: ['seventy two']},
74
+ {multi: false, neg: true, keys: []} ]
75
+ end
76
+ end
77
+
78
+ describe "multipart queries" do
79
+
80
+ it 'should accept a multipart query and convert symbols to strings' do
81
+ RbPath::Query.new(:one, 'two', :three).instance_variable_get('@query').must_equal \
82
+ [ {multi: false, neg: false, keys: ['one']},
83
+ {multi: false, neg: false, keys: ['two']},
84
+ {multi: false, neg: false, keys: ['three']} ]
85
+ end
86
+
87
+ it 'should not convert other objects to strings in a multipart query' do
88
+ RbPath::Query.new(:one, 2, 'three', nil).instance_variable_get('@query').must_equal \
89
+ [ {multi: false, neg: false, keys: ['one']},
90
+ {multi: false, neg: false, keys: [2]},
91
+ {multi: false, neg: false, keys: ['three']},
92
+ {multi: false, neg: false, keys: [nil]} ]
93
+ end
94
+
95
+ it 'should seperately parse all parts of a multipart query' do
96
+ RbPath::Query.new("one (two,three)", :four, "[five,six] seven", "'eighty nine'").instance_variable_get('@query').must_equal \
97
+ [ {multi: false, neg: false, keys: ['one']},
98
+ {multi: false, neg: false, keys: ['two','three']},
99
+ {multi: false, neg: false, keys: ['four']},
100
+ {multi: false, neg: true, keys: ['five','six']},
101
+ {multi: false, neg: false, keys: ['seven']},
102
+ {multi: false, neg: false, keys: ['eighty nine']} ]
103
+ end
104
+ end
105
+ end
106
+
107
+ describe 'query engine' do
108
+
109
+ describe 'literal queries' do
110
+
111
+ it 'should work on hashes and their leaf values' do
112
+ RbPath::Query.new("illinois chicago inventory apples gala", 200).pquery(@store_data).must_match_array \
113
+ [['illinois', 'chicago', 'inventory', 'apples', 'gala', 200]]
114
+ end
115
+
116
+ it 'should work on arrays and their leaf values' do
117
+ RbPath::Query.new("illinois chicago services 0 pharmacy").pquery(@store_data).must_match_array \
118
+ [['illinois', 'chicago', 'services', '0', 'pharmacy']]
119
+ end
120
+
121
+ it 'should work on rbpath objects and their leaf values' do
122
+ RbPath::Query.new("illinois chicago employees 0 last Sansk").pquery(@store_data).must_match_array \
123
+ [['illinois', 'chicago', 'employees', '0', 'last', 'Sansk']]
124
+ end
125
+
126
+ # leaf values should not be included in the main query if they are not strings
127
+ #
128
+ it 'should not match stringified leaf values' do
129
+ RbPath::Query.new("illinois chicago inventory apples gala 200").pquery(@store_data).must_match_array []
130
+ end
131
+
132
+ end
133
+
134
+ describe 'splat queries' do
135
+
136
+ it 'should work on leaf values' do
137
+ RbPath::Query.new("illinois chicago inventory apples gala *").pquery(@store_data).must_match_array \
138
+ [['illinois', 'chicago', 'inventory', 'apples', 'gala', 200]]
139
+ end
140
+
141
+ it 'should work on hashes' do
142
+ RbPath::Query.new("* * *").pquery(@store_data).must_match_array \
143
+ [["illinois", "employees", "0"],
144
+ ["illinois", "chicago", "inventory"],
145
+ ["illinois", "chicago", "employees"],
146
+ ["illinois", "chicago", "address"],
147
+ ["illinois", "chicago", "services"],
148
+ ["illinois", "springfield", "inventory"],
149
+ ["illinois", "springfield", "employees"],
150
+ ["illinois", "springfield", "address"],
151
+ ["illinois", "springfield", "services"]]
152
+ end
153
+
154
+
155
+ it 'should work on arrays' do
156
+ RbPath::Query.new("* chicago services *").pquery(@store_data).must_match_array \
157
+ [["illinois", "chicago", "services", "0"],
158
+ ["illinois", "chicago", "services", "1"],
159
+ ["illinois", "chicago", "services", "2"],
160
+ ["illinois", "chicago", "services", "3"],
161
+ ["illinois", "chicago", "services", "4"]]
162
+ end
163
+
164
+ it 'should work on rbpath objects' do
165
+ RbPath::Query.new("* chicago employees 0 *").pquery(@store_data).must_match_array \
166
+ [["illinois", "chicago", "employees", "0", "first"],
167
+ ["illinois", "chicago", "employees", "0", "last"],
168
+ ["illinois", "chicago", "employees", "0", "position"]]
169
+ end
170
+
171
+ end
172
+
173
+ describe 'OR queries' do
174
+
175
+ it 'should work on hashes' do
176
+ RbPath::Query.new("illinois chicago (services,employees) *").pquery(@store_data).must_match_array \
177
+ [["illinois", "chicago", "services", "0"],
178
+ ["illinois", "chicago", "services", "1"],
179
+ ["illinois", "chicago", "services", "2"],
180
+ ["illinois", "chicago", "services", "3"],
181
+ ["illinois", "chicago", "services", "4"],
182
+ ["illinois", "chicago", "employees", "0"],
183
+ ["illinois", "chicago", "employees", "1"],
184
+ ["illinois", "chicago", "employees", "2"],
185
+ ["illinois", "chicago", "employees", "3"]]
186
+ end
187
+
188
+ it 'should work on arrays' do
189
+ RbPath::Query.new("illinois chicago services (0,1) *").pquery(@store_data).must_match_array \
190
+ [["illinois", "chicago", "services", "0", "pharmacy"],
191
+ ["illinois", "chicago", "services", "1", "groceries"]]
192
+ end
193
+
194
+ it 'should work on rbpath objects' do
195
+ RbPath::Query.new("illinois chicago employees * (first,last) *").pquery(@store_data).must_match_array \
196
+ [["illinois", "chicago", "employees", "0", "first", "John"],
197
+ ["illinois", "chicago", "employees", "0", "last", "Sansk"],
198
+ ["illinois", "chicago", "employees", "1", "first", "Sam"],
199
+ ["illinois", "chicago", "employees", "1", "last", "Bogert"],
200
+ ["illinois", "chicago", "employees", "2", "first", "Gene"],
201
+ ["illinois", "chicago", "employees", "2", "last", "Pollack"],
202
+ ["illinois", "chicago", "employees", "3", "first", "Shane"],
203
+ ["illinois", "chicago", "employees", "3", "last", "Leson"]]
204
+ end
205
+ end
206
+
207
+ describe 'NOR queries' do
208
+
209
+ it 'should work on hashes' do
210
+ RbPath::Query.new("illinois chicago [inventory,address] *").pquery(@store_data).must_match_array \
211
+ [["illinois", "chicago", "services", "0"],
212
+ ["illinois", "chicago", "services", "1"],
213
+ ["illinois", "chicago", "services", "2"],
214
+ ["illinois", "chicago", "services", "3"],
215
+ ["illinois", "chicago", "services", "4"],
216
+ ["illinois", "chicago", "employees", "0"],
217
+ ["illinois", "chicago", "employees", "1"],
218
+ ["illinois", "chicago", "employees", "2"],
219
+ ["illinois", "chicago", "employees", "3"]]
220
+ end
221
+
222
+ it 'should work on arrays' do
223
+ RbPath::Query.new("illinois chicago services [2,3,4] *").pquery(@store_data).must_match_array \
224
+ [["illinois", "chicago", "services", "0", "pharmacy"],
225
+ ["illinois", "chicago", "services", "1", "groceries"]]
226
+ end
227
+
228
+ it 'should work on rbpath objects' do
229
+ RbPath::Query.new("illinois chicago employees * [position] *").pquery(@store_data).must_match_array \
230
+ [["illinois", "chicago", "employees", "0", "first", "John"],
231
+ ["illinois", "chicago", "employees", "0", "last", "Sansk"],
232
+ ["illinois", "chicago", "employees", "1", "first", "Sam"],
233
+ ["illinois", "chicago", "employees", "1", "last", "Bogert"],
234
+ ["illinois", "chicago", "employees", "2", "first", "Gene"],
235
+ ["illinois", "chicago", "employees", "2", "last", "Pollack"],
236
+ ["illinois", "chicago", "employees", "3", "first", "Shane"],
237
+ ["illinois", "chicago", "employees", "3", "last", "Leson"]]
238
+ end
239
+ end
240
+
241
+ describe 'REGEX queries' do
242
+
243
+ it 'should work on hashes' do
244
+ RbPath::Query.new("illinois chicago inventory meat", /(pork.*|beef.*)/).pquery(@store_data).must_match_array \
245
+ [["illinois", "chicago", "inventory", "meat", "pork_chop"],
246
+ ["illinois", "chicago", "inventory", "meat", "pork_loin"],
247
+ ["illinois", "chicago", "inventory", "meat", "beef_brisket"]]
248
+ end
249
+
250
+ it 'should work on arrays' do
251
+ RbPath::Query.new("illinois chicago services", /[01]/, "*").pquery(@store_data).must_match_array \
252
+ [["illinois", "chicago", "services", "0", "pharmacy"],
253
+ ["illinois", "chicago", "services", "1", "groceries"]]
254
+ end
255
+
256
+ it 'should work on rbpath objects' do
257
+ RbPath::Query.new("illinois chicago employees *", /(first|last)/, /(Gene|Pollack)/).pquery(@store_data).must_match_array \
258
+ [["illinois", "chicago", "employees", "2", "first", "Gene"],
259
+ ["illinois", "chicago", "employees", "2", "last", "Pollack"]]
260
+ end
261
+ end
262
+
263
+ describe 'Multilevel wildcard queries' do
264
+ it 'should match no elements' do
265
+ RbPath::Query.new("** illinois").pquery(@store_data).must_match_array \
266
+ [["illinois"]]
267
+ end
268
+
269
+ it 'should match leaf nodes' do
270
+ RbPath::Query.new("illinois chicago services 0 pharmacy **").pquery(@store_data).must_match_array \
271
+ [['illinois','chicago','services','0','pharmacy']]
272
+ end
273
+
274
+ it 'should match with multiple multi-wildcards' do
275
+ RbPath::Query.new("** chicago ** pharmacy").pquery(@store_data).must_match_array \
276
+ [['illinois','chicago','services','0','pharmacy']]
277
+ end
278
+
279
+ it 'should work on hashes' do
280
+ RbPath::Query.new("**", /(pork.*|beef.*)/).pquery(@store_data).must_match_array \
281
+ [["illinois", "chicago", "inventory", "meat", "pork_chop"],
282
+ ["illinois", "chicago", "inventory", "meat", "pork_loin"],
283
+ ["illinois", "chicago", "inventory", "meat", "beef_brisket"],
284
+ ["illinois", "springfield", "inventory", "meat", "beef_brisket"]]
285
+ end
286
+
287
+ it 'should work on arrays' do
288
+ RbPath::Query.new("** services", /[01]/, "*").pquery(@store_data).must_match_array \
289
+ [["illinois", "chicago", "services", "0", "pharmacy"],
290
+ ["illinois", "chicago", "services", "1", "groceries"],
291
+ ["illinois", "springfield", "services", "0", "groceries"],
292
+ ["illinois", "springfield", "services", "1", "kids_corner"]]
293
+ end
294
+
295
+ it 'should work on rbpath objects' do
296
+ RbPath::Query.new("**", /(first|last)/, /(Gene|Pollack)/).pquery(@store_data).must_match_array \
297
+ [["illinois", "chicago", "employees", "2", "first", "Gene"],
298
+ ["illinois", "chicago", "employees", "2", "last", "Pollack"]]
299
+ end
300
+ end
301
+ end
302
+
303
+ describe 'classes which include the RbPath mixin' do
304
+
305
+ before do
306
+ @employee = TestData::Employee.new('Alex', 'Skryl', 'CEO')
307
+ @employee_class = TestData::Employee.dup
308
+ end
309
+
310
+ it 'should be rbpath' do
311
+ @employee.must_be_kind_of(RbPath)
312
+ end
313
+
314
+ it 'should return its rbpath fields' do
315
+ @employee.rbpath_fields.must_equal ['first', 'last', 'position']
316
+ @employee.class.rbpath_fields.must_equal ['first', 'last', 'position']
317
+ end
318
+
319
+ it 'should be able to set rbpath fields' do
320
+ @employee_class.class_eval do
321
+ rbpath :one, :two, :three
322
+ end
323
+ @employee_class.rbpath_fields.must_equal ['one', 'two', 'three']
324
+ @employee_class.new.rbpath_fields.must_equal ['one', 'two', 'three']
325
+ end
326
+
327
+ it 'should have instances that are able to query themselves' do
328
+ @employee.must_respond_to(:pquery)
329
+ @employee.pquery("first *").must_equal [['first', 'Alex']]
330
+ end
331
+
332
+ it 'should have instances that are able to fetch values' do
333
+ @employee.must_respond_to(:path_values)
334
+ @employee.path_values([['first','Alex']]).must_equal ['Alex']
335
+ end
336
+ end
337
+
338
+ describe 'singletons which extend the RbPath mixin' do
339
+
340
+ before do
341
+ @hash = {first: 'Alex', last: 'Skryl', age: '100', address: '101 blah st'}
342
+ @hash.extend RbPath
343
+ end
344
+
345
+ it 'should be able to query itself' do
346
+ @hash.must_respond_to(:query)
347
+ @hash.pquery("first *").must_equal [['first', 'Alex']]
348
+ end
349
+
350
+ it 'should have instances that are able to fetch values' do
351
+ @hash.must_respond_to(:path_values)
352
+ @hash.path_values([['first','Alex']]).must_equal ['Alex']
353
+ end
354
+
355
+ it 'should have no rbpath fields' do
356
+ @hash.rbpath_fields.must_equal nil
357
+ end
358
+ end
359
+
360
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rbpath
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.3
5
+ platform: ruby
6
+ authors:
7
+ - Alex Skryl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: A lightweight library for running XPath like queries on Ruby collections
42
+ and object graphs.
43
+ email:
44
+ - rut216@gmail.com
45
+ executables:
46
+ - rq
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - .gitignore
51
+ - Gemfile
52
+ - Guardfile
53
+ - LICENSE.txt
54
+ - README.md
55
+ - Rakefile
56
+ - TODO.md
57
+ - bin/rq
58
+ - lib/rbpath.rb
59
+ - lib/rbpath/class_mixin.rb
60
+ - lib/rbpath/object_mixin.rb
61
+ - lib/rbpath/query.rb
62
+ - lib/rbpath/utils.rb
63
+ - lib/rbpath/version.rb
64
+ - rbpath.gemspec
65
+ - test/data/data.json
66
+ - test/data/data.yaml
67
+ - test/data/test_data.rb
68
+ - test/extensions/match_array.rb
69
+ - test/test_helper.rb
70
+ - test/test_query.rb
71
+ homepage: http://github.com/skryl/rbpath
72
+ licenses:
73
+ - MIT
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.0.14
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: A lightweight library for running XPath like queries on Ruby collections
95
+ and object graphs
96
+ test_files:
97
+ - test/data/data.json
98
+ - test/data/data.yaml
99
+ - test/data/test_data.rb
100
+ - test/extensions/match_array.rb
101
+ - test/test_helper.rb
102
+ - test/test_query.rb