qo 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +321 -68
- data/lib/qo/matcher.rb +7 -2
- data/lib/qo/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a097911edc42e2a5ebf4ef3ea3fde3493ab82ae4
|
4
|
+
data.tar.gz: 7233bc3005d008543c13a2083b0f7e6560194b0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e53d71122117634454907e9fbe0b960a38aa7bedcebecb9faa876f8da2febd54e0f7da94278bae75bc75740697d2c37f7d8e81677ab0c31d915ffdb45ce7afea
|
7
|
+
data.tar.gz: 329801b537a3ac7d08e2f08741152fe3ca86265a4dc10178121ccc3aaa2f0230b64a882d0e8a509b7713e3dd1638dd987df6f5ee0d2b753c0aaa00bf3bff441a
|
data/README.md
CHANGED
@@ -2,22 +2,6 @@
|
|
2
2
|
|
3
3
|
Short for Query Object, my play at Ruby pattern matching and fluent querying
|
4
4
|
|
5
|
-
## Installation
|
6
|
-
|
7
|
-
Add this line to your application's Gemfile:
|
8
|
-
|
9
|
-
```ruby
|
10
|
-
gem 'qo'
|
11
|
-
```
|
12
|
-
|
13
|
-
And then execute:
|
14
|
-
|
15
|
-
$ bundle
|
16
|
-
|
17
|
-
Or install it yourself as:
|
18
|
-
|
19
|
-
$ gem install qo
|
20
|
-
|
21
5
|
## How does it work?
|
22
6
|
|
23
7
|
Triple equals black magic, mostly.
|
@@ -37,97 +21,347 @@ Most examples are written in terms of `and` and its alias `[]`. `[]` is mostly u
|
|
37
21
|
```ruby
|
38
22
|
Qo[/Rob/, 22]
|
39
23
|
|
40
|
-
# ...is functionally the same as
|
24
|
+
# ...is functionally the same as an and query, which uses `all?` to match
|
41
25
|
Qo.and(/Rob/, 22)
|
42
|
-
```
|
43
26
|
|
44
|
-
|
27
|
+
# This is shorthand for
|
28
|
+
Qo::Matcher.new('and', /Rob/, 22)
|
29
|
+
|
30
|
+
# An `or` matcher uses the same shorthand as `and` but uses `any?` behind the scenes instead:
|
31
|
+
Qo.or(/Rob/, 22)
|
32
|
+
|
33
|
+
# Same with not, except it uses `none?`
|
34
|
+
Qo.not(/Rob/, 22)
|
35
|
+
```
|
45
36
|
|
46
37
|
Qo has a few Qo'isms, mainly based around triple equals in Ruby. See the above articles for tutorials on that count.
|
47
38
|
|
48
|
-
|
39
|
+
We will assume the following data:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
people_arrays = [
|
43
|
+
['Robert', 22],
|
44
|
+
['Roberta', 22],
|
45
|
+
['Foo', 42],
|
46
|
+
['Bar', 18]
|
47
|
+
]
|
48
|
+
|
49
|
+
people_objects = [
|
50
|
+
Person.new('Robert', 22),
|
51
|
+
Person.new('Roberta', 22),
|
52
|
+
Person.new('Foo', 42),
|
53
|
+
Person.new('Bar', 17),
|
54
|
+
]
|
55
|
+
```
|
56
|
+
|
57
|
+
### 1 - Wildcard Matching
|
58
|
+
|
59
|
+
Qo has a concept of a Wildcard, `:*`, which will match against any value
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
Qo[:*, :*] === ['Robert', 22] # true
|
63
|
+
```
|
64
|
+
|
65
|
+
### 2 - Array Matching
|
66
|
+
|
67
|
+
The first way a Qo matcher can be defined is by using `*varargs`:
|
49
68
|
|
50
69
|
```ruby
|
51
|
-
Qo
|
70
|
+
# Qo::Matcher(type, *varargs, **kwargs)
|
52
71
|
```
|
53
72
|
|
54
|
-
|
73
|
+
This gives us the `and` matcher shorthand for array matchers.
|
74
|
+
|
75
|
+
#### 2.1 - Array matched against an Array
|
76
|
+
|
77
|
+
When an Array matcher is run against an Array, it will compare elements by index in the following priority:
|
78
|
+
|
79
|
+
1. Was a wildcard provided?
|
80
|
+
2. Does it case match (`===`)?
|
81
|
+
3. Does it have a predicate method by that name that matches?
|
82
|
+
|
83
|
+
This functionality is left biased and permissive, meaning that if the right side of the argument is longer it will ignore those items in the match. If it's shorter? Not so much.
|
84
|
+
|
85
|
+
##### 2.1.1 - Wildcard provided
|
55
86
|
|
56
87
|
```ruby
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
88
|
+
# Standalone
|
89
|
+
|
90
|
+
Qo[:*, :*] === ['Robert', 22]
|
91
|
+
# => true
|
92
|
+
|
93
|
+
# Case statement
|
94
|
+
|
95
|
+
case ['Roberta', 22]
|
96
|
+
when Qo[:*, :*] then 'it matched'
|
97
|
+
else 'will not ever be reached'
|
61
98
|
end
|
99
|
+
# => 'adult'
|
100
|
+
|
101
|
+
# Select
|
102
|
+
|
103
|
+
people_arrays.select(&Qo[:*, :*])
|
104
|
+
# => [['Robert', 22], ['Roberta', 22], ['Foo', 42], ['Bar', 18]]
|
62
105
|
```
|
63
106
|
|
64
|
-
|
107
|
+
##### 2.1.2 - Case Match present
|
108
|
+
|
109
|
+
We've seen some case matching so far with `Range` and `Regex`:
|
65
110
|
|
66
111
|
```ruby
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
112
|
+
# Standalone
|
113
|
+
|
114
|
+
Qo[/Rob/, :*] === ['Robert', 22]
|
115
|
+
# => true
|
116
|
+
|
117
|
+
# Case statement
|
118
|
+
|
119
|
+
case ['Roberta', 22]
|
120
|
+
when Qo[:*, 0..9] then 'child'
|
121
|
+
when Qo[:*, 10..19] then 'teen'
|
122
|
+
when Qo[:*, 20..99] then 'adult'
|
123
|
+
else 'not sure'
|
71
124
|
end
|
125
|
+
# => 'adult'
|
126
|
+
|
127
|
+
# Select
|
128
|
+
|
129
|
+
people_arrays.select(&Qo[:*, 10..19])
|
130
|
+
# => [['Bar', 18]]
|
72
131
|
```
|
73
132
|
|
74
|
-
|
133
|
+
##### 2.1.3 - Predicate Method matched
|
134
|
+
|
135
|
+
If no wildcard or case match is found, it will attempt to see if a predicate method by the same name exists, call it, and check the result:
|
75
136
|
|
76
137
|
```ruby
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
138
|
+
dirty_values = [nil, '', true]
|
139
|
+
|
140
|
+
# Standalone
|
141
|
+
|
142
|
+
Qo[:nil?] === [nil]
|
143
|
+
# => true
|
144
|
+
|
145
|
+
# Case statement
|
146
|
+
|
147
|
+
case ['Roberta', nil]
|
148
|
+
when Qo[:*, :nil?] then 'no age'
|
149
|
+
else 'not sure'
|
86
150
|
end
|
151
|
+
# => 'adult'
|
152
|
+
|
153
|
+
# Select
|
154
|
+
|
155
|
+
people_arrays.select(&Qo[:*, :even?])
|
156
|
+
# => [["Robert", 22], ["Roberta", 22], ["Foo", 42], ["Bar", 18]]
|
87
157
|
```
|
88
158
|
|
89
|
-
|
159
|
+
#### 2.2 - Array matched against an Object
|
160
|
+
|
161
|
+
When an Array matcher is matched against anything other than an Array it will follow the priority:
|
162
|
+
|
163
|
+
1. Was a wildcard provided?
|
164
|
+
2. Does it case match (`===`)?
|
165
|
+
3. Does it have a predicate method by that name that matches?
|
166
|
+
|
167
|
+
Every argument provided will be run against the target object.
|
168
|
+
|
169
|
+
##### 2.2.1 - Wildcard provided
|
170
|
+
|
171
|
+
A wildcard in an Array to Object match is functionally an always true, but can be used as such:
|
90
172
|
|
91
173
|
```ruby
|
92
|
-
|
174
|
+
Qo[:*] === :literally_anything_here
|
175
|
+
```
|
176
|
+
|
177
|
+
##### 2.2.2 - Case Match present
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
# Standalone
|
181
|
+
|
182
|
+
Qo[Integer, 15..25] === 20
|
183
|
+
# => true
|
184
|
+
|
185
|
+
# Case statement - functionally indistinguishable from a regular case statement
|
186
|
+
|
187
|
+
# Select
|
188
|
+
|
189
|
+
[nil, '', 10, 'string'].select(&Qo.or(/str/, 10..20))
|
190
|
+
# => [10, "string"]
|
191
|
+
```
|
192
|
+
|
193
|
+
##### 2.2.3 - Predicate Method matched
|
194
|
+
|
195
|
+
Now this is where some of the fun starts in
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
# Standalone
|
199
|
+
|
200
|
+
Qo.or(:nil?, :empty?) === nil
|
201
|
+
# => true
|
202
|
+
Qo.not(:nil?, :empty?) === nil
|
203
|
+
# => false
|
204
|
+
|
205
|
+
# Case statement
|
93
206
|
|
94
|
-
case
|
95
|
-
when Qo[
|
96
|
-
|
97
|
-
else 'objection!'
|
207
|
+
case 42
|
208
|
+
when Qo[Integer, :even?, 40..50] then 'oddly specific number criteria'
|
209
|
+
else 'nope'
|
98
210
|
end
|
211
|
+
# => "oddly specific number criteria"
|
212
|
+
|
213
|
+
# Reject
|
214
|
+
|
215
|
+
[nil, '', 10, 'string'].reject(&Qo.or(/str/, 10..20))
|
216
|
+
# => [10, "string"]
|
99
217
|
```
|
100
218
|
|
101
|
-
###
|
219
|
+
### 3 - Hash Matching
|
220
|
+
|
221
|
+
#### 3.1 - Hash matched against a Hash
|
222
|
+
|
223
|
+
1. Does the key exist on the other hash?
|
224
|
+
2. Was a wildcard value provided?
|
225
|
+
3. Does the target object's value case match against the match value?
|
226
|
+
4. Does the target object's value predicate match against the match value?
|
227
|
+
5. What about the String version of the match key? Abort if it can't coerce.
|
228
|
+
|
229
|
+
##### 3.1.1 - Key present
|
230
|
+
|
231
|
+
Checks to see if the key is even present on the other object, false if not.
|
102
232
|
|
103
|
-
|
233
|
+
##### 3.1.2 - Wildcard provided
|
234
|
+
|
235
|
+
As with other wildcards, if the value matched against is a wildcard it'll always get through:
|
104
236
|
|
105
237
|
```ruby
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
238
|
+
Qo[name: :*] === {name: 'Foo'}
|
239
|
+
# => true
|
240
|
+
```
|
241
|
+
|
242
|
+
##### 3.1.3 - Case match present
|
243
|
+
|
244
|
+
If a case match is present for the key, it'll try and compare:
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
# Standalone
|
248
|
+
|
249
|
+
Qo[name: /Foo/] === {name: 'Foo'}
|
250
|
+
# => true
|
251
|
+
|
252
|
+
# Case statement
|
253
|
+
|
254
|
+
case {name: 'Foo', age: 42}
|
255
|
+
when Qo[age: 40..50] then 'Gotcha!'
|
256
|
+
else 'nope'
|
257
|
+
end
|
258
|
+
# => "Gotcha!"
|
112
259
|
|
113
|
-
|
260
|
+
# Select
|
261
|
+
|
262
|
+
people_hashes = people_arrays.map { |(n,a)| {name: n, age: a} }
|
263
|
+
people_hashes.select(&Qo[age: 15..25])
|
264
|
+
# => [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}, {:name=>"Bar", :age=>18}]
|
114
265
|
```
|
115
266
|
|
116
|
-
|
267
|
+
##### 3.1.4 - Predicate match present
|
117
268
|
|
118
|
-
|
269
|
+
Much like our array friend above, if a predicate style method is present see if it'll work
|
119
270
|
|
120
271
|
```ruby
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
272
|
+
# Standalone
|
273
|
+
|
274
|
+
Qo[name: :empty?] === {name: ''}
|
275
|
+
# => true
|
276
|
+
|
277
|
+
# Case statement
|
278
|
+
|
279
|
+
case {name: 'Foo', age: nil}
|
280
|
+
when Qo[age: :nil?] then 'No age provided!'
|
281
|
+
else 'nope'
|
282
|
+
end
|
283
|
+
# => "No age provided!"
|
284
|
+
|
285
|
+
# Select
|
127
286
|
|
128
|
-
|
287
|
+
people_hashes = people_arrays.map { |(n,a)| {name: n, age: a} } << {name: 'Ghost', age: nil}
|
288
|
+
people_hashes.select(&Qo[age: :nil?])
|
289
|
+
# => [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}, {:name=>"Bar", :age=>18}]
|
129
290
|
```
|
130
291
|
|
292
|
+
Careful though, if the key doesn't exist that won't match. I'll have to consider this one later.
|
293
|
+
|
294
|
+
##### 3.1.5 - String variant present
|
295
|
+
|
296
|
+
Coerces the key into a string if possible, and sees if that can provide a valid case match
|
297
|
+
|
298
|
+
#### 3.2 - Hash matched against an Object
|
299
|
+
|
300
|
+
1. Does the object respond to the match key?
|
301
|
+
2. Was a wildcard value provided?
|
302
|
+
3. Does the result of sending the match key as a method case match the provided value?
|
303
|
+
4. Does a predicate method exist for it?
|
304
|
+
|
305
|
+
##### 3.2.1 - Responds to match key
|
306
|
+
|
307
|
+
If it doesn't know how to deal with it, false out.
|
308
|
+
|
309
|
+
##### 3.2.2 - Wildcard provided
|
310
|
+
|
311
|
+
Same as other wildcards
|
312
|
+
|
313
|
+
##### 3.2.3 - Case match present
|
314
|
+
|
315
|
+
This is where we can get into some interesting code, much like the hash selections above
|
316
|
+
|
317
|
+
```ruby
|
318
|
+
# Standalone
|
319
|
+
|
320
|
+
Qo[name: /Rob/] === people_objects.first
|
321
|
+
# => true
|
322
|
+
|
323
|
+
# Case statement
|
324
|
+
|
325
|
+
case people_objects.first
|
326
|
+
when Qo[name: /Rob/] then "It's Rob!"
|
327
|
+
else 'Na, not them'
|
328
|
+
end
|
329
|
+
# => "It's Rob!"
|
330
|
+
|
331
|
+
# Select
|
332
|
+
|
333
|
+
people_objects.select(&Qo[name: /Rob/])
|
334
|
+
# => [Person(Robert, 22), Person(Roberta, 22)]
|
335
|
+
```
|
336
|
+
|
337
|
+
##### 3.2.4 - Predicate match present
|
338
|
+
|
339
|
+
```ruby
|
340
|
+
# Standalone
|
341
|
+
|
342
|
+
Qo[name: :empty?] === Person.new('', 22)
|
343
|
+
# => true
|
344
|
+
|
345
|
+
# Case statement
|
346
|
+
|
347
|
+
case Person.new('', nil)
|
348
|
+
when Qo[age: :nil?] then 'No age provided!'
|
349
|
+
else 'nope'
|
350
|
+
end
|
351
|
+
# => "No age provided!"
|
352
|
+
|
353
|
+
# Select
|
354
|
+
|
355
|
+
people_hashes.select(&Qo[age: :nil?])
|
356
|
+
# => []
|
357
|
+
```
|
358
|
+
|
359
|
+
### 4 - Hacky Fun Time
|
360
|
+
|
361
|
+
These examples will grow over the next few weeks as I think of more fun things to do with Qo. PRs welcome if you find fun uses!
|
362
|
+
|
363
|
+
#### 4.1 - JSON
|
364
|
+
|
131
365
|
Qo tries to be clever though, it assumes Symbol keys first and then String keys, so how about some JSON?:
|
132
366
|
|
133
367
|
```ruby
|
@@ -142,17 +376,36 @@ posts.select(&Qo[userId: 1])
|
|
142
376
|
|
143
377
|
Nifty!
|
144
378
|
|
379
|
+
#### 4.2 - Opsy Stuff
|
380
|
+
|
381
|
+
##### 4.2.1 - NMap
|
382
|
+
|
145
383
|
What about NMap for our Opsy friends? Well, simulated, but still fun.
|
146
384
|
|
147
385
|
```ruby
|
148
386
|
hosts = (`nmap -oG - -sP 192.168.1.* 10.0.0.* | grep Host`).lines.map { |v| v.split[1..2] }
|
149
|
-
=> [["192.168.1.1", "(
|
387
|
+
=> [["192.168.1.1", "(Router)"], ["192.168.1.2", "(My Computer)"], ["10.0.0.1", "(Gateway)"]]
|
150
388
|
|
151
389
|
hosts.select(&Qo[IPAddr.new('192.168.1.1/8')])
|
152
|
-
=> [["192.168.1.1", "(
|
153
|
-
|
390
|
+
=> [["192.168.1.1", "(Router)"], ["192.168.1.2", "(My Computer)"]]
|
391
|
+
```
|
392
|
+
|
393
|
+
## Installation
|
394
|
+
|
395
|
+
Add this line to your application's Gemfile:
|
396
|
+
|
397
|
+
```ruby
|
398
|
+
gem 'qo'
|
154
399
|
```
|
155
400
|
|
401
|
+
And then execute:
|
402
|
+
|
403
|
+
$ bundle
|
404
|
+
|
405
|
+
Or install it yourself as:
|
406
|
+
|
407
|
+
$ gem install qo
|
408
|
+
|
156
409
|
## Development
|
157
410
|
|
158
411
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/qo/matcher.rb
CHANGED
@@ -116,11 +116,14 @@ module Qo
|
|
116
116
|
# Any -> Any -> Bool # Matches against wildcard or a key and value. Coerces key to_s if no matches for JSON.
|
117
117
|
private def hash_against_hash_matcher(match_target)
|
118
118
|
-> match_key, match_value {
|
119
|
+
match_target.key?(match_key) &&
|
119
120
|
wildcard_match(match_value) ||
|
120
|
-
case_match(match_target[match_key], match_value)
|
121
|
+
case_match(match_target[match_key], match_value) ||
|
122
|
+
method_matches?(match_target[match_key], match_value) || (
|
121
123
|
# This is done for JSON responses, but as key can be `Any` we don't want to assume it knows how
|
122
124
|
# to coerce `to_s` either. It's more of a nicety function.
|
123
125
|
match_key.respond_to?(:to_s) &&
|
126
|
+
match_target.key?(match_key.to_s) &&
|
124
127
|
case_match(match_target[match_key.to_s], match_value)
|
125
128
|
)
|
126
129
|
}
|
@@ -134,8 +137,10 @@ module Qo
|
|
134
137
|
# Any -> Any -> Bool # Matches against wildcard or match value versus the public send return of the target
|
135
138
|
private def hash_against_object_matcher(match_target)
|
136
139
|
-> match_key, match_value {
|
140
|
+
match_target.respond_to?(match_key) &&
|
137
141
|
wildcard_match(match_value) ||
|
138
|
-
case_match(method_send(match_target, match_key), match_value)
|
142
|
+
case_match(method_send(match_target, match_key), match_value) ||
|
143
|
+
method_matches?(method_send(match_target, match_key), match_value)
|
139
144
|
}
|
140
145
|
end
|
141
146
|
|
data/lib/qo/version.rb
CHANGED