trailblazer-macro 2.1.11 → 2.1.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34955a47d4c5d006819242d4d9a13ce658a3473afe0d7f6b4570374dbff557c0
4
- data.tar.gz: 4f8fb6b8ba5c8bcc3db19ac977507675550c0defa2be95b771c77914644837b6
3
+ metadata.gz: 14fa43e34517eca4e07e257fc5a172f406b88294b72f222109203156af53b684
4
+ data.tar.gz: 4d357be0a556d5431d7e10971e8ca1f9853e8029ecd5c1835c356d2169c9b7cb
5
5
  SHA512:
6
- metadata.gz: 71cc088d718a756171fd402d12dfb3b55d817e87356ea13088871dbc539085b92af36f57b7e7e43c817181aac9b73c580f8e69208b96deec43c59f551df742f9
7
- data.tar.gz: c400ba9542fd36400b6ebb0bffc142f3a51212a33b48504b527fdd35b15a5060a95d9c8735a9f146e2f259906568515a7199e9eae7087a3ad3a5b59cbb6a0374
6
+ metadata.gz: 600a2b8ae99cb504a0574b929e59ca63edc219b43e219b5af3c414d0c362e323f3cb6ff276f86ce02b19466aab1723ae60bc8b34f91d94764f3b82eadeaa7c2c
7
+ data.tar.gz: f03b36faa58d2e0ea5c265b48194cd14bb5825cfecace98777637d44a91bb2404ebb710e1db3ce7a7b7b383158413db9f4ffac5f6bb7abf7226ea1e02749d97c
@@ -1,3 +1,6 @@
1
+ ## This file is managed by Terraform.
2
+ ## Do not modify this file directly, as it may be overwritten.
3
+ ## Please open an issue instead.
1
4
  name: CI
2
5
  on: [push, pull_request]
3
6
  jobs:
@@ -5,13 +8,12 @@ jobs:
5
8
  strategy:
6
9
  fail-fast: false
7
10
  matrix:
8
- # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
9
- ruby: [2.5, 2.6, 2.7, '3.0', "3.1", head, jruby]
11
+ ruby: [2.7, '3.0', '3.1']
10
12
  runs-on: ubuntu-latest
11
13
  steps:
12
- - uses: actions/checkout@v2
14
+ - uses: actions/checkout@v3
13
15
  - uses: ruby/setup-ruby@v1
14
16
  with:
15
17
  ruby-version: ${{ matrix.ruby }}
16
- bundler-cache: true # runs 'bundle install' and caches installed gems automatically
18
+ bundler-cache: true
17
19
  - run: bundle exec rake
@@ -0,0 +1,19 @@
1
+ ## This file is managed by Terraform.
2
+ ## Do not modify this file directly, as it may be overwritten.
3
+ ## Please open an issue instead.
4
+ name: CI JRuby
5
+ on: [push, pull_request]
6
+ jobs:
7
+ test:
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ ruby: [jruby, jruby-head]
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ bundler-cache: true
19
+ - run: bundle exec rake
@@ -0,0 +1,19 @@
1
+ ## This file is managed by Terraform.
2
+ ## Do not modify this file directly, as it may be overwritten.
3
+ ## Please open an issue instead.
4
+ name: CI with EOL ruby versions
5
+ on: [push, pull_request]
6
+ jobs:
7
+ test:
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ ruby: [2.5, 2.6]
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ bundler-cache: true
19
+ - run: bundle exec rake
@@ -0,0 +1,19 @@
1
+ ## This file is managed by Terraform.
2
+ ## Do not modify this file directly, as it may be overwritten.
3
+ ## Please open an issue instead.
4
+ name: CI TruffleRuby
5
+ on: [push, pull_request]
6
+ jobs:
7
+ test:
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ ruby: [truffleruby, truffleruby-head]
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ bundler-cache: true
19
+ - run: bundle exec rake
data/CHANGES.md CHANGED
@@ -1,3 +1,32 @@
1
+ # 2.1.12
2
+
3
+ ## Internals
4
+
5
+ * Introduce `AssignVariable`.
6
+ * Use trailblazer-activity-dsl-linear-1.1.0.
7
+
8
+ ## Each()
9
+
10
+ * Introducing the `Each()` macro to iterate over datasets while invoking a particular operation, as if
11
+ they were run as separate serially connected operations.
12
+
13
+ ## Nested()
14
+
15
+ * Restructured internals, it's much more readable, `Dynamic` and `Static` are completely separated now.
16
+ * `Static` handles `:auto_wire` and allows to route any nested terminus. This was not possible before,
17
+ you only had `FastTrack` generic termini.
18
+ * Better warning when using `Nested(Operation)` without a dynamic decider.
19
+ * Internal structure of `Nested()` has changed, the trace looks different.
20
+
21
+ ## Wrap()
22
+
23
+ * Now implements patching.
24
+
25
+ ## Model()
26
+
27
+ * Remove `ctx[:"result.model"]`.
28
+ * Don't set `ctx[:model]` unless `Builder` returns a model.
29
+
1
30
  # 2.1.11
