resonad 1.0.2 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 25157450da225c6390b1913ecee5c69b13025b32
4
- data.tar.gz: aa7cfe69d2b80c222da33ad2b6fab421f8bc79a0
2
+ SHA256:
3
+ metadata.gz: ccc1511640a1e8dd3c741c16aa80e689c93e159bb47cfedb09072c0de78d9f95
4
+ data.tar.gz: 8f460dba7216f3da4b03767760a432f7f92d08f166b54fda91e6a076a451db59
5
5
  SHA512:
6
- metadata.gz: b2fe2862418ef2527b10cd97015807115d0a19168332ad3d9793b84f1591c5695f84d3331d8163d0265110023977d093d105c7495aeffed18a2cd0ebf881f3a3
7
- data.tar.gz: 1e8da46e15b5e841c46bea2a3a06590a74c85ea69493d65bc9f5b3178d9e0707a7fcaf97d7daab89a365c036979dbfcc5e0be45b188c1085410511f0478b7fd2
6
+ metadata.gz: 188e6c3752ca5b3f5c0e8c1a631a2378f33d239b448163e4425b486906ad1ddb36b5beb4efee703994909fe6f41b7107c2a9a3bc20e87aa1ca2a6c84302b8f63
7
+ data.tar.gz: bd9ee0e6297db8633ca0826821c1a5a357a53b075625b62b7da822076ae901916b994709e2c784b6e281f919dabd7b642bb9ff2a632ab1f8e05db45e6c0ee88b
data/.rspec CHANGED
@@ -1,2 +1,6 @@
1
+ --require spec_helper
2
+ <% if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7') %>
3
+ --exclude-pattern spec/pattern_matching_spec.rb
4
+ <% end %>
1
5
  --format documentation
2
6
  --color
@@ -0,0 +1 @@
1
+ 2.7
@@ -1,13 +1,25 @@
1
1
  language: ruby
2
+ script: bundle exec rspec
3
+
4
+ # test old rubies
2
5
  rvm:
3
- - 2.2.7
4
- - 2.3.4
5
- - 2.4.1
6
- cache: bundler
6
+ - 2.2.10
7
+ - 2.3.8
8
+ - 2.4.10
9
+ - 2.5.8
10
+ - 2.6.6
11
+
12
+ # test on latest ruby
13
+ matrix:
14
+ include:
15
+ - rvm: 2.7.1
16
+ env: LATEST_RUBY=true
17
+
18
+ # Rubygems deployment
7
19
  deploy:
8
20
  provider: rubygems
9
21
  on:
10
22
  tags: true
11
- rvm: 2.4.1
23
+ condition: $LATEST_RUBY = true
12
24
  api_key:
