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 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