resulting 0.0.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3da1dbc393fe607cb6285fb6e0a3c5fb2c932bd7e50e7312ef17e7896232a85b
4
- data.tar.gz: 87eb2836ec966a6db44d0aa16a8aa4ae46a838e07cd65ba963f5e63249fa22c0
3
+ metadata.gz: a335ef962f6b9785f66c8c4d6830f575ba5db1910d3d49e74e693c83798775a0
4
+ data.tar.gz: fd89e762a7bc39aec9be3678478509c6306482aa4ca81f8954a07c9c61e0e06f
5
5
  SHA512:
6
- metadata.gz: 581c7a452f0bda593ea2659dcf9bae700e4d1695057cc93d9224119f5c14b682c1c8fd10117b7833a52413468714cfcd5ecf90815f26d94c8f6326f56fab86e2
7
- data.tar.gz: 86bcf54897b14ea11ccc04082c7b58dece2f7fd09ece466dd2d74fb748d8f4f9a7f788cf465a2046e03fd397267c3592fb4b561db8e00bc346e6f267b133fbca
6
+ metadata.gz: a2212406ba4ac45959186a25b579d20cfac6bf7b784abe62d81d184daac9ee42a866af16d4a32f87981ae48bcadf7288e21b409482bd3fbdd6c1369f6d2cee8e
7
+ data.tar.gz: 919091b0a3a3c486871815aa105a7714238ba4e16959cc55e272460763a4d222e3d87db412f752bb0a7e8a5ea174b4c25c73e75c5270d5b343e8e9af9d853f7d
data/.projections.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "lib/*.rb": { "alternate": "spec/{}_spec.rb" },
3
+ "spec/*_spec.rb": { "alternate": "lib/{}.rb" }
4
+ }
data/.rspec CHANGED
@@ -1,4 +1,4 @@
1
1
  --color
2
- --format documentation
2
+ --format progress
3
3
  --order random
4
4
  --require spec_helper
data/.rubocop.yml CHANGED
@@ -16,7 +16,7 @@ Layout/MultilineMethodCallIndentation:
16
16
  Enabled: false
17
17
 
18
18
  Layout/LineLength:
19
- Max: 100
19
+ Max: 110
20
20
  Exclude:
21
21
  - Rakefile
22
22
 
@@ -53,6 +53,9 @@ RSpec/MessageSpies:
53
53
  RSpec/MultipleExpectations:
54
54
  Enabled: false
55
55
 
56
+ RSpec/NestedGroups:
57
+ Max: 4
58
+
56
59
  RSpec/NotToNot:
57
60
  EnforcedStyle: to_not
58
61
 
data/CHANGELOG.md CHANGED
@@ -1,2 +1,5 @@
1
+ # 2020-04-15
2
+ Add basic functionality.
3
+
1
4
  # 2020-04-14
2
5
  Initial commit and release.
data/README.md CHANGED
@@ -1,10 +1,7 @@
1
1
  # Resulting
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be
4
- able to package up your Ruby library into a gem. Put your Ruby code in the file
5
- `lib/resulting`. To experiment with that code, run `bin/console` for an interactive prompt.
6
-
7
- TODO: Delete this and the text above, and describe your gem
3
+ Resulting is a gem to help with result handling and coordinating validations and
4
+ saving of (primarily) ActiveRecord objects.
8
5
 
9
6
  ## Installation
10
7
 
@@ -24,7 +21,311 @@ Or install it yourself as:
24
21
 
25
22
  ## Usage
26
23
 
