qo 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +321 -68
  3. data/lib/qo/matcher.rb +7 -2
  4. data/lib/qo/version.rb +1 -1
  5. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a106b00677e65b0d1bc7f87678d234aaba4a7580
4
- data.tar.gz: 56745cdd7c92b3504434209bf269851e95bf6a6a
3
+ metadata.gz: a097911edc42e2a5ebf4ef3ea3fde3493ab82ae4
4
+ data.tar.gz: 7233bc3005d008543c13a2083b0f7e6560194b0f
5
5
  SHA512:
6
- metadata.gz: c74eee58565ad19a3cd268e3f3a704d8c861f8c978f314ab830d011a46b67bd8a4031730a6a178514b0cf749e03b10643d129bc6fac7c12644520c6d3edacfd9
7
- data.tar.gz: 7f976433977c10a5655313e4aacf155cfb5c6c4a4517ea9abfa8f6ce1c048381656f593661047e9d30524918f616c59bcc72c84449d8c55edadd7de057cd0425
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
- ### Qo'isms
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
- It also has a wildcard, `:*`, which will match any value:
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[/Rob/, :*] === ['Robert', 22] # true
70
+ # Qo::Matcher(type, *varargs, **kwargs)
52
71
  ```
53
72
 
54
- As it responds to triple equals, that also means it can be used with `case` statements in a play at pattern matching:
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
- case ['Robert', 22]
58
- when Qo[:*, 10..19] then 'teenager'
59
- when Qo[:*. 20..99] then 'adult'
60
- else 'who knows'
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
- It's even nice enough to work on types, as they respond to `===` too:
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
- case ['Robert', 22]
68
- when Qo[String, Integer] then 'normal person'
69
- when Qo[String, Float] then 'probably a kid'
70
- else 'who knows'
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
- Predicate methods given as symbols will also call through:
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
- case [1, 2]
78
- when Qo[:even?, :odd?], Qo[:odd?, :even?]
79
- 'balanced'
80
- when Qo[:even?, :even?]
81
- 'even better'
82
- when Qo[:odd?, :odd?]
83
- 'odd pair, them'
84
- else
85
- 'what did ya do!?'
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
- Though chances are you don't have tuple-like code, you have objects. How about we play with those:
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
- # Person has a name and an age attr
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 robert
95
- when Qo[age: 10..19] then 'teenager'
96
- when Qo[age: 20..99] then 'adult'
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
- ### Arrays
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
- As Qo returns an Object that responds to `to_proc` it can be used with several Enumerable methods:
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
- people = [
107
- ['Robert', 22],
108
- ['Roberta', 22],
109
- ['Foo', 42],
110
- ['Bar', 18]
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
- people.select(&Qo[:*, 15..25]) # => [["Robert", 22], ["Roberta", 22], ["Bar", 18]]
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
- ### Hashes and Objects
267
+ ##### 3.1.4 - Predicate match present
117
268
 
118
- If you have a lot of JSON or Objects, good news! Qo has tricks:
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
- people = [
122
- Person.new('Robert', 22),
123
- Person.new('Roberta', 22),
124
- Person.new('Foo', 42),
125
- Person.new('Bar', 17),
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
- people.select(&Qo[name: /Rob/]) # => [Person('Robert', 22), Person('Roberta', 22)]
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", "(Linksys03384)"], ["192.168.1.2", "(My Computer)"], ["10.0.0.1", "(Gateway)"]]
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", "(Linksys03384)"], ["192.168.1.2", "(My Computer)"]]
153
- [13] pry(main)>
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
@@ -1,3 +1,3 @@
1
1
  module Qo
2
- VERSION = '0.1.3'
2
+ VERSION = '0.1.4'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Weaver