2
31
 
3
32
  * In `Nested()`, we no longer use `Railway::End::Success` and `Railway::End::Failure` as static outputs but
data/Gemfile CHANGED
@@ -3,13 +3,15 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in trailblazer.gemspec
4
4
  gemspec
5
5
 
6
- # gem "trailblazer", github: "trailblazer/trailblazer"
7
- # gem "trailblazer-activity", path: "../trailblazer-activity"
6
+ # gem "trailblazer-developer", github: "trailblazer/trailblazer-developer"
7
+ # gem "trailblazer-activity", github: "trailblazer/trailblazer-activity"
8
+ # gem "trailblazer-activity-dsl-linear", github: "trailblazer/trailblazer-activity-dsl-linear"
8
9
  # gem "trailblazer-activity-dsl-linear", path: "../trailblazer-activity-dsl-linear"
10
+ # gem "trailblazer-developer", path: "../trailblazer-developer"
11
+ # gem "trailblazer-activity", path: "../trailblazer-activity"
9
12
  # gem "trailblazer-macro-contract", git: "https://github.com/trailblazer/trailblazer-macro-contract"
10
13
  # gem "trailblazer-context", path: "../trailblazer-context"
11
- # gem "trailblazer-operation", path: "../trailblazer-operation"
12
-
14
+ # gem "trailblazer-option", path: "../trailblazer-option"
13
15
 
14
16
  gem "minitest-line"
