waterfall 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +34 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +53 -0
- data/LICENSE.txt +22 -0
- data/README.md +345 -0
- data/lib/waterfall.rb +85 -0
- data/lib/waterfall/predicates/base.rb +23 -0
- data/lib/waterfall/predicates/chain.rb +26 -0
- data/lib/waterfall/predicates/on_dam.rb +13 -0
- data/lib/waterfall/predicates/when_falsy.rb +19 -0
- data/lib/waterfall/predicates/when_truthy.rb +19 -0
- data/lib/waterfall/version.rb +3 -0
- data/spec/integration_spec.rb +197 -0
- data/spec/spec_helper.rb +23 -0
- data/waterfall.gemspec +26 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -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
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -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!
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/lib/waterfall.rb
ADDED
@@ -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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/waterfall.gemspec
ADDED
@@ -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
|