ni 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+