15
- gem "rubocop", require: false
17
+ # gem "trailblazer-core-utils", path: "../trailblazer-core-utils"
@@ -0,0 +1,170 @@
1
+ module Trailblazer
2
+ module Macro
3
+ class Each < Macro::Strategy
4
+ # FIXME: for Strategy that wants to pass-through the exec_context, so it
5
+ # looks "invisible" for steps.
6
+ module Transitive
7
+ def call(args, exec_context:, **circuit_options)
8
+ # exec_context is our hosting Song::Activity::Cover
9
+ to_h[:activity].call(args, exec_context: exec_context, **circuit_options)
10
+ end
11
+ end
12
+
13
+ def self.call((ctx, flow_options), runner:, **circuit_options) # DISCUSS: do we need {start_task}?
14
+ dataset = ctx.fetch(:dataset)
15
+ signal = @state.get(:success_signal)
16
+ item_key = @state.get(:item_key)
17
+ failing_semantic = @state.get(:failing_semantic)
18
+ activity = @state.get(:activity)
19
+
20
+ # I'd like to use {collect} but we can't {break} without losing the last iteration's result.
21
+ dataset.each_with_index do |element, index|
22
+ # This new {inner_ctx} will be disposed of after invoking the item activity.
23
+ inner_ctx = ctx.merge(
24
+ item_key => element, # defaults to {:item}
25
+ :index => index,
26
+ )
27
+
28
+ # TODO: test aliasing
29
+ wrap_ctx, _ = ITERATION_INPUT_PIPE.({aggregate: {}, original_ctx: inner_ctx}, [[ctx, flow_options], circuit_options])
30
+ inner_ctx = wrap_ctx[:input_ctx]
31
+
32
+ # using this runner will make it look as if block_activity is being run consequetively within {Each.iterate} as if they were steps
33
+ # Use TaskWrap::Runner to run the each block. This doesn't create the container_activity
34
+ # and literally simply invokes {block_activity.call}, which will set its own {wrap_static}.
35
+ signal, (returned_ctx, flow_options) = runner.(
36
+ block_activity,
37
+ [inner_ctx, flow_options],
38
+ runner: runner,
39
+ **circuit_options,
40
+ activity: activity,
41
+ )
42
+
43
+ # {returned_ctx} at this point has Each(..., In => Out =>) applied!
44
+ # Without configuration, this means {returned_ctx} is empty.
45
+ # DISCUSS: this is what usually happens in Out().
46
+ # merge all mutable parts into the original_ctx.
47
+ wrap_ctx, _ = ITERATION_OUTPUT_PIPE.({returned_ctx: returned_ctx, aggregate: {}, original_ctx: ctx}, [])
48
+ ctx = wrap_ctx[:aggregate]
49
+
50
+ # Break the loop if {block_activity} emits failure signal
51
+ break if failing_semantic.include?(signal.to_h[:semantic]) # TODO: use generic check from older macro
52
+ end
53
+
54
+ return signal, [ctx, flow_options]
55
+ end
56
+
57
+ # This is basically Out() => {copy all mutable variables}
58
+ ITERATION_OUTPUT_PIPE = Activity::DSL::Linear::VariableMapping::DSL.pipe_for_composable_output()
59
+ # and this In() => {copy everything}
60
+ ITERATION_INPUT_PIPE = Activity::DSL::Linear::VariableMapping::DSL.pipe_for_composable_input()
61
+
62
+ # Gets included in Debugger's Normalizer. Results in IDs like {invoke_block_activity.1}.
63
+ def self.compute_runtime_id(ctx, captured_node:, activity:, compile_id:, **)
64
+ # activity is the host activity
65
+ return compile_id unless activity[:each] == true
66
+
67
+ index = captured_node.captured_input.data[:ctx_snapshot].fetch(:index)
68
+
69
+ ctx[:runtime_id] = "#{compile_id}.#{index}"
70
+ end
71
+ end
72
+
73
+ # @api private The internals here are considered private and might change in the near future.
74
+ def self.Each(block_activity=nil, dataset_from: nil, item_key: :item, id: Macro.id_for(block_activity, macro: :Each, hint: dataset_from), collect: false, **dsl_options_for_iterated, &block)
75
+ dsl_options_for_iterated = block_activity if block_activity.is_a?(Hash) # Ruby 2.5 and 2.6
76
+
77
+ block_activity, outputs_from_block_activity = Macro.block_activity_for(block_activity, &block)
78
+
79
+ collect_options = options_for_collect(collect: collect)
80
+ dataset_from_options = options_for_dataset_from(dataset_from: dataset_from)
81
+
82
+ wrap_static_for_block_activity = task_wrap_for_iterated(
83
+ {Activity::Railway.Out() => []}. # per default, don't let anything out.
84
+ merge(collect_options).
85
+ merge(dsl_options_for_iterated)
86
+ )
87
+
88
+ # This activity is passed into the {Runner} for each iteration of {block_activity}.
89
+ container_activity = Activity::TaskWrap.container_activity_for(
90
+ block_activity,
91
+ each: true, # mark this activity for {compute_runtime_id}.
92
+ nodes: [Activity::NodeAttributes.new("invoke_block_activity", nil, block_activity)], # TODO: use TaskMap::TaskAttributes
93
+ ).merge(
94
+ wrap_static: Hash.new(wrap_static_for_block_activity)
95
+ )
96
+
97
+ # DISCUSS: move to Wrap.
98
+ termini_from_block_activity =
99
+ outputs_from_block_activity.
100
+ # DISCUSS: End.success needs to be the last here, so it's directly behind {Start.default}.
101
+ sort { |a,b| a.semantic == :success ? 1 : -1 }.
102
+ collect { |output|
103
+ [output.signal, id: "End.#{output.semantic}", magnetic_to: output.semantic, append_to: "Start.default"]
104
+ }
105
+
106
+ state = Declarative::State(
107
+ block_activity: [block_activity, {copy: Trailblazer::Declarative::State.method(:subclass)}], # DISCUSS: move to Macro::Strategy.
108
+ item_key: [item_key, {}], # DISCUSS: we could even allow the wrap_handler to be patchable.
109
+ failing_semantic: [[:failure, :fail_fast], {}],
110
+ activity: [container_activity, {}],
111
+ success_signal: [termini_from_block_activity[-1][0], {}] # FIXME: when subclassing (e.g. patching) this must be recomputed.
112
+ )
113
+
114
+ # {block_activity} looped. In the Stack, this will look as if {block_activity} is
115
+ # a child of {iterate_activity}, that's why we add {block_activity} as a Node in
116
+ # {iterate_activity}'s schema.
117
+ iterate_strategy = Class.new(Each) do
118
+ extend Macro::Strategy::State # now, the Wrap subclass can inherit its state and copy the {block_activity}.
119
+ initialize!(state)
120
+ end
121
+
122
+ each_activity = Activity::FastTrack(termini: termini_from_block_activity) # DISCUSS: what base class should we be using?
123
+ each_activity.extend Each::Transitive
124
+
125
+ # {Subprocess} with {strict: true} will automatically wire all {block_activity}'s termini to the corresponding termini
126
+ # of {each_activity} as they have the same semantics (both termini sets are identical).
127
+ each_activity.step Activity::Railway.Subprocess(iterate_strategy, strict: true),
128
+ id: "Each.iterate.#{block ? :block : block_activity}" # FIXME: test :id.
129
+
130
+ Activity::Railway.Subprocess(each_activity).
131
+ merge(id: id).
132
+ merge(dataset_from_options) # FIXME: provide that service via Subprocess.
133
+ end
134
+
135
+ def self.task_wrap_for_iterated(dsl_options)
136
+ # TODO: maybe the DSL API could be more "open" here? I bet it is, but I'm too lazy.
137
+ activity = Class.new(Activity::Railway) do
138
+ step({task: "iterated"}.merge(dsl_options))
139
+ end
140
+
141
+ activity.to_h[:config][:wrap_static]["iterated"]
142
+ end
143
+
144
+ # DSL options added to {block_activity} to implement {collect: true}.
145
+ def self.options_for_collect(collect:)
146
+ return {} unless collect
147
+
148
+ {
149
+ Activity::Railway.Inject(:collected_from_each) => ->(ctx, **) { [] }, # this is called only once.
150
+ Activity::Railway.Out() => ->(ctx, collected_from_each:, **) { {collected_from_each: collected_from_each += [ctx[:value]] } }
151
+ }
152
+ end
153
+
154
+ def self.options_for_dataset_from(dataset_from:)
155
+ return {} unless dataset_from
156
+
157
+ {
158
+ Activity::Railway.Inject(:dataset, override: true) => dataset_from, # {ctx[:dataset]} is private to {each_activity}.
159
+ }
160
+ end
161
+ end
162
+
163
+ if const_defined?(:Developer) # FIXME: how do you properly check for a gem?
164
+ Developer::Trace::Debugger.add_normalizer_step!(
165
+ Macro::Each.method(:compute_runtime_id),
166
+ id: "Each.runtime_id",
167
+ append: :runtime_id, # so that the following {#runtime_path} picks up those changes made here.
168
+ )
169
+ end
170
+ end
@@ -2,7 +2,7 @@ module Trailblazer
2
2
  module Macro
