qo 0.1.10 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +18 -12
- data/Rakefile +72 -0
- data/lib/qo.rb +17 -84
- data/lib/qo/exceptions.rb +39 -0
- data/lib/qo/helpers.rb +37 -0
- data/lib/qo/matchers/array_matcher.rb +63 -0
- data/lib/qo/matchers/base_matcher.rb +104 -0
- data/lib/qo/matchers/guard_block_matcher.rb +37 -0
- data/lib/qo/matchers/hash_matcher.rb +131 -0
- data/lib/qo/matchers/pattern_match.rb +93 -0
- data/lib/qo/public_api.rb +121 -0
- data/lib/qo/version.rb +1 -1
- data/performance_report.txt +24 -24
- metadata +10 -4
- data/lib/qo/guard_block_matcher.rb +0 -19
- data/lib/qo/matcher.rb +0 -286
@@ -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
|
data/lib/qo/version.rb
CHANGED
data/performance_report.txt
CHANGED
@@ -3,58 +3,58 @@ Array * Array - Literal
|
|
3
3
|
=======================
|
4
4
|
|
5
5
|
Warming up --------------------------------------
|
6
|
-
Vanilla
|
7
|
-
Qo.and
|
6
|
+
Vanilla 283.062k i/100ms
|
7
|
+
Qo.and 48.476k i/100ms
|
8
8
|
Calculating -------------------------------------
|
9
|
-
Vanilla 9.
|
10
|
-
Qo.and
|
9
|
+
Vanilla 9.319M (± 2.1%) i/s - 46.705M in 5.013834s
|
10
|
+
Qo.and 573.855k (± 1.3%) i/s - 2.909M in 5.069323s
|
11
11
|
|
12
12
|
Comparison:
|
13
|
-
Vanilla:
|
14
|
-
Qo.and:
|
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
|
22
|
-
Qo.and
|
21
|
+
Vanilla 44.095k i/100ms
|
22
|
+
Qo.and 12.753k i/100ms
|
23
23
|
Calculating -------------------------------------
|
24
|
-
Vanilla
|
25
|
-
Qo.and
|
24
|
+
Vanilla 482.805k (± 5.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:
|
29
|
-
Qo.and:
|
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
|
37
|
-
Qo.and
|
36
|
+
Vanilla 138.847k i/100ms
|
37
|
+
Qo.and 16.157k i/100ms
|
38
38
|
Calculating -------------------------------------
|
39
|
-
Vanilla 2.
|
40
|
-
Qo.and
|
39
|
+
Vanilla 2.153M (± 3.9%) i/s - 10.830M in 5.037676s
|
40
|
+
Qo.and 174.879k (± 3.8%) i/s - 888.635k in 5.089311s
|
41
41
|
|
42
42
|
Comparison:
|
43
|
-
Vanilla:
|
44
|
-
Qo.and:
|
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
|
52
|
-
Qo.and
|
51
|
+
Vanilla 13.175k i/100ms
|
52
|
+
Qo.and 21.534k i/100ms
|
53
53
|
Calculating -------------------------------------
|
54
|
-
Vanilla
|
55
|
-
Qo.and
|
54
|
+
Vanilla 137.128k (± 3.6%) i/s - 685.100k in 5.002872s
|
55
|
+
Qo.and 240.447k (± 2.7%) i/s - 1.206M in 5.019112s
|
56
56
|
|
57
57
|
Comparison:
|
58
|
-
Qo.and:
|
59
|
-
Vanilla:
|
58
|
+
Qo.and: 240447.2 i/s
|
59
|
+
Vanilla: 137128.2 i/s - 1.75x slower
|
60
60
|
|