trailblazer-macro 2.1.11 → 2.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,940 @@
1
+ require "test_helper"
2
+
3
+
4
+ # step Macro::Each(:report_templates, key: :report_template) {
5
+ # step Subprocess(ReportTemplate::Update), input: :input_report_template
6
+ # fail :set_report_template_errors
7
+ # }
8
+
9
+ # def report_templates(ctx, **) ctx["result.contract.default"].report_templates
10
+ # end
11
+
12
+ class EachTest < Minitest::Spec
13
+ class Composer < Struct.new(:full_name, :email)
14
+ end
15
+
16
+ class Mailer
17
+ def self.send(**options)
18
+ @send_options << options
19
+ end
20
+
21
+ class << self
22
+ attr_accessor :send_options
23
+ end
24
+ end
25
+
26
+ #@ operation has {#composers_for_each}
27
+ module B
28
+ class Song < Struct.new(:id, :title, :band, :composers)
29
+ def self.find_by(id:)
30
+ if id == 2
31
+ return Song.new(id, nil, nil, [Composer.new("Fat Mike", "mike@fat.wreck"), Composer.new("El Hefe")])
32
+ end
33
+
34
+ if id == 3
35
+ return Song.new(id, nil, nil, [Composer.new("Fat Mike", "mike@fat.wreck"), Composer.new("El Hefe", "scammer@spam")])
36
+ end
37
+
38
+ Song.new(id, nil, nil, [Composer.new("Fat Mike"), Composer.new("El Hefe")])
39
+ end
40
+ end
41
+
42
+ #:each
43
+ module Song::Activity
44
+ class Cover < Trailblazer::Activity::Railway
45
+ step :model
46
+ #:each-dataset
47
+ step Each(dataset_from: :composers_for_each, collect: true) {
48
+ step :notify_composers
49
+ }
50
+ #:each-dataset end
51
+ step :rearrange
52
+
53
+ # "decider interface"
54
+ def composers_for_each(ctx, model:, **)
55
+ model.composers
56
+ end
57
+
58
+ def notify_composers(ctx, index:, item:, **)
59
+ ctx[:value] = [index, item.full_name]
60
+ end
61
+
62
+ #~meths
63
+ def model(ctx, params:, **)
64
+ ctx[:model] = Song.find_by(id: params[:id])
65
+ end
66
+
67
+ include T.def_steps(:rearrange)
68
+ #~meths end
69
+ end
70
+ end
71
+ #:each end
72
+ end # B
73
+
74
+ it "allows a dataset compute in the hosting activity" do
75
+ #@ {:dataset} is not part of the {ctx}.
76
+ assert_invoke B::Song::Activity::Cover, params: {id: 1},
77
+ expected_ctx_variables: {
78
+ model: B::Song.find_by(id: 1),
79
+ collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
80
+ },
81
+ seq: "[:rearrange]"
82
+
83
+ =begin
84
+ #:collected_from_each
85
+ #~ctx_to_result
86
+ ctx = {params: {id: 1}} # Song 1 has two composers.
87
+
88
+ signal, (ctx, _) = Trailblazer::Activity.(Song::Activity::Cover, ctx)
89
+
90
+ puts ctx[:collected_from_each] #=> [[0, "Fat Mike"], [1, "El Hefe"]]
91
+ #~ctx_to_result end
92
+ #:collected_from_each end
93
+ =end
94
+ end
95
+
96
+ module CoverMethods
97
+ def notify_composers(ctx, index:, item:, **)
98
+ ctx[:value] = [index, item.full_name]
99
+ end
100
+
101
+ def model(ctx, params:, **)
102
+ ctx[:model] = EachTest::B::Song.find_by(id: params[:id])
103
+ end
104
+
105
+ include T.def_steps(:rearrange)
106
+ end
107
+
108
+ module ComposersForEach
109
+ def composers_for_each(ctx, model:, **)
110
+ model.composers
111
+ end
112
+ end
113
+
114
+ #@ operation has dedicated step {#find_composers}
115
+ module C
116
+ class Song < B::Song; end
117
+
118
+ module Song::Activity
119
+ class Cover < Trailblazer::Activity::Railway
120
+ step :model
121
+ step :find_composers
122
+ step Each(collect: true) {
123
+ step :notify_composers
124
+ }, In() => {:composers => :dataset}
125
+ step :rearrange
126
+
127
+ def find_composers(ctx, model:, **)
128
+ # You could also say {ctx[:dataset] = model.composers},
129
+ # and wouldn't need the In() mapping.
130
+ ctx[:composers] = model.composers
131
+ end
132
+ #~meths
133
+ include CoverMethods
134
+ #~meths end
135
+ end
136
+ end
137
+ end # C
138
+
139
+ it "dataset can come from the hosting activity" do
140
+ #@ {:dataset} is not part of the outgoing {ctx}.
141
+ assert_invoke B::Song::Activity::Cover, params: {id: 1},
142
+ expected_ctx_variables: {
143
+ model: B::Song.find_by(id: 1),
144
+ collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
145
+ }, seq: "[:rearrange]"
146
+ end
147
+
148
+ it "dataset coming via In() from the operation" do
149
+ #@ {:dataset} is not part of the {ctx}.
150
+ assert_invoke C::Song::Activity::Cover, params: {id: 1},
151
+ expected_ctx_variables: {
152
+ model: C::Song.find_by(id: 1),
153
+ composers: [Composer.new("Fat Mike"), Composer.new("El Hefe")],
154
+ collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
155
+ }, seq: "[:rearrange]"
156
+ end
157
+
158
+ #@ {:item_key}
159
+ module E
160
+ class Song < B::Song; end
161
+
162
+ Mailer = Class.new(EachTest::Mailer)
163
+
164
+ #:composer
165
+ module Song::Activity
166
+ class Cover < Trailblazer::Activity::Railway
167
+ #~meths
168
+ step :model
169
+ #:item_key
170
+ step Each(dataset_from: :composers_for_each, item_key: :composer) {
171
+ step :notify_composers
172
+ }
173
+ #:item_key end
174
+ step :rearrange
175
+
176
+
177
+ # circuit-step interface! "decider interface"
178
+ def composers_for_each(ctx, model:, **)
179
+ model.composers
180
+ end
181
+ include CoverMethods
182
+ #~meths end
183
+ def notify_composers(ctx, index:, composer:, **)
184
+ Mailer.send(to: composer.email, message: "#{index}) You, #{composer.full_name}, have been warned about your song being copied.")
185
+ end
186
+ end
187
+ end
188
+ #:composer end
189
+ end # E
190
+
191
+ it "{item_key: :composer}" do
192
+ E::Mailer.send_options = []
193
+ assert_invoke E::Song::Activity::Cover, params: {id: 1},
194
+ expected_ctx_variables: {
195
+ model: B::Song.find_by(id: 1),
196
+ # collected_from_each: ["Fat Mike", "El Hefe"]
197
+ },
198
+ seq: "[:rearrange]"
199
+ assert_equal E::Mailer.send_options, [{:to=>nil, :message=>"0) You, Fat Mike, have been warned about your song being copied."}, {:to=>nil, :message=>"1) You, El Hefe, have been warned about your song being copied."}]
200
+ end
201
+
202
+ #@ failure in Each
203
+ module F
204
+ class Song < B::Song; end
205
+
206
+ class Notify
207
+ def self.send_email(email)
208
+ return if email.nil?
209
+ true
210
+ end
211
+ end
212
+
213
+ module Song::Activity
214
+ class Cover < Trailblazer::Activity::Railway
215
+ step :model
216
+ step Each(dataset_from: :composers_for_each, collect: true) {
217
+ step :notify_composers
218
+ }
219
+ step :rearrange
220
+
221
+ def notify_composers(ctx, item:, **)
222
+ if Notify.send_email(item.email)
223
+ ctx[:value] = item.email # let's collect all emails that could be sent.
224
+ return true
225
+ else
226
+ return false
227
+ end
228
+ end
229
+ #~meths
230
+
231
+ # circuit-step interface! "decider interface"
232
+ def composers_for_each(ctx, model:, **)
233
+ model.composers
234
+ end
235
+ include CoverMethods
236
+ #~meths end
237
+ end
238
+ end
239
+ end # F
240
+
241
+ it "failure in Each" do
242
+ assert_invoke F::Song::Activity::Cover, params: {id: 2},
243
+ expected_ctx_variables: {
244
+ model: B::Song.find_by(id: 2),
245
+ collected_from_each: ["mike@fat.wreck", nil],
246
+ },
247
+ seq: "[]",
248
+ terminus: :failure
249
+
250
+ Trailblazer::Developer.wtf?(F::Song::Activity::Cover, [{params: {id: 2}, seq: []}])
251
+ end
252
+
253
+
254
+ #@ Each with operation
255
+ module D
256
+ class Song < B::Song; end
257
+ Mailer = Class.new(EachTest::Mailer)
258
+
259
+ #:operation-class
260
+ module Song::Activity
261
+ class Notify < Trailblazer::Activity::Railway
262
+ step :send_email
263
+
264
+ def send_email(ctx, index:, item:, **)
265
+ Mailer.send(to: item.email, message: "#{index}) You, #{item.full_name}, have been warned about your song being copied.")
266
+ end
267
+ end
268
+ end
269
+ #:operation-class end
270
+
271
+ #:operation
272
+ module Song::Activity
273
+ class Cover < Trailblazer::Activity::Railway
274
+ step :model
275
+ step Each(Notify, dataset_from: :composers_for_each)
276
+ step :rearrange
277
+ #~meths
278
+ def composers_for_each(ctx, model:, **)
279
+ model.composers
280
+ end
281
+ include CoverMethods
282
+ #~meths end
283
+ end
284
+ end
285
+ #:operation end
286
+ end
287
+
288
+ it "Each(Activity::Railway)" do
289
+ D::Mailer.send_options = []
290
+ assert_invoke D::Song::Activity::Cover, params: {id: 1},
291
+ seq: "[:rearrange]",
292
+ expected_ctx_variables: {
293
+ model: D::Song.find_by(id: 1),
294
+ # collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
295
+ }
296
+ assert_equal D::Mailer.send_options, [{:to=>nil, :message=>"0) You, Fat Mike, have been warned about your song being copied."}, {:to=>nil, :message=>"1) You, El Hefe, have been warned about your song being copied."}]
297
+ end
298
+
299
+ #@ Each with operation with three outcomes. Notify terminates on {End.spam_email},
300
+ # which is then routed to End.spam_alert in the hosting activity.
301
+ # NOTE: this is not documented, yet.
302
+ module G
303
+ class Song < B::Song; end
304
+
305
+ module Song::Activity
306
+ class Notify < Trailblazer::Activity::Railway
307
+ terminus :spam_email
308
+ # SpamEmail = Class.new(Trailblazer::Activity::Signal)
309
+
310
+ step :send_email, Output(:failure) => Track(:spam_email)
311
+
312
+ def send_email(ctx, index:, item:, **)
313
+ return false if item.email == "scammer@spam"
314
+ ctx[:value] = [index, item.full_name]
315
+ end
316
+ end
317
+ end
318
+
319
+ module Song::Activity
320
+ class Cover < Trailblazer::Activity::Railway
321
+ terminus :spam_alert
322
+
323
+ step :model
324
+ step Each(Notify, dataset_from: :composers_for_each, collect: true),
325
+ Output(:spam_email) => Track(:spam_alert)
326
+ step :rearrange
327
+ #~meths
328
+ def composers_for_each(ctx, model:, **)
329
+ model.composers
330
+ end
331
+ include CoverMethods
332
+ #~meths end
333
+ end
334
+ end
335
+ end
336
+
337
+ it "Each(Activity::Railway) with End.spam_email" do
338
+ Trailblazer::Developer.wtf?(G::Song::Activity::Cover, [{params: {id: 3}}, {}])
339
+
340
+ assert_invoke G::Song::Activity::Cover, params: {id: 3},
341
+ terminus: :spam_alert,
342
+ seq: "[]",
343
+ expected_ctx_variables: {
344
+ model: G::Song.find_by(id: 3),
345
+ collected_from_each: [[0, "Fat Mike"], nil,]
346
+ }
347
+ end
348
+ end
349
+
350
+ #@ Iteration doesn't add anything to ctx when {collect: false}.
351
+ class EachCtxDiscardedTest < Minitest::Spec
352
+ Composer = EachTest::Composer
353
+ Song = Class.new(EachTest::B::Song)
354
+
355
+ #@ iterated steps write to ctx, gets discarded.
356
+ module Song::Activity
357
+ class Cover < Trailblazer::Activity::Railway
358
+ step :model
359
+ #:write_to_ctx
360
+ step Each(dataset_from: :composers_for_each) {
361
+ step :notify_composers
362
+ step :write_to_ctx
363
+ }
364
+ #:write_to_ctx end
365
+ step :rearrange
366
+
367
+ #:write
368
+ def write_to_ctx(ctx, index:, seq:, **)
369
+ #~meths
370
+ seq << :write_to_ctx
371
+
372
+ #~meths end
373
+ ctx[:variable] = index # this is discarded!
374
+ end
375
+ #:write end
376
+
377
+ #~meths
378
+ include EachTest::CoverMethods
379
+ include EachTest::ComposersForEach
380
+ #~meths end
381
+ end
382
+ end
383
+
384
+ it "discards {ctx[:variable]}" do
385
+ assert_invoke Song::Activity::Cover, params: {id: 1},
386
+ expected_ctx_variables: {
387
+ model: Song.find_by(id: 1),
388
+ # collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
389
+ },
390
+ seq: "[:write_to_ctx, :write_to_ctx, :rearrange]"
391
+ end
392
+ end
393
+
394
+ # We add {:collected_from_each} ourselves.
395
+ class EachCtxAddsCollectedFromEachTest < Minitest::Spec
396
+ Composer = EachTest::Composer
397
+ Song = Class.new(EachTest::B::Song)
398
+
399
+ module Song::Activity
400
+ class Cover < Trailblazer::Activity::Railway
401
+ step :model
402
+ step Each(dataset_from: :composers_for_each,
403
+
404
+ # all filters called before/after each iteration!
405
+ Inject(:collected_from_each) => ->(ctx, **) { [] }, # this is called only once.
406
+ Out() => ->(ctx, collected_from_each:, **) { {collected_from_each: collected_from_each += [ctx[:value]] } }
407
+
408
+
409
+
410
+ ) {
411
+ step :notify_composers
412
+ step :write_to_ctx
413
+ }
414
+ step :rearrange
415
+
416
+ def write_to_ctx(ctx, index:, seq:, item:, **)
417
+ seq << :write_to_ctx
418
+
419
+ ctx[:value] = [index, item.full_name]
420
+ end
421
+
422
+ #~meths
423
+ include EachTest::CoverMethods
424
+ include EachTest::ComposersForEach
425
+ #~meths end
426
+ end
427
+ end
428
+
429
+ it "provides {:collected_from_each}" do
430
+ assert_invoke Song::Activity::Cover, params: {id: 1},
431
+ expected_ctx_variables: {
432
+ model: Song.find_by(id: 1),
433
+ collected_from_each: [[0, "Fat Mike"], [1, "El Hefe"],]
434
+ },
435
+ seq: "[:write_to_ctx, :write_to_ctx, :rearrange]"
436
+ end
437
+ end
438
+
439
+ #@ You can use Inject() to compute new variables.
440
+ #@ and Out() to compute what goes into the iterated {ctx}.
441
+ class EachCtxInOutTest < Minitest::Spec
442
+ Composer = EachTest::Composer
443
+ Song = Class.new(EachTest::B::Song)
444
+
445
+ module Song::Activity
446
+ class Cover < Trailblazer::Activity::Railway
447
+ step :model
448
+ step Each(dataset_from: :composers_for_each,
449
+ # Inject(always: true) => {
450
+ Inject(:composer_index) => ->(ctx, index:, **) { index },
451
+ # all filters called before/after each iteration!
452
+ Out() => ->(ctx, index:, variable:, **) { {:"composer-#{index}-value" => variable} }
453
+
454
+
455
+
456
+
457
+
458
+ ) {
459
+ step :notify_composers
460
+ step :write_to_ctx
461
+ }
462
+ step :rearrange
463
+
464
+ def write_to_ctx(ctx, composer_index:, model:, **)
465
+ ctx[:variable] = "#{composer_index} + #{model.class.name.split('::').last}"
466
+ end
467
+
468
+ #~meths
469
+ include EachTest::CoverMethods
470
+ include EachTest::ComposersForEach
471
+ #~meths end
472
+ end
473
+ end
474
+
475
+ it "discards {ctx[:variable]}" do
476
+ assert_invoke Song::Activity::Cover, params: {id: 1},
477
+ expected_ctx_variables: {
478
+ model: Song.find_by(id: 1),
479
+ :"composer-0-value" => "0 + Song",
480
+ :"composer-1-value" => "1 + Song",
481
+ },
482
+ seq: "[:rearrange]"
483
+ end
484
+ end
485
+
486
+ class EachOuterCtxTest < Minitest::Spec
487
+
488
+ end
489
+
490
+
491
+ #@ {:errors} is first initialized with a default injection,
492
+ #@ then passed across iterations.
493
+ # TODO: similar test above with {:collected_from_each}.
494
+ class EachSharedIterationVariableTest < Minitest::Spec
495
+ Song = Class.new(EachTest::B::Song)
496
+
497
+ #:inject
498
+ module Song::Activity
499
+ class Cover < Trailblazer::Activity::Railway
500
+ step :model
501
+ step Each(dataset_from: :composers_for_each,
502
+ Inject(:messages) => ->(*) { {} },
503
+
504
+ # all filters called before/after each iteration!
505
+ Out() => [:messages]
506
+ ) {
507
+ step :write_to_ctx
508
+ }
509
+ step :rearrange
510
+
511
+ def write_to_ctx(ctx, item:, messages:, index:, **)
512
+ ctx[:messages] = messages.merge(index => item.full_name)
513
+ end
514
+ #~meths
515
+ include EachTest::CoverMethods
516
+ include EachTest::ComposersForEach
517
+ #~meths end
518
+ end
519
+ end
520
+ #:inject end
521
+
522
+ it "passes {ctx[:messages]} across iterations and makes it grow" do
523
+ assert_invoke Song::Activity::Cover, params: {id: 1},
524
+ expected_ctx_variables: {
525
+ model: Song.find_by(id: 1),
526
+ messages: {0=>"Fat Mike", 1=>"El Hefe"}},
527
+ seq: "[:rearrange]"
528
+ end
529
+
530
+ end
531
+
532
+ #@ Each without any option
533
+ class EachPureTest < Minitest::Spec
534
+ Song = Class.new(EachTest::B::Song)
535
+
536
+ Mailer = Class.new(EachTest::Mailer)
537
+
538
+ #:each-pure
539
+ module Song::Activity
540
+ class Cover < Trailblazer::Activity::Railway
541
+ step :model
542
+ #:each-pure-macro
543
+ step Each(dataset_from: :composers_for_each) {
544
+ step :notify_composers
545
+ }
546
+ #:each-pure-macro end
547
+ step :rearrange
548
+
549
+ # "decider interface"
550
+ #:dataset_from
551
+ def composers_for_each(ctx, model:, **)
552
+ model.composers
553
+ end
554
+ #:dataset_from end
555
+
556
+ #:iterated
557
+ def notify_composers(ctx, index:, item:, **)
558
+ Mailer.send(to: item.email, message: "#{index}) You, #{item.full_name}, have been warned about your song being copied.")
559
+ end
560
+ #:iterated end
561
+ #~meths
562
+ def model(ctx, params:, **)
563
+ ctx[:model] = Song.find_by(id: params[:id])
564
+ end
565
+
566
+ include T.def_steps(:rearrange)
567
+ #~meths end
568
+ end
569
+ end
570
+ #:each-pure end
571
+
572
+ it "allows a dataset compute in the hosting activity" do
573
+ Mailer.send_options = []
574
+ #@ {:dataset} is not part of the {ctx}.
575
+ assert_invoke Song::Activity::Cover, params: {id: 1},
576
+ expected_ctx_variables: {
577
+ model: Song.find_by(id: 1),
578
+ },
579
+ seq: "[:rearrange]"
580
+
581
+ assert_equal Mailer.send_options, [{:to=>nil, :message=>"0) You, Fat Mike, have been warned about your song being copied."}, {:to=>nil, :message=>"1) You, El Hefe, have been warned about your song being copied."}]
582
+ end
583
+ end
584
+
585
+ #~ignore
586
+ class EachStrategyComplianceTest < Minitest::Spec
587
+ Song = EachPureTest::Song
588
+
589
+ it do
590
+ EachPureTest::Mailer.send_options = []
591
+
592
+ #:patch
593
+ cover_patched = Trailblazer::Activity::DSL::Linear.Patch(
594
+ Song::Activity::Cover,
595
+ ["Each/composers_for_each", "Each.iterate.block"] => -> { step :log_email }
596
+ )
597
+ #:patch end
598
+ cover_patched.include(T.def_steps(:log_email, :notify_composers))
599
+
600
+ #@ Original class isn't changed.
601
+ assert_invoke Song::Activity::Cover, params: {id: 1}, seq: [],
602
+ expected_ctx_variables: {
603
+ model: Song.find_by(id: 1),
604
+ },
605
+ seq: "[:rearrange]"
606
+
607
+ #@ Patched class runs
608
+ # Trailblazer::Developer.wtf?(cover_patched, [params: {id: 1}, seq: []])
609
+ assert_invoke cover_patched, params: {id: 1}, seq: [],
610
+ expected_ctx_variables: {
611
+ model: Song.find_by(id: 1),
612
+ },
613
+ seq: "[:notify_composers, :log_email, :notify_composers, :log_email, :rearrange]"
614
+ end
615
+
616
+
617
+ it "find_path" do
618
+ assert_equal Trailblazer::Developer::Introspect.find_path(Song::Activity::Cover,
619
+ ["Each/composers_for_each", "Each.iterate.block", "invoke_block_activity", :notify_composers])[0].task.inspect,
620
+ %{#<Trailblazer::Activity::TaskBuilder::Task user_proc=notify_composers>}
621
+
622
+ =begin
623
+ #:find_path
624
+ node, _ = Trailblazer::Developer::Introspect.find_path(
625
+ Song::Activity::Cover,
626
+ ["Each/composers_for_each", "Each.iterate.block", "invoke_block_activity", :notify_composers])
627
+ #=> #<Node ...>
628
+ #:find_path end
629
+ =end
630
+
631
+ end
632
+
633
+ it "{#find_path} for Each(Activity) with anonymous class" do
634
+ id = nil
635
+
636
+ activity = Class.new(Trailblazer::Activity::Railway) do
637
+ sub_activity = Class.new(Trailblazer::Activity::Railway) do
638
+ step :notify_composers
639
+ end
640
+ id = sub_activity.to_s
641
+
642
+ step :model
643
+ step Each(sub_activity, dataset_from: :composers_for_each)
644
+ step :rearrange
645
+
646
+ def composers_for_each(ctx, model:, **)
647
+ model.composers
648
+ end
649
+ # include CoverMethods
650
+ end
651
+
652
+ node, _activity = Trailblazer::Developer::Introspect.find_path(activity,
653
+ [%{Each/#{id}}, "Each.iterate.#{id}", "invoke_block_activity"])
654
+
655
+ assert_equal _activity.class.inspect, "Hash" # container_activity
656
+
657
+ #@ inside {invoke_block_activity}
658
+ node, _activity = Trailblazer::Developer::Introspect.find_path(activity,
659
+ [%{Each/#{id}}, "Each.iterate.#{id}", "invoke_block_activity", :notify_composers])
660
+
661
+ assert_equal node.task.inspect,
662
+ %{#<Trailblazer::Activity::TaskBuilder::Task user_proc=notify_composers>}
663
+ assert_equal _activity.class.inspect, "Trailblazer::Activity"
664
+ end
665
+
666
+
667
+ it "tracing" do
668
+ EachPureTest::Mailer.send_options = []
669
+ #:wtf
670
+ Trailblazer::Developer.wtf?(Song::Activity::Cover, [{
671
+ params: {id: 1},
672
+ #~meths
673
+ seq: []
674
+ #~meths end
675
+ }])
676
+ #:wtf end
677
+ end
678
+ end
679
+
680
+
681
+ #@ dataset: []
682
+ class EachEmptyDatasetTest < Minitest::Spec
683
+ it do
684
+ activity = Class.new(Trailblazer::Activity::Railway) do
685
+ step Each() {
686
+ step :raise
687
+ }
688
+ end
689
+
690
+ assert_invoke activity, dataset: []
691
+ end
692
+ end
693
+
694
+ class EachIDTest < Minitest::Spec
695
+ class Validate < Trailblazer::Activity::Railway
696
+ end
697
+
698
+ it "assigns IDs via {Macro.id_for}" do
699
+ activity = Class.new(Trailblazer::Activity::Railway) do
700
+ step Each() {}
701
+ step Each(Validate)
702
+ step Each() {}, id: "Each-1"
703
+ step Each(dataset_from: :composers_for_each) {}
704
+ end
705
+
706
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each/EachIDTest::Validate"])[0].id, "Each/EachIDTest::Validate"
707
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each-1"])[0].id, "Each-1"
708
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each/composers_for_each"])[0].id, "Each/composers_for_each"
709
+
710
+ assert_match /Each\/\w+/, Trailblazer::Activity::Introspect::TaskMap(activity).values[1].id
711
+ end
712
+ end
713
+
714
+ class DocsEachUnitTest < Minitest::Spec
715
+ module ComputeItem
716
+ def compute_item(ctx, item:, index:, **)
717
+ ctx[:value] = "#{item}-#{index.inspect}"
718
+ end
719
+ end
720
+
721
+ def self.block
722
+ -> (*){
723
+ step :compute_item
724
+ }
725
+ end
726
+
727
+ it "with Trace" do
728
+ activity = Class.new(Trailblazer::Activity::Railway) do
729
+ include T.def_steps(:a, :b)
730
+ include ComputeItem
731
+
732
+ step :a
733
+ step Each(&DocsEachUnitTest.block), id: "Each/1"
734
+ step :b
735
+ end
736
+
737
+ ctx = {seq: [], dataset: [3,2,1]}
738
+
739
+ stack, signal, (ctx, _) = Trailblazer::Developer::Trace.invoke(activity, [ctx, {}])
740
+
741
+ assert_equal Trailblazer::Developer::Trace::Present.(stack,
742
+ node_options: {stack.to_a[0] => {label: "<a-Each-b>"}}), %{<a-Each-b>
743
+ |-- Start.default
744
+ |-- a
745
+ |-- Each/1
746
+ | |-- Start.default
747
+ | |-- Each.iterate.block
748
+ | | |-- invoke_block_activity.0
749
+ | | | |-- Start.default
750
+ | | | |-- compute_item
751
+ | | | `-- End.success
752
+ | | |-- invoke_block_activity.1
753
+ | | | |-- Start.default
754
+ | | | |-- compute_item
755
+ | | | `-- End.success
756
+ | | `-- invoke_block_activity.2
757
+ | | |-- Start.default
758
+ | | |-- compute_item
759
+ | | `-- End.success
760
+ | `-- End.success
761
+ |-- b
762
+ `-- End.success}
763
+
764
+ #@ compile time
765
+ #@ make sure we can find tasks/compile-time artifacts in Each by using their {compile_id}.
766
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity,
767
+ ["Each/1", "Each.iterate.block", "invoke_block_activity", :compute_item])[0].task.inspect,
768
+ %{#<Trailblazer::Activity::TaskBuilder::Task user_proc=compute_item>}
769
+ # puts Trailblazer::Developer::Render::TaskWrap.(activity, ["Each/1", "Each.iterate.block", "invoke_block_activity", :compute_item])
770
+
771
+ # TODO: grab runtime ctx for iteration 134
772
+ end
773
+
774
+ it "Each::Circuit" do
775
+ activity = Trailblazer::Macro.Each(collect: true, &DocsEachUnitTest.block)[:task]
776
+
777
+ my_exec_context = Class.new do
778
+ include ComputeItem
779
+ end.new
780
+
781
+ ctx = {
782
+ dataset: [1,2,3]
783
+ }
784
+
785
+ # signal, (_ctx, _) = Trailblazer::Activity::TaskWrap.invoke(activity, [ctx])
786
+ signal, (_ctx, _) = Trailblazer::Developer.wtf?(activity, [ctx], exec_context: my_exec_context)
787
+ assert_equal _ctx[:collected_from_each], ["1-0", "2-1", "3-2"]
788
+ end
789
+
790
+
791
+ it "accepts iterated {block}" do
792
+ activity = Class.new(Trailblazer::Activity::Railway) do
793
+ include ComputeItem
794
+
795
+ step Each(collect: true) { # expects {:dataset} # NOTE: use {} not {do ... end}
796
+ step :compute_item
797
+ }
798
+ end
799
+
800
+ Trailblazer::Developer.wtf?(activity, [{dataset: ["one", "two", "three"]}, {}])
801
+
802
+ assert_invoke activity, dataset: ["one", "two", "three"], expected_ctx_variables: {collected_from_each: ["one-0", "two-1", "three-2"]}
803
+ end
804
+
805
+ it "can see the entire ctx" do
806
+ activity = Class.new(Trailblazer::Activity::Railway) do
807
+ def compute_item_with_current_user(ctx, item:, index:, current_user:, **)
808
+ ctx[:value] = "#{item}-#{index.inspect}-#{current_user}"
809
+ end
810
+
811
+ step Each(collect: true) { # expects {:dataset}
812
+ step :compute_item_with_current_user
813
+ }
814
+ end
815
+
816
+ Trailblazer::Developer.wtf?(
817
+ activity,
818
+ [{
819
+ dataset: ["one", "two", "three"],
820
+ current_user: Object,
821
+ },
822
+ {}]
823
+ )
824
+ assert_invoke activity, dataset: ["one", "two", "three"], current_user: Object, expected_ctx_variables: {collected_from_each: ["one-0-Object", "two-1-Object", "three-2-Object"]}
825
+ end
826
+
827
+ it "allows taskWrap in Each" do
828
+ activity = Class.new(Trailblazer::Activity::Railway) do
829
+ step Each(collect: true) { # expects {:dataset} # NOTE: use {} not {do ... end}
830
+ step :compute_item, In() => {:current_user => :user}, In() => [:item, :index]
831
+ }
832
+
833
+ def compute_item(ctx, item:, index:, user:, **)
834
+ ctx[:value] = "#{item}-#{index.inspect}-#{user}"
835
+ end
836
+ end
837
+
838
+ assert_invoke activity, dataset: ["one", "two", "three"], current_user: "Yogi", expected_ctx_variables: {collected_from_each: ["one-0-Yogi", "two-1-Yogi", "three-2-Yogi"]}
839
+ end
840
+
841
+ it "accepts operation" do
842
+ nested_activity = Class.new(Trailblazer::Activity::Railway) do
843
+ step :compute_item
844
+
845
+ def compute_item(ctx, item:, index:, **)
846
+ ctx[:value] = "#{item}-#{index.inspect}"
847
+ end
848
+ end
849
+
850
+ activity = Class.new(Trailblazer::Activity::Railway) do
851
+ step Each(nested_activity, collect: true) # expects {:dataset}
852
+ end
853
+ # Trailblazer::Developer.wtf?(activity, [{dataset: ["one", "two", "three"]}, {}])
854
+
855
+ assert_invoke activity, dataset: ["one", "two", "three"], expected_ctx_variables: {collected_from_each: ["one-0", "two-1", "three-2"]}
856
+ end
857
+
858
+ it "doesn't override an existing ctx[:index]" do
859
+ activity = Class.new(Trailblazer::Activity::Railway) do
860
+ include T.def_steps(:a, :b)
861
+ include ComputeItem
862
+
863
+ step Each(collect: true, &DocsEachUnitTest.block), id: "Each/1"
864
+ step :b
865
+ def b(ctx, seq:, index:, **)
866
+ ctx[:seq] = seq + [index]
867
+ end
868
+
869
+ end
870
+
871
+ assert_invoke activity, dataset: [1,2,3], index: 9,
872
+ expected_ctx_variables: {collected_from_each: ["1-0", "2-1", "3-2"]},
873
+ seq: "[9]"
874
+ end
875
+
876
+ it "stops iterating when failure" do
877
+ activity = Class.new(Trailblazer::Activity::Railway) do
878
+ step Each(collect: true) {
879
+ step :check
880
+ }
881
+ step :a
882
+
883
+ include T.def_steps(:a)
884
+ def check(ctx, item:, **)
885
+ ctx[:value] = item.to_s #@ always collect the value, even in failure case.
886
+
887
+ return false if item >= 3
888
+ true
889
+ end
890
+ end
891
+
892
+ #@ all works
893
+ assert_invoke activity, dataset: [1,2],
894
+ expected_ctx_variables: {collected_from_each: ["1", "2"]},
895
+ seq: "[:a]"
896
+
897
+ #@ fail at 3 but still collect 3rd iteration!
898
+ Trailblazer::Developer.wtf?(activity, [{dataset: [1,2,3]}, {}])
899
+ assert_invoke activity, dataset: [1,2,3],
900
+ expected_ctx_variables: {collected_from_each: ["1", "2", "3"]},
901
+ seq: "[]",
902
+ terminus: :failure
903
+
904
+ #@ fail at 3, skip 4
905
+ assert_invoke activity, dataset: [1,2,3,4],
906
+ expected_ctx_variables: {collected_from_each: ["1", "2", "3"]},
907
+ seq: "[]",
908
+ terminus: :failure
909
+ end
910
+ end
911
+
912
+
913
+ class EachInEachTest < Minitest::Spec
914
+ it "what" do
915
+ activity = Class.new(Trailblazer::Activity::Railway) do
916
+ step Each(item_key: :outer) {
917
+ step :capture_outer
918
+
919
+ step Each(dataset_from: :inner_dataset, item_key: :inner) {
920
+ step :capture_inner
921
+ }
922
+ }
923
+
924
+ def capture_outer(ctx, outer:, **)
925
+ ctx[:seq] << outer
926
+ end
927
+
928
+ def capture_inner(ctx, inner:, **)
929
+ ctx[:seq] << inner
930
+ end
931
+
932
+ def inner_dataset(ctx, outer:, **)
933
+ outer.collect { |i| i * 10 }
934
+ end
935
+ end
936
+
937
+ assert_invoke activity, dataset: [[1,2],[3,4],[5,6]],
938
+ seq: %{[[1, 2], 10, 20, [3, 4], 30, 40, [5, 6], 50, 60]}
939
+ end
940
+ end