qo 0.1.6 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +179 -17
- data/docs/.gitkeep +0 -0
- data/docs/_config.yml +1 -0
- data/lib/qo.rb +57 -8
- data/lib/qo/matcher.rb +10 -3
- data/lib/qo/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c98fa2c3b82ac4167eba17b183fe1cea46a0e36f
|
4
|
+
data.tar.gz: ab2527c365781b54fac1290ac64e8a17acdcd07c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f1e9b886e694b8c08f212209db40bc8ca1276b738d1d30c81040e0b651f264bcfc9c83a21d82d96f2ff418d05def3e5abae82a29394f184266fab424f5dad4a6
|
7
|
+
data.tar.gz: ab226ea4d673ea462b2e41084d25c8a61b2c04e37e8aac9354745b1b17c4ae54b5efa6184ae28e0201fa453b525938f4d8f260912300a0f145cc0992607424e1
|
data/README.md
CHANGED
@@ -14,6 +14,38 @@ Fast forward a few months and I kind of wanted to make it real, so here it is. I
|
|
14
14
|
|
15
15
|
## Usage
|
16
16
|
|
17
|
+
### Quick Start
|
18
|
+
|
19
|
+
Qo is used for pattern matching in Ruby. All Qo matchers respond to `===` and `to_proc` meaning they can be used with `case` and Enumerable functions alike:
|
20
|
+
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
case ['Foo', 42]
|
24
|
+
when Qo[:*, 42] then 'Truly the one answer'
|
25
|
+
else nil
|
26
|
+
end
|
27
|
+
|
28
|
+
# Run a select like an AR query, getting the age attribute against a range
|
29
|
+
people.select(&Qo[age: 18..30])
|
30
|
+
|
31
|
+
# How about some "right-hand assignment" pattern matching
|
32
|
+
name_longer_than_three = -> person { person.name.size > 3 }
|
33
|
+
people_with_truncated_names = people.map(&Qo.match_fn(
|
34
|
+
Qo.m(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) },
|
35
|
+
Qo.m(:*) # Identity function, catch-all
|
36
|
+
))
|
37
|
+
|
38
|
+
# And standalone like a case:
|
39
|
+
Qo.match(people.first,
|
40
|
+
Qo.m(age: 10..19) { |person| "#{person.name} is a teen that's #{person.age} years old" },
|
41
|
+
Qo.m(:*) { |person| "#{person.name} is #{person.age} years old" }
|
42
|
+
)
|
43
|
+
```
|
44
|
+
|
45
|
+
Get a lot more expressiveness in your queries and transformations. Read on for the full details.
|
46
|
+
|
47
|
+
### Qo'isms
|
48
|
+
|
17
49
|
Qo supports three main types of queries: `and`, `or`, and `not`.
|
18
50
|
|
19
51
|
Most examples are written in terms of `and` and its alias `[]`. `[]` is mostly used for portable syntax:
|
@@ -62,6 +94,12 @@ Qo has a concept of a Wildcard, `:*`, which will match against any value
|
|
62
94
|
Qo[:*, :*] === ['Robert', 22] # true
|
63
95
|
```
|
64
96
|
|
97
|
+
A single wildcard will match anything, and can frequently be used as an always true:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
Qo[:*] === :literally_anything_here
|
101
|
+
```
|
102
|
+
|
65
103
|
### 2 - Array Matching
|
66
104
|
|
67
105
|
The first way a Qo matcher can be defined is by using `*varargs`:
|
@@ -96,7 +134,7 @@ case ['Roberta', 22]
|
|
96
134
|
when Qo[:*, :*] then 'it matched'
|
97
135
|
else 'will not ever be reached'
|
98
136
|
end
|
99
|
-
# => '
|
137
|
+
# => 'it matched'
|
100
138
|
|
101
139
|
# Select
|
102
140
|
|
@@ -140,7 +178,7 @@ dirty_values = [nil, '', true]
|
|
140
178
|
# Standalone
|
141
179
|
|
142
180
|
Qo[:nil?] === [nil]
|
143
|
-
# => true
|
181
|
+
# => true, though you could also just use Qo[nil]
|
144
182
|
|
145
183
|
# Case statement
|
146
184
|
|
@@ -148,7 +186,7 @@ case ['Roberta', nil]
|
|
148
186
|
when Qo[:*, :nil?] then 'no age'
|
149
187
|
else 'not sure'
|
150
188
|
end
|
151
|
-
# => '
|
189
|
+
# => 'no age'
|
152
190
|
|
153
191
|
# Select
|
154
192
|
|
@@ -212,7 +250,7 @@ end
|
|
212
250
|
|
213
251
|
# Reject
|
214
252
|
|
215
|
-
[nil, '', 10, 'string'].reject(&Qo.or(
|
253
|
+
[nil, '', 10, 'string'].reject(&Qo.or(:nil?, :empty?))
|
216
254
|
# => [10, "string"]
|
217
255
|
```
|
218
256
|
|
@@ -221,16 +259,58 @@ end
|
|
221
259
|
#### 3.1 - Hash matched against a Hash
|
222
260
|
|
223
261
|
1. Does the key exist on the other hash?
|
224
|
-
2.
|
225
|
-
3.
|
226
|
-
4. Does the target object's value
|
227
|
-
5.
|
262
|
+
2. Are the match value and match target hashes?
|
263
|
+
3. Was a wildcard value provided?
|
264
|
+
4. Does the target object's value case match against the match value?
|
265
|
+
5. Does the target object's value predicate match against the match value?
|
266
|
+
6. What about the String version of the match key? Abort if it can't coerce.
|
228
267
|
|
229
268
|
##### 3.1.1 - Key present
|
230
269
|
|
231
270
|
Checks to see if the key is even present on the other object, false if not.
|
232
271
|
|
233
|
-
##### 3.1.2 -
|
272
|
+
##### 3.1.2 - Match value and target are hashes
|
273
|
+
|
274
|
+
If both the match value (`match_key: match_value`) and the match target are hashes, Qo will begin a recursive descent starting at the match key until it finds a matcher to try out:
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
Qo[a: {b: {c: 5..15}}] === {a: {b: {c: 10}}}
|
278
|
+
# => true
|
279
|
+
|
280
|
+
# Na, no fun. Deeper!
|
281
|
+
Qo.and(a: {
|
282
|
+
f: 5..15,
|
283
|
+
b: {
|
284
|
+
c: /foo/,
|
285
|
+
d: 10..30
|
286
|
+
}
|
287
|
+
}).call(a: {
|
288
|
+
f: 10,
|
289
|
+
b: {
|
290
|
+
c: 'foobar',
|
291
|
+
d: 20
|
292
|
+
}
|
293
|
+
})
|
294
|
+
# => true
|
295
|
+
|
296
|
+
# It can get chaotic with `or` though. Anything anywhere in there matches and
|
297
|
+
# it'll pass.
|
298
|
+
Qo.or(a: {
|
299
|
+
f: false,
|
300
|
+
b: {
|
301
|
+
c: /nope/,
|
302
|
+
d: 10..30
|
303
|
+
}
|
304
|
+
}).call(a: {
|
305
|
+
f: 10,
|
306
|
+
b: {
|
307
|
+
c: 'foobar',
|
308
|
+
d: 20
|
309
|
+
}
|
310
|
+
})
|
311
|
+
```
|
312
|
+
|
313
|
+
##### 3.1.3 - Wildcard provided
|
234
314
|
|
235
315
|
As with other wildcards, if the value matched against is a wildcard it'll always get through:
|
236
316
|
|
@@ -239,7 +319,7 @@ Qo[name: :*] === {name: 'Foo'}
|
|
239
319
|
# => true
|
240
320
|
```
|
241
321
|
|
242
|
-
##### 3.1.
|
322
|
+
##### 3.1.4 - Case match present
|
243
323
|
|
244
324
|
If a case match is present for the key, it'll try and compare:
|
245
325
|
|
@@ -259,12 +339,12 @@ end
|
|
259
339
|
|
260
340
|
# Select
|
261
341
|
|
262
|
-
people_hashes = people_arrays.map { |
|
342
|
+
people_hashes = people_arrays.map { |n, a| {name: n, age: a} }
|
263
343
|
people_hashes.select(&Qo[age: 15..25])
|
264
344
|
# => [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}, {:name=>"Bar", :age=>18}]
|
265
345
|
```
|
266
346
|
|
267
|
-
##### 3.1.
|
347
|
+
##### 3.1.5 - Predicate match present
|
268
348
|
|
269
349
|
Much like our array friend above, if a predicate style method is present see if it'll work
|
270
350
|
|
@@ -282,16 +362,16 @@ else 'nope'
|
|
282
362
|
end
|
283
363
|
# => "No age provided!"
|
284
364
|
|
285
|
-
#
|
365
|
+
# Reject
|
286
366
|
|
287
367
|
people_hashes = people_arrays.map { |(n,a)| {name: n, age: a} } << {name: 'Ghost', age: nil}
|
288
|
-
people_hashes.
|
368
|
+
people_hashes.reject(&Qo[age: :nil?])
|
289
369
|
# => [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}, {:name=>"Bar", :age=>18}]
|
290
370
|
```
|
291
371
|
|
292
372
|
Careful though, if the key doesn't exist that won't match. I'll have to consider this one later.
|
293
373
|
|
294
|
-
##### 3.1.
|
374
|
+
##### 3.1.6 - String variant present
|
295
375
|
|
296
376
|
Coerces the key into a string if possible, and sees if that can provide a valid case match
|
297
377
|
|
@@ -308,7 +388,7 @@ If it doesn't know how to deal with it, false out.
|
|
308
388
|
|
309
389
|
##### 3.2.2 - Wildcard provided
|
310
390
|
|
311
|
-
Same as other wildcards
|
391
|
+
Same as other wildcards, but if the object doesn't respond to the method you specify it'll have false'd out before it reaches here.
|
312
392
|
|
313
393
|
##### 3.2.3 - Case match present
|
314
394
|
|
@@ -402,22 +482,89 @@ people_objects.map(&Qo.match_fn(
|
|
402
482
|
|
403
483
|
So we just truncated everyone's name that was longer than three characters.
|
404
484
|
|
485
|
+
### 6 - Helper functions
|
486
|
+
|
487
|
+
There are a few functions added for convenience, and it should be noted that because all Qo matchers respond to `===` that they can be used as helpers as well.
|
488
|
+
|
489
|
+
#### 6.1 - Dig
|
490
|
+
|
491
|
+
Dig is used to get in deep at a nested hash value. It takes a dot-path and a `===` respondant matcher:
|
492
|
+
|
493
|
+
```ruby
|
494
|
+
Qo.dig('a.b.c', Qo.or(1..5, 15..25)) === {a: {b: {c: 1}}}
|
495
|
+
# => true
|
496
|
+
|
497
|
+
Qo.dig('a.b.c', Qo.or(1..5, 15..25)) === {a: {b: {c: 20}}}
|
498
|
+
# => true
|
499
|
+
```
|
500
|
+
|
501
|
+
To be fair that means anything that can respond to `===`, including classes and other such things.
|
502
|
+
|
503
|
+
#### 6.2 - Count By
|
504
|
+
|
505
|
+
This ends up coming up a lot, especially around querying, so let's get a way to count by!
|
506
|
+
|
507
|
+
```ruby
|
508
|
+
Qo.count_by([1,2,3,2,2,2,1]
|
509
|
+
|
510
|
+
# => {
|
511
|
+
# 1 => 2,
|
512
|
+
# 2 => 4,
|
513
|
+
# 3 => 1
|
514
|
+
# }
|
515
|
+
|
516
|
+
Qo.count_by([1,2,3,2,2,2,1], &:even?)
|
517
|
+
|
518
|
+
# => {
|
519
|
+
# false => 3,
|
520
|
+
# true => 4
|
521
|
+
# }
|
522
|
+
```
|
523
|
+
|
405
524
|
### 5 - Hacky Fun Time
|
406
525
|
|
407
526
|
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!
|
408
527
|
|
409
528
|
#### 5.1 - JSON
|
410
529
|
|
530
|
+
> Note that Qo does not support deep querying of hashes (yet)
|
531
|
+
|
532
|
+
##### 5.1.1 - JSON Placeholder
|
533
|
+
|
411
534
|
Qo tries to be clever though, it assumes Symbol keys first and then String keys, so how about some JSON?:
|
412
535
|
|
413
536
|
```ruby
|
414
537
|
require 'json'
|
415
538
|
require 'net/http'
|
539
|
+
|
416
540
|
posts = JSON.parse(
|
417
|
-
Net::HTTP.get(URI("https://jsonplaceholder.typicode.com/posts")),symbolize_names: true
|
541
|
+
Net::HTTP.get(URI("https://jsonplaceholder.typicode.com/posts")), symbolize_names: true
|
542
|
+
)
|
543
|
+
|
544
|
+
users = JSON.parse(
|
545
|
+
Net::HTTP.get(URI("https://jsonplaceholder.typicode.com/users")), symbolize_names: true
|
418
546
|
)
|
419
547
|
|
548
|
+
# Get all posts where the userId is 1.
|
420
549
|
posts.select(&Qo[userId: 1])
|
550
|
+
|
551
|
+
# Get users named Nicholas or have two names and an address somewhere with a zipcode
|
552
|
+
# that starts with 9 or 4.
|
553
|
+
#
|
554
|
+
# Qo matchers return a `===` respondant object, remember, so we can totally nest them.
|
555
|
+
users.select(&Qo.and(
|
556
|
+
name: Qo.or(/^Nicholas/, /^\w+ \w+$/),
|
557
|
+
address: {
|
558
|
+
zipcode: Qo.or(/^9/, /^4/)
|
559
|
+
}
|
560
|
+
))
|
561
|
+
|
562
|
+
# We could even use dig to get at some of the same information. This and the above will
|
563
|
+
# return the same results even.
|
564
|
+
users.select(&Qo.and(
|
565
|
+
Qo.dig('address.zipcode', Qo.or(/^9/, /^4/)),
|
566
|
+
name: Qo.or(/^Nicholas/, /^\w+ \w+$/)
|
567
|
+
))
|
421
568
|
```
|
422
569
|
|
423
570
|
Nifty!
|
@@ -436,6 +583,21 @@ hosts.select(&Qo[IPAddr.new('192.168.1.1/8')])
|
|
436
583
|
=> [["192.168.1.1", "(Router)"], ["192.168.1.2", "(My Computer)"]]
|
437
584
|
```
|
438
585
|
|
586
|
+
##### 5.2.2 - `du`
|
587
|
+
|
588
|
+
The nice thing about Unix style commands is that they use headers, which means CSV can get a hold of them for some good formatting. It's also smart enough to deal with space seperators that may vary in length:
|
589
|
+
|
590
|
+
```ruby
|
591
|
+
rows = CSV.new(`df -h`, col_sep: " ", headers: true).read.map(&:to_h)
|
592
|
+
|
593
|
+
rows.map(&Qo.match_fn(
|
594
|
+
Qo.m(Avail: /Gi$/) { |row|
|
595
|
+
"#{row['Filesystem']} mounted on #{row['Mounted']} [#{row['Avail']} / #{row['Size']}]"
|
596
|
+
}
|
597
|
+
)).compact
|
598
|
+
# => ["/dev/***** mounted on / [186Gi / 466Gi]"]
|
599
|
+
```
|
600
|
+
|
439
601
|
## Installation
|
440
602
|
|
441
603
|
Add this line to your application's Gemfile:
|
data/docs/.gitkeep
ADDED
File without changes
|
data/docs/_config.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
theme: jekyll-theme-cayman
|
data/lib/qo.rb
CHANGED
@@ -6,31 +6,61 @@ module Qo
|
|
6
6
|
WILDCARD_MATCH = :*
|
7
7
|
|
8
8
|
class << self
|
9
|
-
|
9
|
+
|
10
|
+
# Creates a Guard Block matcher.
|
11
|
+
#
|
12
|
+
# A guard block matcher is used to guard a function from running unless
|
13
|
+
# the left-hand matcher passes. Once called with a value, it will either
|
14
|
+
# return `[false, false]` or `[true, Any]`.
|
15
|
+
#
|
16
|
+
# This wrapping is done to preserve intended false or nil responses,
|
17
|
+
# and is unwrapped with match below.
|
18
|
+
#
|
19
|
+
# @param *array_matchers [Array] varargs matchers
|
20
|
+
# @param **keyword_matchers [Hash] kwargs matchers
|
21
|
+
# @param &fn [Proc] Guarded function
|
22
|
+
#
|
23
|
+
# @return [Proc[Any]]
|
24
|
+
# Any -> Proc[Any]
|
25
|
+
def matcher(*array_matchers, **keyword_matchers, &fn)
|
10
26
|
Qo::GuardBlockMatcher.new(*array_matchers, **keyword_matchers, &fn)
|
11
27
|
end
|
12
28
|
|
13
|
-
|
14
|
-
|
29
|
+
alias_method :m, :matcher
|
30
|
+
|
31
|
+
|
32
|
+
# Takes a set of Guard Block matchers, runs each in sequence, then
|
33
|
+
# unfolds the response from the first passing block.
|
34
|
+
#
|
35
|
+
# @param target [Any] Target object to run against
|
36
|
+
# @param *qo_matchers [Array[GuardBlockMatcher]] Collection of matchers to run
|
37
|
+
#
|
38
|
+
# @return [type] [description]
|
39
|
+
def match(target, *qo_matchers)
|
40
|
+
all_are_guards = qo_matchers.all? { |q| q.is_a?(Qo::GuardBlockMatcher) }
|
15
41
|
raise 'Must patch Qo GuardBlockMatchers!' unless all_are_guards
|
16
42
|
|
17
43
|
qo_matchers.reduce(nil) { |_, matcher|
|
18
|
-
did_match, match_result = matcher.call(
|
44
|
+
did_match, match_result = matcher.call(target)
|
19
45
|
break match_result if did_match
|
20
46
|
}
|
21
47
|
end
|
22
48
|
|
49
|
+
# Wraps match to allow it to be used in a points-free style like regular matchers.
|
50
|
+
#
|
51
|
+
# @param *qo_matchers [Array[GuardBlockMatcher]] Collection of matchers to run
|
52
|
+
#
|
53
|
+
# @return [Proc[Any]]
|
54
|
+
# Any -> Any
|
23
55
|
def match_fn(*qo_matchers)
|
24
|
-
->
|
56
|
+
-> target { match(target, *qo_matchers) }
|
25
57
|
end
|
26
58
|
|
27
59
|
def and(*array_matchers, **keyword_matchers)
|
28
60
|
Qo::Matcher.new('and', *array_matchers, **keyword_matchers)
|
29
61
|
end
|
30
62
|
|
31
|
-
|
32
|
-
Qo::Matcher.new('and', *array_matchers, **keyword_matchers)
|
33
|
-
end
|
63
|
+
alias_method :[], :and
|
34
64
|
|
35
65
|
def or(*array_matchers, **keyword_matchers)
|
36
66
|
Qo::Matcher.new('or', *array_matchers, **keyword_matchers)
|
@@ -39,5 +69,24 @@ module Qo
|
|
39
69
|
def not(*array_matchers, **keyword_matchers)
|
40
70
|
Qo::Matcher.new('not', *array_matchers, **keyword_matchers)
|
41
71
|
end
|
72
|
+
|
73
|
+
# Utility functions. Consider placing these elsewhere.
|
74
|
+
|
75
|
+
def dig(path_map, expected_value)
|
76
|
+
-> hash {
|
77
|
+
segments = path_map.split('.')
|
78
|
+
|
79
|
+
expected_value === hash.dig(*segments) ||
|
80
|
+
expected_value === hash.dig(*segments.map(&:to_sym))
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def count_by(targets, &fn)
|
85
|
+
fn ||= -> v { v }
|
86
|
+
|
87
|
+
targets.each_with_object(Hash.new(0)) { |target, counts|
|
88
|
+
counts[fn[target]] += 1
|
89
|
+
}
|
90
|
+
end
|
42
91
|
end
|
43
92
|
end
|
data/lib/qo/matcher.rb
CHANGED
@@ -116,7 +116,13 @@ 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
|
+
return false unless match_target.key?(match_key)
|
120
|
+
|
121
|
+
# If both the match value and target are hashes, descend if the key exists
|
122
|
+
if match_value.is_a?(Hash) && match_target.is_a?(Hash)
|
123
|
+
return match_against_hash(match_value)[match_target[match_key]]
|
124
|
+
end
|
125
|
+
|
120
126
|
wildcard_match(match_value) ||
|
121
127
|
case_match(match_target[match_key], match_value) ||
|
122
128
|
method_matches?(match_target[match_key], match_value) || (
|
@@ -137,7 +143,8 @@ module Qo
|
|
137
143
|
# Any -> Any -> Bool # Matches against wildcard or match value versus the public send return of the target
|
138
144
|
private def hash_against_object_matcher(match_target)
|
139
145
|
-> match_key, match_value {
|
140
|
-
match_target.respond_to?(match_key)
|
146
|
+
return false unless match_target.respond_to?(match_key)
|
147
|
+
|
141
148
|
wildcard_match(match_value) ||
|
142
149
|
case_match(method_send(match_target, match_key), match_value) ||
|
143
150
|
method_matches?(method_send(match_target, match_key), match_value)
|
@@ -191,7 +198,7 @@ module Qo
|
|
191
198
|
|
192
199
|
# Wraps a case equality statement to make it a bit easier to read. The
|
193
200
|
# typical left bias of `===` can be confusing reading down a page, so
|
194
|
-
# more of a clarity thing than anything.
|
201
|
+
# more of a clarity thing than anything. Also makes for nicer stack traces.
|
195
202
|
#
|
196
203
|
# @param target [Any] Target to match against
|
197
204
|
# @param value [respond_to?(:===)]
|
data/lib/qo/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: qo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandon Weaver
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-04-
|
11
|
+
date: 2018-04-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -99,6 +99,8 @@ files:
|
|
99
99
|
- Rakefile
|
100
100
|
- bin/console
|
101
101
|
- bin/setup
|
102
|
+
- docs/.gitkeep
|
103
|
+
- docs/_config.yml
|
102
104
|
- lib/qo.rb
|
103
105
|
- lib/qo/guard_block_matcher.rb
|
104
106
|
- lib/qo/matcher.rb
|