qo 0.1.10 → 0.2.0

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: 1af9159c107b7761e1b30c7062b108e692403067
4
- data.tar.gz: 483b4e35c8c9bba6a7e84c7516ea3e8a1de6e5b2
3
+ metadata.gz: dcbbe9e009a9ccf04769b8ccb7eca4343ea972a5
4
+ data.tar.gz: 53dae10eca53508dacfb75ad940207f0f047d288
5
5
  SHA512:
6
- metadata.gz: 3db647525ff5685025108dacbe62b7945f135399d56c8a68eb9f39eb63dda3704614da143608a97d66251754c7d57c97d69d5e1a0d73f0c56e5d256cf813acdf
7
- data.tar.gz: 72582ba2f1d0f81d149e2f0b9e3d35d10dcb929418c2cde4f88d3773a734f93c7e78594dc59689058f8de0d2cf3edfbc9dc7bb448ec7cfe9bda237fe101a07d0
6
+ metadata.gz: 62bceca4a54804a1e18c86a26802e3903693a61d213fa27b959e0e6a362b54470aeb22f45101ebd8b73cb5dfa90a7c1214c7d78574d3a366a679734744005007
7
+ data.tar.gz: 40a0319fa994a9e82e1f55001deab434e95187819efc1ef5eaa6bdd9be015987c481b7b7097b00820974cdc43eeed815fc4b9f7d442f5388b1881549ad00eb62
data/README.md CHANGED
@@ -10,11 +10,19 @@ Short for Query Object, my play at Ruby pattern matching and fluent querying
10
10
 
11
11
  ## How does it work?
12
12
 
13
- Triple equals black magic, mostly.
13
+ Mostly by using Ruby language features like `to_proc` and `===`.
14
14
 
15
- Want to understand more of how that works? Check out this post: https://medium.com/rubyinside/triple-equals-black-magic-d934936a6379
15
+ There's an article explaining most of the base mechanics behind Qo:
16
16
 
