qo 0.1.6 → 0.1.7
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 +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
|