trailblazer-macro 2.1.11 → 2.1.12

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,907 @@
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", :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", :notify_composers])
627
+ #=> #<Node ...>
628
+ #:find_path end
629
+ =end
630
+
631
+ end
632
+
633
+
634
+ it "tracing" do
635
+ EachPureTest::Mailer.send_options = []
636
+ #:wtf
637
+ Trailblazer::Developer.wtf?(Song::Activity::Cover, [{
638
+ params: {id: 1},
639
+ #~meths
640
+ seq: []
641
+ #~meths end
642
+ }])
643
+ #:wtf end
644
+ end
645
+ end
646
+
647
+
648
+ #@ dataset: []
649
+ class EachEmptyDatasetTest < Minitest::Spec
650
+ it do
651
+ activity = Class.new(Trailblazer::Activity::Railway) do
652
+ step Each() {
653
+ step :raise
654
+ }
655
+ end
656
+
657
+ assert_invoke activity, dataset: []
658
+ end
659
+ end
660
+
661
+ class EachIDTest < Minitest::Spec
662
+ class Validate < Trailblazer::Activity::Railway
663
+ end
664
+
665
+ it "assigns IDs via {Macro.id_for}" do
666
+ activity = Class.new(Trailblazer::Activity::Railway) do
667
+ step Each() {}
668
+ step Each(Validate)
669
+ step Each() {}, id: "Each-1"
670
+ step Each(dataset_from: :composers_for_each) {}
671
+ end
672
+
673
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each/EachIDTest::Validate"])[0].id, "Each/EachIDTest::Validate"
674
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each-1"])[0].id, "Each-1"
675
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity, ["Each/composers_for_each"])[0].id, "Each/composers_for_each"
676
+
677
+ assert_match /Each\/\w+/, Trailblazer::Activity::Introspect::TaskMap(activity).values[1].id
678
+ end
679
+ end
680
+
681
+ class DocsEachUnitTest < Minitest::Spec
682
+ module ComputeItem
683
+ def compute_item(ctx, item:, index:, **)
684
+ ctx[:value] = "#{item}-#{index.inspect}"
685
+ end
686
+ end
687
+
688
+ def self.block
689
+ -> (*){
690
+ step :compute_item
691
+ }
692
+ end
693
+
694
+ it "with Trace" do
695
+ activity = Class.new(Trailblazer::Activity::Railway) do
696
+ include T.def_steps(:a, :b)
697
+ include ComputeItem
698
+
699
+ step :a
700
+ step Each(&DocsEachUnitTest.block), id: "Each/1"
701
+ step :b
702
+ end
703
+
704
+ ctx = {seq: [], dataset: [3,2,1]}
705
+
706
+ stack, signal, (ctx, _) = Trailblazer::Developer::Trace.invoke(activity, [ctx, {}])
707
+
708
+ assert_equal Trailblazer::Developer::Trace::Present.(stack,
709
+ node_options: {stack.to_a[0] => {label: "<a-Each-b>"}}), %{<a-Each-b>
710
+ |-- Start.default
711
+ |-- a
712
+ |-- Each/1
713
+ | |-- Start.default
714
+ | |-- Each.iterate.block
715
+ | | |-- invoke_block_activity.0
716
+ | | | |-- Start.default
717
+ | | | |-- compute_item
718
+ | | | `-- End.success
719
+ | | |-- invoke_block_activity.1
720
+ | | | |-- Start.default
721
+ | | | |-- compute_item
722
+ | | | `-- End.success
723
+ | | `-- invoke_block_activity.2
724
+ | | |-- Start.default
725
+ | | |-- compute_item
726
+ | | `-- End.success
727
+ | `-- End.success
728
+ |-- b
729
+ `-- End.success}
730
+
731
+ #@ compile time
732
+ #@ make sure we can find tasks/compile-time artifacts in Each by using their {compile_id}.
733
+ assert_equal Trailblazer::Developer::Introspect.find_path(activity,
734
+ ["Each/1", "Each.iterate.block", :compute_item])[0].task.inspect,
735
+ %{#<Trailblazer::Activity::TaskBuilder::Task user_proc=compute_item>}
736
+ # puts Trailblazer::Developer::Render::TaskWrap.(activity, ["Each/1", "Each.iterate.block", "invoke_block_activity", :compute_item])
737
+
738
+ # TODO: grab runtime ctx for iteration 134
739
+ end
740
+
741
+ it "Each::Circuit" do
742
+ activity = Trailblazer::Macro.Each(collect: true, &DocsEachUnitTest.block)[:task]
743
+
744
+ my_exec_context = Class.new do
745
+ include ComputeItem
746
+ end.new
747
+
748
+ ctx = {
749
+ dataset: [1,2,3]
750
+ }
751
+
752
+ # signal, (_ctx, _) = Trailblazer::Activity::TaskWrap.invoke(activity, [ctx])
753
+ signal, (_ctx, _) = Trailblazer::Developer.wtf?(activity, [ctx], exec_context: my_exec_context)
754
+ assert_equal _ctx[:collected_from_each], ["1-0", "2-1", "3-2"]
755
+ end
756
+
757
+
758
+ it "accepts iterated {block}" do
759
+ activity = Class.new(Trailblazer::Activity::Railway) do
760
+ include ComputeItem
761
+
762
+ step Each(collect: true) { # expects {:dataset} # NOTE: use {} not {do ... end}
763
+ step :compute_item
764
+ }
765
+ end
766
+
767
+ Trailblazer::Developer.wtf?(activity, [{dataset: ["one", "two", "three"]}, {}])
768
+
769
+ assert_invoke activity, dataset: ["one", "two", "three"], expected_ctx_variables: {collected_from_each: ["one-0", "two-1", "three-2"]}
770
+ end
771
+
772
+ it "can see the entire ctx" do
773
+ activity = Class.new(Trailblazer::Activity::Railway) do
774
+ def compute_item_with_current_user(ctx, item:, index:, current_user:, **)
775
+ ctx[:value] = "#{item}-#{index.inspect}-#{current_user}"
776
+ end
777
+
778
+ step Each(collect: true) { # expects {:dataset}
779
+ step :compute_item_with_current_user
780
+ }
781
+ end
782
+
783
+ Trailblazer::Developer.wtf?(
784
+ activity,
785
+ [{
786
+ dataset: ["one", "two", "three"],
787
+ current_user: Object,
788
+ },
789
+ {}]
790
+ )
791
+ 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"]}
792
+ end
793
+
794
+ it "allows taskWrap in Each" do
795
+ activity = Class.new(Trailblazer::Activity::Railway) do
796
+ step Each(collect: true) { # expects {:dataset} # NOTE: use {} not {do ... end}
797
+ step :compute_item, In() => {:current_user => :user}, In() => [:item, :index]
798
+ }
799
+
800
+ def compute_item(ctx, item:, index:, user:, **)
801
+ ctx[:value] = "#{item}-#{index.inspect}-#{user}"
802
+ end
803
+ end
804
+
805
+ 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"]}
806
+ end
807
+
808
+ it "accepts operation" do
809
+ nested_activity = Class.new(Trailblazer::Activity::Railway) do
810
+ step :compute_item
811
+
812
+ def compute_item(ctx, item:, index:, **)
813
+ ctx[:value] = "#{item}-#{index.inspect}"
814
+ end
815
+ end
816
+
817
+ activity = Class.new(Trailblazer::Activity::Railway) do
818
+ step Each(nested_activity, collect: true) # expects {:dataset}
819
+ end
820
+ # Trailblazer::Developer.wtf?(activity, [{dataset: ["one", "two", "three"]}, {}])
821
+
822
+ assert_invoke activity, dataset: ["one", "two", "three"], expected_ctx_variables: {collected_from_each: ["one-0", "two-1", "three-2"]}
823
+ end
824
+
825
+ it "doesn't override an existing ctx[:index]" do
826
+ activity = Class.new(Trailblazer::Activity::Railway) do
827
+ include T.def_steps(:a, :b)
828
+ include ComputeItem
829
+
830
+ step Each(collect: true, &DocsEachUnitTest.block), id: "Each/1"
831
+ step :b
832
+ def b(ctx, seq:, index:, **)
833
+ ctx[:seq] = seq + [index]
834
+ end
835
+
836
+ end
837
+
838
+ assert_invoke activity, dataset: [1,2,3], index: 9,
839
+ expected_ctx_variables: {collected_from_each: ["1-0", "2-1", "3-2"]},
840
+ seq: "[9]"
841
+ end
842
+
843
+ it "stops iterating when failure" do
844
+ activity = Class.new(Trailblazer::Activity::Railway) do
845
+ step Each(collect: true) {
846
+ step :check
847
+ }
848
+ step :a
849
+
850
+ include T.def_steps(:a)
851
+ def check(ctx, item:, **)
852
+ ctx[:value] = item.to_s #@ always collect the value, even in failure case.
853
+
854
+ return false if item >= 3
855
+ true
856
+ end
857
+ end
858
+
859
+ #@ all works
860
+ assert_invoke activity, dataset: [1,2],
861
+ expected_ctx_variables: {collected_from_each: ["1", "2"]},
862
+ seq: "[:a]"
863
+
864
+ #@ fail at 3 but still collect 3rd iteration!
865
+ Trailblazer::Developer.wtf?(activity, [{dataset: [1,2,3]}, {}])
866
+ assert_invoke activity, dataset: [1,2,3],
867
+ expected_ctx_variables: {collected_from_each: ["1", "2", "3"]},
868
+ seq: "[]",
869
+ terminus: :failure
870
+
871
+ #@ fail at 3, skip 4
872
+ assert_invoke activity, dataset: [1,2,3,4],
873
+ expected_ctx_variables: {collected_from_each: ["1", "2", "3"]},
874
+ seq: "[]",
875
+ terminus: :failure
876
+ end
877
+ end
878
+
879
+
880
+ class EachInEachTest < Minitest::Spec
881
+ it "what" do
882
+ activity = Class.new(Trailblazer::Activity::Railway) do
883
+ step Each(item_key: :outer) {
884
+ step :capture_outer
885
+
886
+ step Each(dataset_from: :inner_dataset, item_key: :inner) {
887
+ step :capture_inner
888
+ }
889
+ }
890
+
891
+ def capture_outer(ctx, outer:, **)
892
+ ctx[:seq] << outer
893
+ end
894
+
895
+ def capture_inner(ctx, inner:, **)
896
+ ctx[:seq] << inner
897
+ end
898
+
899
+ def inner_dataset(ctx, outer:, **)
900
+ outer.collect { |i| i * 10 }
901
+ end
902
+ end
903
+
904
+ assert_invoke activity, dataset: [[1,2],[3,4],[5,6]],
905
+ seq: %{[[1, 2], 10, 20, [3, 4], 30, 40, [5, 6], 50, 60]}
906
+ end
907
+ end