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.
@@ -0,0 +1,37 @@
1
+ module Qo
2
+ module Matchers
3
+ # A GuardBlockMatcher is like a regular matcher, except in that if it
4
+ # "matches" it will provide its match target to its associated block.
5
+ #
6
+ # It returns tuples of (status, result) in order to prevent masking of
7
+ # legitimate falsy or nil values returned.
8
+ #
9
+ # @author [baweaver]
10
+ #
11
+ class GuardBlockMatcher < BaseMatcher
12
+ # Identity function that returns its argument directly
13
+ IDENTITY = -> v { v }
14
+
15
+ # Definition of a non-match
16
+ NON_MATCH = [false, false]
17
+
18
+ def initialize(*array_matchers, **keyword_matchers, &fn)
19
+ @fn = fn || IDENTITY
20
+
21
+ super('and', *array_matchers, **keyword_matchers)
22
+ end
23
+
24
+ # Overrides the base matcher's #to_proc to wrap the value in a status
25
+ # and potentially call through to the associated block if a base
26
+ # matcher would have passed
27
+ #
28
+ # @return [Proc]
29
+ # Any -> [Bool, Any] # (status, result) tuple
30
+ def to_proc
31
+ Proc.new { |target|
32
+ super[target] ? [true, @fn.call(target)] : NON_MATCH
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,131 @@
1
+ module Qo
2
+ module Matchers
3
+ # A Hash Matcher is a matcher that uses only keyword args to define a sequence
4
+ # of matches to perform against either an Object or another Hash.
5
+ #
6
+ # In the case of a Hash matching against a Hash, it will compare the intersection
7
+ # of keys and match the values against eachother.
8
+ #
9
+ # In the case of a Hash matching against an Object, it will treat the keys as
10
+ # method property invocations to be matched against the provided values.
11
+ #
12
+ # All variants present in the BaseMatcher are present here, including 'and',
13
+ # 'not', and 'or'.
14
+ #
15
+ # @author [baweaver]
16
+ #
17
+ class HashMatcher < BaseMatcher
18
+ # Used to match against a matcher made from an Array, like:
19
+ #
20
+ # Qo['Foo', 'Bar']
21
+ #
22
+ # @param matchers [Array[respond_to?(===)]] indexed tuple to match the target object against
23
+ #
24
+ # @return [Proc[Any]]
25
+ # Array -> Bool # Tuple match against targets index
26
+ # Object -> Bool # Boolean public send
27
+ def to_proc
28
+ Proc.new { |target| self.call(target) }
29
+ end
30
+
31
+ # Used to match against a matcher made from Keyword Arguments (a Hash)
32
+ #
33
+ # @param matchers [Hash[Any, respond_to?(:===)]]
34
+ # Any key mapping to any value that responds to `===`. Notedly more
35
+ # satisfying when `===` does something fun.
36
+ #
37
+ # @return [Proc[Any]]
38
+ # Hash -> Bool # Value matching against similar keys, will attempt to coerce to_s because JSON
39
+ # Object -> Bool # Uses keys as methods with public send to `===` match against the value
40
+ def call(target)
41
+ return true if @keyword_matchers == target
42
+
43
+ match_fn = target.is_a?(::Hash) ?
44
+ Proc.new { |match_key, matcher| match_hash_value?(target, match_key, matcher) } :
45
+ Proc.new { |match_key, matcher| match_object_value?(target, match_key, matcher) }
46
+
47
+ match_with(@keyword_matchers, &match_fn)
48
+ end
49
+
50
+ # Checks if a hash value matches a given matcher
51
+ #
52
+ # @param target [Any] Target of the match
53
+ # @param match_key [Symbol] Key of the hash to reference
54
+ # @param matcher [respond_to?(:===)] Any matcher responding to ===
55
+ #
56
+ # @return [Boolean] Match status
57
+ private def match_hash_value?(target, match_key, matcher)
58
+ return false unless target.key?(match_key)
59
+ return true if wildcard_match?(matcher)
60
+
61
+ return hash_recurse(target[match_key], matcher) if target.is_a?(Hash) && matcher.is_a?(Hash)
62
+
63
+ hash_case_match?(target, match_key, matcher) ||
64
+ hash_method_predicate_match?(target, match_key, matcher)
65
+ end
66
+
67
+ # Checks if an object property matches a given matcher
68
+ #
69
+ # @param target [Any] Target of the match
70
+ # @param match_property [Symbol] Property of the object to reference
71
+ # @param matcher [respond_to?(:===)] Any matcher responding to ===
72
+ #
73
+ # @return [Boolean] Match status
74
+ private def match_object_value?(target, match_property, matcher)
75
+ return false unless target.respond_to?(match_property)
76
+
77
+ wildcard_match?(matcher) ||
78
+ hash_method_case_match?(target, match_property, matcher)
79
+ end
80
+
81
+ # Double wraps case match in order to ensure that we try against both Symbol
82
+ # and String variants of the keys, as this is a very common mixup in Ruby.
83
+ #
84
+ # @param target [Hash] Target of the match
85
+ # @param match_key [Symbol] Key to match against
86
+ # @param matcher [respond_to?(:===)] Matcher
87
+ #
88
+ # @return [Boolean]
89
+ private def hash_case_match?(target, match_key, matcher)
90
+ return true if case_match?(target[match_key], matcher)
91
+
92
+ match_key.respond_to?(:to_s) &&
93
+ target.key?(match_key.to_s) &&
94
+ case_match?(target[match_key.to_s], matcher)
95
+ end
96
+
97
+ # Attempts to run a matcher as a predicate method against the target
98
+ #
99
+ # @param target [Hash] Target of the match
100
+ # @param match_key [Symbol] Method to call
101
+ # @param match_predicate [Symbol] Matcher
102
+ #
103
+ # @return [Boolean]
104
+ private def hash_method_predicate_match?(target, match_key, match_predicate)
105
+ method_matches?(target[match_key], match_predicate)
106
+ end
107
+
108
+ # Attempts to run a case match against a method call derived from a hash
109
+ # key, and checks the result.
110
+ #
111
+ # @param target [Hash] Target of the match
112
+ # @param match_property [Symbol] Method to call
113
+ # @param matcher [respond_to?(:===)] Matcher
114
+ #
115
+ # @return [Boolean]
116
+ private def hash_method_case_match?(target, match_property, matcher)
117
+ case_match?(method_send(target, match_property), matcher)
118
+ end
119
+
120
+ # Recurses on nested hashes.
121
+ #
122
+ # @param target [Hash]
123
+ # @param matcher [Hash]
124
+ #
125
+ # @return [Boolean]
126
+ private def hash_recurse(target, matcher)
127
+ Qo::Matchers::HashMatcher.new(@type, **matcher).call(target)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,93 @@
1
+ require 'qo/exceptions'
2
+
3
+ module Qo
4
+ module Matchers
5
+ # Creates a PatternMatch, which will evaluate once given a match target.
6
+ #
7
+ # Each GuardBlockMatcher given will be run in sequence until a "match" is
8
+ # located. Once that condition is met, it will call the associated block
9
+ # function of that GuardBlockMatcher with the match target.
10
+ #
11
+ # This is done as an effort to emulate Right Hand Assignment seen in other
12
+ # functionally oriented languages pattern matching systems. Most notably this
13
+ # is done in Scala: (https://docs.scala-lang.org/tour/pattern-matching.html)
14
+ #
15
+ # ```scala
16
+ # notification match {
17
+ # case Email(email, title, _) =>
18
+ # s"You got an email from $email with title: $title"
19
+ #
20
+ # case SMS(number, message) =>
21
+ # s"You got an SMS from $number! Message: $message"
22
+ #
23
+ # case VoiceRecording(name, link) =>
24
+ # s"You received a Voice Recording from $name! Click the link to hear it: $link"
25
+ #
26
+ # case other => other
27
+ # }
28
+ # ```
29
+ #
30
+ # Qo will instead pipe the entire matched object, and might look something like this:
31
+ #
32
+ # ```ruby
33
+ # # Assuming notification is in the form of a tuple
34
+ #
35
+ # Qo.match(notification,
36
+ # Qo.m(EMAIL_REGEX, String, :*) { |email, title, _|
37
+ # "You got an email from #{email} with title: #{title}"
38
+ # },
39
+ #
40
+ # Qo.m(PHONE_REGEX, String) { |number, message, _|
41
+ # "You got an SMS from #{number}! Message: #{message}"
42
+ # },
43
+ #
44
+ # Qo.m(String, LINK_REGEX) { |name, link, _|
45
+ # "You received a Voice Recording from #{name}! Click the link to hear it: #{link}"
46
+ # },
47
+ #
48
+ # Qo.m(:*)
49
+ # )
50
+ # ```
51
+ #
52
+ # Efforts to emulate the case class mechanic may be present in later versions,
53
+ # but for now the associated performance penalty may be too steep to consider.
54
+ #
55
+ # We'll evaluate those options in a few experiments later.
56
+ #
57
+ # @author [baweaver]
58
+ #
59
+ class PatternMatch
60
+ def initialize(*matchers)
61
+ raise Qo::Exceptions::NotAllGuardMatchersProvided unless matchers.all? { |q|
62
+ q.is_a?(Qo::Matchers::GuardBlockMatcher)
63
+ }
64
+
65
+ @matchers = matchers
66
+ end
67
+
68
+ # Function return of a PatternMatch waiting for a target to run
69
+ #
70
+ # @return [Proc]
71
+ # Any -> Any | nil
72
+ def to_proc
73
+ Proc.new { |target| self.call(target) }
74
+ end
75
+
76
+ # Immediately invokes a PatternMatch
77
+ #
78
+ # @param target [Any]
79
+ # Target to run against and pipe to the associated block if it
80
+ # "matches" any of the GuardBlocks
81
+ #
82
+ # @return [Any | nil] Result of the piped block, or nil on a miss
83
+ def call(target)
84
+ @matchers.each { |guard_block_matcher|
85
+ did_match, match_result = guard_block_matcher.call(target)
86
+ return match_result if did_match
87
+ }
88
+
89
+ nil
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,121 @@
1
+ module Qo
2
+ # The Public API consists of methods that should be openly accessible to the
3
+ # top level Qo namespace, and should not change. It should be used as the
4
+ # subject of Acceptance level tests for the library and should not have its
5
+ # externally facing methods renamed or moved under pain of a look of profound
6
+ # disappointment from the creator.
7
+ #
8
+ # @author [baweaver]
9
+ #
10
+ module PublicApi
11
+ include Qo::Exceptions
12
+
13
+ # Creates an `and` type query matcher. All conditions in this type of matcher
14
+ # must pass to be considered a "match". It will short-circuit in the case of
15
+ # a false match.
16
+ #
17
+ # @param *array_matchers [Array] Array-like conditionals
18
+ # @param **keyword_matchers [Hash] Keyword style conditionals
19
+ #
20
+ # @return [Proc[Any]]
21
+ # Any -> Bool # Given a target, will return if it "matches"
22
+ def and(*array_matchers, **keyword_matchers)
23
+ create_matcher('and', *array_matchers, **keyword_matchers)
24
+ end
25
+
26
+ # The magic that lets you use `Qo[...]` instead of `Qo.and(...)`. Use wisely
27
+ alias_method :[], :and
28
+
29
+ # Creates an `or` type query matcher. Any conditions in this type of matcher
30
+ # must pass to be considered a "match". It will short-circuit in the case of
31
+ # a true match.
32
+ #
33
+ # @param *array_matchers [Array] Array-like conditionals
34
+ # @param **keyword_matchers [Hash] Keyword style conditionals
35
+ #
36
+ # @return [Proc[Any]]
37
+ # Any -> Bool # Given a target, will return if it "matches"
38
+ def or(*array_matchers, **keyword_matchers)
39
+ create_matcher('or', *array_matchers, **keyword_matchers)
40
+ end
41
+
42
+ # Creates a `not` type query matcher. No conditions in this type of matcher
43
+ # should pass to be considered a "match". It will short-circuit in the case of
44
+ # a true match.
45
+ #
46
+ # @param *array_matchers [Array] Array-like conditionals
47
+ # @param **keyword_matchers [Hash] Keyword style conditionals
48
+ #
49
+ # @return [Proc[Any]]
50
+ # Any -> Bool # Given a target, will return if it "matches"
51
+ def not(*array_matchers, **keyword_matchers)
52
+ create_matcher('not', *array_matchers, **keyword_matchers)
53
+ end
54
+
55
+ # Creates a Guard Block matcher.
56
+ #
57
+ # A guard block matcher is used to guard a function from running unless
58
+ # the left-hand matcher passes. Once called with a value, it will either
59
+ # return `[false, false]` or `[true, Any]`.
60
+ #
61
+ # This wrapping is done to preserve intended false or nil responses,
62
+ # and is unwrapped with match below.
63
+ #
64
+ # @param *array_matchers [Array] varargs matchers
65
+ # @param **keyword_matchers [Hash] kwargs matchers
66
+ # @param &fn [Proc] Guarded function
67
+ #
68
+ # @return [Proc[Any]]
69
+ # Any -> Proc[Any]
70
+ def matcher(*array_matchers, **keyword_matchers, &fn)
71
+ Qo::Matchers::GuardBlockMatcher.new(*array_matchers, **keyword_matchers, &fn)
72
+ end
73
+
74
+ # Might be a tinge fond of shorthand
75
+ alias_method :m, :matcher
76
+
77
+ # "Curried" function that waits for a target, or evaluates immediately if given
78
+ # one.
79
+ #
80
+ # A PatternMatch will try and run all GuardBlock matchers in sequence until
81
+ # it finds one that "matches". Once found, it will pass the target into the
82
+ # associated matcher's block function.
83
+ #
84
+ # @param *args [Array[Any, *GuardBlockMatcher]]
85
+ # Collection of matchers to run, potentially prefixed by a target object
86
+ #
87
+ # @return [Qo::PatternMatch | Any]
88
+ # Returns a PatternMatch waiting for a target, or an evaluated PatternMatch response
89
+ def match(*args)
90
+ if args.first.is_a?(Qo::Matchers::GuardBlockMatcher)
91
+ Qo::Matchers::PatternMatch.new(*args)
92
+ else
93
+ match_target, *qo_matchers = args
94
+ Qo::Matchers::PatternMatch.new(*qo_matchers).call(match_target)
95
+ end
96
+ end
97
+
98
+ # Abstraction for creating a matcher, allowing for common error handling scenarios.
99
+ #
100
+ # @param type [String] Type of matcher
101
+ # @param *array_matchers [Array] Array-like conditionals
102
+ # @param **keyword_matchers [Hash] Keyword style conditionals
103
+ #
104
+ # @raises Qo::Exceptions::NoMatchersProvided
105
+ # @raises Qo::Exceptions::MultipleMatchersProvided
106
+ #
107
+ # @return [Qo::Matcher]
108
+ private def create_matcher(type, *array_matchers, **keyword_matchers)
109
+ array_empty, hash_empty = array_matchers.empty?, keyword_matchers.empty?
110
+
111
+ raise Qo::NoMatchersProvided if array_empty && hash_empty
112
+ raise Qo::MultipleMatchersProvided if !(array_empty || hash_empty)
113
+
114
+ if hash_empty
115
+ Qo::Matchers::ArrayMatcher.new(type, *array_matchers)
116
+ else
117
+ Qo::Matchers::HashMatcher.new(type, **keyword_matchers)
118
+ end
119
+ end
120
+ end
121
+ end
@@ -1,3 +1,3 @@
1
1
  module Qo
2
- VERSION = '0.1.10'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -3,58 +3,58 @@ Array * Array - Literal
3
3
  =======================
4
4
 
5
5
  Warming up --------------------------------------
6
- Vanilla 290.029k i/100ms
7
- Qo.and 37.778k i/100ms
6
+ Vanilla 283.062k i/100ms
7
+ Qo.and 48.476k i/100ms
8
8
  Calculating -------------------------------------
9
- Vanilla 9.559M (± 2.3%) i/s - 47.855M in 5.009272s
10
- Qo.and 468.514k2.5%) i/s - 2.342M in 5.002419s
9
+ Vanilla 9.319M (± 2.1%) i/s - 46.705M in 5.013834s
10
+ Qo.and 573.855k1.3%) i/s - 2.909M in 5.069323s
11
11
 
