qo 0.5.0 → 0.99.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ module Qo
2
+ module Branches
3
+ # A default branch to for when other conditions fail.
4
+ #
5
+ # ```ruby
6
+ # Qo.case(1) { |m|
7
+ # m.else { |v| v + 2 }
8
+ # }
9
+ # # => 3
10
+ # ```
11
+ #
12
+ # @author baweaver
13
+ # @since 1.0.0
14
+ class ElseBranch < Branch
15
+ def initialize(destructure: false)
16
+ super(name: 'else', destructure: destructure, default: true)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ module Qo
2
+ module Branches
3
+ # A tuple branch that will be triggered when the first value is
4
+ # `:err`.
5
+ #
6
+ # ```ruby
7
+ # ResultPatternMatch.new { |m|
8
+ # m.error { |v| "This is the error: #{v}" }
9
+ # }.call([:err, 'OH NO!'])
10
+ # # => "This is the error: OH NO!"
11
+ # ```
12
+ #
13
+ # @author baweaver
14
+ # @since 1.0.0
15
+ class ErrorBranch < Branch
16
+ def initialize(destructure: false)
17
+ super(
18
+ name: 'error',
19
+ destructure: destructure,
20
+ precondition: -> v { v.first == :err },
21
+ extractor: :last,
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module Qo
2
+ module Branches
3
+ # A tuple branch that will be triggered when the first value is
4
+ # `:err`.
5
+ #
6
+ # ```ruby
7
+ # ResultPatternMatch.new { |m|
8
+ # m.failure { |v| "This is the error: #{v}" }
9
+ # }.call([:err, 'OH NO!'])
10
+ # # => "This is the error: OH NO!"
11
+ # ```
12
+ #
13
+ # @author baweaver
14
+ # @since 1.0.0
15
+ class FailureBranch < Branch
16
+ def initialize(destructure: false)
17
+ super(
18
+ name: 'failure',
19
+ destructure: destructure,
20
+ precondition: -> v { v.first == :err },
21
+ extractor: :last,
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module Qo
2
+ module Branches
3
+ # Based on the `else` branch, except deals with monadic values by attempting
4
+ # to extract the `value` before yielding to the given function on a match:
5
+ #
6
+ # ```ruby
7
+ # Matcher.new.call(Some[1]) { |m|
8
+ # m.else { |v| v + 2 }
9
+ # }
10
+ # # => 3
11
+ # ```
12
+ #
13
+ # @author baweaver
14
+ # @since 1.0.0
15
+ class MonadicElseBranch < Branch
16
+ def initialize(destructure: false, extractor: :value)
17
+ super(
18
+ name: 'else',
19
+ destructure: destructure,
20
+ extractor: extractor,
21
+ default: true,
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module Qo
2
+ module Branches
3
+ # Based on the `where` branch, except deals with monadic values by attempting
4
+ # to extract the `value` before yielding to the given function on a match:
5
+ #
6
+ # ```ruby
7
+ # Matcher.new.call(Some[1]) { |m|
8
+ # m.where(Some) { |v| v + 2 }
9
+ # }
10
+ # # => 3
11
+ # ```
12
+ #
13
+ # @author baweaver
14
+ # @since 1.0.0
15
+ class MonadicWhenBranch < Branch
16
+ def initialize(destructure: false, extractor: :value)
17
+ super(
18
+ name: 'where',
19
+ destructure: destructure,
20
+ extractor: extractor,
21
+ default: false,
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module Qo
2
+ module Branches
3
+ # A tuple branch that will be triggered when the first value is
4
+ # `:ok`.
5
+ #
6
+ # ```ruby
7
+ # ResultPatternMatch.new { |m|
8
+ # m.success { |v| v + 2 }
9
+ # }.call([:ok, 1])
10
+ # # => 3
11
+ # ```
12
+ #
13
+ # @author baweaver
14
+ # @since 1.0.0
15
+ class SuccessBranch < Branch
16
+ def initialize(destructure: false)
17
+ super(
18
+ name: 'success',
19
+ destructure: destructure,
20
+ precondition: -> v { v.first == :ok },
21
+ extractor: :last,
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ module Qo
2
+ module Branches
3
+ class WhenBranch < Branch
4
+ # The traditional pattern matching branch, based off of `when` from
5
+ # Ruby's `case` statement:
6
+ #
7
+ # ```ruby
8
+ # Qo.case(1) { |m|
9
+ # m.when(Integer) { |v| v + 2 }
10
+ # }
11
+ # # => 3
12
+ # ```
13
+ #
14
+ # @author baweaver
15
+ # @since 1.0.0
16
+ def initialize(destructure: false)
17
+ super(name: 'when', destructure: destructure, default: false)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,85 @@
1
+ module Qo
2
+ module Destructurers
3
+ # Classic destructuring. This gives great value to the names of a function's
4
+ # arguments, transforming the way blocks are normally yielded to. Take for
5
+ # example this function:
6
+ #
7
+ # ```ruby
8
+ # Proc.new { |name, age| ... }
9
+ # ```
10
+ #
11
+ # The names of the arguments are `name` and `age`. Destructuring involves
12
+ # using these names to extract the values of an object before the function
13
+ # is called:
14
+ #
15
+ # 1. Get the names of the arguments
16
+ # 2. Map over those names to extract values from an object by sending them
17
+ # as method calls
18
+ # 3. Call the function with the newly extracted values
19
+ #
20
+ # It's highly suggested to read through the "Destructuring in Ruby" article
21
+ # here:
22
+ #
23
+ # @see https://medium.com/rubyinside/destructuring-in-ruby-9e9bd2be0360
24
+ #
25
+ # @author baweaver
26
+ # @since 1.0.0
27
+ class Destructurer
28
+ # Creates a destructurer
29
+ #
30
+ # @param destructure: [Boolean]
31
+ # Whether or not to destructure an object before calling a function
32
+ #
33
+ # @param &function [Proc]
34
+ # Associated function to be called
35
+ #
36
+ # @return [Qo::Destructurers::Destructurer]
37
+ def initialize(destructure:, &function)
38
+ @destructure = destructure
39
+ @function = function || IDENTITY
40
+ @argument_names = argument_names
41
+ end
42
+
43
+ # Calls the destructurer to extract values from a target and call the
44
+ # function with those extracted values.
45
+ #
46
+ # @param target [Any]
47
+ # Target to extract values from
48
+ #
49
+ # @return [Any]
50
+ # Return of the given function
51
+ def call(target)
52
+ destructured_arguments = destructure? ? destructure_values(target) : target
53
+
54
+ @function.call(destructured_arguments)
55
+ end
56
+
57
+ # Whether or not this method will destructure a passed object
58
+ #
59
+ # @return [Boolean]
60
+ def destructure?
61
+ @destructure
62
+ end
63
+
64
+ # Destructures values from a target object
65
+ #
66
+ # @param target [Any]
67
+ # Object to extract values from
68
+ #
69
+ # @return [Array[Any]]
70
+ # Extracted values
71
+ def destructure_values(target)
72
+ target.is_a?(::Hash) ?
73
+ argument_names.map { |n| target[n] } :
74
+ argument_names.map { |n| target.respond_to?(n) && target.public_send(n) }
75
+ end
76
+
77
+ # Names of the function's arguments
78
+ #
79
+ # @return [Array[Symbol]]
80
+ def argument_names
81
+ @argument_names ||= @function.parameters.map(&:last)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,15 @@
1
+ module Qo
2
+ # Classes that allow for the destructuring of values from an object, meant
3
+ # to emulate Javascript object destructuring. While this is a very powerful
4
+ # expressive feature, it can also slow down execution by a small amount, so
5
+ # use destructuring wisely.
6
+ #
7
+ # @see https://medium.com/rubyinside/destructuring-in-ruby-9e9bd2be0360
8
+ #
9
+ # @author baweaver
10
+ # @since 1.0.0
11
+ module Destructurers
12
+ end
13
+ end
14
+
15
+ require 'qo/destructurers/destructurer'
@@ -1,41 +1,12 @@
1
1
  module Qo
2
2
  # Defines common exception classes for use throughout the library
3
3
  #
4
+ # Currently there aren't any exceptions being used, but keeping this
5
+ # around for later use.
6
+ #
4
7
  # @author baweaver
5
8
  # @since 0.2.0
6
9
  #
7
10
  module Exceptions
8
- # If no matchers in either Array or Hash style are provided.
9
- #
10
- # @author baweaver
11
- # @since 0.2.0
12
- #
13
- class NoMatchersProvided < ArgumentError
14
- def to_s
15
- "No Qo matchers were provided!"
16
- end
17
- end
18
-
19
- # If both Array and Hash style matchers are provided.
20
- #
21
- # @author baweaver
22
- # @since 0.2.0
23
- #
24
- class MultipleMatchersProvided < ArgumentError
25
- def to_s
26
- "Cannot provide both array and keyword matchers!"
27
- end
28
- end
29
-
30
- # In the case of a Pattern Match, we should only have one "else" clause
31
- #
32
- # @author baweaver
33
- # @since 0.3.0
34
- #
35
- class MultipleElseClauses < ArgumentError
36
- def to_s
37
- "Cannot have more than one `else` clause in a Pattern Match."
38
- end
39
- end
40
11
  end
41
12
  end
@@ -0,0 +1,298 @@
1
+ module Qo
2
+ module Matchers
3
+ # Matcher used to determine whether a value matches a certain set of
4
+ # conditions
5
+ #
6
+ # @author baweaver
7
+ # @since 1.0.0
8
+ #
9
+ class Matcher
10
+ # Creates a new matcher
11
+ #
12
+ # @param type [String]
13
+ # Type of the matcher: any, all, or none. Used to determine how a
14
+ # match is determined
15
+ #
16
+ # @param array_matchers = [] [Array[Any]]
17
+ # Conditions given as an array
18
+ #
19
+ # @param keyword_matchers = {} [Hash[Any, Any]]
20
+ # Conditions given as keywords
21
+ #
22
+ # @return [Qo::Matchers::Matcher]
23
+ def initialize(type, array_matchers = [], keyword_matchers = {})
24
+ @type = type
25
+ @array_matchers = array_matchers
26
+ @keyword_matchers = keyword_matchers
27
+ end
28
+
29
+ # Proc-ified version of `call`
30
+ #
31
+ # @return [Proc[Any] => Boolean]
32
+ def to_proc
33
+ -> target { self.call(target) }
34
+ end
35
+
36
+ # Calls the matcher on a given target value
37
+ #
38
+ # @param target [Any]
39
+ # Target to match against
40
+ #
41
+ # @return [Boolean]
42
+ # Whether or not the target matched
43
+ def call(target)
44
+ combined_check(array_call(target), keyword_call(target))
45
+ end
46
+
47
+ alias_method :===, :call
48
+ alias_method :[], :call
49
+ alias_method :match?, :call
50
+
51
+ # Used to match against a matcher made from Keyword Arguments (a Hash)
52
+ #
53
+ # @param matchers [Hash[Any, #===]]
54
+ # Any key mapping to any value that responds to `===`. Notedly more
55
+ # satisfying when `===` does something fun.
56
+ #
57
+ # @return [Boolean]
58
+ # Result of the match
59
+ private def keyword_call(target)
60
+ return true if @keyword_matchers == target
61
+
62
+ match_fn = target.is_a?(::Hash) ?
63
+ Proc.new { |match_key, matcher| match_hash_value?(target, match_key, matcher) } :
64
+ Proc.new { |match_key, matcher| match_object_value?(target, match_key, matcher) }
65
+
66
+ match_with(@keyword_matchers, &match_fn)
67
+ end
68
+
69
+ # Runs the matcher directly.
70
+ #
71
+ # If the target is an Array, it will be matched via index
72
+ #
73
+ # If the target is an Object, it will be matched via public send
74
+ #
75
+ # @param target [Any]
76
+ # Target to match against
77
+ #
78
+ # @return [Boolean]
79
+ # Result of the match
80
+ private def array_call(target)
81
+ return true if @array_matchers == target
82
+
83
+ if target.is_a?(::Array)
84
+ return false unless target.size == @array_matchers.size
85
+
86
+ match_with(@array_matchers.each_with_index) { |matcher, i|
87
+ match_value?(target[i], matcher)
88
+ }
89
+ else
90
+ match_with(@array_matchers) { |matcher|
91
+ match_value?(target, matcher)
92
+ }
93
+ end
94
+ end
95
+
96
+ # Wraps a case equality statement to make it a bit easier to read. The
97
+ # typical left bias of `===` can be confusing reading down a page, so
98
+ # more of a clarity thing than anything. Also makes for nicer stack traces.
99
+ #
100
+ # @param target [Any]
101
+ # Target to match against
102
+ #
103
+ # @param matcher [#===]
104
+ # Anything that responds to ===, preferably in a unique and entertaining way.
105
+ #
106
+ # @return [Boolean]
107
+ private def case_match?(target, matcher)
108
+ matcher === target
109
+ end
110
+
111
+ # Guarded version of `public_send` meant to stamp out more
112
+ # obscure errors when running against non-matching types.
113
+ #
114
+ # @param target [Any]
115
+ # Object to send to
116
+ #
117
+ # @param matcher [#to_sym]
118
+ # Anything that can be coerced into a method name
119
+ #
120
+ # @return [Any]
121
+ # Response of sending to the method, or false if failed
122
+ private def method_send(target, matcher)
123
+ matcher.respond_to?(:to_sym) &&
124
+ target.respond_to?(matcher.to_sym) &&
125
+ target.public_send(matcher)
126
+ end
127
+
128
+ # Predicate variant of `method_send` with the same guard concerns
129
+ #
130
+ # @param target [Any]
131
+ # Object to send to
132
+ #
133
+ # @param matcher [#to_sym]
134
+ # Anything that can be coerced into a method name
135
+ #
136
+ # @return [Boolean]
137
+ # Success status of predicate
138
+ private def method_matches?(target, matcher)
139
+ !!method_send(target, matcher)
140
+ end
141
+
142
+ # Defines what it means for a value to match a matcher
143
+ #
144
+ # @param target [Any]
145
+ # Target to match against
146
+ #
147
+ # @param matcher [Any]
148
+ # Any matcher to run against, most frequently responds to ===
149
+ #
150
+ # @return [Boolean]
151
+ # Match status
152
+ private def match_value?(target, matcher)
153
+ case_match?(target, matcher) ||
154
+ method_matches?(target, matcher)
155
+ end
156
+
157
+ # Checks if a hash value matches a given matcher
158
+ #
159
+ # @param target [Any]
160
+ # Target of the match
161
+ #
162
+ # @param match_key [Symbol]
163
+ # Key of the hash to reference
164
+ #
165
+ # @param matcher [#===]
166
+ # Any matcher responding to ===
167
+ #
168
+ # @return [Boolean]
169
+ # Match status
170
+ private def match_hash_value?(target, match_key, matcher)
171
+ return false unless target.key?(match_key)
172
+
173
+ return hash_recurse(target[match_key], matcher) if target.is_a?(Hash) && matcher.is_a?(Hash)
174
+
175
+ hash_case_match?(target, match_key, matcher) ||
176
+ hash_method_predicate_match?(target, match_key, matcher)
177
+ end
178
+
179
+ # Checks if an object property matches a given matcher
180
+ #
181
+ # @param target [Any]
182
+ # Target of the match
183
+ #
184
+ # @param match_property [Symbol]
185
+ # Property of the object to reference
186
+ #
187
+ # @param matcher [#===]
188
+ # Any matcher responding to ===
189
+ #
190
+ # @return [Boolean] Match status
191
+ private def match_object_value?(target, match_property, matcher)
192
+ return false unless target.respond_to?(match_property)
193
+
194
+ hash_method_case_match?(target, match_property, matcher)
195
+ end
196
+
197
+ # Double wraps case match in order to ensure that we try against both Symbol
198
+ # and String variants of the keys, as this is a very common mixup in Ruby.
199
+ #
200
+ # @param target [Hash]
201
+ # Target of the match
202
+ #
203
+ # @param match_key [Symbol]
204
+ # Key to match against
205
+ #
206
+ # @param matcher [#===]
207
+ # Matcher
208
+ #
209
+ # @return [Boolean]
210
+ private def hash_case_match?(target, match_key, matcher)
211
+ return true if case_match?(target[match_key], matcher)
212
+ return false unless target.keys.first.is_a?(String)
213
+
214
+ match_key.respond_to?(:to_s) &&
215
+ target.key?(match_key.to_s) &&
216
+ case_match?(target[match_key.to_s], matcher)
217
+ end
218
+
219
+ # Attempts to run a matcher as a predicate method against the target
220
+ #
221
+ # @param target [Hash]
222
+ # Target of the match
223
+ #
224
+ # @param match_key [Symbol]
225
+ # Method to call
226
+ #
227
+ # @param match_predicate [Symbol]
228
+ # Matcher
229
+ #
230
+ # @return [Boolean]
231
+ private def hash_method_predicate_match?(target, match_key, match_predicate)
232
+ method_matches?(target[match_key], match_predicate)
233
+ end
234
+
235
+ # Attempts to run a case match against a method call derived from a hash
236
+ # key, and checks the result.
237
+ #
238
+ # @param target [Hash]
239
+ # Target of the match
240
+ #
241
+ # @param match_property [Symbol]
242
+ # Method to call
243
+ #
244
+ # @param matcher [#===]
245
+ # Matcher
246
+ #
247
+ # @return [Boolean]
248
+ private def hash_method_case_match?(target, match_property, matcher)
249
+ case_match?(method_send(target, match_property), matcher)
250
+ end
251
+
252
+ # Recurses on nested hashes.
253
+ #
254
+ # @param target [Hash]
255
+ # Target to recurse into
256
+ #
257
+ # @param matcher [Hash]
258
+ # Matcher to use to recurse with
259
+ #
260
+ # @return [Boolean]
261
+ private def hash_recurse(target, matcher)
262
+ Qo::Matchers::Matcher.new(@type, [], matcher).call(target)
263
+ end
264
+
265
+ # Runs the relevant match method against the given collection with the
266
+ # given matcher function.
267
+ #
268
+ # @param collection [Enumerable] Any collection that can be enumerated over
269
+ # @param fn [Proc] Function to match with
270
+ #
271
+ # @return [Boolean] Result of the match
272
+ private def match_with(collection, &fn)
273
+ case @type
274
+ when 'and' then collection.all?(&fn)
275
+ when 'or' then collection.any?(&fn)
276
+ when 'not' then collection.none?(&fn)
277
+ else false
278
+ end
279
+ end
280
+
281
+ # When combining array and keyword type matchers, depending on how
282
+ # we're matching we may need to combine them slightly differently.
283
+ #
284
+ # @param *checks [Array]
285
+ # The checks we're combining
286
+ #
287
+ # @return [Boolean]
288
+ # Whether or not there's a match
289
+ private def combined_check(*checks)
290
+ case @type
291
+ when 'and', 'not' then checks.all?
292
+ when 'or' then checks.any?
293
+ else false
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end