3
3
 
4
4
  def self.Model(model_class = nil, action = :new, find_by_key = :id, id: 'model.build', not_found_terminus: false)
5
- task = Activity::TaskBuilder::Binary(Model.new)
5
+ task = Activity::Circuit::TaskAdapter.for_step(Model.new)
6
6
 
7
7
  injections = {
8
8
  Activity::Railway.Inject() => [:params], # pass-through {:params} if it's in ctx.
@@ -24,11 +24,10 @@ module Trailblazer
24
24
 
25
25
  class Model
26
26
  def call(ctx, params: {}, **)
27
- builder = Model::Builder.new
28
- ctx[:model] = model = builder.call(ctx, params)
29
- ctx[:"result.model"] = result = Operation::Result.new(!model.nil?, {})
27
+ builder = Builder.new
28
+ model = builder.call(ctx, params) or return
30
29
 
31
- result.success?
30
+ ctx[:model] = model
32
31
  end
33
32
 
34
33
  class Builder
@@ -1,111 +1,173 @@
1
- # per default, everything we pass into a circuit is immutable. it's the ops/act's job to allow writing (via a Context)
2
1
  module Trailblazer
3
2
  module Macro
4
3
  # {Nested} macro.
5
- def self.Nested(callable, id: "Nested(#{callable})", auto_wire: [])
4
+ # DISCUSS: rename auto_wire => static
5
+ def self.Nested(callable, id: Macro.id_for(callable, macro: :Nested, hint: callable), auto_wire: [])
6
+ # Warn developers when they confuse Nested with Subprocess (for simple nesting, without a dynamic decider).
6
7
  if callable.is_a?(Class) && callable < Nested.operation_class
7
- caller_location = caller_locations(2, 1)[0]
8
- warn "[Trailblazer]#{caller_location.absolute_path}: " \
9
- "Using the `Nested()` macro with operations and activities is deprecated. " \
10
- "Replace `Nested(#{callable})` with `Subprocess(#{callable})`."
8
+ caller_locations = caller_locations(1, 2)
9
+ caller_location = caller_locations[0].to_s =~ /forwardable/ ? caller_locations[1] : caller_locations[0]
10
+
11
+ Activity::Deprecate.warn caller_location,
12
+ "Using the `Nested()` macro without a dynamic decider is deprecated.\n" \
13
+ "To simply nest an activity or operation, replace `Nested(#{callable})` with `Subprocess(#{callable})`.\n" \
14
+ "Check the Subprocess API docs to learn more about nesting: https://trailblazer.to/2.1/docs/activity.html#activity-wiring-api-subprocess"
11
15
 
12
16
  return Activity::Railway.Subprocess(callable)
13
17
  end
14
18
 
15
- task, outputs, compute_legacy_return_signal = Nested.Dynamic(callable, auto_wire: auto_wire)
19
+ task =
20
+ if auto_wire.any?
21
+ Nested.Static(callable, id: id, auto_wire: auto_wire)
22
+ else # no {auto_wire}
23
+ Nested.Dynamic(callable, id: id)
24
+ end
16
25
 
26
+ # |-- Nested.compute_nested_activity...Trailblazer::Macro::Nested::Decider
27
+ # `-- task_wrap.call_task..............Method
17
28
  merge = [
18
- [task, id: "Nested.compute_nested_activity", prepend: "task_wrap.call_task"],
29
+ [Nested::Decider.new(callable), id: "Nested.compute_nested_activity", prepend: "task_wrap.call_task"],
19
30
  ]
20
31
 
21
- if compute_legacy_return_signal
22
- merge << [compute_legacy_return_signal, id: "Nested.compute_return_signal", append: "task_wrap.call_task"]
23
- end
24
-
25
32
  task_wrap_extension = Activity::TaskWrap::Extension::WrapStatic.new(extension: Activity::TaskWrap::Extension(*merge))
26
33
 
27
- {
28
- task: task,
34
+ Activity::Railway.Subprocess(task).merge( # FIXME: allow this directly in Subprocess
29
35
  id: id,
30
36
  extensions: [task_wrap_extension],
31
- outputs: outputs,
32
- }
37
+ )
33
38
  end
34
39
 
35
40
  # @private
36
- module Nested
37
- def self.operation_class
38
- Operation
41
+ # @api private The internals here are considered private and might change in the near future.
42
+ #
43
+ # We don't need to override {Strategy.call} here to prevent {:exec_context} from being changed.
44
+ # The decider is run in the taskWrap before the {Nested} subclass is actually called.
45
+ class Nested < Trailblazer::Activity::Railway
46
+ def self.operation_class # TODO: remove once we don't need the deprecation anymore.
47
+ Trailblazer::Activity::DSL::Linear::Strategy
39
48
  end
40
49
 
41
- # For dynamic `Nested`s that do not expose an {Activity} interface.
42
- #
43
- # Dynamic doesn't automatically connect outputs of runtime {Activity}
44
- # at compile time (as we don't know which activity will be nested, obviously).
45
- # So by default, it only connects good old success/failure ends. But it is also
46
- # possible to connect all the ends of all possible dynamic activities
47
- # by passing their list to {:auto_wire} option.
48
- #
49
- # step Nested(:compute_nested, auto_wire: [Create, Update])
50
- def self.Dynamic(nested_activity_decider, auto_wire:)
51
- if auto_wire.empty?
52
- is_legacy = true # no auto_wire means we need to compute the legacy return signal.
53
- auto_wire = [Class.new(Activity::Railway)]
50
+ # TaskWrap step to run the decider.
51
+ # It's part of the API that the decider sees the original ctx.
52
+ # So this has to be placed in tW because we this step needs to be run *before* In() filters
53
+ # to resemble the behavior from pre 2.1.12.
54
+ class Decider
55
+ def initialize(nested_activity_decider)
56
+ @nested_activity_decider = Activity::Circuit.Step(nested_activity_decider, option: true)
54
57
  end
55
58
 
56
- outputs = outputs_for(auto_wire)
57
- task = Dynamic.new(nested_activity_decider)
58
- compute_legacy_return_signal = Dynamic::ComputeLegacyReturnSignal.new(outputs) if is_legacy
59
+ # TaskWrap API.
60
+ def call(wrap_ctx, original_args)
61
+ (ctx, flow_options), original_circuit_options = original_args
59
62
 
60
- return task, outputs, compute_legacy_return_signal
63
+ # FIXME: allow calling a Step task without the Binary decision (in Activity::TaskAdapter).
64
+ nested_activity, _ = @nested_activity_decider.([ctx, flow_options], **original_circuit_options) # no TaskWrap::Runner because we shall not trace!
65
+
66
+ new_flow_options = flow_options.merge(
67
+ decision: nested_activity
68
+ )
69
+
70
+ return wrap_ctx, [[ctx, new_flow_options], original_circuit_options]
71
+ end
61
72
  end
62
73
 
63
- # Go through the list of all possible nested activities and compile the total sum of possible outputs.
64
- # FIXME: WHAT IF WE HAVE TWO IDENTICALLY NAMED OUTPUTS?
65
- # @private
66
- def self.outputs_for(activities)
67
- activities.map do |activity|
68
- Activity::Railway.Subprocess(activity)[:outputs]
69
- end.inject(:merge)
74
+ # Dynamic is without auto_wire where we don't even know what *could* be the actual
75
+ # nested activity until it's runtime.
76
+ def self.Dynamic(decider, id:)
77
+ _task = Class.new(Macro::Nested) do
78
+ step task: Dynamic.method(:call_dynamic_nested_activity),
79
+ id: :call_dynamic_nested_activity
80
+ end
70
81
  end
71
82
 
72
83
  class Dynamic
73
- def initialize(nested_activity_decider)
74
- @nested_activity_decider = Trailblazer::Option(nested_activity_decider)
75
- end
84
+ SUCCESS_SEMANTICS = [:success, :pass_fast] # TODO: make this injectable/or get it from operation.
76
85
 
77
- # TaskWrap step.
78
- def call(wrap_ctx, original_args)
79
- (ctx, _), original_circuit_options = original_args
86
+ def self.call_dynamic_nested_activity((ctx, flow_options), runner:, **circuit_options)
87
+ nested_activity = flow_options[:decision]
88
+ original_flow_options = flow_options.slice(*(flow_options.keys - [:decision]))
80
89
 
81
- # TODO: evaluate the option to get the actual "object" to call.
82
- activity = @nested_activity_decider.(ctx, keyword_arguments: ctx.to_hash, **original_circuit_options)
90
+ host_activity = Dynamic.host_activity_for(activity: nested_activity)
83
91
 
84
- # Overwrite :task so task_wrap.call_task will call this activity.
85
- # This is a taskWrap trick so we don't have to repeat logic from #call_task here.
86
- wrap_ctx[:task] = activity
92
+ # TODO: make activity here that has only one step (plus In and Out config) which is {nested_activity}
87
93
 
88
- return wrap_ctx, original_args
94
+ return_signal, (ctx, flow_options) = runner.(
95
+ nested_activity,
96
+ [ctx, original_flow_options], # pass {flow_options} without a {:decision}.
97
+ runner: runner,
98
+ **circuit_options,
99
+ activity: host_activity
100
+ )
101
+
102
+ return compute_legacy_return_signal(return_signal), [ctx, flow_options]
103
+ end
104
+
105
+ def self.compute_legacy_return_signal(return_signal)
106
+ actual_semantic = return_signal.to_h[:semantic]
107
+ applied_signal = SUCCESS_SEMANTICS.include?(actual_semantic) ? Activity::Right : Activity::Left # TODO: we could also provide PassFast/FailFast.
89
108
  end
90
109
 
91
- # TODO: remove me when we make {:auto_wire} mandatory.
92
- class ComputeLegacyReturnSignal
93
- SUCCESS_SEMANTICS = [:success, :pass_fast] # TODO: make this injectable/or get it from operation.
110
+ # This is used in Nested and Each where some tasks don't have a corresponding, hard-wired
111
+ # activity. This is needed for {TaskWrap.invoke} and the Debugging API in tracing.
112
+ # @private
113
+ def self.host_activity_for(activity:)
114
+ Activity::TaskWrap.container_activity_for(
115
+ activity,
116
+ nodes: [Trailblazer::Activity::NodeAttributes.new(activity.to_s, nil, activity)],
117
+ )
118
+ end
94
119
 
95
- def initialize(outputs)
96
- @outputs = outputs # not needed for auto_wire!
120
+ end
121
+
122
+ # Code to handle [:auto_wire]. This is called "static" as you configure the possible activities at
123
+ # compile-time. This is the recommended way.
124
+ #
125
+ # TODO: allow configuring Output of Nested per internal nested activity, e.g.
126
+ # step Nested(.., Id3Tag => {Output(:invalid_metadata) => ...}
127
+ # this will help when semantics overlap.
128
+ #
129
+ def self.Static(decider, id:, auto_wire:)
130
+ decider_outputs = auto_wire.collect do |activity|
131
+ [Activity::Railway.Output(activity, "decision:#{activity}"), Activity::Railway.Track(activity)]
132
+ end.to_h
133
+
134
+ _task = Class.new(Macro::Nested) do
135
+ step(
136
+ {
137
+ task: Static.method(:return_route_signal),
138
+ id: :route_to_nested_activity, # returns the {nested_activity} signal
139
+ }.merge(decider_outputs)
140
+ )
141
+
142
+ auto_wire.each do |activity|
143
+ activity_step = Subprocess(activity)
144
+
145
+ outputs = activity_step[:outputs]
146
+
147
+ # TODO: detect if we have two identical "special" termini.
148
+ output_wirings = outputs.collect do |semantic, output|
149
+ [Output(semantic), End(semantic)] # this will add a new termins to this activity.
150
+ end.to_h
151
+
152
+ # Each nested activity is a Subprocess.
153
+ # They have Output(semantic) => End(semantic) for each of their termini.
154
+ step activity_step,
155
+ {magnetic_to: activity}.merge(output_wirings)
156
+ # failure and success are wired to respective termini of {nesting_activity}.
97
157
  end
158
+ end
159
+ end
98
160
 
99
- def call(wrap_ctx, original_args)
100
- actual_semantic = wrap_ctx[:return_signal].to_h[:semantic]
101
- applied_semantic = SUCCESS_SEMANTICS.include?(actual_semantic) ? :success : :failure
161
+ module Static
162
+ def self.return_route_signal((ctx, flow_options), **circuit_options)
163
+ nested_activity = flow_options[:decision] # we use the decision class as a signal.
102
164
 
103
- wrap_ctx[:return_signal] = @outputs.fetch(applied_semantic).signal
165
+ original_flow_options = flow_options.slice(*(flow_options.keys - [:decision]))
104
166
 
105
- return wrap_ctx, original_args
106
- end
107
- end # ComputeLegacyReturnSignal
167
+ return nested_activity, [ctx, original_flow_options]
168
+ end
108
169
  end
109
- end
170
+
171
+ end # Nested
110
172
  end
111
173
  end
@@ -1,3 +1,5 @@
1
+ require "securerandom"
2
+
1
3
  module Trailblazer
2
4
  module Macro
3
5
  NoopHandler = lambda { |*| }
@@ -0,0 +1,32 @@
1
+ module Trailblazer
2
+ module Macro
3
+ # {Macro::Strategy} always keeps a {block_activity} and has to define a `#call` method per macro type.
4
+ class Strategy # We want to look like a real {Linear::Strategy}.
5
+ class << self
6
+ extend Forwardable
7
+ def_delegators :block_activity, :step, :pass, :fail # TODO: add all DSL::Helper
8
+ end
9
+
10
+ # This makes {Wrap} look like {block_activity}.
11
+ def self.to_h
12
+ block_activity.to_h
13
+ end
14
+
15
+ def self.block_activity
16
+ @state.get(:block_activity)
17
+ end
18
+
19
+ # DISCUSS: move this to Linear::Strategy.
20
+ module State
21
+ def initialize!(state)
22
+ @state = state
23
+ end
24
+
25
+ def inherited(inheritor)
26
+ super
27
+ inheritor.initialize!(@state.copy)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,7 +1,7 @@
1
1
  module Trailblazer
2
2
  module Version
3
3
  module Macro
4
- VERSION = "2.1.11"
4
+ VERSION = "2.1.12"
5
5
  end
6
6
  end
7
7
  end