17
- The original inspiration was from a chat I'd had with a few other Rubyists about pattern matching, which led to this experiment: https://gist.github.com/baweaver/611389c41c9005d025fb8e55448bf5f5
17
+ [For Want of Pattern Matching in Ruby - The Creation of Qo](https://medium.com/@baweaver/for-want-of-pattern-matching-in-ruby-the-creation-of-qo-c3b267109b25)
18
+
19
+ Most of it, though, utilizes Triple Equals. If you're not familiar with what all you can do with it in Ruby, I would encourage you to read this article as well:
20
+
21
+ [Triple Equals Black Magic](https://medium.com/rubyinside/triple-equals-black-magic-d934936a6379)
22
+
23
+ The original inspiration was from a chat I'd had with a few other Rubyists about pattern matching, which led to this experiment:
24
+
25
+ [Having fun with M and Q](https://gist.github.com/baweaver/611389c41c9005d025fb8e55448bf5f5)
18
26
 
19
27
  Fast forward a few months and I kind of wanted to make it real, so here it is. Introducing Qo!
20
28
 
@@ -36,7 +44,7 @@ people.select(&Qo[age: 18..30])
36
44
 
37
45
  # How about some "right-hand assignment" pattern matching
38
46
  name_longer_than_three = -> person { person.name.size > 3 }
39
- people_with_truncated_names = people.map(&Qo.match_fn(
47
+ people_with_truncated_names = people.map(&Qo.match(
40
48
  Qo.m(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) },
41
49
  Qo.m(:*) # Identity function, catch-all
42
50
  ))
@@ -63,7 +71,7 @@ Qo[/Rob/, 22]
63
71
  Qo.and(/Rob/, 22)
64
72
 
65
73
  # This is shorthand for
66
- Qo::Matcher.new('and', /Rob/, 22)
74
+ Qo::Matchers::BaseMatcher.new('and', /Rob/, 22)
67
75
 
68
76
  # An `or` matcher uses the same shorthand as `and` but uses `any?` behind the scenes instead:
69
77
  Qo.or(/Rob/, 22)
@@ -111,7 +119,7 @@ Qo[:*] === :literally_anything_here
111
119
  The first way a Qo matcher can be defined is by using `*varargs`:
112
120
 
113
121
  ```ruby
114
- # Qo::Matcher(type, *varargs, **kwargs)
122
+ Qo::Matchers::BaseMatcher(type, *varargs, **kwargs)
115
123
  ```
116
124
 
117
125
  This gives us the `and` matcher shorthand for array matchers.
@@ -277,7 +285,7 @@ Checks to see if the key is even present on the other object, false if not.
277
285
 
278
286
  ##### 3.1.2 - Match value and target are hashes
279
287
 
280
- 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:
288
+ If both the match value (`match_key: matcher`) 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:
281
289
 
282
290
  ```ruby
283
291
  Qo[a: {b: {c: 5..15}}] === {a: {b: {c: 10}}}
@@ -444,8 +452,6 @@ people_hashes.select(&Qo[age: :nil?])
444
452
 
445
453
  ### 4 - Right Hand Pattern Matching
446
454
 
447
- > ALPHA - This feature is alpha, currently testing. Considering whether or not to add `or` and `not` as `m_or` and `m_not`.
448
-
449
455
  This is where I start going a bit off into the weeds. We're going to try and get RHA style pattern matching in Ruby.
450
456
 
451
457
  ```ruby
@@ -470,12 +476,12 @@ In this case it's trying to do a few things:
470
476
 
471
477
  If no block function is provided, it assumes an identity function (`-> v { v }`) instead. If no match is found, `nil` will be returned.
472
478
 
473
- Now you _can_ also use a reversed version, `match_fn` (name pending better ideas), to run with map:
479
+ If an initial target is not furnished, the matcher will become a curried proc awaiting a target. In more simple terms it just wants a target to run against, so let's give it a few with map:
474
480
 
475
481
  ```ruby
476
482
  name_longer_than_three = -> person { person.name.size > 3 }
477
483
 
478
- people_objects.map(&Qo.match_fn(
484
+ people_objects.map(&Qo.match(
479
485
  Qo.m(name_longer_than_three) { |person|
480
486
  person.name = person.name[0..2]
481
487
  person
@@ -596,7 +602,7 @@ The nice thing about Unix style commands is that they use headers, which means C
596
602
  ```ruby
597
603
  rows = CSV.new(`df -h`, col_sep: " ", headers: true).read.map(&:to_h)
598
604
 
599
- rows.map(&Qo.match_fn(
605
+ rows.map(&Qo.match(
600
606
  Qo.m(Avail: /Gi$/) { |row|
601
607
  "#{row['Filesystem']} mounted on #{row['Mounted']} [#{row['Avail']} / #{row['Size']}]"
602
608
  }
data/Rakefile CHANGED
@@ -105,3 +105,75 @@ task :perf do
105
105
  }
106
106
  )
107
107
  end
108
+
109
+ # Below this mark are mostly my experiments to see what features perform a bit better
110
+ # than others, and are mostly left to check different versions of Ruby against eachother.
111
+ #
112
+ # Feel free to use them in development, but the general consensus of them is that
113
+ # `send` type methods are barely slower. One _could_ write an IIFE to get around
114
+ # that and maintain the flexibility but it's a net loss of clarity.
115
+ #
116
+ # Proc wise, they're all within margin of error. We just need to be really careful
117
+ # of the 2.4+ bug of lambdas not destructuring automatically, which will wreak
118
+ # havoc on hash matchers.
119
+
120
+ task :perf_predicates do
121
+ array = (1..1000).to_a
122
+
123
+ run_benchmark('Predicates any?',
124
+ 'block_any?': -> { array.any? { |v| v.even? } },
125
+ 'proc_any?': -> { array.any?(&:even?) },
126
+ 'send_proc_any?': -> { array.public_send(:any?, &:even?) }
127
+ )
128
+
129
+ run_benchmark('Predicates all?',
130
+ 'block_all?': -> { array.all? { |v| v.even? } },
131
+ 'proc_all?': -> { array.all?(&:even?) },
132
+ 'send_proc_all?': -> { array.public_send(:all?, &:even?) }
133
+ )
134
+
135
+ run_benchmark('Predicates none?',
136
+ 'block_none?': -> { array.none? { |v| v.even? } },
137
+ 'proc_none?': -> { array.none?(&:even?) },
138
+ 'send_proc_none?': -> { array.public_send(:none?, &:even?) },
139
+ )
140
+
141
+ even_stabby_lambda = -> n { n % 2 == 0 }
142
+ even_lambda = lambda { |n| n % 2 == 0 }
143
+ even_proc_new = Proc.new { |n| n % 2 == 0 }
144
+ even_proc_short = proc { |n| n % 2 == 0 }
145
+ even_to_proc = :even?.to_proc
146
+
147
+ run_benchmark('Types of Functions in Ruby',
148
+ even_stabby_lambda: -> { array.all?(&even_stabby_lambda) },
149
+ even_lambda: -> { array.all?(&even_lambda) },
150
+ even_proc_new: -> { array.all?(&even_proc_new) },
151
+ even_proc_short: -> { array.all?(&even_proc_short) },
152
+ even_to_proc: -> { array.all?(&even_to_proc) },
153
+ )
154
+ end
155
+
156
+ task :perf_random do
157
+ # run_benchmark('Empty on blank array',
158
+ # 'empty?': -> { [].empty? },
159
+ # 'size == 0': -> { [].size == 0 },
160
+ # 'size.zero?': -> { [].size.zero? },
161
+ # )
162
+
163
+ array = (1..1000).to_a
164
+ # run_benchmark('Empty on several elements array',
165
+ # 'empty?': -> { array.empty? },
166
+ # 'size == 0': -> { array.size == 0 },
167
+ # 'size.zero?': -> { array.size.zero? },
168
+ # )
169
+
170
+ hash = array.map { |v| [v, v] }.to_h
171
+
172
+ run_benchmark('Empty on blank hash vs array',
173
+ 'hash empty?': -> { {}.empty? },
174
+ 'array empty?': -> { [].empty? },
175
+
176
+ 'full hash empty?': -> { hash.empty? },
177
+ 'full array empty?': -> { array.empty? },
178
+ )
179
+ end
data/lib/qo.rb CHANGED
@@ -1,92 +1,25 @@
1
1
  require "qo/version"
2
- require 'qo/matcher'
3
- require 'qo/guard_block_matcher'
4
2
 
5
- module Qo
6
- WILDCARD_MATCH = :*
7
-
8
- class << self
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)
26
- Qo::GuardBlockMatcher.new(*array_matchers, **keyword_matchers, &fn)
27
- end
28
-
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) }
41
- raise 'Must patch Qo GuardBlockMatchers!' unless all_are_guards
42
-
43
- qo_matchers.reduce(nil) { |_, matcher|
44
- did_match, match_result = matcher.call(target)
45
- break match_result if did_match
46
- }
47
- end
3
+ # Matchers
4
+ require 'qo/matchers/base_matcher'
5
+ require 'qo/matchers/array_matcher'
6
+ require 'qo/matchers/hash_matcher'
7
+ require 'qo/matchers/guard_block_matcher'
48
8
 
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
55
- def match_fn(*qo_matchers)
56
- -> target { match(target, *qo_matchers) }
57
- end
9
+ # Meta Matchers
10
+ require 'qo/matchers/pattern_match'
58
11
 
59
- def and(*array_matchers, **keyword_matchers)
60
- Qo::Matcher.new('and', *array_matchers, **keyword_matchers)
61
- end
12
+ # Helpers
13
+ require 'qo/helpers'
62
14
 
63
- alias_method :[], :and
15
+ # Public API
16
+ require 'qo/exceptions'
17
+ require 'qo/public_api'
64
18
 
65
- def or(*array_matchers, **keyword_matchers)
66
- Qo::Matcher.new('or', *array_matchers, **keyword_matchers)
67
- end
68
-
69
- def not(*array_matchers, **keyword_matchers)
70
- Qo::Matcher.new('not', *array_matchers, **keyword_matchers)
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 }
19
+ module Qo
20
+ WILDCARD_MATCH = :*
86
21
 
87
- targets.each_with_object(Hash.new(0)) { |target, counts|
88
- counts[fn[target]] += 1
89
- }
90
- end
91
- end
22
+ extend Qo::Exceptions
23
+ extend Qo::Helpers
24
+ extend Qo::PublicApi
92
25
  end
@@ -0,0 +1,39 @@
1
+ module Qo
2
+ # Defines common exception classes for use throughout the library
3
+ #
4
+ # @author [baweaver]
5
+ #
6
+ module Exceptions
7
+ # If no matchers in either Array or Hash style are provided.
8
+ #
9
+ # @author [lemur]
10
+ #
11
+ class NoMatchersProvided < ArgumentError
12
+ def to_s
13
+ "No Qo matchers were provided!"
14
+ end
15
+ end
16
+
17
+ # If both Array and Hash style matchers are provided.
18
+ #
19
+ # @author [lemur]
20
+ #
21
+ class MultipleMatchersProvided < ArgumentError
22
+ def to_s
23
+ "Cannot provide both array and keyword matchers!"
24
+ end
25
+ end
26
+
27
+ # In the case of a Pattern Match, we need to ensure all arguments are
28
+ # GuardBlockMatchers.
29
+ #
30
+ # @author [lemur]
31
+ #
32
+ class NotAllGuardMatchersProvided < ArgumentError
33
+ def to_s
34
+ "All provided matchers must be of type Qo::Matchers::GuardBlockMatcher " +
35
+ "defined with `Qo.matcher` or `Qo.m` instead of regular matchers."
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ module Qo
2
+ module Helpers
3
+ # A curried variant of Hash#dig meant to be passed as a matcher util.
4
+ #
5
+ # @note This method will attempt to coerce path segments to Symbols
6
+ # if unsuccessful in first dig.
7
+ #
8
+ # @param path_map [String] Dot-delimited path
9
+ # @param expected_value [Any] Matcher
10
+ #
11
+ # @return [Proc]
12
+ # Hash -> Bool # Status of digging against the hash
13
+ def dig(path_map, expected_value)
14
+ Proc.new { |hash|
15
+ segments = path_map.split('.')
16
+
17
+ expected_value === hash.dig(*segments) ||
18
+ expected_value === hash.dig(*segments.map(&:to_sym))
19
+ }
20
+ end
21
+
22
+ # Counts by a function. This is entirely because I hackney this everywhere in
23
+ # pry anyways, so I want a function to do it for me already.
24
+ #
25
+ # @param targets [Array[Any]] Targets to count
26
+ # @param &fn [Proc] Function to define count key
27
+ #
28
+ # @return [Hash[Any, Integer]] Counts
29
+ def count_by(targets, &fn)
30
+ fn ||= -> v { v }
31
+
32
+ targets.each_with_object(Hash.new(0)) { |target, counts|
33
+ counts[fn[target]] += 1
34
+ }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ module Qo
2
+ module Matchers
3
+ # An Array Matcher is a matcher that uses only varargs to define a sequence
4
+ # of matches to perform against either an object or another Array.
5
+ #
6
+ # In the case of an Array matching against an Array it will compare via index.
7
+ #
8
+ # In the case of an Array matching against an Object, it will match each provided
9
+ # matcher against the object.
10
+ #
11
+ # All variants present in the BaseMatcher are present here, including 'and',
12
+ # 'not', and 'or'.
13
+ #
14
+ # @author [baweaver]
15
+ #
16
+ class ArrayMatcher < BaseMatcher
17
+ # Used to match against a matcher made from an Array, like:
18
+ #
19
+ # Qo['Foo', 'Bar']
20
+ #
21
+ # @param matchers [Array[respond_to?(===)]] indexed tuple to match the target object against
22
+ #
23
+ # @return [Proc[Any]]
24
+ # Array -> Bool # Tuple match against targets index
25
+ # Object -> Bool # Boolean public send
26
+ def to_proc
27
+ Proc.new { |target| self.call(target) }
28
+ end
29
+
30
+ # Invocation for the match sequence. Will determine the target and applicable
31
+ # matchers to run against it.
32
+ #
33
+ # @param target [Any]
34
+ #
35
+ # @return [Boolean] Match status
36
+ def call(target)
37
+ return true if @array_matchers == target
38
+
39
+ if target.is_a?(::Array)
40
+ match_with(@array_matchers.each_with_index) { |matcher, i|
41
+ match_value?(target[i], matcher)
42
+ }
43
+ else
44
+ match_with(@array_matchers) { |matcher|
45
+ match_value?(target, matcher)
46
+ }
47
+ end
48
+ end
49
+
50
+ # Defines what it means for a value to match a matcher
51
+ #
52
+ # @param target [Any] Target to match against
53
+ # @param matcher [Any] Any matcher to run against, most frequently responds to ===
54
+ #
55
+ # @return [Boolean] Match status
56
+ private def match_value?(target, matcher)
57
+ wildcard_match?(matcher) ||
58
+ case_match?(target, matcher) ||
59
+ method_matches?(target, matcher)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,104 @@
1
+ module Qo
2
+ module Matchers
3
+ # Base instance of matcher which is meant to take in either Array style or
4
+ # Keyword style arguments to run a match against various datatypes.
5
+ #
6
+ # Will delegate responsibilities to either Array or Hash style matchers if
7
+ # invoked directly.
8
+ #
9
+ # @author [baweaver]
10
+ #
11
+ class BaseMatcher
12
+ def initialize(type, *array_matchers, **keyword_matchers)
13
+ @array_matchers = array_matchers
14
+ @keyword_matchers = keyword_matchers
15
+ @type = type
16
+ end
17
+
18
+ # Converts a Matcher to a proc for use in querying, such as:
19
+ #
20
+ # data.select(&Qo[...])
21
+ #
22
+ # @return [Proc]
23
+ def to_proc
24
+ @array_matchers.empty? ?
25
+ Qo::Matchers::HashMatcher.new(@type, **@keyword_matchers).to_proc :
26
+ Qo::Matchers::ArrayMatcher.new(@type, *@array_matchers).to_proc
27
+ end
28
+
29
+ # You can directly call a matcher as well, much like a Proc,
30
+ # using one of call, ===, or []
31
+ #
32
+ # @param target [Any] Object to match against
33
+ #
34
+ # @return [type] [description]
35
+ def call(target)
36
+ self.to_proc.call(target)
37
+ end
38
+
39
+ alias_method :===, :call
40
+ alias_method :[], :call
41
+
42
+ # Wrapper around public send to encapsulate the matching method (any, all, none)
43
+ #
44
+ # @param collection [Enumerable] Any collection that can be enumerated over
45
+ # @param fn [Proc] Function to match with
46
+ #
47
+ # @return [Enumerable] Resulting collection
48
+ private def match_with(collection, &fn)
49
+ return collection.any?(&fn) if @type == 'or'
50
+ return collection.none?(&fn) if @type == 'not'
51
+
52
+ collection.all?(&fn)
53
+ end
54
+
55
+ # Wraps wildcard in case we want to do anything fun with it later
56
+ #
57
+ # @param value [Any] Value to test against the wild card
58
+ #
59
+ # @note The rescue is because some classes override `==` to do silly things,
60
+ # like IPAddr, and I kinda want to use that.
61
+ #
62
+ # @return [Boolean]
63
+ private def wildcard_match?(value)
64
+ value == WILDCARD_MATCH rescue false
65
+ end
66
+
67
+ # Wraps a case equality statement to make it a bit easier to read. The
68
+ # typical left bias of `===` can be confusing reading down a page, so
69
+ # more of a clarity thing than anything. Also makes for nicer stack traces.
70
+ #
71
+ # @param target [Any] Target to match against
72
+ # @param matcher [respond_to?(:===)]
73
+ # Anything that responds to ===, preferably in a unique and entertaining way.
74
+ #
75
+ # @return [Boolean]
76
+ private def case_match?(target, matcher)
77
+ matcher === target
78
+ end
79
+
80
+ # Guarded version of `public_send` meant to stamp out more
81
+ # obscure errors when running against non-matching types.
82
+ #
83
+ # @param target [Any] Object to send to
84
+ # @param matcher [respond_to?(:to_sym)] Anything that can be coerced into a method name
85
+ #
86
+ # @return [Any] Response of sending to the method, or false if failed
87
+ private def method_send(target, matcher)
88
+ matcher.respond_to?(:to_sym) &&
89
+ target.respond_to?(matcher.to_sym) &&
90
+ target.public_send(matcher)
91
+ end
92
+
93
+ # Predicate variant of `method_send` with the same guard concerns
94
+ #
95
+ # @param target [Any] Object to send to
96
+ # @param matcher [respond_to?(:to_sym)] Anything that can be coerced into a method name
97
+ #
98
+ # @return [Boolean] Success status of predicate
99
+ private def method_matches?(target, matcher)
100
+ !!method_send(target, matcher)
101
+ end
102
+ end
103
+ end
104
+ end