12
12
  Comparison:
13
- Vanilla: 9558516.6 i/s
14
- Qo.and: 468514.2 i/s - 20.40x slower
13
+ Vanilla: 9319318.3 i/s
14
+ Qo.and: 573855.1 i/s - 16.24x slower
15
15
 
16
16
 
17
17
  Array * Array - Index pattern match
18
18
  ===================================
19
19
 
20
20
  Warming up --------------------------------------
21
- Vanilla 47.088k i/100ms
22
- Qo.and 14.227k i/100ms
21
+ Vanilla 44.095k i/100ms
22
+ Qo.and 12.753k i/100ms
23
23
  Calculating -------------------------------------
24
- Vanilla 540.415k3.3%) i/s - 2.731M in 5.059509s
25
- Qo.and 149.040k (± 4.2%) i/s - 754.031k in 5.068772s
24
+ Vanilla 482.805k5.9%) i/s - 2.425M in 5.041737s
25
+ Qo.and 128.970k (± 4.3%) i/s - 650.403k in 5.052438s
26
26
 
27
27
  Comparison:
28
- Vanilla: 540414.9 i/s
29
- Qo.and: 149040.0 i/s - 3.63x slower
28
+ Vanilla: 482804.7 i/s
29
+ Qo.and: 128969.8 i/s - 3.74x slower
30
30
 
