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,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Errgonomic
4
+ module Result
5
+ # The base class for Result's Ok and Err class variants. We implement as
6
+ # much logic as possible here, and let Ok and Err handle their
7
+ # initialization and self identification.
8
+ class Any
9
+ attr_reader :value
10
+
11
+ def initialize(value)
12
+ @value = value
13
+ end
14
+
15
+ # Equality comparison for Result objects is based on value not reference.
16
+ #
17
+ # @param other [Object]
18
+ #
19
+ # @example
20
+ # Ok(1) == Ok(1) # => true
21
+ # Ok(1) == Err(1) # => false
22
+ # Ok(1).object_id == Ok(1).object_id # => false
23
+ # Ok(1) == 1 # => false
24
+ # Err() == nil # => false
25
+ def ==(other)
26
+ return false if self.class != other.class
27
+
28
+ value == other.value
29
+ end
30
+
31
+ # Indicate that this is some kind of result object. Contrast to
32
+ # Object#result? which is false for all other types.
33
+ #
34
+ # @example
35
+ # Ok("a").result? # => true
36
+ # Err("a").result? # => true
37
+ # "a".result? # => false
38
+ def result?
39
+ true
40
+ end
41
+
42
+ # Return true if the inner value is an Ok and the result of the block is
43
+ # truthy.
44
+ #
45
+ # @param [Proc] block The block to evaluate if the inner value is an Ok.
46
+ #
47
+ # @example
48
+ # Ok(1).ok_and?(&:odd?) # => true
49
+ # Ok(1).ok_and?(&:even?) # => false
50
+ # Err(:a).ok_and? { |_| true } # => false
51
+ # Err(:b).ok_and? { |_| false } # => false
52
+ def ok_and?(&block)
53
+ if ok?
54
+ !!block.call(value)
55
+ else
56
+ false
57
+ end
58
+ end
59
+
60
+ # Return true if the inner value is an Err and the result of the block is
61
+ # truthy.
62
+ #
63
+ # @example
64
+ # Ok(1).err_and?(&:odd?) # => false
65
+ # Ok(1).err_and?(&:even?) # => false
66
+ # Err(:a).err_and? { |_| true } # => true
67
+ # Err(:b).err_and? { |_| false } # => false
68
+ def err_and?(&block)
69
+ if err?
70
+ !!block.call(value)
71
+ else
72
+ false
73
+ end
74
+ end
75
+
76
+ # Return the inner value of an Ok, else raise an exception when Err.
77
+ #
78
+ # @example
79
+ # Ok(1).unwrap! # => 1
80
+ # Err(:c).unwrap! # => raise Errgonomic::UnwrapError, "value is an Err"
81
+ def unwrap!
82
+ raise Errgonomic::UnwrapError, 'value is an Err' unless ok?
83
+
84
+ @value
85
+ end
86
+
87
+ # Return the inner value of an Ok, else raise an exception with the given
88
+ # message when Err.
89
+ #
90
+ # @param msg [String]
91
+ #
92
+ # @example
93
+ # Ok(1).expect!("should have worked") # => 1
94
+ # Err(:d).expect!("should have worked") # => raise Errgonomic::ExpectError, "should have worked"
95
+ def expect!(msg)
96
+ raise Errgonomic::ExpectError, msg unless ok?
97
+
98
+ @value
99
+ end
100
+
101
+ # Return the inner value of an Err, else raise an exception when Ok.
102
+ #
103
+ # @example
104
+ # Ok(1).unwrap_err! # => raise Errgonomic::UnwrapError, 1
105
+ # Err(:e).unwrap_err! # => :e
106
+ def unwrap_err!
107
+ raise Errgonomic::UnwrapError, value unless err?
108
+
109
+ @value
110
+ end
111
+
112
+ # Given another result, return it if the inner result is Ok, else return
113
+ # the inner Err. Raise an exception if the other value is not a Result.
114
+ #
115
+ # @param other [Errgonomic::Result::Any]
116
+ #
117
+ # @example
118
+ # Ok(1).and(Ok(2)) # => Ok(2)
119
+ # Ok(1).and(Err(:f)) # => Err(:f)
120
+ # Err(:g).and(Ok(1)) # => Err(:g)
121
+ # Err(:h).and(Err(:i)) # => Err(:h)
122
+ # Ok(1).and(2) # => raise Errgonomic::ArgumentError, "other must be a Result"
123
+ def and(other)
124
+ raise Errgonomic::ArgumentError, 'other must be a Result' unless other.is_a?(Errgonomic::Result::Any)
125
+ return self if err?
126
+
127
+ other
128
+ end
129
+
130
+ # Given a block, evaluate it and return its result if the inner result is
131
+ # Ok, else return the inner Err. This is lazy evaluated, and we
132
+ # pedantically check the type of the block's return value at runtime. This
133
+ # is annoying, sorry, but better than an "undefined method" error.
134
+ # Hopefully it gives your test suite a chance to detect incorrect usage.
135
+ #
136
+ # @param block [Proc]
137
+ #
138
+ # @example
139
+ # Ok(1).and_then { |x| Ok(x + 1) } # => Ok(2)
140
+ # Ok(1).and_then { |_| Err(:error) } # => Err(:error)
141
+ # Err(:error).and_then { |x| Ok(x + 1) } # => Err(:error)
142
+ # Err(:error).and_then { |x| Err(:error2) } # => Err(:error)
143
+ def and_then(&block)
144
+ return self if err?
145
+
146
+ res = block.call(value)
147
+ if !res.is_a?(Errgonomic::Result::Any) && Errgonomic.give_me_ambiguous_downstream_errors?
148
+ raise Errgonomic::ArgumentError, 'and_then block must return a Result'
149
+ end
150
+
151
+ res
152
+ end
153
+
154
+ # Return other if self is Err, else return the original Option. Raises a
155
+ # pedantic runtime exception if other is not a Result.
156
+ #
157
+ # @param other [Errgonomic::Result::Any]
158
+ #
159
+ # @example
160
+ # Err(:j).or(Ok(1)) # => Ok(1)
161
+ # Err(:k).or(Err(:l)) # => Err(:l)
162
+ # Err(:m).or("oops") # => raise Errgonomic::ArgumentError, "other must be a Result; you might want unwrap_or"
163
+ def or(other)
164
+ unless other.is_a?(Errgonomic::Result::Any)
165
+ raise Errgonomic::ArgumentError,
166
+ 'other must be a Result; you might want unwrap_or'
167
+ end
168
+ return other if err?
169
+
170
+ self
171
+ end
172
+
173
+ # Return self if it is Ok, else lazy evaluate the block and return its
174
+ # result. Raises a pedantic runtime check that the block returns a Result.
175
+ # Sorry about that, hopefully it helps your tests. Better than ambiguous
176
+ # downstream "undefined method" errors, probably.
177
+ #
178
+ # @param block [Proc]
179
+ #
180
+ # @example
181
+ # Ok(1).or_else { Ok(2) } # => Ok(1)
182
+ # Err(:o).or_else { Ok(1) } # => Ok(1)
183
+ # Err(:q).or_else { Err(:r) } # => Err(:r)
184
+ # Err(:s).or_else { "oops" } # => raise Errgonomic::ArgumentError, "or_else block must return a Result"
185
+ def or_else(&block)
186
+ return self if ok?
187
+
188
+ res = block.call(self)
189
+ if !res.is_a?(Errgonomic::Result::Any) && Errgonomic.give_me_ambiguous_downstream_errors?
190
+ raise Errgonomic::ArgumentError, 'or_else block must return a Result'
191
+ end
192
+
193
+ res
194
+ end
195
+
196
+ # Return the inner value if self is Ok, else return the provided default.
197
+ #
198
+ # @param other [Object]
199
+ #
200
+ # @example
201
+ # Ok(1).unwrap_or(2) # => 1
202
+ # Err(:t).unwrap_or(:u) # => :u
203
+ def unwrap_or(other)
204
+ return value if ok?
205
+
206
+ other
207
+ end
208
+
209
+ # Return the inner value if self is Ok, else lazy evaluate the block and
210
+ # return its result.
211
+ #
212
+ # @param block [Proc]
213
+ #
214
+ # @example
215
+ # Ok(1).unwrap_or_else { 2 } # => 1
216
+ # Err(:v).unwrap_or_else { :w } # => :w
217
+ def unwrap_or_else(&block)
218
+ return value if ok?
219
+
220
+ block.call(self)
221
+ end
222
+ end
223
+
224
+ # The Ok variant.
225
+ class Ok < Any
226
+ attr_accessor :value
227
+
228
+ # Ok is always ok
229
+ #
230
+ # @example
231
+ # Ok(1).ok? # => true
232
+ def ok?
233
+ true
234
+ end
235
+
236
+ # Ok is never err
237
+ #
238
+ # @example
239
+ # Ok(1).err? # => false
240
+ def err?
241
+ false
242
+ end
243
+ end
244
+
245
+ class Err < Any
246
+ class Arbitrary; end
247
+
248
+ attr_accessor :value
249
+
250
+ # Err may be constructed without a value, if you want.
251
+ #
252
+ # @example
253
+ # Err(:y).value # => :y
254
+ # Err().value # => Arbitrary
255
+ def initialize(value = Arbitrary)
256
+ super(value)
257
+ end
258
+
259
+ # Err is always err
260
+ #
261
+ # @example
262
+ # Err(:z).err? # => true
263
+ def err?
264
+ true
265
+ end
266
+
267
+ # Err is never ok
268
+ #
269
+ # @example
270
+ # Err(:A).ok? # => false
271
+ def ok?
272
+ false
273
+ end
274
+ end
275
+ end
276
+ end
277
+
278
+ # Introduce certain helper methods into the Object class.
279
+ #
280
+ # @example
281
+ # "foo".result? # => false
282
+ # "foo".assert_result! # => raise Errgonomic::ResultRequiredError
283
+ class Object
284
+ # Convenience method to indicate whether we are working with a result.
285
+ # TBD whether we implement some stubs for the rest of the Result API; I want
286
+ # to think about how effectively these map to truthiness or presence.
287
+ #
288
+ # @example
289
+ # "foo".result? # => false
290
+ # Ok("foo").result? # => true
291
+ def result?
292
+ false
293
+ end
294
+
295
+ # Lacking static typing, we are going to want to make it easy to enforce at
296
+ # runtime that a given object is a Result.
297
+ #
298
+ # @example
299
+ # "foo".assert_result! # => raise Errgonomic::ResultRequiredError
300
+ # Ok("foo").assert_result! # => true
301
+ def assert_result!
302
+ return true if result?
303
+
304
+ raise Errgonomic::ResultRequiredError
305
+ end
306
+ end
307
+
308
+ # Global convenience method for constructing an Ok result.
309
+ def Ok(value)
310
+ Errgonomic::Result::Ok.new(value)
311
+ end
312
+
313
+ # Global convenience method for constructing an Err result.
314
+ def Err(value = Errgonomic::Result::Err::Arbitrary)
315
+ Errgonomic::Result::Err.new(value)
316
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Object
4
+ # Returns the receiver if it matches the expected type, otherwise raises a TypeMismatchError.
5
+ # This is useful for enforcing type expectations in method arguments.
6
+ #
7
+ # @param type [Class] The expected type or module the receiver should be.
8
+ # @param message [String] Optional error message to raise if type doesn't match.
9
+ # @return [Object] The receiver if it is of the expected type.
10
+ # @example
11
+ # 'hello'.type_or_raise!(String) #=> "hello"
12
+ # 123.type_or_raise!(String, "We need a string!") #=> raise Errgonomic::TypeMismatchError, "We need a string!"
13
+ # 123.type_or_raise!(String) #=> raise Errgonomic::TypeMismatchError, "Expected String but got Integer"
14
+ def type_or_raise!(type, message = nil)
15
+ message ||= "Expected #{type} but got #{self.class}"
16
+ raise Errgonomic::TypeMismatchError, message unless is_a?(type)
17
+
18
+ self
19
+ end
20
+
21
+ alias type_or_raise type_or_raise!
22
+
23
+ # Returns the receiver if it matches the expected type, otherwise returns the default value.
24
+ #
25
+ # @param type [Class] The expected type or module the receiver should be.
26
+ # @param default [Object] The value to return if type doesn't match.
27
+ # @return [Object] The receiver if it is of the expected type, otherwise the default value.
28
+ # @example
29
+ # 'hello'.type_or(String, 'default') # => "hello"
30
+ # 123.type_or(String, 'default') # => "default"
31
+ def type_or(type, default)
32
+ return self if is_a?(type)
33
+
34
+ default
35
+ end
36
+
37
+ # Returns the receiver if it matches the expected type, otherwise returns the result of the block.
38
+ # Useful when constructing the default value is expensive.
39
+ #
40
+ # @param type [Class] The expected type or module the receiver should be.
41
+ # @param block [Proc] The block to call if type doesn't match.
42
+ # @return [Object] The receiver if it is of the expected type, otherwise the block result.
43
+ # @example
44
+ # 'hello'.type_or_else(String) { 'default' } # => "hello"
45
+ # 123.type_or_else(String) { 'default' } # => "default"
46
+ def type_or_else(type, &block)
47
+ return self if is_a?(type)
48
+
49
+ block.call
50
+ end
51
+
52
+ # Returns the receiver if it does not match the expected type, otherwise raises a TypeMismatchError.
53
+ #
54
+ # @param type [Class] The type or module the receiver should not be.
55
+ # @param message [String] Optional error message to raise if type matches.
56
+ # @return [Object] The receiver if it is not of the specified type.
57
+ # @example
58
+ # 'hello'.not_type_or_raise!(Integer) #=> "hello"
59
+ # 123.not_type_or_raise!(Integer, "We dont want an integer!") #=> raise Errgonomic::TypeMismatchError, "We dont want an integer!"
60
+ # 123.not_type_or_raise!(Integer) #=> raise Errgonomic::TypeMismatchError, "Expected anything but Integer but got Integer"
61
+ def not_type_or_raise!(type, message = nil)
62
+ message ||= "Expected anything but #{type} but got #{self.class}"
63
+ raise Errgonomic::TypeMismatchError, message if is_a?(type)
64
+
65
+ self
66
+ end
67
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Errgonomic
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/errgonomic.rb CHANGED
@@ -1,59 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "errgonomic/version"
3
+ require_relative 'errgonomic/version' unless defined?(Errgonomic::VERSION)
4
4
 
