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 +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
|