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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +11 -1
- data/Rakefile +8 -4
- data/doctest_helper.rb +3 -0
- data/flake.nix +16 -10
- data/gemset.nix +186 -21
- data/lib/errgonomic/core_ext/blank.rb +4 -4
- data/lib/errgonomic/option.rb +358 -0
- data/lib/errgonomic/presence.rb +92 -0
- data/lib/errgonomic/result.rb +316 -0
- data/lib/errgonomic/type.rb +67 -0
- data/lib/errgonomic/version.rb +1 -1
- data/lib/errgonomic.rb +44 -42
- metadata +36 -1
@@ -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
|