5
- # The semantics here borrow heavily from ActiveSupport. Let's prefer that if
6
- # loaded, otherwise just copypasta the bits we like. Or convince me to make that
7
- # gem a dependency.
8
- if !Object.methods.include?(:blank?)
9
- require_relative "errgonomic/core_ext/blank"
10
- end
5
+ # A more opinionated blend with Rails presence.
6
+ require_relative 'errgonomic/presence'
7
+
8
+ # Bring in a subtle and manual type checker.
9
+ require_relative 'errgonomic/type'
10
+
11
+ # Bring in our Option and Result.
12
+ require_relative 'errgonomic/option'
13
+ require_relative 'errgonomic/result'
11
14
 
15
+ # Errgonomic adds opinionated abstractions to handle errors in a way that blends
16
+ # Rust and Ruby ergonomics. This library leans on Rails conventions for some
17
+ # presence-related methods; when in doubt, make those feel like Rails. It also
18
+ # has an implementation of Option and Result; when in doubt, make those feel
19
+ # more like Rust.
12
20
  module Errgonomic
13
21
  class Error < StandardError; end
14
22
 
15
23
  class NotPresentError < Error; end
16
24
 
17
25
  class TypeMismatchError < Error; end
18
- end
19
26
 
20
- class Object
21
- # Returns the receiver if it is present, otherwise raises a NotPresentError.
22
- # This method is useful to enforce strong expectations, where it is preferable
23
- # to fail early rather than risk causing an ambiguous error somewhere else.
24
- #
25
- # @param message [String] The error message to raise if the receiver is not present.
26
- # @return [Object] The receiver if it is present, otherwise raises a NotPresentError.
27
- def present_or_raise(message)
28
- raise Errgonomic::NotPresentError, message if blank?
29
- self
27
+ class UnwrapError < Error; end
28
+
29
+ class ExpectError < Error; end
30
+
31
+ class ArgumentError < Error; end
32
+
33
+ class ResultRequiredError < Error; end
34
+
35
+ class NotComparableError < StandardError; end
36
+
37
+ # A little bit of control over how pedantic we are in our runtime type checks.
38
+ def self.give_me_ambiguous_downstream_errors?
39
+ @give_me_ambiguous_downstream_errors || true
40
+ end
41
+
42
+ # You can opt out of the pedantic runtime checks for lazy block evaluation,
43
+ # but not quietly.
44
+ def self.with_ambiguous_downstream_errors
45
+ original_value = @give_me_ambiguous_downstream_errors
46
+ @give_me_ambiguous_downstream_errors = true
47
+ yield
48
+ ensure
49
+ @give_me_ambiguous_downstream_errors = original_value
30
50
  end
