ni 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +1061 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/ni/version.rb +3 -0
- data/lib/ni.rb +30 -0
- data/ni.gemspec +42 -0
- metadata +101 -0
data/README.md
ADDED
@@ -0,0 +1,1061 @@
|
|
1
|
+
# Ni
|
2
|
+
|
3
|
+
## The Developers Who Say Ni [nee]
|
4
|
+
|
5
|
+
- What it the easiest way to build a service with ruby?
|
6
|
+
- Ni
|
7
|
+
- What it the most convinient way to implement any business logic flow?
|
8
|
+
- Ni
|
9
|
+
- How can I stop care about the flow control and focus on things are really matter?
|
10
|
+
- Ni
|
11
|
+
- How can I build an ERP, CRM, BPM and other three letter things?
|
12
|
+
- Ni + Peng + NeeeWom => Graal
|
13
|
+
|
14
|
+
## Warning
|
15
|
+
|
16
|
+
Because of my strong intentions to try these ideas in the real world projects ASAP sometimes I didn't have enough time for solid solutions or decent tests. So, please use only DSL described below. If it doesn't allows you to solve your issues, do not use interactor then
|
17
|
+
|
18
|
+
All things described below are possible because of the ruby power. A lot metaprogrammings and high complexity code under the hood Ni to implement whatever flow you need. But, in some cases I've add additional stricts, which should not allow you to make a bad choices. They are described as **Strict! Bold text ...**
|
19
|
+
|
20
|
+
## Features
|
21
|
+
|
22
|
+
### The most simple interactor
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
class Interactor
|
26
|
+
include Ni::Main
|
27
|
+
|
28
|
+
receive :param_1
|
29
|
+
|
30
|
+
def perform
|
31
|
+
self.context.errors.add(:base, 'An error') if context.param_1 == '2'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
result = Interactor.perform(param_1: '1')
|
36
|
+
|
37
|
+
result.success? # false
|
38
|
+
```
|
39
|
+
|
40
|
+
Or with using the chain DSL. The chain DSL allows all power of Ni to comes in
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class Interactor
|
44
|
+
include Ni::Main
|
45
|
+
|
46
|
+
receive :param_1
|
47
|
+
|
48
|
+
action :perform do
|
49
|
+
self.context.errors.add(:base, 'An error') if context.param_1 == '2'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
result = Interactor.perform(param_1: '1')
|
54
|
+
|
55
|
+
result.success? # false
|
56
|
+
```
|
57
|
+
|
58
|
+
### Interactor result
|
59
|
+
|
60
|
+
You can get any context value via the context method.
|
61
|
+
But, there is also another method to read the context values with assign multiple variables
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class Interactor
|
65
|
+
include Ni::Main
|
66
|
+
|
67
|
+
provide :a
|
68
|
+
provide :b
|
69
|
+
provide :c
|
70
|
+
|
71
|
+
action :perform do
|
72
|
+
context.a = 1
|
73
|
+
context.b = 2
|
74
|
+
context.c = 3
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
result = Interactor.perform
|
79
|
+
|
80
|
+
result.success? # true
|
81
|
+
result.context.a # 1
|
82
|
+
result.context.b # 2
|
83
|
+
result.context.c # 3
|
84
|
+
|
85
|
+
result, a, b, c = Interactor.perform
|
86
|
+
a # 1
|
87
|
+
b # 2
|
88
|
+
c # 3
|
89
|
+
```
|
90
|
+
|
91
|
+
But, if the list of provided values will be specified manually, only these params will be returned. Check the `provide(:c)` part of the chain
|
92
|
+
```ruby
|
93
|
+
class Interactor
|
94
|
+
include Ni::Main
|
95
|
+
|
96
|
+
provide :a
|
97
|
+
provide :b
|
98
|
+
provide :c
|
99
|
+
|
100
|
+
action :perform do
|
101
|
+
context.a = 1
|
102
|
+
context.b = 2
|
103
|
+
context.c = 3
|
104
|
+
end
|
105
|
+
.provide(:c)
|
106
|
+
end
|
107
|
+
|
108
|
+
result, c = Interactor.perform
|
109
|
+
|
110
|
+
result.context.a # 1
|
111
|
+
result.context.b # 2
|
112
|
+
c # 3
|
113
|
+
```
|
114
|
+
|
115
|
+
### Interactor context and contracts
|
116
|
+
|
117
|
+
Shared state is one of the Interactor pattern cons. To control it Ni has access and contract DSL.
|
118
|
+
You can't manipulate with the context data without explicitly defining your intentions.
|
119
|
+
|
120
|
+
The `receive` allows to read from context.
|
121
|
+
The `mutate` allows to read and write.
|
122
|
+
The `provide` allows only to write.
|
123
|
+
|
124
|
+
**Note! If there are no any rules then all access granted!**
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
class Interactor1
|
128
|
+
include Ni::Main
|
129
|
+
|
130
|
+
action :perform do
|
131
|
+
context.param_1
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class Interactor2
|
136
|
+
include Ni::Main
|
137
|
+
|
138
|
+
receive :param_1
|
139
|
+
|
140
|
+
action :perform do
|
141
|
+
context.param_2 = context.param_1
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
Interactor1.perform(param_1: 1) # will not raise any exceptions
|
146
|
+
Interactor2.perform(param_1: 1) # will raise "The `param_2` is not allowed to write"
|
147
|
+
```
|
148
|
+
|
149
|
+
Please note that defining read/write rules are not working as a contracts.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
class Interactor
|
153
|
+
include Ni::Main
|
154
|
+
|
155
|
+
receive :param_1
|
156
|
+
mutate :param_2
|
157
|
+
provide :param_3
|
158
|
+
|
159
|
+
action :perform do
|
160
|
+
#do nothing
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
result = Interactor.perform
|
165
|
+
result.success? # true
|
166
|
+
```
|
167
|
+
|
168
|
+
You can define contracts with two different ways:
|
169
|
+
|
170
|
+
Method contracts
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
class Interactor
|
174
|
+
include Ni::Main
|
175
|
+
|
176
|
+
receive :param_1, :present?
|
177
|
+
mutate :param_2, :zero?
|
178
|
+
provide :param_3, :zero?
|
179
|
+
|
180
|
+
action :perform do
|
181
|
+
context.param_3 = 1
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
Interactor.perform # will raise "Value of `param_1` doesn't match to contract :present?"
|
186
|
+
Interactor.perform(param_1: true, param_2: 1) # will raise "Value of `param_2` doesn't match to contract :zero?"
|
187
|
+
Interactor.perform(param_1: true, param_2: 0) # will raise "Value of `param_3` doesn't match to contract :zero?"
|
188
|
+
```
|
189
|
+
|
190
|
+
Lambda contracts
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
class Interactor
|
194
|
+
include Ni::Main
|
195
|
+
|
196
|
+
receive :param_1, -> (val) { val == true }
|
197
|
+
mutate :param_2, -> (val) { val == 0 }
|
198
|
+
provide :param_3, -> (val) { val == 0 }
|
199
|
+
|
200
|
+
action :perform do
|
201
|
+
context.param_3 = 1
|
202
|
+
end
|
203
|
+
en
|
204
|
+
|
205
|
+
Interactor.perform # will raise "Value of `param_1` doesn't match to contract"
|
206
|
+
Interactor.perform(param_1: true, param_2: 1) # will raise "Value of `param_2` doesn't match to contract"
|
207
|
+
Interactor.perform(param_1: true, param_2: 0) # will raise "Value of `param_3` doesn't match to contract"
|
208
|
+
```
|
209
|
+
|
210
|
+
### An actions DSL
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
class Interactor1
|
214
|
+
include Ni::Main
|
215
|
+
|
216
|
+
receive :param_1
|
217
|
+
|
218
|
+
action :perform do
|
219
|
+
self.context.errors.add(:base, 'An error') if context.param_1 == '2'
|
220
|
+
end
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
You can define new actions based on the existing ones
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
class Interactor
|
228
|
+
include Ni::Main
|
229
|
+
|
230
|
+
receive :custom_option
|
231
|
+
mutate :param_1
|
232
|
+
|
233
|
+
action :create do
|
234
|
+
if context.custom_option
|
235
|
+
context.param_1 = 'new_value_1'
|
236
|
+
else
|
237
|
+
context.param_1 = 'new_value'
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def self.create!(params={})
|
242
|
+
create params.merge(custom_option: true)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
result = Interactor.create!(param_1)
|
247
|
+
result.context.param_1 # new_value_1
|
248
|
+
```
|
249
|
+
|
250
|
+
Or, even redefine an interface
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
class Interactor
|
254
|
+
include Ni::Main
|
255
|
+
|
256
|
+
receive :custom_option
|
257
|
+
mutate :param_1
|
258
|
+
|
259
|
+
action :create do
|
260
|
+
if context.custom_option
|
261
|
+
context.param_1 = 'new_value_1'
|
262
|
+
else
|
263
|
+
context.param_1 = 'new_value'
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def self.create(params={})
|
268
|
+
perform_custom :create, params.merge(custom_option: true)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
result = Interactor.create!(param_1)
|
273
|
+
result.context.param_1 # new_value_1
|
274
|
+
```
|
275
|
+
|
276
|
+
There are two callbacks which called when need to check a condition `on_checking_continue_signal` and `on_continue_signal_checked`. You can use them to get an additional control for your flow
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
class Organizer
|
280
|
+
include Ni::Main
|
281
|
+
|
282
|
+
storage Ni::Storages::Default
|
283
|
+
metadata_repository Ni::Storages::ActiveRecordMetadataRepository
|
284
|
+
|
285
|
+
mutate :user_1
|
286
|
+
mutate :user_2
|
287
|
+
mutate :before_cheking
|
288
|
+
mutate :after_cheking
|
289
|
+
|
290
|
+
action :perform do
|
291
|
+
context.user_1 = User.create! email: 'user1@test.com', password: '111111'
|
292
|
+
end
|
293
|
+
.then(:create_second_user)
|
294
|
+
.wait_for(:outer_action_performed)
|
295
|
+
|
296
|
+
private
|
297
|
+
|
298
|
+
def on_checking_continue_signal(unit)
|
299
|
+
context.before_cheking = User.where(email: 'before@test.com').first_or_create! password: '111111'
|
300
|
+
end
|
301
|
+
|
302
|
+
def on_continue_signal_checked(unit, wait_cheking_result)
|
303
|
+
context.after_cheking = User.where(email: 'after@test.com').first_or_create! password: '111111'
|
304
|
+
end
|
305
|
+
|
306
|
+
def create_second_user
|
307
|
+
context.user_2 = User.create! email: 'user2@test.com', password: '111111'
|
308
|
+
end
|
309
|
+
end
|
310
|
+
```
|
311
|
+
|
312
|
+
### Callbacks
|
313
|
+
|
314
|
+
When you use an action DSL you can define a callbacks. The Ni will call it automatically.
|
315
|
+
|
316
|
+
**Note! All actions will have the same callbacks and will receive action name as an argument**
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
class Interactor
|
320
|
+
include Ni::Main
|
321
|
+
|
322
|
+
mutate :param_1
|
323
|
+
mutate :param_2
|
324
|
+
mutate :param_3
|
325
|
+
|
326
|
+
def before_action(name)
|
327
|
+
if name == :perform
|
328
|
+
context.param_1 = 'perform_1'
|
329
|
+
else
|
330
|
+
context.param_1 = 'custom_perform_1'
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def after_action(name)
|
335
|
+
if name == :perform
|
336
|
+
context.param_3 = 'perform_3'
|
337
|
+
else
|
338
|
+
context.param_3 = 'custom_perform_3'
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
action :perform do
|
343
|
+
context.param_2 = 'perform_2'
|
344
|
+
end
|
345
|
+
.provide(:param_1, :param_2, :param_3)
|
346
|
+
|
347
|
+
action :custom_perform do
|
348
|
+
context.param_2 = 'custom_perform_2'
|
349
|
+
end
|
350
|
+
.provide(:param_1, :param_2, :param_3)
|
351
|
+
end
|
352
|
+
|
353
|
+
result, param_1, param_2, param_3 = Interactor.perform
|
354
|
+
|
355
|
+
param_1 # 'perform_1'
|
356
|
+
param_2 # 'perform_2'
|
357
|
+
param_3 # 'perform_3'
|
358
|
+
|
359
|
+
result, param_1, param_2, param_3 = Interactor.custom_perform
|
360
|
+
|
361
|
+
param_1 # 'custom_perform_1'
|
362
|
+
param_2 # 'custom_perform_2'
|
363
|
+
param_3 # 'custom_perform_3'
|
364
|
+
```
|
365
|
+
|
366
|
+
### Organizers
|
367
|
+
|
368
|
+
Ni allows to build your domain flow. It has a super simple DSL for organizing Interactors into a chains.
|
369
|
+
|
370
|
+
```ruby
|
371
|
+
class Interactor2
|
372
|
+
include Ni::Main
|
373
|
+
|
374
|
+
receive :param_2
|
375
|
+
provide :param_3
|
376
|
+
|
377
|
+
def perform
|
378
|
+
context.param_3 = context.param_2 + 1
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
class Interactor3
|
383
|
+
include Ni::Main
|
384
|
+
|
385
|
+
receive :param_4
|
386
|
+
provide :param_5
|
387
|
+
|
388
|
+
action :custom_action do
|
389
|
+
context.param_5 = context.param_4 + 1
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
class Interactor
|
394
|
+
include Ni::Main
|
395
|
+
|
396
|
+
mutate :param_1
|
397
|
+
mutate :param_2
|
398
|
+
mutate :param_3
|
399
|
+
mutate :param_4
|
400
|
+
mutate :param_5
|
401
|
+
mutate :final_value
|
402
|
+
|
403
|
+
action :perform do
|
404
|
+
context.param_1 = 1
|
405
|
+
end
|
406
|
+
.then(:step_1)
|
407
|
+
.then(Interactor2)
|
408
|
+
.then do
|
409
|
+
context.param_4 = context.param_3 + 1
|
410
|
+
end
|
411
|
+
.then(Interactor3, :custom_action)
|
412
|
+
.then(:step_2)
|
413
|
+
.then do
|
414
|
+
context.final_value = context.param_5 + 1
|
415
|
+
end
|
416
|
+
|
417
|
+
private
|
418
|
+
|
419
|
+
def step_1
|
420
|
+
context.param_2 = context.param_1 + 1
|
421
|
+
end
|
422
|
+
|
423
|
+
def step_2
|
424
|
+
context.param_5 = context.param_4 + 1
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
result = Interactor.perform
|
429
|
+
|
430
|
+
result.context.param_1 # 1
|
431
|
+
result.context.param_2 # 2
|
432
|
+
result.context.param_3 # 3
|
433
|
+
result.context.param_4 # 4
|
434
|
+
result.context.param_5 # 5
|
435
|
+
result.context.final_value # 6
|
436
|
+
```
|
437
|
+
|
438
|
+
### Context Isolation
|
439
|
+
|
440
|
+
Building application within the set of small reusable modules are good approach. This is exactly what interactor pattern means - follow the SRP principle. Use each Interactor independently or gather them into Organizer chain.
|
441
|
+
The contracts will help you to keep your code solid.
|
442
|
+
|
443
|
+
But, there are a case, when a shared state could be an issue. Because the DSL allows you to build and combine interactors in any way you want with a single interface, you can face the situation when one chain will perform another one as it's step. So, in this case the step will be not a single Interactor, but a chain of the another ones.
|
444
|
+
|
445
|
+
And this second chain can contain interactors, which changes the same params. Or just contain the same Interactors.
|
446
|
+
|
447
|
+
In this case you can perform it in isolation.
|
448
|
+
|
449
|
+
```ruby
|
450
|
+
class Interactor2
|
451
|
+
include Ni::Main
|
452
|
+
|
453
|
+
mutate :param_1
|
454
|
+
provide :param_2
|
455
|
+
|
456
|
+
action :custom_action do
|
457
|
+
context.param_2 = context.param_1 + 1
|
458
|
+
context.param_1 = 25
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
class Organizer1
|
463
|
+
include Ni::Main
|
464
|
+
|
465
|
+
mutate :param_1
|
466
|
+
mutate :param_2
|
467
|
+
mutate :param_3
|
468
|
+
|
469
|
+
action :perform do
|
470
|
+
# an empty initializer
|
471
|
+
end
|
472
|
+
.then(Interactor1)
|
473
|
+
.isolate(Interactor2, :custom_action, receive: [:param_1], provide: [:param_2])
|
474
|
+
.then(Interactor3)
|
475
|
+
end
|
476
|
+
```
|
477
|
+
|
478
|
+
The `Interactor2` can't change `param_1` value. The `Interactor3` will receive original `param_1` value.
|
479
|
+
|
480
|
+
### Errors and Exceptions
|
481
|
+
|
482
|
+
The Ni has a DSL to control the flow and exceptions when performing the chain.
|
483
|
+
|
484
|
+
To stop the interaction execution just add an error to context. Also you could specify the failure callback
|
485
|
+
|
486
|
+
```ruby
|
487
|
+
class Interactor1
|
488
|
+
include Ni::Main
|
489
|
+
|
490
|
+
def perform
|
491
|
+
context.errors.add :base, 'Something went wrong'
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
class Interactor2
|
496
|
+
include Ni::Main
|
497
|
+
|
498
|
+
provide :param_1
|
499
|
+
|
500
|
+
def perform
|
501
|
+
context.param_1 = 1
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
class Organizer1
|
506
|
+
include Ni::Main
|
507
|
+
|
508
|
+
provide :failure_value
|
509
|
+
|
510
|
+
action :perform do
|
511
|
+
# empty initializer
|
512
|
+
end
|
513
|
+
.then(Interactor1)
|
514
|
+
.then(Interactor2)
|
515
|
+
.failure do
|
516
|
+
context.failure_value = 'fail'
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
result = Organizer1.perform
|
521
|
+
|
522
|
+
result.success? # false
|
523
|
+
result.context.param_1 # nil
|
524
|
+
result.context.failure_value # 'fail'
|
525
|
+
```
|
526
|
+
|
527
|
+
Also allows to handle exceptions. Check the `rescue_from` section. You can specify the exceptions handlers or specify the default handler for all exceptions.
|
528
|
+
|
529
|
+
```ruby
|
530
|
+
class Ex1 < Exception
|
531
|
+
end
|
532
|
+
|
533
|
+
class Ex2 < Exception
|
534
|
+
end
|
535
|
+
|
536
|
+
class Interactor1
|
537
|
+
include Ni::Main
|
538
|
+
|
539
|
+
def perform
|
540
|
+
raise Es::Ex2.new
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
class Interactor2
|
545
|
+
include Ni::Main
|
546
|
+
|
547
|
+
provide :param_1
|
548
|
+
|
549
|
+
def perform
|
550
|
+
context.param_1 = 1
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
class Organizer1
|
555
|
+
include Ni::Main
|
556
|
+
|
557
|
+
provide :exception_value
|
558
|
+
|
559
|
+
action :perform do
|
560
|
+
# empty initializer
|
561
|
+
end
|
562
|
+
.then(Es::Interactor1)
|
563
|
+
.then(Es::Interactor2)
|
564
|
+
.rescue_from Es::Ex1, Es::Ex2 do
|
565
|
+
context.exception_value = 'fail'
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
class Organizer2
|
570
|
+
include Ni::Main
|
571
|
+
|
572
|
+
provide :exception_value
|
573
|
+
|
574
|
+
action :perform do
|
575
|
+
# empty initializer
|
576
|
+
end
|
577
|
+
.then(Es::Interactor1)
|
578
|
+
.then(Es::Interactor2)
|
579
|
+
.rescue_from do
|
580
|
+
context.exception_value = 'fail'
|
581
|
+
end
|
582
|
+
end
|
583
|
+
|
584
|
+
result = Organizer1.perform
|
585
|
+
|
586
|
+
result.success? # false
|
587
|
+
result.context.param_1 # eq nil
|
588
|
+
result.context.exception_value # 'fail'
|
589
|
+
```
|
590
|
+
|
591
|
+
### Pause your flow
|
592
|
+
|
593
|
+
Ni allows you to pause your flow and continue it later. Ni will do the job, but it will require some configuration.
|
594
|
+
|
595
|
+
When chain meets the `wait_for(:outer_action_performed)` it stops the chain performance. Then you can call the same method and pass two options to tell Ni that you want to continue your performance, but not to start a new one
|
596
|
+
|
597
|
+
**Strict! The `:outer_action_performed` not just a human readable name, but also an unique ID and it's global for the whole application. You can't use the same names in different interactors!**
|
598
|
+
|
599
|
+
```ruby
|
600
|
+
Organizer.perform(wait_completed_for: :outer_action_performed, system_uid: uid)
|
601
|
+
```
|
602
|
+
|
603
|
+
The uid you can get from the previous interactor execution. It's stored in context as `system_uid`. It will not just continue the performance, but also will restore yours context, if you want.
|
604
|
+
|
605
|
+
Also you can set the multiple conditions and even add a dynamic condition.
|
606
|
+
|
607
|
+
```ruby
|
608
|
+
wait_for(more_users_expected:
|
609
|
+
[
|
610
|
+
:moderator_user_registered,
|
611
|
+
[:user_registration, -> (context) { User.count >= 6 }]
|
612
|
+
]
|
613
|
+
)
|
614
|
+
```
|
615
|
+
|
616
|
+
The `more_users_expected` is just a syntax key. To back to this steps you need to use a symbol names from the described conditions.
|
617
|
+
|
618
|
+
```ruby
|
619
|
+
Organizer.perform(wait_completed_for: :moderator_user_registered, system_uid: uid)
|
620
|
+
Organizer.perform(wait_completed_for: :user_registration, system_uid: uid)
|
621
|
+
```
|
622
|
+
|
623
|
+
In the example above you need to pass the `:user_registration` until you will have 6 users. But please be aware, you should stop calling the interactor by yourself, when the condition becomes wrong. For now Ni has no any internal logic to track when conditions becomes irrelevant
|
624
|
+
|
625
|
+
Also you need to know about multiple conditions it's that the Metadata Repository is required. What is it will be described right after the code example.
|
626
|
+
|
627
|
+
```ruby
|
628
|
+
class Organizer
|
629
|
+
include Ni::Main
|
630
|
+
|
631
|
+
storage Ni::Storages::Default
|
632
|
+
metadata_repository Ni::Storages::ActiveRecordMetadataRepository
|
633
|
+
|
634
|
+
mutate :user_1
|
635
|
+
mutate :user_2
|
636
|
+
mutate :user_3
|
637
|
+
mutate :admin_user
|
638
|
+
mutate :done
|
639
|
+
|
640
|
+
action :perform do
|
641
|
+
context.user_1 = User.create! email: 'user1@test.com', password: '111111'
|
642
|
+
end
|
643
|
+
.then(:create_second_user)
|
644
|
+
.wait_for(:outer_action_performed)
|
645
|
+
.then(:create_third_user)
|
646
|
+
.wait_for(more_users_expected:
|
647
|
+
[
|
648
|
+
:moderator_user_registered,
|
649
|
+
[:user_registration, -> (context) { User.count >= 6 }]
|
650
|
+
]
|
651
|
+
)
|
652
|
+
.then(:create_admin)
|
653
|
+
.wait_for(:all_thigs_done)
|
654
|
+
.then do
|
655
|
+
context.done = true
|
656
|
+
end
|
657
|
+
|
658
|
+
private
|
659
|
+
|
660
|
+
def create_second_user
|
661
|
+
context.user_2 = User.create! email: 'user2@test.com', password: '111111'
|
662
|
+
end
|
663
|
+
|
664
|
+
def create_third_user
|
665
|
+
context.user_3 = User.create! email: 'user3@test.com', password: '111111'
|
666
|
+
end
|
667
|
+
|
668
|
+
def create_admin
|
669
|
+
context.admin_user = User.create! email: 'admin@test.com', password: '111111'
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
first_result = Organizer.perform.context
|
674
|
+
uid = first_result.system_uid
|
675
|
+
|
676
|
+
first_result.user_1.email # 'user1@test.com'
|
677
|
+
first_result.user_2.email #'user2@test.com'
|
678
|
+
first_result.user_3 # nil
|
679
|
+
|
680
|
+
second_result = Organizer.perform(wait_completed_for: :outer_action_performed, system_uid: uid).context
|
681
|
+
|
682
|
+
second_result.user_1.email # 'user1@test.com'
|
683
|
+
second_result.user_2.email # 'user2@test.com'
|
684
|
+
second_result.user_3.email # 'user3@test.com'
|
685
|
+
second_result.admin_user # nil
|
686
|
+
|
687
|
+
User.create! email: 'moderator@test.com', password: '111111'
|
688
|
+
|
689
|
+
# The third result will be the same because users count less then 6
|
690
|
+
third_result = Organizer.perform(wait_completed_for: :moderator_user_registered, system_uid: uid).context
|
691
|
+
third_result.user_1.email # 'user1@test.com'
|
692
|
+
third_result.user_2.email # 'user2@test.com'
|
693
|
+
third_result.user_3.email # 'user3@test.com'
|
694
|
+
third_result.admin_user # nil
|
695
|
+
|
696
|
+
User.create! email: 'user4@test.com', password: '111111'
|
697
|
+
|
698
|
+
# The fourth result will be the same because need one more user
|
699
|
+
fourth_result = Organizer.perform(wait_completed_for: :user_registration, system_uid: uid).context
|
700
|
+
fourth_result.user_1.email # 'user1@test.com'
|
701
|
+
fourth_result.user_2.email # 'user2@test.com'
|
702
|
+
fourth_result.user_3.email # 'user3@test.com'
|
703
|
+
fourth_result.admin_user # nil
|
704
|
+
|
705
|
+
User.create! email: 'user5@test.com', password: '111111'
|
706
|
+
|
707
|
+
# Now the admin creation is available
|
708
|
+
result = Organizer.perform(wait_completed_for: :user_registration, system_uid: uid).context
|
709
|
+
result.user_1.email # 'user1@test.com'
|
710
|
+
result.user_2.email # 'user2@test.com'
|
711
|
+
result.user_3.email # 'user3@test.com'
|
712
|
+
result.admin_user.email # 'admin@test.com'
|
713
|
+
result.done # nil
|
714
|
+
|
715
|
+
# This last step checks that skip for multiple conditions work as well
|
716
|
+
result = Organizer.perform(wait_completed_for: :all_thigs_done, system_uid: uid).context
|
717
|
+
result.done # true
|
718
|
+
```
|
719
|
+
|
720
|
+
It's smart enough to get, that the wait part is not in the top level flow. For example Organizer1 calls Organizer2 as a step and Organizer2 has the `wait_for` part.
|
721
|
+
|
722
|
+
More detailed example. The Organizer interactor uses the OrganizerLevel2 one. The OrganizerLevel2 interactor uses the OrganizerLevel3 one. Each of them has own `wait_for` logic. And it works correct, because Ni recursively parse the interactors tree and use it to control it's flow.
|
723
|
+
|
724
|
+
```ruby
|
725
|
+
class OrganizerLevel3
|
726
|
+
include Ni::Main
|
727
|
+
|
728
|
+
storage Ni::Storages::Default
|
729
|
+
metadata_repository Ni::Storages::ActiveRecordMetadataRepository
|
730
|
+
|
731
|
+
mutate :user_3
|
732
|
+
|
733
|
+
action :perform do
|
734
|
+
# empty initializer
|
735
|
+
end
|
736
|
+
.wait_for(:ready_create_third_user)
|
737
|
+
.then do
|
738
|
+
context.user_3 = User.create! email: 'user3@test.com', password: '111111'
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
class OrganizerLevel2
|
743
|
+
include Ni::Main
|
744
|
+
|
745
|
+
storage Ni::Storages::Default
|
746
|
+
metadata_repository Ni::Storages::ActiveRecordMetadataRepository
|
747
|
+
|
748
|
+
mutate :user_2
|
749
|
+
|
750
|
+
action :perform do
|
751
|
+
# empty initializer
|
752
|
+
end
|
753
|
+
.wait_for(:ready_create_second_user)
|
754
|
+
.then do
|
755
|
+
context.user_2 = User.create! email: 'user2@test.com', password: '111111'
|
756
|
+
end
|
757
|
+
.then(OrganizerLevel3)
|
758
|
+
end
|
759
|
+
|
760
|
+
class Organizer
|
761
|
+
include Ni::Main
|
762
|
+
|
763
|
+
storage Ni::Storages::Default
|
764
|
+
metadata_repository Ni::Storages::ActiveRecordMetadataRepository
|
765
|
+
|
766
|
+
mutate :user_1
|
767
|
+
mutate :user_2
|
768
|
+
mutate :user_3
|
769
|
+
mutate :admin_user
|
770
|
+
mutate :done
|
771
|
+
|
772
|
+
action :perform do
|
773
|
+
context.user_1 = User.create! email: 'user1@test.com', password: '111111'
|
774
|
+
end
|
775
|
+
.then(OrganizerLevel2)
|
776
|
+
.wait_for(:ready_create_admin)
|
777
|
+
.then(:create_admin)
|
778
|
+
.wait_for(:final_step)
|
779
|
+
.then do
|
780
|
+
context.done = true
|
781
|
+
end
|
782
|
+
|
783
|
+
private
|
784
|
+
|
785
|
+
def create_admin
|
786
|
+
context.admin_user = User.create! email: 'admin@test.com', password: '111111'
|
787
|
+
end
|
788
|
+
end
|
789
|
+
|
790
|
+
first_result = Sowf::Organizer.perform.context
|
791
|
+
uid = first_result.system_uid
|
792
|
+
|
793
|
+
first_result.user_1.email # 'user1@test.com'
|
794
|
+
first_result.user_2 # nil
|
795
|
+
first_result.user_3 # nil
|
796
|
+
|
797
|
+
second_result = Sowf::Organizer.perform(wait_completed_for: :ready_create_second_user, system_uid: uid).context
|
798
|
+
|
799
|
+
second_result.user_1.email # 'user1@test.com'
|
800
|
+
second_result.user_2.email # 'user2@test.com'
|
801
|
+
second_result.user_3 # nil
|
802
|
+
second_result.admin_user # nil
|
803
|
+
|
804
|
+
second_result = Sowf::Organizer.perform(wait_completed_for: :ready_create_third_user, system_uid: uid).context
|
805
|
+
|
806
|
+
second_result.user_1.email # 'user1@test.com'
|
807
|
+
second_result.user_2.email # 'user2@test.com'
|
808
|
+
second_result.user_3.email # 'user3@test.com'
|
809
|
+
second_result.admin_user # nil
|
810
|
+
|
811
|
+
# Now the admin creation is available
|
812
|
+
result = Sowf::Organizer.perform(wait_completed_for: :ready_create_admin, system_uid: uid).context
|
813
|
+
result.user_1.email # 'user1@test.com'
|
814
|
+
result.user_2.email # 'user2@test.com'
|
815
|
+
result.user_3.email # 'user3@test.com'
|
816
|
+
result.admin_user.email # 'admin@test.com'
|
817
|
+
result.done # nil
|
818
|
+
|
819
|
+
# This last step checks that skip for multiple conditions work as well
|
820
|
+
result = Sowf::Organizer.perform(wait_completed_for: :final_step, system_uid: uid).context
|
821
|
+
result.done # true
|
822
|
+
```
|
823
|
+
|
824
|
+
So, in this example you may notice the two new configurations: Storage and Metadata Repository.
|
825
|
+
|
826
|
+
```ruby
|
827
|
+
storage Ni::Storages::Default
|
828
|
+
metadata_repository Ni::Storages::ActiveRecordMetadataRepository
|
829
|
+
```
|
830
|
+
|
831
|
+
The Storage class implements logic for storing your context and restoring it. There is a default storage `Ni::Storages::Default` you can use. For now it supports only the ActiveRecord records and collections, relations and so one.
|
832
|
+
|
833
|
+
But it's easy to implement your own one. Just create a new class, inherite from `Ni::Storages::Default`. And you will have a two option, how to work with context. Check the `storages/default_spec.rb` to get an examples.
|
834
|
+
|
835
|
+
The Metadata Repository needed to store some metadata. I.e. for Storage it will store the metadata, which will allow to restore you context. For the multiple wait_for it will store the list of already passed conditions.
|
836
|
+
|
837
|
+
Ni has an implemented ActiveRecord repository, which can be used with rails. Just create a relavant table for it.
|
838
|
+
|
839
|
+
```ruby
|
840
|
+
create_table :ni_metadata do |t|
|
841
|
+
t.string :uid, null: false
|
842
|
+
t.string :key, null: false
|
843
|
+
t.datetime :run_timer_at
|
844
|
+
t.text :data
|
845
|
+
|
846
|
+
t.timestamps
|
847
|
+
end
|
848
|
+
|
849
|
+
add_index :ni_metadata, [:uid, :key], unique: true
|
850
|
+
```
|
851
|
+
|
852
|
+
These classes has a pretty simple interfaces, so it will be easy to develop your own ones for your needs.
|
853
|
+
|
854
|
+
### Unique IDs
|
855
|
+
|
856
|
+
Interactors can have an explicit `unique_id`. This will allow to use an identifier when building your flow. I.e. the WaitFor could receive interactor class as an option, if the unique id was provided.
|
857
|
+
|
858
|
+
Unique_id should be a symbol. By default it's a class name.
|
859
|
+
|
860
|
+
```ruby
|
861
|
+
class Interactor1
|
862
|
+
include Ni::Interactor
|
863
|
+
end
|
864
|
+
class Interactor2
|
865
|
+
include Ni::Interactor
|
866
|
+
|
867
|
+
unique_id :some_unique_id
|
868
|
+
end
|
869
|
+
|
870
|
+
Interactor1.interactor_id # 'Interactor1'
|
871
|
+
Interactor1.interactor_id! # Will raise "The Interactor1 requires an explicit definition of the unique id"
|
872
|
+
|
873
|
+
Interactor2.interactor_id # :some_unique_id
|
874
|
+
Interactor2.interactor_id! # :some_unique_id
|
875
|
+
```
|
876
|
+
|
877
|
+
The WaitFor example
|
878
|
+
|
879
|
+
```ruby
|
880
|
+
class ExternalThirdUser
|
881
|
+
include Ni::Main
|
882
|
+
|
883
|
+
unique_id :external_third_user
|
884
|
+
|
885
|
+
action :perform do
|
886
|
+
end
|
887
|
+
end
|
888
|
+
|
889
|
+
class OrganizerLevel3
|
890
|
+
include Ni::Main
|
891
|
+
|
892
|
+
storage Ni::Storages::Default
|
893
|
+
metadata_repository Ni::Storages::ActiveRecordMetadataRepository
|
894
|
+
|
895
|
+
mutate :user_3
|
896
|
+
|
897
|
+
action :perform do
|
898
|
+
# empty initializer
|
899
|
+
end
|
900
|
+
.wait_for(Sowf::ExternalThirdUser)
|
901
|
+
.then do
|
902
|
+
context.user_3 = User.create! email: 'user3@test.com', password: '111111'
|
903
|
+
end
|
904
|
+
end
|
905
|
+
```
|
906
|
+
|
907
|
+
It doesn't matter how to continue chain, by a symbol ID or providing a class
|
908
|
+
|
909
|
+
### Flow branches
|
910
|
+
|
911
|
+
Describing a flow you may face with the situation when flow splits to several branches and it depends on some conditions which one will be performed.
|
912
|
+
|
913
|
+
There are two ways to define a branch.
|
914
|
+
|
915
|
+
1. Use an existing Interactor.
|
916
|
+
2. Use a branch id
|
917
|
+
|
918
|
+
```ruby
|
919
|
+
action :perform do
|
920
|
+
end
|
921
|
+
.branch(SomeExistingInteractor, when: -> (context) { context.param_1 == 666 })
|
922
|
+
.branch(:first_level_valid_branch, when: -> (context) { context.param_1 == 1 }) do
|
923
|
+
receive :param_1
|
924
|
+
|
925
|
+
action :perform do
|
926
|
+
end
|
927
|
+
end
|
928
|
+
```
|
929
|
+
|
930
|
+
In both cases you need to specify a when condition by defining a lambda. Branches supports all features of the interactors, like WaitFor and others.
|
931
|
+
|
932
|
+
**There are some pitfals with branches. Because all interactors share a single state, the first branch may change the context and made the second one also valid**
|
933
|
+
|
934
|
+
More detailed example
|
935
|
+
|
936
|
+
```ruby
|
937
|
+
class Level1NotUsedBranch
|
938
|
+
include Ni::Main
|
939
|
+
|
940
|
+
def perform
|
941
|
+
raise 'Should not be here'
|
942
|
+
end
|
943
|
+
end
|
944
|
+
|
945
|
+
class Level2NotUsedBranch
|
946
|
+
include Ni::Main
|
947
|
+
|
948
|
+
def perform
|
949
|
+
raise 'Should not be here'
|
950
|
+
end
|
951
|
+
end
|
952
|
+
|
953
|
+
class Organizer1
|
954
|
+
include Ni::Main
|
955
|
+
|
956
|
+
mutate :param_1
|
957
|
+
|
958
|
+
action :perform do
|
959
|
+
context.param_1 = 1
|
960
|
+
end
|
961
|
+
.branch(Level1NotUsedBranch, when: -> (context) { context.param_1 == 666 })
|
962
|
+
.branch :first_level_valid_branch, when: -> (context) { context.param_1 == 1 } do
|
963
|
+
|
964
|
+
receive :param_1
|
965
|
+
|
966
|
+
action :perform do
|
967
|
+
end
|
968
|
+
.branch :second_level_valid_branch, when: -> (context) { context.param_1 == 1 } do
|
969
|
+
mutate :param_1
|
970
|
+
|
971
|
+
action :perform do
|
972
|
+
context.param_1 = 2
|
973
|
+
end
|
974
|
+
end
|
975
|
+
.branch(Level2NotUsedBranch, when: -> (context) { context.param_1 == 666 })
|
976
|
+
end
|
977
|
+
end
|
978
|
+
|
979
|
+
expect(Organizer1.perform.context.param_1 # 2
|
980
|
+
```
|
981
|
+
|
982
|
+
|
983
|
+
|
984
|
+
### TODO:
|
985
|
+
|
986
|
+
Continue logic:
|
987
|
+
- For now Ni has no any internal logic to track when conditions becomes irrelevant
|
988
|
+
Parallel execution
|
989
|
+
Implement some wrap logic. I.e. a way to put operations in transaction
|
990
|
+
Ensure all features will also work for the Ancestors
|
991
|
+
|
992
|
+
Allow to break execution with success or failure, cancel or terminate
|
993
|
+
- fix tests
|
994
|
+
- Chain methods
|
995
|
+
- Inline flow + Branches
|
996
|
+
- Refactoring to allow just a simple methods, not only from chain
|
997
|
+
|
998
|
+
```ruby
|
999
|
+
|
1000
|
+
class Organizer1
|
1001
|
+
include Ni::Main
|
1002
|
+
|
1003
|
+
provide :failure_value
|
1004
|
+
|
1005
|
+
storage CustomStorage
|
1006
|
+
metadata_repository MetadataRepository
|
1007
|
+
|
1008
|
+
action :perform do
|
1009
|
+
# empty initializer
|
1010
|
+
end
|
1011
|
+
.then(Interactor1, on_cancel: CancelInteractor, on_failure: FailureInteractor, on_terminate: TerminateInteractor) # Same for branches
|
1012
|
+
.wait_for(:interactor2_ready)
|
1013
|
+
.then(Interactor2)
|
1014
|
+
.async(continue_from: :interactor10_ready,
|
1015
|
+
steps: [
|
1016
|
+
Interactor3,
|
1017
|
+
[Intreractor4, :custom_action]
|
1018
|
+
]
|
1019
|
+
)
|
1020
|
+
.wait_for(:interactor10_ready)
|
1021
|
+
.handoff_to('Remote microservice',
|
1022
|
+
via: MyHTTPChannel,
|
1023
|
+
continue_from: :interactor11_ready
|
1024
|
+
)
|
1025
|
+
.wait_for(:interactor11_ready)
|
1026
|
+
.then(Interactor11)
|
1027
|
+
.branch :my_new_branch, when: -> (context) { context.param_1 == 1 } do
|
1028
|
+
action :perform do
|
1029
|
+
end
|
1030
|
+
.then(Interactor12)
|
1031
|
+
.wait_for(:interactor13_ready)
|
1032
|
+
.then(Interactor13)
|
1033
|
+
.cancel!
|
1034
|
+
end
|
1035
|
+
.branch :other_branch, when: -> (context) { context.param_1 == 2 } do
|
1036
|
+
action :perform do
|
1037
|
+
end
|
1038
|
+
.then(Interactor14)
|
1039
|
+
.branch :inner_branch, when: -> (context) { context.param_1 == 1 } do
|
1040
|
+
action :perform do
|
1041
|
+
end
|
1042
|
+
.then(Interactor15)
|
1043
|
+
end
|
1044
|
+
.branch(Intreractor4, :custom_action, when: -> (context) { context.param_1 == 1 })
|
1045
|
+
.terminate!
|
1046
|
+
|
1047
|
+
end
|
1048
|
+
.wait_for(
|
1049
|
+
all_ready: [
|
1050
|
+
:interactor15,
|
1051
|
+
[:interactor16, -> (context) { context.param_1 == '10' } ],
|
1052
|
+
:interactor17
|
1053
|
+
],
|
1054
|
+
timer: [ -> { 10.minutes.from_now }, InteractorTimer, :custom]
|
1055
|
+
)
|
1056
|
+
.failure do
|
1057
|
+
context.failure_value = 'fail'
|
1058
|
+
end
|
1059
|
+
end
|
1060
|
+
```
|
1061
|
+
|