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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dcbbe9e009a9ccf04769b8ccb7eca4343ea972a5
|
4
|
+
data.tar.gz: 53dae10eca53508dacfb75ad940207f0f047d288
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
13
|
+
Mostly by using Ruby language features like `to_proc` and `===`.
|
14
14
|
|
15
|
-
|
15
|
+
There's an article explaining most of the base mechanics behind Qo:
|
16
16
|
|
17
|
-
|
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.
|
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::
|
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
|
-
|
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:
|
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
|
-
|
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.
|
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.
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
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
|
-
|
60
|
-
|
61
|
-
end
|
12
|
+
# Helpers
|
13
|
+
require 'qo/helpers'
|
62
14
|
|
63
|
-
|
15
|
+
# Public API
|
16
|
+
require 'qo/exceptions'
|
17
|
+
require 'qo/public_api'
|
64
18
|
|
65
|
-
|
66
|
-
|
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
|
-
|
88
|
-
|
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
|
data/lib/qo/helpers.rb
ADDED
@@ -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
|