much-result 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f4805f13dbc5498186070c28c422564440a511d0b9e4af9f695802b969e3f7da
4
+ data.tar.gz: 811cb65cfb9bb197329e06b44ce7511daab25adc9bbdbcd1610d307c2f1b4cb3
5
+ SHA512:
6
+ metadata.gz: 2e3a53796159c10e8b8e05ab1afd862cdbc77bc0eb729d3874808f731ae8f82a69f71479fb3664dffc3cb69c0bc4a815a9e29f1c1ed5c4ba1f71b584bdd29377
7
+ data.tar.gz: 77b2ac454e76ba063bc3ff2812a063caafe776d42e0875f111fa17868d58a1716f0ced1e423bb045f723250e5f6e72f11f3bc5acd2cd3db89cff3b4e77e704ba
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ ruby "~> 2.5"
4
+
5
+ gemspec
6
+
7
+ gem "pry", "~> 0.12.2"
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2020-Present Kelly Redding and Collin Redding
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,233 @@
1
+ # MuchResult
2
+
3
+ API for managing the results of operations.
4
+
5
+ ## Usage
6
+
7
+ Have services/methods return a MuchResult based on the whether an exception was raised or not:
8
+
9
+ ```ruby
10
+ class PerformSomeOperation
11
+ def self.call
12
+ # Do something that could fail by raising an exception.
13
+ MuchResult.success(message: "it worked!")
14
+ rescue => exception
15
+ MuchResult.failure(exception: exception)
16
+ end
17
+ end
18
+
19
+ result = PerformSomeOperation.call
20
+
21
+ result.success? # => true
22
+ result.failure? # => false
23
+ result.message # => "it worked!"
24
+ result.sub_results # => []
25
+ ```
26
+
27
+ Have services/methods return a MuchResult based on a result value (i.e. "truthy" = success; "false-y" = failure):
28
+
29
+ ```ruby
30
+ def perform_some_operation(success:)
31
+ # Do something that could fail.
32
+ MuchResult.for(success, message: "it ran :shrug:")
33
+ end
34
+
35
+ result = perform_some_operation(success: true)
36
+ result.success? # => true
37
+ result.failure? # => false
38
+ result.message # => "it ran :shrug:"
39
+
40
+ result = perform_some_operation(success: false)
41
+ result.success? # => false
42
+ result.failure? # => true
43
+ result.message # => "it ran :shrug:"
44
+
45
+ result = perform_some_operation(success: nil)
46
+ result.success? # => false
47
+ result.failure? # => true
48
+ result.message # => "it ran :shrug:"
49
+ ```
50
+
51
+ Set arbitrary values on MuchResults before or after they are created:
52
+
53
+ ```ruby
54
+ result = MuchResult.success(message: "it worked!")
55
+ result.set(
56
+ other_value1: "something else 1",
57
+ other_value2: "something else 2"
58
+ )
59
+ result.message # => "it worked!"
60
+ result.other_value1 # => "something else 1"
61
+ result.other_value2 # => "something else 2"
62
+ ```
63
+
64
+ ### Capture sub-Results
65
+
66
+ Capture MuchResults for sub-operations into a parent MuchResult:
67
+
68
+ ```ruby
69
+ class PerformSomeOperation
70
+ def self.call
71
+ MuchResult.tap(description: "Do both parts") { |result|
72
+ result # => <MuchResult ...>
73
+ result.success? # => true
74
+
75
+ result.capture { do_part_1 }
76
+ # OR you can use `capture_all` to capture from an Array of MuchResults
77
+
78
+ # raise an Exception if failure
79
+ result.capture! { do_part_2 }
80
+ # OR you can use `capture_all!` to capture from an Array of MuchResults
81
+
82
+ # set some arbitrary values b/c it worked.
83
+ result.set(message: "it worked!")
84
+ } # => result
85
+ end
86
+
87
+ def self.do_part_1
88
+ MuchResult.failure(description: "Part 1")
89
+ end
90
+
91
+ def self.do_part_2
92
+ MuchResult.success(description: "Part 2")
93
+ end
94
+ end
95
+
96
+ result = PerformSomeOperation.call
97
+
98
+ result.success? # => true
99
+ result.message # => "it worked!"
100
+
101
+ # Get just the immediate sub-results that were captured for the MuchResult.
102
+ result.sub_results # => [<MuchResult Part 1>, <MuchResult Part 2>]
103
+
104
+ # Get all MuchResults that make up this MuchResult (including those captured
105
+ # on all recursive sub-results).
106
+ result.all_results # => [result, <MuchResult Part 1>, <MuchResult Part 2>]
107
+
108
+ # Get aggregated values for each MuchResult that makes up this MuchResult.
109
+ result.get_for_sub_results(:description)
110
+ # => ["Part 1", "Part 2"]
111
+ result.get_for_success_sub_results(:description)
112
+ # => ["Part 2"]
113
+ result.get_for_failure_sub_results(:description)
114
+ # => ["Part 1"]
115
+
116
+ result.get_for_all_results(:description)
117
+ # => ["Do both parts", "Part 1", "Part 2"]
118
+ result.get_for_all_success_results(:description)
119
+ # => ["Part 2"]
120
+ result.get_for_all_failure_results(:description)
121
+ # => ["Do both parts", "Part 1"]
122
+ ```
123
+
124
+ ### Transactions
125
+
126
+ Run transactions capturing sub-Results:
127
+
128
+ ```ruby
129
+ class PerformSomeOperation
130
+ def self.call
131
+ MuchResult.transaction(
132
+ ActiveRecord::Base,
133
+ value: "something",
134
+ description: "Do both parts"
135
+ ) { |transaction|
136
+ transaction # => <MuchResult::Transaction ...>
137
+ transaction.result # => <MuchResult ...>
138
+
139
+ # You can interact with a transaction as if it were a MuchResult.
140
+ transaction.value # => "something"
141
+ transaction.success? # => true
142
+
143
+ transaction.capture { do_part_1 }
144
+ # OR you can use `capture_all` to capture from an Array of MuchResults
145
+
146
+ # raise an Exception if failure (which will rollback the transaction)
147
+ transaction.capture! { do_part_2 }
148
+ # OR you can use `capture_all!` to capture from an Array of MuchResults
149
+
150
+ # manually rollback the transaction if needed
151
+ # (stops processing and doesn't commit the transaction)
152
+ transaction.rollback if rollback_needed?
153
+
154
+ # manually halt the transaction if needed
155
+ # (stops processing and commits the transaction)
156
+ transaction.halt if halt_needed?
157
+
158
+ # set some arbitrary values b/c it worked.
159
+ transaction.set(message: "it worked!")
160
+ } # => transaction.result
161
+ end
162
+
163
+ def self.do_part_1
164
+ MuchResult.failure(description: "Part 1")
165
+ end
166
+
167
+ def self.do_part_2
168
+ MuchResult.success(description: "Part 2")
169
+ end
170
+
171
+ def rollback_needed?
172
+ false
173
+ end
174
+
175
+ def halt_needed?
176
+ false
177
+ end
178
+ end
179
+
180
+ result = PerformSomeOperation.call
181
+
182
+ result.success? # => true
183
+ result.message # => "it worked!"
184
+
185
+ result.much_result_transaction_rolled_back # => false
186
+ result.much_result_transaction_halted # => false
187
+
188
+ # Get just the immediate sub-results that were captured for the MuchResult.
189
+ result.sub_results # => [<MuchResult Part 1>, <MuchResult Part 2>]
190
+
191
+ # Get all MuchResults that make up this MuchResult (including those captured
192
+ # on all recursive sub-results).
193
+ result.all_results # => [result, <MuchResult Part 1>, <MuchResult Part 2>]
194
+
195
+ # Get aggregated values for each MuchResult that makes up this MuchResult.
196
+ result.get_for_sub_results(:description)
197
+ # => ["Part 1", "Part 2"]
198
+ result.get_for_success_sub_results(:description)
199
+ # => ["Part 2"]
200
+ result.get_for_failure_sub_results(:description)
201
+ # => ["Part 1"]
202
+
203
+ result.get_for_all_results(:description)
204
+ # => ["Do both parts", "Part 1", "Part 2"]
205
+ result.get_for_all_success_results(:description)
206
+ # => ["Part 2"]
207
+ result.get_for_all_failure_results(:description)
208
+ # => ["Do both parts", "Part 1"]
209
+ ```
210
+
211
+ Note: MuchResult::Transactions are designed to delegate to their MuchResult. You can interact with a MuchResult::Transaction as if it were a MuchResult.
212
+
213
+ ## Installation
214
+
215
+ Add this line to your application's Gemfile:
216
+
217
+ gem "much-result"
218
+
219
+ And then execute:
220
+
221
+ $ bundle
222
+
223
+ Or install it yourself as:
224
+
225
+ $ gem install much-result
226
+
227
+ ## Contributing
228
+
229
+ 1. Fork it
230
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
231
+ 3. Commit your changes (`git commit -am "Added some feature"`)
232
+ 4. Push to the branch (`git push origin my-new-feature`)
233
+ 5. Create new Pull Request
@@ -0,0 +1,205 @@
1
+ require "much-result/version"
2
+ require "much-result/aggregate"
3
+ require "much-result/transaction"
4
+
5
+ class MuchResult
6
+ SUCCESS = "success".freeze
7
+ FAILURE = "failure".freeze
8
+
9
+ Error = Class.new(StandardError)
10
+ Rollback = Class.new(RuntimeError)
11
+
12
+ def self.success(backtrace: caller, **kargs)
13
+ new(MuchResult::SUCCESS, **kargs, backtrace: backtrace)
14
+ end
15
+
16
+ def self.failure(backtrace: caller, **kargs)
17
+ new(MuchResult::FAILURE, **kargs, backtrace: backtrace)
18
+ end
19
+
20
+ def self.for(value, backtrace: caller, **kargs)
21
+ if value.respond_to?(:to_much_result)
22
+ return value.to_much_result(**kargs, backtrace: backtrace)
23
+ end
24
+
25
+ new(
26
+ !!value ? MuchResult::SUCCESS : MuchResult::FAILURE,
27
+ **kargs,
28
+ backtrace: backtrace
29
+ )
30
+ end
31
+
32
+ def self.tap(backtrace: caller, **kargs)
33
+ success(backtrace: backtrace, **kargs).tap { |result|
34
+ yield result if block_given?
35
+ }
36
+ end
37
+
38
+ def self.transaction(receiver, backtrace: caller, **kargs, &block)
39
+ MuchResult::Transaction.call(receiver, backtrace: backtrace, **kargs, &block)
40
+ end
41
+
42
+ attr_reader :sub_results, :description, :backtrace
43
+
44
+ def initialize(result_value, description: nil, backtrace: caller, **kargs)
45
+ @result_value = result_value
46
+ @description = description
47
+ @backtrace = backtrace
48
+
49
+ set(**kargs)
50
+
51
+ @sub_results = []
52
+ reset_sub_results_cache
53
+ end
54
+
55
+ def set(**kargs)
56
+ @data = ::OpenStruct.new((@data || {}).to_h.merge(**kargs))
57
+ self
58
+ end
59
+
60
+ def success?
61
+ if @success_predicate.nil?
62
+ @success_predicate =
63
+ @sub_results.reduce(@result_value == MuchResult::SUCCESS) { |acc, result|
64
+ acc && result.success?
65
+ }
66
+ end
67
+
68
+ @success_predicate
69
+ end
70
+
71
+ def failure?
72
+ !success?
73
+ end
74
+
75
+ def capture_for(value, backtrace: caller, **kargs)
76
+ self.class.for(value, backtrace: backtrace, **kargs).tap { |result|
77
+ @sub_results.push(result)
78
+ reset_sub_results_cache
79
+ }
80
+ end
81
+
82
+ def capture_for!(value, backtrace:caller, **kargs)
83
+ capture_for(value, **kargs, backtrace: backtrace).tap { |result|
84
+ raise(result.capture_exception) if result.failure?
85
+ }
86
+ end
87
+
88
+ def capture_for_all(values, backtrace: caller, **kargs)
89
+ [*values].map { |value| capture_for(value, **kargs, backtrace: backtrace) }
90
+ end
91
+
92
+ def capture_for_all!(values, backtrace: caller, **kargs, &block)
93
+ capture_for_all(values, **kargs, backtrace: backtrace).tap { |results|
94
+ if (first_failure_result = results.detect(&:failure?))
95
+ raise(first_failure_result.capture_exception)
96
+ end
97
+ }
98
+ end
99
+
100
+ def capture(backtrace: caller, **kargs)
101
+ capture_for((yield if block_given?), **kargs, backtrace: backtrace)
102
+ end
103
+
104
+ def capture!(backtrace: caller, **kargs, &block)
105
+ capture_for!((yield if block_given?), **kargs, backtrace: backtrace)
106
+ end
107
+
108
+ def capture_all(backtrace: caller, **kargs)
109
+ capture_for_all((yield if block_given?), **kargs, backtrace: backtrace)
110
+ end
111
+
112
+ def capture_all!(backtrace: caller, **kargs, &block)
113
+ capture_for_all!((yield if block_given?), **kargs, backtrace: backtrace)
114
+ end
115
+
116
+ # Prefer any `#exception` set on the data. Fallback to building an exception
117
+ # from the description/backtrace of the result.
118
+ def capture_exception
119
+ @data.exception || build_default_capture_exception
120
+ end
121
+
122
+ def success_sub_results
123
+ @success_sub_results ||= @sub_results.select { |result| result.success? }
124
+ end
125
+
126
+ def failure_sub_results
127
+ @failure_sub_results ||= @sub_results.select { |result| result.failure? }
128
+ end
129
+
130
+ def all_results
131
+ @all_results ||=
132
+ [self] +
133
+ @sub_results.flat_map { |result| result.all_results }
134
+ end
135
+
136
+ def all_success_results
137
+ @all_success_results ||=
138
+ [*(self if success?)] +
139
+ @sub_results.flat_map { |result| result.all_success_results }
140
+ end
141
+
142
+ def all_failure_results
143
+ @all_failure_results ||=
144
+ [*(self if failure?)] +
145
+ @sub_results.flat_map { |result| result.all_failure_results }
146
+ end
147
+
148
+ def get_for_sub_results(attribute_name)
149
+ MuchResult::Aggregate.(sub_results.map(&attribute_name.to_sym))
150
+ end
151
+
152
+ def get_for_success_sub_results(attribute_name)
153
+ MuchResult::Aggregate.(success_sub_results.map(&attribute_name.to_sym))
154
+ end
155
+
156
+ def get_for_failure_sub_results(attribute_name)
157
+ MuchResult::Aggregate.(failure_sub_results.map(&attribute_name.to_sym))
158
+ end
159
+
160
+ def get_for_all_results(attribute_name)
161
+ MuchResult::Aggregate.(all_results.map(&attribute_name.to_sym))
162
+ end
163
+
164
+ def get_for_all_success_results(attribute_name)
165
+ MuchResult::Aggregate.(all_success_results.map(&attribute_name.to_sym))
166
+ end
167
+
168
+ def get_for_all_failure_results(attribute_name)
169
+ MuchResult::Aggregate.(all_failure_results.map(&attribute_name.to_sym))
170
+ end
171
+
172
+ def to_much_result(backtrace: caller, **kargs)
173
+ self.set(**kargs)
174
+ end
175
+
176
+ def inspect
177
+ "#<#{self.class}:#{"0x0%x" % (object_id << 1)} "\
178
+ "#{success? ? "SUCCESS" : "FAILURE"} "\
179
+ "#{"@description=#{@description.inspect} " if @description}"\
180
+ "@sub_results=#{@sub_results.inspect}>"
181
+ end
182
+
183
+ private
184
+
185
+ def build_default_capture_exception
186
+ Error.new(description).tap { |exception| exception.set_backtrace(backtrace) }
187
+ end
188
+
189
+ def reset_sub_results_cache
190
+ @success_predicate = nil
191
+ @success_sub_results = nil
192
+ @failure_sub_results = nil
193
+ @all_results = nil
194
+ @all_success_results = nil
195
+ @all_failure_results = nil
196
+ end
197
+
198
+ def respond_to_missing?(*args)
199
+ @data.send(:respond_to_missing?, *args)
200
+ end
201
+
202
+ def method_missing(method, *args, &block)
203
+ @data.public_send(method, *args, &block)
204
+ end
205
+ end