waterfall 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 129bb57a29df2394c12511053c406490b8a99674
4
+ data.tar.gz: e7260b30ce91910cdb8d1466a39898000bde0fa5
5
+ SHA512:
6
+ metadata.gz: 80c9da8ab97a26854f8fa5700e1a8669409f2212fe16660805dff074894df2ba89aea2bec52efd232b874646de286cc992f9ad8d3be679fd63beefffdaa132fd
7
+ data.tar.gz: c9253aa6ce5ce77577398006957eab695b8489db2b914adfb68e44b0319820d6e8fa92159ed6a77aed93726f803e476194e7227bb8a844f82adf7283de6ddd9a
@@ -0,0 +1,34 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /lib/bundler/man/
26
+
27
+ # for a library or gem, you might want to ignore these files since the code is
28
+ # intended to run in multiple environments; otherwise, check them in:
29
+ # Gemfile.lock
30
+ # .ruby-version
31
+ # .ruby-gemset
32
+
33
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
34
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.1.0
5
+ script: bundle exec rspec spec
6
+ addons:
7
+ code_climate:
8
+ repo_token: 06ba7b4d95c2d056ef2b66c86e9d69f24dceee54354c1ab2ce1397f8f309b0a4
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in waterfall.gemspec
4
+ gemspec
5
+
6
+ gem "codeclimate-test-reporter", group: :test, require: nil
@@ -0,0 +1,53 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ waterfall (1.0.2)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ codeclimate-test-reporter (0.4.7)
10
+ simplecov (>= 0.7.1, < 1.0.0)
11
+ coderay (1.1.0)
12
+ diff-lcs (1.2.5)
13
+ docile (1.1.5)
14
+ json (1.8.2)
15
+ method_source (0.8.2)
16
+ pry (0.10.1)
17
+ coderay (~> 1.1.0)
18
+ method_source (~> 0.8.1)
19
+ slop (~> 3.4)
20
+ pry-nav (0.2.4)
21
+ pry (>= 0.9.10, < 0.11.0)
22
+ rake (10.3.2)
23
+ rspec (3.2.0)
24
+ rspec-core (~> 3.2.0)
25
+ rspec-expectations (~> 3.2.0)
26
+ rspec-mocks (~> 3.2.0)
27
+ rspec-core (3.2.3)
28
+ rspec-support (~> 3.2.0)
29
+ rspec-expectations (3.2.1)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.2.0)
32
+ rspec-mocks (3.2.1)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.2.0)
35
+ rspec-support (3.2.2)
36
+ simplecov (0.10.0)
37
+ docile (~> 1.1.0)
38
+ json (~> 1.8)
39
+ simplecov-html (~> 0.10.0)
40
+ simplecov-html (0.10.0)
41
+ slop (3.6.0)
42
+
43
+ PLATFORMS
44
+ ruby
45
+
46
+ DEPENDENCIES
47
+ bundler (~> 1.3)
48
+ codeclimate-test-reporter
49
+ pry (> 0.10)
50
+ pry-nav
51
+ rake
52
+ rspec (= 3.2)
53
+ waterfall!
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Benjamin Roth
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,345 @@
1
+ ![Waterfall Logo](http://apneadiving.github.io/images/waterfall_logo.png)
2
+ [![Code Climate](https://codeclimate.com/github/apneadiving/waterfall/badges/gpa.svg)](https://codeclimate.com/github/apneadiving/waterfall)
3
+ [![Test Coverage](https://codeclimate.com/github/apneadiving/waterfall/badges/coverage.svg)](https://codeclimate.com/github/apneadiving/waterfall/coverage)
4
+ [![Build Status](https://travis-ci.org/apneadiving/waterfall.svg?branch=master)](https://travis-ci.org/apneadiving/waterfall)
5
+ #### Goal
6
+
7
+ Be able to chain ruby commands, and treat them like a flow.
8
+
9
+ General presentation slides can [be found here](https://slides.com/apneadiving/code-ruby-like-you-build-legos).
10
+
11
+ Check [the slides here](https://slides.com/apneadiving/handling-error-in-ruby-rails) for a refactoring example.
12
+
13
+
14
+ #### Basic example
15
+
16
+ ```ruby
17
+ Wf.new
18
+ .when_falsy { @user.update(user_params) }
19
+ .dam { @user.errors }
20
+ .chain { render json: @user }
21
+ .on_dam { |errors| render json: { errors: errors.full_messages }, status: 422 }
22
+ ```
23
+
24
+ When logic is complicated, waterfalls show their true power and let you write intention revealing code. Above all they excel at chaining services.
25
+
26
+ #### Rationale
27
+ Coding is all about writing a flow of commands.
28
+
29
+ Generally you basically go on, unless something wrong happens. Whenever this happens you have to halt the flow and send feedback to the user.
30
+
31
+ When conditions stack up, readability decreases.
32
+
33
+ One way to solve it is to create abstractions to wrap your business logic (service objects or the like). There some questions arise:
34
+ * what should a good service return?
35
+ * how to handle errors?
36
+ * how to call a service within a service?
37
+ * how to chain services / commands
38
+
39
+ Those topics are discussed in [the slides here](https://slides.com/apneadiving/service-objects-waterfall-rails).
40
+
41
+
42
+ ## Wf object
43
+
44
+ The `Wf` class just includes the `Waterfall` module. It makes it easy to create standalone waterfalls mostly to chain actions or to chain services including `Waterfall` or returning a `Wf` object.
45
+
46
+ Basically `chain` statements are executed in the order they appear. But if ever the waterfall is dammed, they are skipped.
47
+
48
+ If a main waterfall chains another waterfall and the child waterfall is dammed, the main waterfall would be dammed.
49
+
50
+ The point is to be able to be able to chain an expected set of actions whenever everything works fine. And to be able to quickly stop and get the errors back whenever something wrong happens.
51
+
52
+ ## Installation
53
+
54
+ There exists a gem on rubygem with the same name but its not mine :)
55
+
56
+ For installation:
57
+
58
+ gem 'waterfall', git: 'git://github.com/apneadiving/waterfall.git'
59
+
60
+
61
+ ## Waterfall mixin
62
+
63
+ ### Overview
64
+
65
+ The following are equivalent:
66
+ ```ruby
67
+ # 1
68
+ Wf.new.chain{ 1 + 1 }
69
+
70
+ # 2
71
+ class MyService
72
+ include Waterfall
73
+
74
+ def call
75
+ self.chain{ 1 + 1 }
76
+ end
77
+ end
78
+
79
+ MyService.new.call
80
+ ```
81
+
82
+ This illustrates one convention classes including the mixin should obey: respond to `call`
83
+
84
+ ### Outputs
85
+
86
+ Each waterfall has its own `outflow` and `error_pool`.
87
+
88
+ `outflow` is an Openstruct so you can get/set its property like a hash or like a standard object.
89
+
90
+ For the `error_pool`, its up to you. But using Rails, I usually `include ActiveModel::Validations` in my services.
91
+
92
+ Thus you:
93
+
94
+ * have a standard way to deal with errors
95
+ * can deal with multiple errors
96
+ * support I18n out of the box
97
+ * can use your model errors out of the box
98
+
99
+ ## Illustration of chaining
100
+ Doing
101
+ ```ruby
102
+ Wf.new
103
+ .chain(foo: :bar) { Wf.new.chain(:bar){ 1 } }
104
+ ```
105
+
106
+ is the same as doing:
107
+
108
+ ```ruby
109
+ Wf.new
110
+ .chain do |outflow, parent_waterfall|
111
+ unless parent_waterfall.dammed?
112
+ child = Wf.new.chain(:bar){ 1 }
113
+ if child.dammed?
114
+ parent_waterfall.dam(child.error_pool)
115
+ else
116
+ parent_waterfall.ouflow.foo = child.outflow.bar
117
+ end
118
+ end
119
+ end
120
+ ```
121
+
122
+ Hopefully you better get the chaining power this way.
123
+
124
+ ## Predicates
125
+
126
+ ### chain(name_or_mapping = nil, &block) | block signature: (outflow, waterfall)
127
+
128
+ Chain is the main predicate, what it does depends on what the block returns
129
+ ```ruby
130
+ # main waterfall
131
+ Wf.new
132
+ .chain(foo: :bar) do
133
+ # child waterfall
134
+ Wf.new.chain(:bar){ 1 }.chain(:baz){ 2 }.chain{ 3 }
135
+ end
136
+ ```
137
+ ##### when block doesn't return a waterfall
138
+
139
+ The child waterfall would have the following outflow: `{ bar: 1, baz: 2 }`
140
+
141
+ This illustrates that when the block returns a value which is not a waterfall, it stores the returned value of the block inside the `name_or_mapping` key of the `outflow` or doesn't store it if `name_or_mapping` is `nil`.
142
+
143
+ Be aware those are equivalent:
144
+
145
+ ```ruby
146
+ Wf.new.chain(:foo) { 1 }
147
+ Wf.new.chain{|outflow| outflow[:foo] = 1 }
148
+ Wf.new.chain{|outflow| outflow.foo = 1 }
149
+ Wf.new.chain{|outflow, waterfall| waterfall.update_outflow(:foo, 1) }
150
+ Wf.new.chain{|outflow, waterfall| waterfall.outflow.foo = 1 }
151
+ ```
152
+ ##### when block returns a waterfall
153
+
154
+ The main waterfall would have the following outflow: `{ foo: 1 }`
155
+
156
+ The main waterfall above receives the child waterfall as a return value of its `chain` block.
157
+ All waterfalls have independent outflows.
158
+
159
+ If `name_or_mapping` is `nil`, the main waterfall's `outflow` wouldnt be affected by its child (but if the child is dammed, the parent will be dammed).
160
+
161
+ If `name_or_mapping` is a `hash`, the format must be read as `{ name_in_parent_waterfall: :name_from_child_waterfall}`. In the above example, the child returned an `outflow` with a `bar` key which has be renamed as `foo` in the main one.
162
+
163
+ It may look useless, because most of the time you may not rename, but... It makes things clear. You know exactly what you expect and you know exactly that you dont expect the rest the child may provide.
164
+
165
+ ### when_falsy(&block) | block signature: (error_pool, waterfall)
166
+
167
+ This predicate must ***always*** be used followed with `dam` like:
168
+
169
+ ```ruby
170
+ Wf.new
171
+ .chain(:foo) { 1 }
172
+ .when_falsy { true }
173
+ .dam { "this wouldnt be executed" }
174
+ .when_falsy { false }
175
+ .dam { "errrrr" }
176
+ .chain(:bar) { 2 }
177
+ .on_dam {|error_pool| puts error_pool }
178
+ ```
179
+
180
+ If the block returns a falsy value, it executes the `dam` block, which will store the returned value in the `error_pool`.
181
+
182
+ Once the waterfall is dammed, all following `chain` blocks are skipped (wont be executed). And all the following `on_dam` block would be executed.
183
+
184
+ As a result the example above would return a waterfall object having its `outflow` equal to `{ foo: 1 }`. Remember: it has been dammed before `bar` would have been set.
185
+
186
+ Its `error_pool` would be `"errrrr"` and it would be `puts` as a result of the `on_dam`
187
+
188
+ Be aware those are equivalent:
189
+
190
+ ```ruby
191
+ Wf.new.when_falsy{ false }.dam{ 'errrr' }
192
+ Wf.new.chain{ |outflow, waterfall| waterfall.dam('errrr') unless false }
193
+ ```
194
+
195
+ ### when_truthy(&block) | block signature: (error_pool, waterfall)
196
+
197
+ Behaves the same as `when_falsy` except it dams when its return value is truthy
198
+
199
+ ## Syntactic sugar
200
+ Given:
201
+ ```ruby
202
+ class MyWaterfall
203
+ include Waterfall
204
+ def call
205
+ self.chain { 1 }
206
+ end
207
+ end
208
+ ```
209
+ You may have noticed that I usually write:
210
+
211
+ ```ruby
212
+ Wf.new
213
+ .chain { MyWaterfall.new }
214
+ ```
215
+ instead of
216
+ ```ruby
217
+ Wf.new
218
+ .chain { MyWaterfall.new.call }
219
+ ```
220
+ Both are the same: if a block returns a waterfall which was not executed, it will execute it (hence the `call` convention)
221
+
222
+ ### on_dam(&block) | block signature: (error_pool, outflow, waterfall)
223
+
224
+ Its block is executed whenever the waterfall is dammed, skipped otherwise.
225
+
226
+ ```ruby
227
+ Wf.new
228
+ .when_falsy { false }
229
+ .on_dam {|error_pool, outflow, waterfall| puts error_pool }
230
+ ```
231
+
232
+ ## Error propagation
233
+
234
+ Whenever a a waterfall is dammed, all the following chains are skipped.
235
+
236
+ * all the following chains are skipped
237
+ * all `on_dam` blocks are executed
238
+
239
+ ## Testing a Waterfall service
240
+
241
+ Say I have this service:
242
+ ```ruby
243
+ class AuthenticateUser
244
+ include Waterfall
245
+ include ActiveModel::Validations
246
+
247
+ validates :user, presence: true
248
+ attr_reader :user
249
+
250
+ def initialize(email, password)
251
+ @email, @password = email, @password
252
+ end
253
+
254
+ def call
255
+ self
256
+ .chain { @user = User.authenticate(@email, @password) }
257
+ .when_falsy { valid? }
258
+ .dam { errors }
259
+ .chain(:user) { user }
260
+ end
261
+ end
262
+ ```
263
+ I could spec it this way:
264
+ ```ruby
265
+ describe AuthenticateUser do
266
+ let(:email) { 'email@email.com' }
267
+ let(:password) { 'password' }
268
+ subject(:service) { AuthenticateUser.new(email, password).call }
269
+
270
+ context "when given valid credentials" do
271
+ let(:user) { double(:user) }
272
+
273
+ before do
274
+ allow(User).to receive(:authenticate).with(email, password).and_return(user)
275
+ end
276
+
277
+ it "succeeds" do
278
+ expect(service.dammed?).to be false
279
+ end
280
+
281
+ it "provides the user" do
282
+ expect(service.outflow.user).to eq(user)
283
+ end
284
+ end
285
+
286
+ context "when given invalid credentials" do
287
+ before do
288
+ allow(User).to receive(:authenticate).with(email, password).and_return(nil)
289
+ end
290
+
291
+ it "fails" do
292
+ expect(service.dammed?).to be true
293
+ end
294
+
295
+ it "provides a failure message" do
296
+ expect(service.error_pool).to be_present
297
+ end
298
+ end
299
+ end
300
+ ```
301
+ Syntax advice
302
+ =========
303
+ ```ruby
304
+ # this is valid
305
+ self
306
+ .chain { Service1.new }
307
+ .chain { Service2.new }
308
+
309
+ # this is equivalent
310
+ self.chain { Service1.new }
311
+ self.chain { Service2.new }
312
+
313
+ # this is equivalent too
314
+ chain { Service1.new }
315
+ chain { Service2.new }
316
+
317
+ # this is invalid Ruby due to the extra line
318
+ self
319
+ .chain { Service1.new }
320
+
321
+ .chain { Service2.new }
322
+ ```
323
+
324
+ Tips
325
+ =========
326
+ ### Conditional Flow
327
+ In a service, there is one and single flow, so if you need conditionals to branch off, you can do:
328
+ ```ruby
329
+ self.chain { Service1.new }
330
+
331
+ if foo?
332
+ self.chain { Service2.new }
333
+ else
334
+ self.chain { Service3.new }
335
+ end
336
+ ```
337
+
338
+
339
+ Examples
340
+ =========
341
+ Check the [wiki for other examples](https://github.com/apneadiving/waterfall/wiki).
342
+
343
+ Thanks
344
+ =========
345
+ Huge thanks to [laxrph10](https://github.com/laxrph10) for the help during infinite naming brainstorming.
@@ -0,0 +1,85 @@
1
+ require 'ostruct'
2
+ require 'waterfall/version'
3
+ require 'waterfall/predicates/base'
4
+ require 'waterfall/predicates/on_dam'
5
+ require 'waterfall/predicates/when_falsy'
6
+ require 'waterfall/predicates/when_truthy'
7
+ require 'waterfall/predicates/chain'
8
+
9
+ module Waterfall
10
+
11
+ attr_reader :error_pool, :outflow, :flowing, :_wf_rolled_back
12
+
13
+ def when_falsy(&block)
14
+ handler = ::Waterfall::WhenFalsy.new(self)
15
+ _wf_run { handler.call(&block) }
16
+ handler
17
+ end
18
+
19
+ def when_truthy(&block)
20
+ handler = ::Waterfall::WhenTruthy.new(self)
21
+ _wf_run { handler.call(&block) }
22
+ handler
23
+ end
24
+
25
+ def chain(mapping_or_var_name = nil, &block)
26
+ _wf_run do
27
+ ::Waterfall::Chain
28
+ .new(self, mapping_or_var_name)
29
+ .call(&block)
30
+ end
31
+ end
32
+
33
+ def chain_wf(mapping_hash = nil, &block)
34
+ chain(mapping_hash, &block)
35
+ end
36
+
37
+ def on_dam(&block)
38
+ ::Waterfall::OnDam
39
+ .new(self)
40
+ .call(&block)
41
+ self
42
+ end
43
+
44
+ def dam(obj)
45
+ @error_pool = obj
46
+ self
47
+ end
48
+
49
+ def undam
50
+ dam nil
51
+ self
52
+ end
53
+
54
+ def dammed?
55
+ !error_pool.nil?
56
+ end
57
+
58
+ def is_waterfall?
59
+ true
60
+ end
61
+
62
+ def flowing?
63
+ !! @flowing
64
+ end
65
+
66
+ def update_outflow(key, value)
67
+ @outflow[key] = value
68
+ self
69
+ end
70
+
71
+ def _wf_run
72
+ @flowing = true
73
+ @outflow ||= OpenStruct.new({})
74
+ yield unless dammed?
75
+ self
76
+ end
77
+
78
+ end
79
+
80
+ class Wf
81
+ include Waterfall
82
+ def initialize
83
+ @outflow = OpenStruct.new({})
84
+ end
85
+ end
@@ -0,0 +1,23 @@
1
+ module Waterfall
2
+ class Base
3
+
4
+ def waterfall?(obj)
5
+ obj.respond_to?(:is_waterfall?) && obj.is_waterfall?
6
+ end
7
+
8
+ def chained_waterfall(child_waterfall)
9
+ child_waterfall.call unless child_waterfall.flowing?
10
+
11
+ if child_waterfall.dammed?
12
+ @root.dam child_waterfall.error_pool
13
+ else
14
+ yield
15
+ end
16
+ self
17
+ end
18
+
19
+ def yield_args
20
+ [@root.outflow, @root]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ module Waterfall
2
+ class Chain < Base
3
+
4
+ def initialize(root, mapping_or_var_name)
5
+ @root, @mapping_or_var_name = root, mapping_or_var_name
6
+ end
7
+
8
+ def call
9
+ output = yield(*yield_args)
10
+
11
+ if waterfall?(output)
12
+ map_waterfalls(output, @mapping_or_var_name || {})
13
+ else
14
+ @root.update_outflow(@mapping_or_var_name, output) if @mapping_or_var_name
15
+ end
16
+ end
17
+
18
+ def map_waterfalls(child_waterfall, mapping)
19
+ chained_waterfall(child_waterfall) do
20
+ mapping.each do |k, v|
21
+ @root.update_outflow(k, child_waterfall.outflow[v])
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ module Waterfall
2
+ class OnDam < Base
3
+
4
+ def initialize(root)
5
+ @root = root
6
+ end
7
+
8
+ def call
9
+ return unless @root.dammed?
10
+ yield @root.error_pool, @root.outflow, @root
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Waterfall
2
+ class WhenFalsy < Base
3
+
4
+ def initialize(root)
5
+ @root = root
6
+ end
7
+
8
+ def call
9
+ @output = yield(*yield_args)
10
+ end
11
+
12
+ def dam
13
+ if !@root.dammed? && !@output
14
+ @root.dam yield(*yield_args)
15
+ end
16
+ @root
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Waterfall
2
+ class WhenTruthy < Base
3
+
4
+ def initialize(root)
5
+ @root = root
6
+ end
7
+
8
+ def call
9
+ @output = yield(*yield_args)
10
+ end
11
+
12
+ def dam
13
+ if !@root.dammed? && @output
14
+ @root.dam yield(*yield_args)
15
+ end
16
+ @root
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module Waterfall
2
+ VERSION = "1.0.2"
3
+ end
@@ -0,0 +1,197 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Wf' do
4
+ let(:wf) { Wf.new }
5
+
6
+ describe "chain" do
7
+
8
+ it "yields wf outflow" do
9
+ wf
10
+ .chain {|outflow| outflow[:bar] = 'bar' }
11
+ .chain {|outflow| @bar = outflow[:bar] }
12
+ expect(wf.outflow[:bar]).to eq 'bar'
13
+ expect(@bar).to eq 'bar'
14
+ end
15
+
16
+ it "assigns outflow's key the value of the block" do
17
+ wf
18
+ .chain(:bar) { 'bar' }
19
+ expect(wf.outflow[:bar]).to eq 'bar'
20
+ end
21
+
22
+ context "wf internals" do
23
+ it "dam from within" do
24
+ wf
25
+ .chain {|outflow, waterfall| waterfall.dam('errrrr') }
26
+ .on_dam {|error_pool| @errors = error_pool }
27
+
28
+ expect(wf.dammed?).to be true
29
+ expect(@errors).to eq 'errrrr'
30
+ end
31
+
32
+ it "outflow from within" do
33
+ wf
34
+ .chain {|outflow, waterfall| waterfall.outflow[:foo] = 1 }
35
+
36
+ expect(wf.outflow[:foo]).to eq 1
37
+ end
38
+ end
39
+
40
+ describe "chaining waterfalls" do
41
+
42
+ shared_examples "a waterfall chain" do
43
+ describe 'chain_wf' do
44
+ it "takes expected vars only and rename them" do
45
+ wf
46
+ .chain_wf(baz: :foo) { waterfall }
47
+
48
+ expect(wf.outflow.foo).to be nil
49
+ expect(wf.outflow.bar).to be nil
50
+ expect(wf.outflow[:baz]).to eq waterfall.outflow[:foo]
51
+ end
52
+ end
53
+ end
54
+
55
+ context "from an instance of a custom waterfall class" do
56
+ class FakeService
57
+ include Waterfall
58
+
59
+ def call
60
+ self
61
+ .chain(:foo) { 1 }
62
+ .chain(:bar) { 2 }
63
+ end
64
+ end
65
+
66
+ let(:waterfall) { FakeService.new }
67
+
68
+ it_behaves_like "a waterfall chain"
69
+
70
+ context "only calls waterfall service if it was never called before" do
71
+ it "when passed as an instance responding to call" do
72
+ expect(waterfall).to receive(:call).once.and_call_original
73
+ wf
74
+ .chain { waterfall }
75
+ end
76
+
77
+ it "already called" do
78
+ expect(waterfall).to receive(:call).once.and_call_original
79
+ wf
80
+ .chain { waterfall.call }
81
+ end
82
+ end
83
+ end
84
+
85
+ context "from a mere wf" do
86
+ let(:waterfall) do
87
+ Wf.new
88
+ .chain(:foo) { 1 }
89
+ .chain(:bar) { 2 }
90
+ end
91
+
92
+ it_behaves_like "a waterfall chain"
93
+ end
94
+ end
95
+ end
96
+
97
+ describe "when falsy" do
98
+ let(:my_proc) { ->(val){ val } }
99
+
100
+ def action(bool)
101
+ wf
102
+ .chain { wf.dam('dammed') if dam? }
103
+ .when_falsy { my_proc.call(bool) }
104
+ .dam { 'err' }
105
+ .chain { @foo = 1 }
106
+ .on_dam { |error_pool| @error = error_pool }
107
+ end
108
+
109
+ context "main context not dammed" do
110
+ let(:dam?) { false }
111
+
112
+ it "when actually falsy" do
113
+ action false
114
+ expect(@error).to eq 'err'
115
+ expect(@foo).to_not eq 1
116
+ end
117
+
118
+ it "when actually truthy" do
119
+ action true
120
+ expect(@error).to_not eq 'err'
121
+ expect(@foo).to eq 1
122
+ end
123
+ end
124
+
125
+ context "main context dammed" do
126
+ let(:dam?) { true }
127
+
128
+ it "when actually falsy" do
129
+ expect(my_proc).to_not receive(:call)
130
+ action false
131
+ end
132
+ end
133
+ end
134
+
135
+ describe "when truthy" do
136
+ let(:my_proc) { ->(val){ val } }
137
+
138
+ def action(bool)
139
+ wf
140
+ .chain { wf.dam('dammed') if dam? }
141
+ .when_truthy { my_proc.call(bool) }
142
+ .dam { 'err' }
143
+ .chain { @foo = 1 }
144
+ .on_dam { |error_pool| @error = error_pool }
145
+ end
146
+
147
+ context "main context not dammed" do
148
+ let(:dam?) { false }
149
+
150
+ it "when actually falsy" do
151
+ action false
152
+ expect(@error).to_not eq 'err'
153
+ expect(@foo).to eq 1
154
+ end
155
+
156
+ it "when actually truthy" do
157
+ action true
158
+ expect(@error).to eq 'err'
159
+ expect(@foo).to_not eq 1
160
+ end
161
+ end
162
+
163
+ context "main context dammed" do
164
+ let(:dam?) { true }
165
+
166
+ it "when actually truthy" do
167
+ expect(my_proc).to_not receive(:call)
168
+ action true
169
+ end
170
+ end
171
+ end
172
+
173
+ describe "error propagation" do
174
+ class FailingChain
175
+ include Waterfall
176
+
177
+ def call
178
+ self
179
+ .chain {|error_pool, waterfall| waterfall.dam(self.class.error) }
180
+ end
181
+
182
+ def self.error
183
+ 'err'
184
+ end
185
+ end
186
+
187
+ it "error propagates" do
188
+ wf
189
+ .chain { FailingChain.new }
190
+ .chain { @foo = 1 }
191
+ .on_dam { |error_pool| @error = error_pool }
192
+
193
+ expect(@foo).to_not eq 1
194
+ expect(@error).to eq FailingChain.error
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,23 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+
8
+ require "codeclimate-test-reporter"
9
+ CodeClimate::TestReporter.start
10
+
11
+ require File.expand_path('../../lib/waterfall', __FILE__)
12
+ require 'pry'
13
+
14
+ RSpec.configure do |config|
15
+ config.run_all_when_everything_filtered = true
16
+ config.filter_run :focus
17
+
18
+ # Run specs in random order to surface order dependencies. If you find an
19
+ # order dependency and want to debug it, you can fix the order by providing
20
+ # the seed, which is printed after each run.
21
+ # --seed 1234
22
+ config.order = 'random'
23
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'waterfall/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "waterfall"
8
+ spec.version = Waterfall::VERSION
9
+ spec.authors = ["Benjamin Roth"]
10
+ spec.email = ["benjamin@rubyist.fr"]
11
+ spec.description = %q{A slice of functional programming to chain ruby services and blocks. Make them flow!}
12
+ spec.summary = %q{A slice of functional programming to chain ruby services and blocks. Make them flow!}
13
+ spec.homepage = "https://github.com/apneadiving/waterfall"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(spec)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "pry", '>0.10'
23
+ spec.add_development_dependency "pry-nav"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec", "3.2"
26
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: waterfall
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Benjamin Roth
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>'
32
+ - !ruby/object:Gem::Version
33
+ version: '0.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>'
39
+ - !ruby/object:Gem::Version
40
+ version: '0.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry-nav
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: '3.2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: '3.2'
83
+ description: A slice of functional programming to chain ruby services and blocks.
84
+ Make them flow!
85
+ email:
86
+ - benjamin@rubyist.fr
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - .gitignore
92
+ - .rspec
93
+ - .travis.yml
94
+ - Gemfile
95
+ - Gemfile.lock
96
+ - LICENSE.txt
97
+ - README.md
98
+ - lib/waterfall.rb
99
+ - lib/waterfall/predicates/base.rb
100
+ - lib/waterfall/predicates/chain.rb
101
+ - lib/waterfall/predicates/on_dam.rb
102
+ - lib/waterfall/predicates/when_falsy.rb
103
+ - lib/waterfall/predicates/when_truthy.rb
104
+ - lib/waterfall/version.rb
105
+ - spec/integration_spec.rb
106
+ - spec/spec_helper.rb
107
+ - waterfall.gemspec
108
+ homepage: https://github.com/apneadiving/waterfall
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.0.3
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: A slice of functional programming to chain ruby services and blocks. Make
132
+ them flow!
133
+ test_files:
134
+ - spec/integration_spec.rb
135
+ - spec/spec_helper.rb