rbpath 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +10 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +351 -0
- data/Rakefile +1 -0
- data/TODO.md +7 -0
- data/bin/rq +66 -0
- data/lib/rbpath.rb +17 -0
- data/lib/rbpath/class_mixin.rb +11 -0
- data/lib/rbpath/object_mixin.rb +26 -0
- data/lib/rbpath/query.rb +105 -0
- data/lib/rbpath/utils.rb +20 -0
- data/lib/rbpath/version.rb +3 -0
- data/rbpath.gemspec +23 -0
- data/test/data/data.json +0 -0
- data/test/data/data.yaml +0 -0
- data/test/data/test_data.rb +48 -0
- data/test/extensions/match_array.rb +57 -0
- data/test/test_helper.rb +13 -0
- data/test/test_query.rb +360 -0
- metadata +102 -0
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
data/Gemfile
ADDED
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
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,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
|
data/lib/rbpath/query.rb
ADDED
@@ -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
|
data/lib/rbpath/utils.rb
ADDED
@@ -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
|
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
|
data/test/data/data.json
ADDED
File without changes
|
data/test/data/data.yaml
ADDED
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
|
data/test/test_helper.rb
ADDED
@@ -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'
|
data/test/test_query.rb
ADDED
@@ -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
|