much-result 0.1.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,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