27
- TODO: Write usage instructions here
24
+ There is a common pattern in a rails controller doing something like this:
25
+
26
+ - Controller action calls a service (or two or three)
27
+ - That service returns an object (or two or three)
28
+ - If you want to see if it was successful, that service may have its own result
29
+ object, or you can check if the object was persisted.
30
+ - Maybe the service does that and just returns and object for serialization
31
+ (more likely since we should have skinny controllers). But then you have the
32
+ same problem.
33
+ - Result objects are hard to manage outside a framework explicitly and
34
+ ruthlessly designed to use them.
35
+
36
+ There are of course some very complicated situations, but many situations can be
37
+ solved using `Resulting`.
38
+
39
+ While `Resulting` can be used in any way you see fit, the way I use it is
40
+ described below.
41
+
42
+ ### Custom Results
43
+
44
+ First we create a result object specific to our controller action. The reason
45
+ for this is we don't need a result object that is so flexible it is basically an
46
+ openstruct, but we want something with a slightly nicer API than a hash.
47
+
48
+ Additionally, this adds clarity, with a tested class, to the result object. So
49
+ anyone looking for what this result contains knows to just look for a result
50
+ named after the controller action.
51
+
52
+ ```ruby
53
+ class CreateUserAndWidgetResult
54
+ include Resulting::Resultable
55
+
56
+ def user
57
+ value[:user]
58
+ end
59
+
60
+ def widget
61
+ value[:widget]
62
+ end
63
+ end
64
+ ```
65
+
66
+ ### Controller
67
+
68
+ Now in our controller, since we know the result looks like, we can simply grab
69
+ the relevant objects out of it and set them to instance variables for a view, or
70
+ put them into JSON with [JBuilder](https://github.com/rails/jbuilder) or whatever.
71
+
72
+ We can also just check to see if our result was successful, the simplest API for
73
+ a result object.
74
+
75
+ ```ruby
76
+ class NewController
77
+ def create
78
+ result = UserAndWidgetCreateService.call(params)
79
+ @user = result.user
80
+ @widget = result.widget
81
+
82
+ if result.success?
83
+ redirect_to :show
84
+ else
85
+ render :new
86
+ end
87
+ end
88
+ end
89
+ ```
90
+
91
+ ### Result Object and Handlers
92
+
93
+ Finally, in our service, we initialize the result object we first defined with
94
+ our objects that we need to validate, save, and do whatever on.
95
+
96
+ Then we call the shortcut `.validate_and_save` method which will validate all
97
+ objects, this ensures that all objects will have `errors` even if the first one
98
+ fails validation.
99
+
100
+ If they are all valid, it will call `.save` (not `save!`) on each of them, inside an
101
+ `ActiveRecord::Base.transaction`. If all `.save` calls return `true`. Then we
102
+ will return a succesful result. If any of them return `false`, we will bail out
103
+ early and raise an `ActiveRecord::Rollback` error inside of the transaction.
104
+
105
+ ```ruby
106
+ class UserAndWidgetCreateService
107
+ def call(params)
108
+ new_result = CreateResult.success({
109
+ user: User.build(params[:user])
110
+ widget: Widget.build(params[:widget])
111
+ role: Role.build(user: user, widget: widget, role: :admin)
112
+ })
113
+
114
+ Resulting.validate_and_save(result)
115
+ end
116
+ end
117
+ ```
118
+
119
+ ## Details
120
+
121
+ 1. [`Resulting::Runner`](#resultingrunner)
122
+ 1. [`.run_all`](#run_allresult-method)
123
+ 1. [`.run_until_failure`](#run_until_failureresult-method)
124
+ 1. [With blocks](#with-blocks)
125
+ 1. [Options (`:failure_case`, `:wrapper`)](#options-failure_case-wrapper)
126
+ 1. [With Rails](#with-rails)
127
+ 1. [`Resulting::Handler`](#resultinghandler)
128
+ 1. [`Resulting::Result`](#resultingresult)
129
+ 1. [Constructors (`.new`, `.success`, and `.failure`](#constructors-new-success-failure)
130
+ 1. [`.wrap`](#wrap)
131
+ 1. [Methods (`#value`, `#success?`, and `#failure?`](#methods-value-success-and-failure)
132
+ 1. [Values](#values)
133
+ 1. [Resulting::Helpers](#resultinghelpers)
134
+
135
+ ## Resulting::Runner
136
+
137
+ The `Resulting::Runner`'s will take a result object, and if that result is
138
+ failing, return immediately. That way you can safely pass the results to any
139
+ method that takes them without worrying about acting on a failed result. (It's
140
+ almost like a monad, but definitely not a monad).
141
+
142
+ ### `.run_all(result, method:)`
143
+
144
+ This will call the given `method` on every object in `result.values`. It will
145
+ keep track whether or not all calls to `method` on each object were true.
146
+
147
+ If all calls to `method` were `true`, it will return a successful result.
148
+
149
+ ### `.run_until_failure(result, method:)`
150
+
151
+ This will call the given `method` on every object in `result.values` UNTIL it
152
+ sees a failure. At that point, it will bail out and stop calling the method.
153
+
154
+ ### With Blocks
155
+
156
+ Both of these methods take an optional block:
157
+
158
+ - In `run_all`, the block will be run no matter what. The return value of the
159
+ block will be `&&`'d with the current success value of calling `method` on all
160
+ the values. That new success value will determine whether the call was
161
+ successful.
162
+ ```ruby
163
+ Resulting::Runner.run_all(result, method: :validate) do
164
+ # Validate other things
165
+ # return true
166
+ end
167
+ ```
168
+ - In `run_until_falure`, the block will be run no matter what. The return value of the
169
+ block will be `&&`'d with the current success value of calling `method` on all
170
+ the values. That new success value will determine whether the call was
171
+ successful.
172
+ ```ruby
173
+ Resulting::Runner.run_until_failure(result, method: :validate) do
174
+ # Save other things
175
+ # return true
176
+ end
177
+ ```
178
+
179
+ ****NOTE: The return value of the block is what is used to determine
180
+ success.**** Be mindful of the return value.
181
+
182
+ ### Options (`:failure_case`, `:wrapper`)
183
+
184
+ `failure_case` is an optional argument. It should be a lambda that describes
185
+ what to do at the end if a failure is encountered. By default it's just a lambda
186
+ that returns false.
187
+
188
+ For example, when validating, if all `:validate` calls have returned false, we
189
+ just want to return `false`. However, if we are saving, and one of the saves
190
+ returns false, we actually want to do `raise ActiveRecord::Rollback`.
191
+
192
+ Odds are you will either return false or raise some error, but any lambda will
193
+ do.
194
+
195
+ `wrapper` is something that will wrap the whole result handling process. The
196
+ common example here would be to wrap all saves in an
197
+ `ActiveRecord::Base.transaction` block to ensure we can rollback safely.
198
+
199
+ ### With Rails (`.validate`, `.save`, and `.validate_and_save`)
200
+
201
+ Most of the time this is used within rails, and as described there are some
202
+ things you will commonly want to do.
203
+
204
+ ```ruby
205
+ Resulting.validate(param)
206
+ ```
207
+
208
+ Is equivalent to:
209
+
210
+ ```ruby
211
+ Resulting::Runner.run_all(param, method: :validate)
212
+ ```
213
+
214
+ ```ruby
215
+ Resulting.save(param)
216
+ ```
217
+
218
+ Is equivalent to:
219
+
220
+ ```ruby
221
+ Resulting::Runner.run_until_failure(
222
+ param,
223
+ method: :save,
224
+ failure_case: -> { raise ActiveRecord::Rollback },
225
+ wrapper: -> { ActiveRecord::Base.method(:transaction) },
226
+ )
227
+ ```
228
+
229
+ Both of these still take blocks.
230
+
231
+ Finally, `Resulting.validate_and_save` will just call one after the other. This
232
+ one does not take a block, so it assumes you just want to validating everything
233
+ and then save it.
234
+
235
+ ## Resulting::Result
236
+
237
+ This is a generic result class that implements `Resulting::Resultable`.
238
+
239
+ ### Constructors: (`.new`, `.success`, `.failure`)
240
+
241
+ - `.new(success, value)` stores the value and sets success to the first
242
+ parameter
243
+ - `.success(value)` stores the value and sets success to true
244
+ - `.failure(value)` stores the value and sets success as false
245
+
246
+ ### `.wrap`
247
+
248
+ `Resulting::Result.wrap` is worth calling out on its own. `Result.wrap(value)`
249
+ will do the following:
250
+
251
+ - If `value` is a result (i.e. implements `Resulting::Resultable`) it returns
252
+ the value.
253
+ - If `value` is anything else, it will return `Result.success(value)`.
254
+
255
+ ```ruby
256
+ $ foo = Object.new
257
+ $ result = Resulting::Result.wrap(foo)
258
+ $ result
259
+ => #<Resulting::Result:0x00007f91dd072238 @success=true, @value=#<Object:0x00007f91db929950>>
260
+ $ Resulting::Result.wrap(result)
261
+ => #<Resulting::Result:0x00007f91dd072238 @success=true, @value=#<Object:0x00007f91db929950>>
262
+ ```
263
+
264
+ You can use wrap to ensure you have a result object if you need it.
265
+
266
+ ### Methods: `#value`, `#success?`, and `#failure?`
267
+
268
+ A result has helper methods, `#success?` and `#failure?` which just check whether
269
+ `success` is truthy, and the obj is stored as the `value`.
270
+
271
+ ```ruby
272
+ success = obj.validate # => true
273
+ result = Resulting::Result.new(success, obj)
274
+
275
+ result.success? # => true
276
+ result.value # => obj
277
+ ```
278
+
279
+ ### `#values`
280
+
281
+ `values` returns the `value` collapsed into an array. This variable is iterated
282
+ over by the two runner methods.
283
+
284
+ ****NOTE: Resulting assumes any methods calls on the value mutate the value
285
+ itself and that it is passed by reference.****
286
+
287
+ - If `value` is a `Hash`, `values` is `value.values.flatten`
288
+ - If `value` is anything else, `values` is `Array(value).flatten`
289
+ - (This will wrap objects in an array, and leave arrays alone.)
290
+
291
+ When building your own result you can override this to provide different
292
+ behavior. You could use this maintain access to an object but not call a method
293
+ on it, or to add data you want acted on from a side effect.
294
+
295
+ ```ruby
296
+ class MyResult
297
+ def user
298
+ values[:user]
299
+ end
300
+
301
+ def hashed_password
302
+ values[:password] # Omit from values, so it's not acted on
303
+ end
304
+
305
+ def values
306
+ [user, user.side_effect_record]
307
+ end
308
+ end
309
+ ```
310
+
311
+ In this case, we have a password (or any object in memory we don't/can't
312
+ persist). It will be on the result object so we can do something with it, but by
313
+ omitting it from `#values`, we don't have to worry about it being acted on.
314
+
315
+ In contrast, let's say in our services we create some record as a side effect
316
+ which couldn't be created at the time we created the result (this is pretty
317
+ contrived, but go with it), then we can add that to the values as something to
318
+ be validated, saved, or whatever when the runners process the result.
319
+
320
+ ## Resulting::Helpers
321
+
322
+ If you include `Resulting::Helpers` in a given class or module, you get the
323
+ some nifty helper shortcuts.
324
+
325
+ ```ruby
326
+ Success(value) # Equal to Resulting::Result.success(value)
327
+ Failure(value) # Equal to Resulting::Result.failure(value)
328
+ ```
28
329
 
29
330
  ## Development
30
331
 
@@ -35,4 +336,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
35
336
  ## Contributing
36
337
 
37
338
  Bug reports and pull requests are welcome on GitHub at
38
- https://github.com/[USERNAME]/resulting.
339
+ https://github.com/dewyze/resulting.
data/TODO.md ADDED
@@ -0,0 +1,3 @@
1
+ - Chain methods together?
2
+ - Failure case should take the values or the result so someone can manually do a
3
+ rollback or something.
data/lib/resulting.rb CHANGED
@@ -1,6 +1,29 @@
1
1
  require "resulting/version"
2
+ require "resulting/resultable"
3
+ require "resulting/helpers"
4
+ require "resulting/handler"
5
+ require "resulting/result"
6
+ require "resulting/runner"
2
7
 
3
8
  module Resulting
4
- class Error < StandardError; end
5
- # Your code goes here...
9
+ class << self
10
+ def validate(result_or_value, &blk)
11
+ Resulting::Runner.run_all(result_or_value, method: :validate, &blk)
12
+ end
13
+
14
+ def save(result_or_value, &blk)
15
+ params = { method: :save }
16
+
17
+ if defined?(ActiveRecord::Base) && defined?(ActiveRecord::Rollback)
18
+ params[:failure_case] = -> { raise ActiveRecord::Rollback }
19
+ params[:wrapper] = ActiveRecord::Base.method(:transaction)
20
+ end
21
+
22
+ Resulting::Runner.run_until_failure(result_or_value, params, &blk)
23
+ end
24
+
25
+ def validate_and_save(result_or_value)
26
+ save(validate(result_or_value))
27
+ end
28
+ end
6
29
  end
@@ -0,0 +1,17 @@
1
+ module Resulting
2
+ module Handler
3
+ include Resulting::Helpers
4
+
5
+ def self.handle(result_or_value, wrapper: ->(&blk) { return blk.call })
6
+ wrapper.call do
7
+ result = Resulting::Result.wrap(result_or_value)
8
+
9
+ return result if result.failure?
10
+
11
+ success = yield
12
+
13
+ result.class.new(success, result.value)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Resulting
2
+ module Helpers
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def Success(*args, &block) # rubocop:disable Naming/MethodName
9
+ Resulting::Result.success(*args, &block)
10
+ end
11
+
12
+ def Failure(*args, &block) # rubocop:disable Naming/MethodName
13
+ Resulting::Result.failure(*args, &block)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ module Resulting
2
+ class Result
3
+ include Resultable
4
+ end
5
+ end
@@ -0,0 +1,45 @@
1
+ module Resulting
2
+ module Resultable
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ attr_reader :success, :value
6
+ end
7
+
8
+ def initialize(success, value)
9
+ @success = success
10
+ @value = value.is_a?(Resulting::Resultable) ? value.value : value
11
+ end
12
+
13
+ def success?
14
+ @success
15
+ end
16
+
17
+ def failure?
18
+ !@success
19
+ end
20
+
21
+ def values
22
+ if value.is_a?(Hash)
23
+ value.values.flatten
24
+ else
25
+ Array(value).flatten
26
+ end
27
+ end
28
+
29
+ module ClassMethods
30
+ def success(value)
31
+ new(true, value)
32
+ end
33
+
34
+ def failure(value)
35
+ new(false, value)
36
+ end
37
+
38
+ def wrap(param)
39
+ return param if param.is_a?(Resulting::Resultable)
40
+
41
+ success(param)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ module Resulting
2
+ module Runner
3
+ def self.run_all(result, method:, failure_case: -> { false }, wrapper: ->(&blk) { blk.call })
4
+ Resulting::Handler.handle(result, wrapper: wrapper) do
5
+ new_result = result.values.reduce(true) do |success, v|
6
+ v.send(method) ? success : false
7
+ end
8
+
9
+ if block_given?
10
+ block_result = yield
11
+ new_result &&= block_result
12
+ end
13
+
14
+ new_result ? true : failure_case.call
15
+ end
16
+ end
17
+
18
+ def self.run_until_failure(result, method:, failure_case: -> { false }, wrapper: ->(&blk) { blk.call })
19
+ Resulting::Handler.handle(result, wrapper: wrapper) do
20
+ result = result.values.all?(&method)
21
+
22
+ result &&= yield if block_given?
23
+
24
+ result ? true : failure_case.call
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module Resulting
2
- VERSION = "0.0.1".freeze
2
+ VERSION = "0.1.0".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resulting
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John DeWyze
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-14 00:00:00.000000000 Z
11
+ date: 2020-04-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry-byebug
@@ -93,6 +93,7 @@ extensions: []
93
93
  extra_rdoc_files: []
94
94
  files:
95
95
  - ".gitignore"
96
+ - ".projections.json"
96
97
  - ".rspec"
97
98
  - ".rubocop.yml"
98
99
  - ".travis.yml"
@@ -102,11 +103,17 @@ files:
102
103
  - LICENSE
103
104
  - README.md
104
105
  - Rakefile
106
+ - TODO.md
105
107
  - bin/console
106
108
  - bin/rake
107
109
  - bin/rubocop
108
110
  - bin/setup
109
111
  - lib/resulting.rb
112
+ - lib/resulting/handler.rb
113
+ - lib/resulting/helpers.rb
114
+ - lib/resulting/result.rb
115
+ - lib/resulting/resultable.rb
116
+ - lib/resulting/runner.rb
110
117
  - lib/resulting/version.rb
111
118
  - resulting.gemspec
112
119
  homepage: https://github.com/dewyze/resulting