qo 0.1.10 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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