31
31
 
32
32
  Array * Object - Predicate match
33
33
  ================================
34
34
 
35
35
  Warming up --------------------------------------
36
- Vanilla 139.244k i/100ms
37
- Qo.and 20.096k i/100ms
36
+ Vanilla 138.847k i/100ms
37
+ Qo.and 16.157k i/100ms
38
38
  Calculating -------------------------------------
39
- Vanilla 2.356M (± 3.4%) i/s - 11.836M in 5.030228s
40
- Qo.and 218.039k2.9%) i/s - 1.105M in 5.073725s
39
+ Vanilla 2.153M (± 3.9%) i/s - 10.830M in 5.037676s
40
+ Qo.and 174.879k3.8%) i/s - 888.635k in 5.089311s
41
41
 
42
42
  Comparison:
43
- Vanilla: 2355717.5 i/s
44
- Qo.and: 218038.9 i/s - 10.80x slower
43
+ Vanilla: 2153240.3 i/s
44
+ Qo.and: 174878.6 i/s - 12.31x slower
45
45
 
46
46
 
47
47
  Array * Array - Select index pattern match
48
48
  ==========================================
49
49
 
50
50
  Warming up --------------------------------------
51
- Vanilla 14.015k i/100ms
52
- Qo.and 20.325k i/100ms
51
+ Vanilla 13.175k i/100ms
52
+ Qo.and 21.534k i/100ms
53
53
  Calculating -------------------------------------
54
- Vanilla 140.673k (± 3.6%) i/s - 714.765k in 5.087715s
55
- Qo.and 219.533k3.8%) i/s - 1.098M in 5.006844s
54
+ Vanilla 137.128k (± 3.6%) i/s - 685.100k in 5.002872s
55
+ Qo.and 240.447k2.7%) i/s - 1.206M in 5.019112s
56
56
 
57
57
  Comparison:
58
- Qo.and: 219533.2 i/s
59
- Vanilla: 140672.7 i/s - 1.56x slower
58
+ Qo.and: 240447.2 i/s
59
+ Vanilla: 137128.2 i/s - 1.75x slower
60
60