qo 0.5.0 → 0.99.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,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