resulting 0.0.1 → 0.1.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
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