13
25
  secure: 0+R2SaUaKOZE0U4FwHiC/0DLepizJ1anjfvFEWk5pYVkaZl6HuaSYq50g8ff4VqXBH955Y5W7tFI8gfg4ZY9jrivHyEZT9Yoz3PFFPxcqSdVbDsV12RQt7ZjzW0HbvUhFu9nlrVACfFsLt4WFG61pyhXvewghp1p4JBp2UxeTk1kyL7U+wfWsp1RpcKiHkaLBeWJ0N87j4QkhTfcWlModLqN/19ATigvfyDjrwerkTescVmQi4TzfAiwkeAiDgUB32FkwJjbX9RfIko33CsuctuDRU/HT+RWiXcGAxdHZx4dtQpP9pAiNTVxXdIq+QAvitekIah70pdCTNKrOh3tDj3kglkUK23MqU6kdeXjmUn9r4SuzHCX0sVf16UpfnwuryDDPlG1ISY+mf7BARr/wNQWuAD4MA4kWiPGMqT/0uoysbY7dt44lkO9NV7HBbqs2shqqMtmgPgDoJ+SGTXo8LvrjuL0jHB2/MoHziiqWDKLQ9PS2Fcqp/06d5u8GcSRt7dcgo2rvwnMwRdUW6iCV0zQgzrNccWn/SsJROtgzEIkuLJYO0GIOCgNBJ0euorWqQBEEPniltwh5StSgH2bL5khdPxiI+LhsL7UDhWGiNFl5tlQp8Ob98WmmQ39ZyVAGKlJoe2rdE/IaDVfysVvQV5XzYUVRn4ZKftwubo5Zvw=
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [1.3.0] - 2020-07-12
8
+ ### Added
9
+ - more documentation
10
+ - support for Ruby 2.7 pattern matching
11
+ - `#otherwise` alias for `#or_else`
12
+ - `#map_value` alias for `#map`
13
+ - alternate constructor methods
14
+ - aliases for `#on_success` and `#on_failure`
15
+ - `Success` and `Failure` class constants to `Resonad::Mixin`
16
+ - `Resonad::PublicMixin` for people who want the previous behaviour of
17
+ `Resonad::Mixin`
18
+ ### Changed
19
+ - The methods provided by `Resonad::Mixin` are now private. Replace with
20
+ `Resonad::PublicMixin` to revert them back to being public.
21
+
22
+ ## [1.2.0] - 2017-05-22
23
+ ### Added
24
+ - `Resonad.Success()` and `Resonad.Failure()` arguments are optional, and
25
+ default to nil.
26
+ - `#successful?` and `#ok?` as new aliases for `#success?`
27
+ - `#failed?` and `#bad?` as new aliases for `#failure?`
28
+
29
+ ## [1.1.1] - 2017-05-09
30
+ ### Fixed
31
+ - Aliased methods `#and_then` and `#or_else` were not aliased properly.
32
+
33
+ ## [1.1.0] - 2017-05-01
34
+ ### Added
35
+ - `Resonad::Mixin`
36
+ ### Changed
37
+ - `Resonad` is now a class. `Resonad::Success` and `Resonad::Failure` now
38
+ inherit from `Resonad`.
39
+
40
+ ## [1.0.2] - 2017-04-22
41
+ ### Fixed
42
+ - Typo in class names
43
+
44
+ ## [1.0.1] - 2017-04-22
45
+ ### Changed
46
+ - Nothing (bumped to force a deploy)
47
+
48
+ ## [1.0.0] - 2017-04-22
49
+ ### Added
50
+ - Everything (initial release)
data/README.md CHANGED
@@ -1,15 +1,26 @@
1
+ [![Build Status](https://travis-ci.org/tomdalling/resonad.svg?branch=master)](https://travis-ci.org/tomdalling/resonad)
2
+
1
3
  # Resonad
2
4
 
3
- Usage example (assume each method returns a `Resonad`):
5
+ Lightweight, functional "result" objects that can be used instead of exceptions.
6
+
7
+ Read: [Result Objects - Errors Without Exceptions](https://www.rubypigeon.com/posts/result-objects-errors-without-exceptions/)
8
+
9
+ ## Typical Usage Example
10
+
11
+ Assuming each method returns a `Resonad` (Ruby 2.7 syntax):
4
12
 
5
13
  ```ruby
6
14
  find_widget(widget_id)
7
- .and_then { |widget| update_widget(widget) }
8
- .on_success { |widget| logger.info("Updated #{widget}" }
9
- .on_failure { |error| logger.warn("Widget update failed because #{error}") }
15
+ .and_then { update_widget(_1) }
16
+ .on_success { logger.info("Updated #{_1}" }
17
+ .on_failure { logger.warn("Widget update failed because #{_1}") }
10
18
  ```
11
19
 
12
- Success type:
20
+ ## Success Type
21
+
22
+ A value that represents success. Wraps a `value` that can be any arbitrary
23
+ object.
13
24
 
14
25
  ```ruby
15
26
  result = Resonad.Success(5)
@@ -19,7 +30,10 @@ result.value #=> 5
19
30
  result.error #=> raises an exception
20
31
  ```
21
32
 
22
- Failure type:
33
+ ## Failure Type
34
+
35
+ A value that represents a failure. Wraps an `error` that can be any arbitrary
36
+ object.
23
37
 
24
38
  ```ruby
25
39
  result = Resonad.Failure(:buzz)
@@ -29,39 +43,223 @@ result.value #=> raises an exception
29
43
  result.error #=> :buzz
30
44
  ```
31
45
 
32
- Mapping monads:
46
+ ## Mapping
47
+
48
+ Non-destructive update for the `value` of a `Success` object. Does nothing to
49
+ `Failure` objects.
50
+
51
+ The block takes the `value` as an argument, and returns the new `value`.
33
52
 
34
53
  ```ruby
35
54
  result = Resonad.Success(5)
36
- .map { |i| i + 1 }
37
- .map { |i| i + 1 }
38
- .map { |i| i + 1 }
55
+ .map { _1 + 1 } # 5 + 1 -> 6
56
+ .map { _1 + 1 } # 6 + 1 -> 7
57
+ .map { _1 + 1 } # 7 + 1 -> 8
39
58
  result.success? #=> true
40
59
  result.value #=> 8
41
60
 
42
61
  result = Resonad.Failure(:buzz)
43
- .map { |i| i + 1 }
44
- .map { |i| i + 1 }
45
- .map { |i| i + 1 }
62
+ .map { _1 + 1 } # not run
63
+ .map { _1 + 1 } # not run
64
+ .map { _1 + 1 } # not run
46
65
  result.success? #=> false
47
66
  result.error #=> :buzz
48
67
  ```
49
68
 
50
- Flat mapping monads (a.k.a. `and_then`):
69
+
70
+ ## Aliases
71
+
72
+ Lots of the Resonad methods have aliases.
73
+
74
+ Personally, I can never remember if it's `success?` or `successful?` or `ok?`,
75
+ so let's just do it the Ruby way and allow all of them.
51
76
 
52
77
  ```ruby
78
+ # >>> object creation aliases (same for failure) <<<
53
79
  result = Resonad.Success(5)
54
- .and_then { |i| Resonad.Success(i + 1) }
55
- .and_then { |i| Resonad.Failure("buzz #{i}") }
56
- .and_then { |i| Resonad.Success(i + 1) }
80
+ result = Resonad.success(5) # lowercase, for those offended by capital letters
81
+ result = Resonad::Success[5] # class constructor method
82
+
83
+ # >>> success aliases <<<
84
+ result.success? #=> true
85
+ result.successful? #=> true
86
+ result.ok? #=> true
87
+
88
+ # >>> failure aliases <<<
89
+ result.failure? #=> false
90
+ result.failed? #=> false
91
+ result.bad? #=> false
92
+
93
+ # >>> mapping aliases <<<
94
+ result.map { _1 + 1 } #=> Success(6)
95
+ result.map_value { _1 + 1 } #=> Success(6)
96
+
97
+ # >>> flat mapping aliases <<<
98
+ result.and_then { Resonad.Success(_1 + 1) } #=> Success(6)
99
+ result.flat_map { Resonad.Success(_1 + 1) } #=> Success(6)
100
+
101
+ # >>> error flat mapping aliases <<<
102
+ result.or_else { Resonad.Failure(_1 + 1) } # not run
103
+ result.otherwise { Resonad.Failure(_1 + 1) } # not run
104
+ result.flat_map_error { Resonad.Success(_1 + 1) } # not run
105
+
106
+ # >>> conditional tap aliases <<<
107
+ # pattern: (on_|if_|when_)(success_alias|failure_alias)
108
+ result.on_success { puts "hi" } # outputs "hi"
109
+ result.if_success { puts "hi" } # outputs "hi"
110
+ result.when_success { puts "hi" } # outputs "hi"
111
+ result.on_ok { puts "hi" } # outputs "hi"
112
+ result.if_ok { puts "hi" } # outputs "hi"
113
+ result.when_ok { puts "hi" } # outputs "hi"
114
+ result.on_successful { puts "hi" } # outputs "hi"
115
+ result.if_successful { puts "hi" } # outputs "hi"
116
+ result.when_successful { puts "hi" } # outputs "hi"
117
+ result.on_failure { puts "hi" } # not run
118
+ result.if_failure { puts "hi" } # not run
119
+ result.when_failure { puts "hi" } # not run
120
+ result.on_bad { puts "hi" } # not run
121
+ result.if_bad { puts "hi" } # not run
122
+ result.when_bad { puts "hi" } # not run
123
+ result.on_failed { puts "hi" } # not run
124
+ result.if_failed { puts "hi" } # not run
125
+ result.when_failed { puts "hi" } # not run
126
+ ```
127
+
128
+
129
+ ## Flat Mapping (a.k.a. `and_then`)
130
+
131
+ Non-destructive update for a `Success` object. Either turns it into another
132
+ `Success` (can have a different `value`), or turns it into a `Failure`. Does
133
+ nothing to `Failure` objects.
134
+
135
+ The block takes the `value` as an argument, and returns a `Resonad` (either
136
+ `Success` or `Failure`).
137
+
138
+ ```ruby
139
+ result = Resonad.Success(5)
140
+ .and_then { Resonad.Success(_1 + 1) } # updates to Success(6)
141
+ .and_then { Resonad.Failure("buzz #{_1}") } # updates to Failure("buzz 6")
142
+ .and_then { Resonad.Success(_1 + 1) } # not run (because it's a failure)
57
143
  .error #=> "buzz 6"
58
144
 
59
- # can also use the less-nice `flat_map` method
60
- result
61
- .flat_map { |i| Resonad.Success(i + 1) }
145
+ # also has a less-friendly but more-technically-descriptive alias: `flat_map`
146
+ result.flat_map { Resonad.Success(_1 + 1) }
147
+ ```
148
+
149
+ This is different to Ruby's `#then` method added in 2.6. The block for `#then`
150
+ would take a Resonad argument, regardless of whether it's `Success` or
151
+ `Failure`. The block for `#and_then` takes a _`Success` object's value_, and
152
+ only runs on `Success` objects, not `Failure` objects.
153
+
154
+
155
+ ## Error Mapping
156
+
157
+ Just as `Success` objects can be chained with `#map` and `#and_then`, so can
158
+ `Failure` objects with `#map_error` and `#or_else`. This isn't used as often,
159
+ but has a few use cases such as:
160
+
161
+ ```ruby
162
+ # Use Case: convert an error value into another error value
163
+ make_http_request #=> Failure(404)
164
+ .map_error { |status_code| "HTTP #{status_code} Error" }
165
+ .error #=> "HTTP 404 Error"
166
+
167
+ # Use Case: recover from error, turning into Success
168
+ load_config_file #=> Failure(:config_file_missing)
169
+ .or_else { try_recover_from(_1) }
170
+ .value #=> { :setting => 'default' }
171
+
172
+ def try_recover_from(error)
173
+ if error == :config_file_missing
174
+ Resonad.Success({ setting: 'default' })
175
+ else
176
+ Resonad.Failure(error)
177
+ end
178
+ end
179
+ ```
180
+
181
+
182
+ ## Conditional Tap
183
+
184
+ If you're in the middle of a long chain of methods, and you don't want to break
185
+ the chain to run some kind of side effect, you can use the `#on_success` and
186
+ `#on_failure` methods. These run an arbitrary block code, but do not affect the
187
+ result object in any way. They work like Ruby's `#tap` method, but `Failure`
188
+ objects will not run `on_success` blocks, and `Success` objects will not run
189
+ `on_failure` blocks.
190
+
191
+ ```ruby
192
+ do_step_1
193
+ .and_then { do_step_2(_1) }
194
+ .and_then { do_step_3(_1) }
195
+ .on_success { puts "Successful step 3 result: #{_1}" }
196
+ .and_then { do_step_4(_1) }
197
+ .and_then { do_step_5(_1) }
198
+ .on_failure { puts "Uh oh! Step 5 failed: #{_1} }
199
+ .and_then { do_step_6(_1) }
200
+ .and_then { do_step_7(_1) }
201
+ ```
202
+
203
+ There are lots of aliases for these methods. See the "Aliases" section above.
204
+
205
+ ## Callable Object Arguments
206
+
207
+ Anywhere that you can use a block argument, you have the ability to
208
+ provide a callable object instead.
209
+
210
+ For example, this block argument:
211
+
212
+ ```ruby
213
+ Resonad.Success(42).map { |x| x * 2 }
214
+ #=> 84
215
+ ```
216
+
217
+ Could also be given as an object that implements `#call`:
218
+
219
+ ```ruby
220
+ class Doubler
221
+ def call(x)
222
+ x * 2
223
+ end
224
+ end
225
+
226
+ Resonad.Success(42).map(Doubler.new)
227
+ #=> 84
62
228
  ```
63
229
 
64
- Automatic exception rescuing:
230
+ ## Pattern Matching Support
231
+
232
+ If you are using Ruby 2.7 or later, you can pattern match on Resonad objects.
233
+ For example:
234
+
235
+ ```ruby
236
+ case result
237
+ in { value: } # match any Success
238
+ puts value
239
+ in { error: :not_found } # match Failure(:not_found)
240
+ puts "Thing not found"
241
+ in { error: String => msg } # match any Failure with a String error
242
+ puts "Failed to fetch thing because #{msg}"
243
+ in { error: } # match any Failure
244
+ raise "Unhandled error: #{error.inspect}"
245
+ end
246
+ ```
247
+
248
+ `Resonad.Success(5)` deconstructs to:
249
+
250
+ - Hash: `{ value: 5 }`
251
+ - Array: `[:success, 5]`
252
+
253
+ And `Resonad.Failure('yikes')` deconstructs to:
254
+
255
+ - Hash: `{ error: 'yikes' }`
256
+ - Array: `[:failure, 'yikes']`
257
+
258
+
259
+ ## Automatic Exception Rescuing
260
+
261
+ If no exception is raised, wraps the block's return value in `Success`. If an
262
+ exception is raised, wraps the exception object in `Failure`.
65
263
 
66
264
  ```ruby
67
265
  def try_divide(top, bottom)
@@ -76,3 +274,55 @@ nope = try_divide(6, 0)
76
274
  nope.success? #=> false
77
275
  node.error #=> #<ZeroDivisionError: ZeroDivisionError>
78
276
  ```
277
+
278
+
279
+ ## Convenience Mixin
280
+
281
+ If you're tired of typing "Resonad." in front of everything, you can include
282
+ the `Resonad::Mixin` mixin.
283
+
284
+ ```ruby
285
+ class RobotFortuneTeller
286
+ include Resonad::Mixin
287
+
288
+ def next_fortune
289
+ case rand(0..100)
290
+ when 0..70
291
+ # title-case constructor from Resonad::Mixin
292
+ Success("today is auspicious")
293
+ when 71..95
294
+ # lower-case constructor from Resonad::Mixin
295
+ success("ill omens abound")
296
+ else
297
+ # direct access to classes from Resonad::Mixin
298
+ Failure.new("MALFUNCTION")
299
+ end
300
+ end
301
+ end
302
+ ```
303
+
304
+ Note that `Resonad::Mixin` provides private methods, and private constants, so
305
+ you can't do this:
306
+
307
+ ```ruby
308
+ RobotFortuneTeller.new.Success(5)
309
+ #=> NoMethodError: private method `Success' called for #<RobotFortuneTeller:0x00007fe7fc0ff0c8>
310
+
311
+ RobotFortuneTeller::Success
312
+ #=> NameError: private constant Resonad::Mixin::Success referenced
313
+ ```
314
+
315
+ If you want the methods/constants to be public, then use `Resonad::PublicMixin`
316
+ instead.
317
+
318
+
319
+ ## Contributing
320
+
321
+ Bug reports and pull requests are welcome on GitHub at:
322
+ https://github.com/tomdalling/resonad
323
+
324
+ I'm open to PRs that make the gem more convenient, or that makes calling code
325
+ read better.
326
+
327
+ Make sure your PR has full test coverage.
328
+
@@ -1,29 +1,17 @@
1
- module Resonad
1
+ class Resonad
2
2
  class NonExistentError < StandardError; end
3
3
  class NonExistentValue < StandardError; end
4
4
 
5
- def self.Success(*args)
6
- Success.new(*args)
7
- end
8
-
9
- def self.Failure(*args)
10
- Failure.new(*args)
11
- end
5
+ class Success < Resonad
6
+ attr_accessor :value
12
7
 
13
- def self.rescuing_from(*exception_classes)
14
- Success(yield)
15
- rescue Exception => e
16
- if exception_classes.empty?
17
- Failure(e) # rescue from all exceptions
18
- elsif exception_classes.any? { |klass| e.is_a?(klass) }
19
- Failure(e) # rescue from specified exception type
20
- else
21
- raise # reraise unhandled exception
8
+ def self.[](value = nil)
9
+ if nil == value
10
+ NIL_SUCCESS
11
+ else
12
+ new(value)
13
+ end
22
14
  end
23
- end
24
-
25
- class Success
26
- attr_accessor :value
27
15
 
28
16
  def initialize(value)
29
17
  @value = value
@@ -34,50 +22,62 @@ module Resonad
34
22
  true
35
23
  end
36
24
 
37
- def failure?
38
- false
25
+ def deconstruct
26
+ [:success, value]
39
27
  end
40
28
 
41
- def on_success
42
- yield value
43
- self
44
- end
45
-
46
- def on_failure
47
- self
29
+ def deconstruct_keys(_)
30
+ { value: value }
48
31
  end
49
32
 
50
33
  def error
51
34
  raise NonExistentError, "Success resonads do not have errors"
52
35
  end
53
36
 
54
- def map
55
- new_value = yield(value)
56
- if new_value.__id__ == value.__id__
37
+ private
38
+
39
+ def __on_success(callable)
40
+ callable.(value)
57
41
  self
58
- else
59
- self.class.new(new_value)
60
42
  end
61
- end
62
43
 
63
- def map_error
64
- self
65
- end
44
+ def __on_failure(_)
45
+ self
46
+ end
66
47
 
67
- def flat_map
68
- yield value
69
- end
70
- alias_method :and_then, :flat_map
48
+ def __map(callable)
49
+ new_value = callable.(value)
50
+ if new_value.__id__ == value.__id__
51
+ self
52
+ else
53
+ self.class.new(new_value)
54
+ end
55
+ end
71
56
 
72
- def flat_map_error
73
- self
74
- end
75
- alias_method :or_else, :flat_map_error
57
+ def __map_error(_)
58
+ self
59
+ end
60
+
61
+ def __flat_map(callable)
62
+ callable.(value)
63
+ end
64
+
65
+ def __flat_map_error(_)
66
+ self
67
+ end
76
68
  end
77
69
 
78
- class Failure
70
+ class Failure < Resonad
79
71
  attr_accessor :error
80
72
 
73
+ def self.[](error = nil)
74
+ if nil == error
75
+ NIL_FAILURE
76
+ else
77
+ new(error)
78
+ end
79
+ end
80
+
81
81
  def initialize(error)
82
82
  @error = error
83
83
  freeze
@@ -87,45 +87,168 @@ module Resonad
87
87
  false
88
88
  end
89
89
 
90
- def failure?
91
- true
90
+ def value
91
+ raise NonExistentValue, "Failure resonads do not have values"
92
92
  end
93
93
 
94
- def on_success
95
- self
94
+ def deconstruct
95
+ [:failure, error]
96
96
  end
97
97
 
98
- def on_failure
99
- yield error
100
- self
98
+ def deconstruct_keys(_)
99
+ { error: error }
101
100
  end
102
101
 
103
- def value
104
- raise NonExistentValue, "Failure resonads do no have values"
105
- end
102
+ private
106
103
 
107
- def map
108
- self
109
- end
104
+ def __map(_)
105
+ self
106
+ end
110
107
 
111
- def map_error
112
- new_error = yield(error)
113
- if new_error.__id__ == error.__id__
108
+ def __on_success(_)
114
109
  self
115
- else
116
- self.class.new(new_error)
117
110
  end
118
- end
119
111
 
120
- def flat_map
121
- self
112
+ def __on_failure(callable)
113
+ callable.(error)
114
+ self
115
+ end
116
+
117
+ def __map_error(callable)
118
+ new_error = callable.(error)
119
+ if new_error.__id__ == error.__id__
120
+ self
121
+ else
122
+ self.class.new(new_error)
123
+ end
124
+ end
125
+
126
+ def __flat_map(_)
127
+ self
128
+ end
129
+
130
+ def __flat_map_error(callable)
131
+ callable.(error)
132
+ end
133
+ end
134
+
135
+ module PublicMixin
136
+ Success = ::Resonad::Success
137
+ Failure = ::Resonad::Failure
138
+
139
+ def Success(*args); Success[*args]; end
140
+ def success(*args); Success[*args]; end
141
+ def Failure(*args); Failure[*args]; end
142
+ def failure(*args); Failure[*args]; end
143
+ end
144
+
145
+ Mixin = PublicMixin.dup.tap do |mixin|
146
+ mixin.module_eval do
147
+ private(*public_instance_methods)
148
+ private_constant(*constants)
122
149
  end
123
- alias_method :and_then, :flat_map
150
+ end
151
+
152
+ extend PublicMixin
124
153
 
125
- def flat_map_error
126
- yield error
154
+ def self.rescuing_from(*exception_classes)
155
+ Success(yield)
156
+ rescue Exception => e
157
+ if exception_classes.empty?
158
+ Failure(e) # rescue from all exceptions
159
+ elsif exception_classes.any? { |klass| e.is_a?(klass) }
160
+ Failure(e) # rescue from specified exception type
161
+ else
162
+ raise # reraise unhandled exception
127
163
  end
128
- alias_method :or_else, :flat_map_error
129
164
  end
130
165
 
166
+ def initialize(*args)
167
+ raise NotImplementedError, "This is an abstract class. Use Resonad::Success or Resonad::Failure instead."
168
+ end
169
+
170
+ def success?
171
+ raise NotImplementedError, "should be implemented in subclass"
172
+ end
173
+ def successful?; success?; end
174
+ def ok?; success?; end
175
+
176
+ def failure?
177
+ not success?
178
+ end
179
+ def failed?; failure?; end
180
+ def bad?; failure?; end
181
+
182
+ def map(callable=nil, &block); __map(callable_from_args(callable, block)); end
183
+ def map_value(callable=nil, &block); __map(callable_from_args(callable, block)); end
184
+
185
+ def map_error(callable=nil, &block); __map_error(callable_from_args(callable, block)); end
186
+
187
+ def flat_map(callable=nil, &block); __flat_map(callable_from_args(callable, block)); end
188
+ def and_then(callable=nil, &block); __flat_map(callable_from_args(callable, block)); end
189
+
190
+ def flat_map_error(callable=nil, &block); __flat_map_error(callable_from_args(callable, block)); end
191
+ def or_else(callable=nil, &block); __flat_map_error(callable_from_args(callable, block)); end
192
+ def otherwise(callable=nil, &block); __flat_map_error(callable_from_args(callable, block)); end
193
+
194
+ def on_success(callable=nil, &block); __on_success(callable_from_args(callable, block)); end
195
+ def if_success(callable=nil, &block); __on_success(callable_from_args(callable, block)); end
196
+ def when_success(callable=nil, &block); __on_success(callable_from_args(callable, block)); end
197
+ def on_ok(callable=nil, &block); __on_success(callable_from_args(callable, block)); end
198
+ def if_ok(callable=nil, &block); __on_success(callable_from_args(callable, block)); end
199
+ def when_ok(callable=nil, &block); __on_success(callable_from_args(callable, block)); end
200
+ def on_successful(callable=nil, &block); __on_success(callable_from_args(callable, block)); end
201
+ def if_successful(callable=nil, &block); __on_success(callable_from_args(callable, block)); end
202
+ def when_successful(callable=nil, &block); __on_success(callable_from_args(callable, block)); end
203
+
204
+ def on_failure(callable=nil, &block); __on_failure(callable_from_args(callable, block)); end
205
+ def if_failure(callable=nil, &block); __on_failure(callable_from_args(callable, block)); end
206
+ def when_failure(callable=nil, &block); __on_failure(callable_from_args(callable, block)); end
207
+ def on_bad(callable=nil, &block); __on_failure(callable_from_args(callable, block)); end
208
+ def if_bad(callable=nil, &block); __on_failure(callable_from_args(callable, block)); end
209
+ def when_bad(callable=nil, &block); __on_failure(callable_from_args(callable, block)); end
210
+ def on_failed(callable=nil, &block); __on_failure(callable_from_args(callable, block)); end
211
+ def if_failed(callable=nil, &block); __on_failure(callable_from_args(callable, block)); end
212
+ def when_failed(callable=nil, &block); __on_failure(callable_from_args(callable, block)); end
213
+
214
+ NIL_SUCCESS = Success.new(nil)
215
+ NIL_FAILURE = Failure.new(nil)
216
+
217
+ private
218
+
219
+ def __map(callable)
220
+ raise NotImplementedError, "should be implemented in subclass"
221
+ end
222
+
223
+ def __flat_map(callable)
224
+ raise NotImplementedError, "should be implemented in subclass"
225
+ end
226
+
227
+ def __flat_map_error(callable)
228
+ raise NotImplementedError, "should be implemented in subclass"
229
+ end
230
+
231
+ def __on_success(callable)
232
+ raise NotImplementedError, "should be implemented in subclass"
233
+ end
234
+
235
+ def __on_failure(callable)
236
+ raise NotImplementedError, "should be implemented in subclass"
237
+ end
238
+
239
+ def callable_from_args(positional, block)
240
+ if block
241
+ if positional
242
+ raise ArgumentError, "expected _either_ a callable or a block argument, but _both_ were given"
243
+ else
244
+ block
245
+ end
246
+ else
247
+ if positional
248
+ positional
249
+ else
250
+ raise ArgumentError, "expected either a callable or a block argument, but neither were given"
251
+ end
252
+ end
253
+ end
131
254
  end
@@ -1,3 +1,3 @@
1
- module Resonad
2
- VERSION = '1.0.2'
1
+ class Resonad
2
+ VERSION = '1.4.0'
3
3
  end
@@ -20,8 +20,8 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.add_development_dependency "bundler", "~> 1.14"
24
- spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "bundler", ">= 1.15"
25
24
  spec.add_development_dependency "rspec", "~> 3.0"
26
- spec.add_development_dependency "gem-release", "~> 0.7"
25
+ spec.add_development_dependency "gem-release", "~> 2.1"
26
+ spec.add_development_dependency "byebug"
27
27
  end
metadata CHANGED
@@ -1,71 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resonad
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Dalling
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-04-22 00:00:00.000000000 Z
11
+ date: 2020-11-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.14'
19
+ version: '1.15'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.14'
26
+ version: '1.15'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '3.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '3.0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rspec
42
+ name: gem-release
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.0'
47
+ version: '2.1'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '3.0'
54
+ version: '2.1'
55
55
  - !ruby/object:Gem::Dependency
56
- name: gem-release
56
+ name: byebug
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '0.7'
61
+ version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '0.7'
68
+ version: '0'
69
69
  description: Objects that represent success or failure
70
70
  email:
71
71
  - tom@tomdalling.com
@@ -75,12 +75,13 @@ extra_rdoc_files: []
75
75
  files:
76
76
  - ".gitignore"
77
77
  - ".rspec"
78
+ - ".ruby-version"
78
79
  - ".travis.yml"
80
+ - CHANGELOG.md
79
81
  - CODE_OF_CONDUCT.md
80
82
  - Gemfile
81
83
  - LICENSE.txt
82
84
  - README.md
83
- - Rakefile
84
85
  - bin/console
85
86
  - bin/setup
86
87
  - lib/resonad.rb
@@ -105,8 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
106
  - !ruby/object:Gem::Version
106
107
  version: '0'
107
108
  requirements: []
108
- rubyforge_project:
109
- rubygems_version: 2.4.5
109
+ rubygems_version: 3.0.8
110
110
  signing_key:
111
111
  specification_version: 4
112
112
  summary: Objects that represent success or failure
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec