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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d097b1d36fb935478c96f41a34ac4e7e74ccdcc7
4
- data.tar.gz: 51ea508193f5156aafc7e86a1a8de2fc0e77c36b
3
+ metadata.gz: c98fa2c3b82ac4167eba17b183fe1cea46a0e36f
4
+ data.tar.gz: ab2527c365781b54fac1290ac64e8a17acdcd07c
5
5
  SHA512:
6
- metadata.gz: 8fc0fa88d3264cd2c8cafefea4be82bdd14bc7eed37d41d0e890d6c55a2352f784327db6d718c24c011b3d39b813501952c4f9859571b5ffaa812d5bf1c1259c
7
- data.tar.gz: f6d73da83547b82ab3ad62822d1ce3fd7cab7feb396f5542431d015d907d66353bd126bcf3b4baf06a2951622468524420f0cd69a59ac5c56d6cede08dc40582
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
- # => 'adult'
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
- # => 'adult'
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(/str/, 10..20))
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. 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.
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 - Wildcard provided
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.3 - Case match present
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 { |(n,a)| {name: n, age: a} }
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.4 - Predicate match present
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
- # Select
365
+ # Reject
286
366
 
287
367
  people_hashes = people_arrays.map { |(n,a)| {name: n, age: a} } << {name: 'Ghost', age: nil}
288
- people_hashes.select(&Qo[age: :nil?])
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.5 - String variant present
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:
File without changes
@@ -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
- def m(*array_matchers, **keyword_matchers, &fn)
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
- def match(data, *qo_matchers)
14
- all_are_guards = qo_matchers.all? { |q| q.is_a?(Qo::GuardBlockMatcher)}
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(data)
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
- -> data { match(data, *qo_matchers) }
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
- def [](*array_matchers, **keyword_matchers)
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
@@ -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?(:===)]
@@ -1,3 +1,3 @@
1
1
  module Qo
2
- VERSION = '0.1.6'
2
+ VERSION = '0.1.7'
3
3
  end
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.6
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-12 00:00:00.000000000 Z
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