errgonomic 0.1.0 → 0.3.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,358 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Errgonomic
4
+ module Option
5
+ # The base class for all options. Some and None are subclasses.
6
+ #
7
+ class Any
8
+ # An option of the same type with an equal inner value is equal.
9
+ #
10
+ # Because we're going to monkey patch this into other libraries Rails, we
11
+ # allow some "pass through" functionality into the inner value of a Some,
12
+ # such as comparability here.
13
+ #
14
+ # TODO: does None == null?
15
+ #
16
+ # strict:
17
+ # Some(1) == 1 # => raise Errgonomic::NotComparableError, "Cannot compare Errgonomic::Option::Some with Integer"
18
+ #
19
+ # @example
20
+ # Some(1) == Some(1) # => true
21
+ # Some(1) == Some(2) # => false
22
+ # Some(1) == None() # => false
23
+ # None() == None() # => true
24
+ # Some(1) == 1 # => false
25
+ # None() == nil # => false
26
+ def ==(other)
27
+ return false if self.class != other.class
28
+ return true if none?
29
+
30
+ value == other.value
31
+ end
32
+
33
+ # @example
34
+ # measurement = Errgonomic::Option::Some.new(1)
35
+ # case measurement
36
+ # in Errgonomic::Option::Some, value
37
+ # "Measurement is #{measurement.value}"
38
+ # in Errgonomic::Option::None
39
+ # "Measurement is not available"
40
+ # else
41
+ # "not matched"
42
+ # end # => "Measurement is 1"
43
+ def deconstruct
44
+ return [self, value] if some?
45
+
46
+ [Errgonomic::Option::None]
47
+ end
48
+
49
+ # return true if the contained value is Some and the block returns truthy
50
+ #
51
+ # @example
52
+ # Some(1).some_and { |x| x > 0 } # => true
53
+ # Some(0).some_and { |x| x > 0 } # => false
54
+ # None().some_and { |x| x > 0 } # => false
55
+ def some_and(&block)
56
+ return false if none?
57
+
58
+ !!block.call(value)
59
+ end
60
+
61
+ alias some_and? some_and
62
+
63
+ # return true if the contained value is None or the block returns truthy
64
+ #
65
+ # @example
66
+ # None().none_or { false } # => true
67
+ # Some(1).none_or { |x| x > 0 } # => true
68
+ # Some(1).none_or { |x| x < 0 } # => false
69
+ def none_or(&block)
70
+ return true if none?
71
+
72
+ !!block.call(value)
73
+ end
74
+
75
+ alias none_or? none_or
76
+
77
+ # return an Array with the contained value, if any
78
+ # @example
79
+ # Some(1).to_a # => [1]
80
+ # None().to_a # => []
81
+ def to_a
82
+ return [] if none?
83
+
84
+ [value]
85
+ end
86
+
87
+ # returns the inner value if present, else raises an error
88
+ # @example
89
+ # Some(1).unwrap! # => 1
90
+ # None().unwrap! # => raise Errgonomic::UnwrapError, "cannot unwrap None"
91
+ def unwrap!
92
+ raise Errgonomic::UnwrapError, 'cannot unwrap None' if none?
93
+
94
+ value
95
+ end
96
+
97
+ # returns the inner value if pressent, else raises an error with the given
98
+ # message
99
+ # @example
100
+ # Some(1).expect!("msg") # => 1
101
+ # None().expect!("msg") # => raise Errgonomic::ExpectError, "msg"
102
+ def expect!(msg)
103
+ raise Errgonomic::ExpectError, msg if none?
104
+
105
+ value
106
+ end
107
+
108
+ # returns the inner value if present, else returns the default value
109
+ # @example
110
+ # Some(1).unwrap_or(2) # => 1
111
+ # None().unwrap_or(2) # => 2
112
+ def unwrap_or(default)
113
+ return default if none?
114
+
115
+ value
116
+ end
117
+
118
+ # returns the inner value if present, else returns the result of the
119
+ # provided block
120
+ # @example
121
+ # Some(1).unwrap_or_else { 2 } # => 1
122
+ # None().unwrap_or_else { 2 } # => 2
123
+ def unwrap_or_else(&block)
124
+ return block.call if none?
125
+
126
+ value
127
+ end
128
+
129
+ # Calls a function with the inner value, if Some, but returns the original
130
+ # option. In Rust, this is "inspect" but that clashes with Ruby
131
+ # conventions. We call this "tap_some" to avoid further clashing with
132
+ # "tap."
133
+ #
134
+ # @example
135
+ # tapped = false
136
+ # Some(1).tap_some { |x| tapped = x } # => Some(1)
137
+ # tapped # => 1
138
+ # tapped = false
139
+ # None().tap_some { tapped = true } # => None()
140
+ # tapped # => false
141
+ def tap_some(&block)
142
+ block.call(value) if some?
143
+ self
144
+ end
145
+
146
+ # Maps the Option to another Option by applying a function to the
147
+ # contained value (if Some) or returns None. Raises a pedantic exception
148
+ # if the return value of the block is not an Option.
149
+ #
150
+ # @example
151
+ # Some(1).map { |x| x + 1 } # => Some(2)
152
+ # None().map { |x| x + 1 } # => None()
153
+ def map(&block)
154
+ return self if none?
155
+
156
+ Some(block.call(value))
157
+ end
158
+
159
+ # Returns the provided default (if none), or applies a function to the
160
+ # contained value (if some). If you want lazy evaluation for the provided
161
+ # value, use +map_or_else+.
162
+ #
163
+ # @example
164
+ # None().map_or(1) { 100 } # => Some(1)
165
+ # Some(1).map_or(100) { |x| x + 1 } # => Some(2)
166
+ # Some("foo").map_or(0) { |str| str.length } # => Some(3)
167
+ def map_or(default, &block)
168
+ return Some(default) if none?
169
+
170
+ Some(block.call(value))
171
+ end
172
+
173
+ # Computes a default from the given Proc if None, or applies the block to
174
+ # the contained value (if Some).
175
+ #
176
+ # @example
177
+ # None().map_or_else(-> { :foo }) { :bar } # => Some(:foo)
178
+ # Some("str").map_or_else(-> { 100 }) { |str| str.length } # => Some(3)
179
+ # None().map_or_else( -> { nil }) { |str| str.length } # => None()
180
+ def map_or_else(proc, &block)
181
+ if none?
182
+ val = proc.call
183
+ return val ? Some(val) : None()
184
+ end
185
+
186
+ Some(block.call(value))
187
+ end
188
+
189
+ # convert the option into a result where Some is Ok and None is Err
190
+ # @example
191
+ # None().ok # => Err()
192
+ # Some(1).ok # => Ok(1)
193
+ def ok
194
+ return Errgonomic::Result::Ok.new(value) if some?
195
+
196
+ Errgonomic::Result::Err.new
197
+ end
198
+
199
+ # Transforms the option into a result, mapping Some(v) to Ok(v) and None to Err(err)
200
+ #
201
+ # @example
202
+ # None().ok_or("wow") # => Err("wow")
203
+ # Some(1).ok_or("such err") # => Ok(1)
204
+ def ok_or(err)
205
+ return Errgonomic::Result::Ok.new(value) if some?
206
+
207
+ Errgonomic::Result::Err.new(err)
208
+ end
209
+
210
+ # Transforms the option into a result, mapping Some(v) to Ok(v) and None to Err(err).
211
+ # TODO: block or proc?
212
+ #
213
+ # @example
214
+ # None().ok_or_else { "wow" } # => Err("wow")
215
+ # Some("foo").ok_or_else { "such err" } # => Ok("foo")
216
+ def ok_or_else(&block)
217
+ return Errgonomic::Result::Ok.new(value) if some?
218
+
219
+ Errgonomic::Result::Err.new(block.call)
220
+ end
221
+
222
+ # Returns the option if it contains a value, otherwise returns the provided Option. Returns an Option.
223
+ #
224
+ # @example
225
+ # None().or(Some(1)) # => Some(1)
226
+ # Some(2).or(Some(3)) # => Some(2)
227
+ # None().or(2) # => raise Errgonomic::ArgumentError.new, "other must be an Option, was Integer"
228
+ def or(other)
229
+ raise ArgumentError, "other must be an Option, was #{other.class.name}" unless other.is_a?(Any)
230
+
231
+ return self if some?
232
+
233
+ other
234
+ end
235
+
236
+ # Returns the option if it contains a value, otherwise calls the block and returns the result. Returns an Option.
237
+ #
238
+ # @example
239
+ # None().or_else { Some(1) } # => Some(1)
240
+ # Some(2).or_else { Some(3) } # => Some(2)
241
+ # None().or_else { 2 } # => raise Errgonomic::ArgumentError.new, "block must return an Option, was Integer"
242
+ def or_else(&block)
243
+ return self if some?
244
+
245
+ val = block.call
246
+ if !val.is_a?(Errgonomic::Option::Any) && Errgonomic.give_me_ambiguous_downstream_errors?
247
+ raise Errgonomic::ArgumentError.new, "block must return an Option, was #{val.class.name}"
248
+ end
249
+
250
+ val
251
+ end
252
+
253
+ # If self is Some, return the provided other Option.
254
+ #
255
+ # @example
256
+ # None().and(Some(1)) # => None()
257
+ # Some(2).and(Some(3)) # => Some(3)
258
+ def and(other)
259
+ return self if none?
260
+
261
+ other
262
+ end
263
+
264
+ # If self is Some, call the given block and return its value. Block most return an Option.
265
+ #
266
+ # @example
267
+ # None().and_then { Some(1) } # => None()
268
+ # Some(2).and_then { Some(3) } # => Some(3)
269
+ def and_then(&block)
270
+ return self if none?
271
+
272
+ val = block.call
273
+ if Errgonomic.give_me_ambiguous_downstream_errors? && !val.is_a?(Errgonomic::Option::Any)
274
+ raise Errgonomic::ArgumentError.new, "block must return an Option, was #{val.class.name}"
275
+ end
276
+
277
+ val
278
+ end
279
+
280
+ # Zips self with another Option.
281
+ #
282
+ # If self is Some(s) and other is Some(o), this method returns
283
+ # Some([s, o]). Otherwise, None is returned.
284
+ #
285
+ # @example
286
+ # None().zip(Some(1)) # => None()
287
+ # Some(1).zip(None()) # => None()
288
+ # Some(2).zip(Some(3)) # => Some([2, 3])
289
+ def zip(other)
290
+ return None() unless some? && other.some?
291
+
292
+ Some([value, other.value])
293
+ end
294
+
295
+ # Zip two options using the block passed. If self is Some and Other is
296
+ # some, yield both of their values to the block and return its value as
297
+ # Some. Else return None.
298
+ #
299
+ # @example
300
+ # None().zip_with(Some(1)) { |a, b| a + b } # => None()
301
+ # Some(1).zip_with(None()) { |a, b| a + b } # => None()
302
+ # Some(2).zip_with(Some(3)) { |a, b| a + b } # => Some(5)
303
+ def zip_with(other, &block)
304
+ return None() unless some? && other.some?
305
+ other = block.call(value, other.value)
306
+ Some(other)
307
+ end
308
+
309
+ # filter
310
+ # xor
311
+ # insert
312
+ # get_or_insert
313
+ # get_or_insert_with
314
+ # take
315
+ # take_if
316
+ # replace
317
+ # zip
318
+ # zip_with
319
+ end
320
+
321
+ # Represent a value
322
+ class Some < Any
323
+ attr_accessor :value
324
+
325
+ def initialize(value)
326
+ @value = value
327
+ end
328
+
329
+ def some?
330
+ true
331
+ end
332
+
333
+ def none?
334
+ false
335
+ end
336
+ end
337
+
338
+ class None < Any
339
+ def some?
340
+ false
341
+ end
342
+
343
+ def none?
344
+ true
345
+ end
346
+ end
347
+ end
348
+ end
349
+
350
+ # Global convenience for constructing a Some value.
351
+ def Some(value)
352
+ Errgonomic::Option::Some.new(value)
353
+ end
354
+
355
+ # Global convenience for constructing a None value.
356
+ def None
357
+ Errgonomic::Option::None.new
358
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The semantics here borrow heavily from ActiveSupport. Let's prefer that if
4
+ # loaded, otherwise just copypasta the bits we like. Or convince me to make that
5
+ # gem a dependency.
6
+ require_relative './core_ext/blank' unless Object.methods.include?(:blank?)
7
+
8
+ class Object
9
+ # Returns the receiver if it is present, otherwise raises a NotPresentError.
10
+ # This method is useful to enforce strong expectations, where it is preferable
11
+ # to fail early rather than risk causing an ambiguous error somewhere else.
12
+ #
13
+ # @param message [String] The error message to raise if the receiver is not present.
14
+ # @return [Object] The receiver if it is present, otherwise raises a NotPresentError.
15
+ def present_or_raise!(message)
16
+ raise Errgonomic::NotPresentError, message if blank?
17
+
18
+ self
19
+ end
20
+
21
+ alias_method :present_or_raise, :present_or_raise!
22
+
23
+ # Returns the receiver if it is present, otherwise returns the given value. If
24
+ # constructing the default value is expensive, consider using
25
+ # +present_or_else+.
26
+ #
27
+ # @param value [Object] The value to return if the receiver is not present.
28
+ # @return [Object] The receiver if it is present, otherwise the given value.
29
+ def present_or(value)
30
+ # TBD whether this is *too* strict
31
+ if value.class != self.class && self.class != NilClass
32
+ raise Errgonomic::TypeMismatchError,
33
+ "Type mismatch: default value is a #{value.class} but original was a #{self.class}"
34
+ end
35
+
36
+ return self if present?
37
+
38
+ value
39
+ end
40
+
41
+ # Returns the receiver if it is present, otherwise returns the result of the
42
+ # block. Invoking a block may be preferable to returning a default value with
43
+ # +present_or+, if constructing the default value is expensive.
44
+ #
45
+ # @param block [Proc] The block to call if the receiver is not present.
46
+ # @return [Object] The receiver if it is present, otherwise the result of the block.
47
+ def present_or_else(&block)
48
+ return block.call if blank?
49
+
50
+ self
51
+ end
52
+
53
+ # Returns the receiver if it is blank, otherwise raises a NotPresentError.
54
+ # This method is helpful to enforce expectations where blank objects are required.
55
+ #
56
+ # @param message [String] The error message to raise if the receiver is not blank.
57
+ # @return [Object] The receiver if it is blank, otherwise raises a NotPresentError.
58
+ def blank_or_raise!(message)
59
+ raise Errgonomic::NotPresentError, message unless blank?
60
+
61
+ self
62
+ end
63
+
64
+ alias_method :blank_or_raise, :blank_or_raise!
65
+
66
+ # Returns the receiver if it is blank, otherwise returns the given value.
67
+ #
68
+ # @param value [Object] The value to return if the receiver is not blank.
69
+ # @return [Object] The receiver if it is blank, otherwise the given value.
70
+ def blank_or(value)
71
+ # TBD whether this is *too* strict
72
+ if value.class != self.class && self.class != NilClass
73
+ raise Errgonomic::TypeMismatchError,
74
+ "Type mismatch: default value is a #{value.class} but original was a #{self.class}"
75
+ end
76
+
77
+ return self if blank?
78
+
79
+ value
80
+ end
81
+
82
+ # Returns the receiver if it is blank, otherwise returns the result of the
83
+ # block.
84
+ #
85
+ # @param block [Proc] The block to call if the receiver is not blank.
86
+ # @return [Object] The receiver if it is blank, otherwise the result of the block.
87
+ def blank_or_else(&block)
88
+ return block.call unless blank?
89
+
90
+ self
91
+ end
92
+ end