31
51
 
32
- # Returns the receiver if it is present, otherwise returns the given value. If
33
- # constructing the default value is expensive, consider using
34
- # +present_or_else+.
35
- #
36
- # @param value [Object] The value to return if the receiver is not present.
37
- # @return [Object] The receiver if it is present, otherwise the given value.
38
- def present_or(value)
39
- # TBD whether this is *too* strict
40
- if value.class != self.class && self.class != NilClass
41
- raise Errgonomic::TypeMismatchError, "Type mismatch: default value is a #{value.class} but original was a #{self.class}"
42
- end
43
-
44
- return self if present?
45
-
46
- value
52
+ # Lenient inner value comparison means the inner value of a Some or Ok can be
53
+ # compared to some other non-Result or non-Option value.
54
+ def self.lenient_inner_value_comparison?
55
+ @lenient_inner_value_comparison ||= true
47
56
  end
48
57
 
49
- # Returns the receiver if it is present, otherwise returns the result of the
50
- # block. Invoking a block may be preferable to returning a default value with
51
- # +present_or+, if constructing the default value is expensive.
52
- #
53
- # @param block [Proc] The block to call if the receiver is not present.
54
- # @return [Object] The receiver if it is present, otherwise the result of the block.
55
- def present_or_else(&block)
56
- return block.call if blank?
57
- self
58
+ def self.give_me_lenient_inner_value_comparison=(value)
59
+ @lenient_inner_value_comparison = value
58
60
  end
59
61
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: errgonomic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Zadrozny
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: yard
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.9'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: yard-doctest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.1'
27
55
  description: Let's blend the Rails 'present' and 'blank' conventions with a few patterns
28
56
  from Rust Option types.
29
57
  email:
@@ -34,17 +62,24 @@ extra_rdoc_files: []
34
62
  files:
35
63
  - ".envrc"
36
64
  - ".rspec"
65
+ - ".rubocop.yml"
37
66
  - ".standard.yml"
67
+ - ".yardopts"
38
68
  - CHANGELOG.md
39
69
  - CODE_OF_CONDUCT.md
40
70
  - LICENSE.txt
41
71
  - README.md
42
72
  - Rakefile
73
+ - doctest_helper.rb
43
74
  - flake.lock
44
75
  - flake.nix
45
76
  - gemset.nix
46
77
  - lib/errgonomic.rb
47
78
  - lib/errgonomic/core_ext/blank.rb
79
+ - lib/errgonomic/option.rb
80
+ - lib/errgonomic/presence.rb
81
+ - lib/errgonomic/result.rb
82
+ - lib/errgonomic/type.rb
48
83
  - lib/errgonomic/version.rb
49
84
  - sig/errgonomic.rbs
50
85
  homepage: https://omc.io/