trailblazer-macro 2.1.11 → 2.1.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -4
- data/.github/workflows/ci_jruby.yml +19 -0
- data/.github/workflows/ci_legacy.yml +19 -0
- data/.github/workflows/ci_truffleruby.yml +19 -0
- data/CHANGES.md +33 -0
- data/Gemfile +7 -5
- data/lib/trailblazer/macro/each.rb +187 -0
- data/lib/trailblazer/macro/model.rb +4 -5
- data/lib/trailblazer/macro/nested.rb +130 -68
- data/lib/trailblazer/macro/rescue.rb +2 -0
- data/lib/trailblazer/macro/strategy.rb +32 -0
- data/lib/trailblazer/macro/version.rb +1 -1
- data/lib/trailblazer/macro/wrap.rb +72 -65
- data/lib/trailblazer/macro.rb +53 -2
- data/test/docs/autogenerated/operation_each_test.rb +585 -0
- data/test/docs/autogenerated/operation_model_test.rb +263 -0
- data/test/docs/each_test.rb +940 -0
- data/test/docs/macro_test.rb +18 -0
- data/test/docs/model_test.rb +204 -88
- data/test/docs/nested_static_test.rb +730 -0
- data/test/docs/wrap_test.rb +348 -66
- data/test/test_helper.rb +29 -0
- data/trailblazer-macro.gemspec +2 -6
- metadata +21 -58
- data/.rubocop.yml +0 -6
- data/.rubocop_todo.yml +0 -423
- data/test/docs/nested_test.rb +0 -218
- data/test/operation/model_test.rb +0 -89
- data/test/operation/nested_test.rb +0 -98
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5cdf91d3afe4c31815401fd46ca1b6a8778dd2b1b89db4871c7dcff708cc73ff
|
4
|
+
data.tar.gz: 48c050c6086a5be67804385a427a95e0df1ab4cf0802de5004b61a96a8e54ced
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3a27a00484972d2c65a6ffad50bd34e1fff0cfee69382bb5fabd3a06f20e15f818f95d44ce03a13727095fe352901ca0fdfcc544e15ec066bc73e43d14b8b6df
|
7
|
+
data.tar.gz: c64786889c195182f399c9df157f67926622ae7c032d9ef8c8de47db89b37750544721b1261a7ea5fe5f731208eccb3192aac2fe2c92b77036cbcd3ce68e4070
|
data/.github/workflows/ci.yml
CHANGED
@@ -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
|
-
|
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@
|
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
|
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,36 @@
|
|
1
|
+
# 2.1.13
|
2
|
+
|
3
|
+
* Fix an introspect bug in `Each()` where we couldn't look into `"invoke_block_activity"`.
|
4
|
+
|
5
|
+
# 2.1.12
|
6
|
+
|
7
|
+
## Internals
|
8
|
+
|
9
|
+
* Introduce `AssignVariable`.
|
10
|
+
* Use trailblazer-activity-dsl-linear-1.1.0.
|
11
|
+
|
12
|
+
## Each()
|
13
|
+
|
14
|
+
* Introducing the `Each()` macro to iterate over datasets while invoking a particular operation, as if
|
15
|
+
they were run as separate serially connected operations.
|
16
|
+
|
17
|
+
## Nested()
|
18
|
+
|
19
|
+
* Restructured internals, it's much more readable, `Dynamic` and `Static` are completely separated now.
|
20
|
+
* `Static` handles `:auto_wire` and allows to route any nested terminus. This was not possible before,
|
21
|
+
you only had `FastTrack` generic termini.
|
22
|
+
* Better warning when using `Nested(Operation)` without a dynamic decider.
|
23
|
+
* Internal structure of `Nested()` has changed, the trace looks different.
|
24
|
+
|
25
|
+
## Wrap()
|
26
|
+
|
27
|
+
* Now implements patching.
|
28
|
+
|
29
|
+
## Model()
|
30
|
+
|
31
|
+
* Remove `ctx[:"result.model"]`.
|
32
|
+
* Don't set `ctx[:model]` unless `Builder` returns a model.
|
33
|
+
|
1
34
|
# 2.1.11
|
2
35
|
|
3
36
|
* 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",
|
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-
|
12
|
-
|
14
|
+
# gem "trailblazer-option", path: "../trailblazer-option"
|
13
15
|
|
14
16
|
gem "minitest-line"
|
15
|
-
gem "
|
17
|
+
# gem "trailblazer-core-utils", path: "../trailblazer-core-utils"
|
@@ -0,0 +1,187 @@
|
|
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 #call
|
56
|
+
|
57
|
+
def self.to_h
|
58
|
+
container_activity = @state.get(:activity)
|
59
|
+
# FIXME: this is needed for a proper {find_path} introspect lookup.
|
60
|
+
container_activity.to_h.merge(activity: container_activity)
|
61
|
+
end
|
62
|
+
|
63
|
+
# This is basically Out() => {copy all mutable variables}
|
64
|
+
ITERATION_OUTPUT_PIPE = Activity::DSL::Linear::VariableMapping::DSL.pipe_for_composable_output()
|
65
|
+
# and this In() => {copy everything}
|
66
|
+
ITERATION_INPUT_PIPE = Activity::DSL::Linear::VariableMapping::DSL.pipe_for_composable_input()
|
67
|
+
|
68
|
+
# Gets included in Debugger's Normalizer. Results in IDs like {invoke_block_activity.1}.
|
69
|
+
def self.compute_runtime_id(ctx, captured_node:, activity:, compile_id:, **)
|
70
|
+
# activity is the host activity
|
71
|
+
return compile_id unless activity[:each] == true
|
72
|
+
|
73
|
+
index = captured_node.captured_input.data[:ctx_snapshot].fetch(:index)
|
74
|
+
|
75
|
+
ctx[:runtime_id] = "#{compile_id}.#{index}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# @api private The internals here are considered private and might change in the near future.
|
80
|
+
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)
|
81
|
+
dsl_options_for_iterated = block_activity if block_activity.is_a?(Hash) # Ruby 2.5 and 2.6
|
82
|
+
|
83
|
+
block_activity, outputs_from_block_activity = Macro.block_activity_for(block_activity, &block)
|
84
|
+
|
85
|
+
collect_options = options_for_collect(collect: collect)
|
86
|
+
dataset_from_options = options_for_dataset_from(dataset_from: dataset_from)
|
87
|
+
|
88
|
+
wrap_static_for_block_activity = task_wrap_for_iterated(
|
89
|
+
{Activity::Railway.Out() => []}. # per default, don't let anything out.
|
90
|
+
merge(collect_options).
|
91
|
+
merge(dsl_options_for_iterated)
|
92
|
+
)
|
93
|
+
|
94
|
+
# This activity is passed into the {Runner} for each iteration of {block_activity}.
|
95
|
+
container_activity = Activity::TaskWrap.container_activity_for(
|
96
|
+
block_activity,
|
97
|
+
each: true, # mark this activity for {compute_runtime_id}.
|
98
|
+
nodes: [Activity::NodeAttributes.new("invoke_block_activity", nil, block_activity)], # TODO: use TaskMap::TaskAttributes
|
99
|
+
outputs: outputs_from_block_activity,
|
100
|
+
).merge(
|
101
|
+
# FIXME: we can't pass {wrap_static: wrap_static_for_block_activity} into {#container_activity_for}
|
102
|
+
# because when patching, the container_activity is not recompiled, so we need the Hash here
|
103
|
+
# with defaulting.
|
104
|
+
wrap_static: Hash.new(wrap_static_for_block_activity)
|
105
|
+
)
|
106
|
+
|
107
|
+
# DISCUSS: move to Wrap.
|
108
|
+
termini_from_block_activity =
|
109
|
+
outputs_from_block_activity.
|
110
|
+
# DISCUSS: End.success needs to be the last here, so it's directly behind {Start.default}.
|
111
|
+
sort { |a,b| a.semantic == :success ? 1 : -1 }.
|
112
|
+
collect { |output|
|
113
|
+
[output.signal, id: "End.#{output.semantic}", magnetic_to: output.semantic, append_to: "Start.default"]
|
114
|
+
}
|
115
|
+
|
116
|
+
state = Declarative::State(
|
117
|
+
block_activity: [block_activity, {copy: Trailblazer::Declarative::State.method(:subclass)}], # DISCUSS: move to Macro::Strategy.
|
118
|
+
item_key: [item_key, {}], # DISCUSS: we could even allow the wrap_handler to be patchable.
|
119
|
+
failing_semantic: [[:failure, :fail_fast], {}],
|
120
|
+
activity: [container_activity, {}],
|
121
|
+
success_signal: [termini_from_block_activity[-1][0], {}] # FIXME: when subclassing (e.g. patching) this must be recomputed.
|
122
|
+
)
|
123
|
+
|
124
|
+
# |-- Each/composers_for_each
|
125
|
+
# | |-- Start.default
|
126
|
+
# | |-- Each.iterate.block This is Class.new(Each), outputs_from_block_activity
|
127
|
+
# | | |-- invoke_block_activity.0 step :invoke_block_activity.0
|
128
|
+
# | | | |-- Start.default
|
129
|
+
# | | | |-- notify_composers
|
130
|
+
# | | | `-- End.success
|
131
|
+
# | | `-- invoke_block_activity.1 step "invoke_block_activity.1"
|
132
|
+
# | | |-- Start.default
|
133
|
+
# | | |-- notify_composers
|
134
|
+
iterate_strategy = Class.new(Each) do
|
135
|
+
extend Macro::Strategy::State # now, the Wrap subclass can inherit its state and copy the {block_activity}.
|
136
|
+
initialize!(state)
|
137
|
+
end
|
138
|
+
|
139
|
+
each_activity = Activity::FastTrack(termini: termini_from_block_activity) # DISCUSS: what base class should we be using?
|
140
|
+
each_activity.extend Each::Transitive
|
141
|
+
|
142
|
+
# {Subprocess} with {strict: true} will automatically wire all {block_activity}'s termini to the corresponding termini
|
143
|
+
# of {each_activity} as they have the same semantics (both termini sets are identical).
|
144
|
+
each_activity.step Activity::Railway.Subprocess(iterate_strategy, strict: true),
|
145
|
+
id: "Each.iterate.#{block ? :block : block_activity}" # FIXME: test :id.
|
146
|
+
|
147
|
+
Activity::Railway.Subprocess(each_activity).
|
148
|
+
merge(id: id).
|
149
|
+
merge(dataset_from_options) # FIXME: provide that service via Subprocess.
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.task_wrap_for_iterated(dsl_options)
|
153
|
+
# TODO: maybe the DSL API could be more "open" here? I bet it is, but I'm too lazy.
|
154
|
+
activity = Class.new(Activity::Railway) do
|
155
|
+
step({task: "iterated"}.merge(dsl_options))
|
156
|
+
end
|
157
|
+
|
158
|
+
activity.to_h[:config][:wrap_static]["iterated"]
|
159
|
+
end
|
160
|
+
|
161
|
+
# DSL options added to {block_activity} to implement {collect: true}.
|
162
|
+
def self.options_for_collect(collect:)
|
163
|
+
return {} unless collect
|
164
|
+
|
165
|
+
{
|
166
|
+
Activity::Railway.Inject(:collected_from_each) => ->(ctx, **) { [] }, # this is called only once.
|
167
|
+
Activity::Railway.Out() => ->(ctx, collected_from_each:, **) { {collected_from_each: collected_from_each += [ctx[:value]] } }
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.options_for_dataset_from(dataset_from:)
|
172
|
+
return {} unless dataset_from
|
173
|
+
|
174
|
+
{
|
175
|
+
Activity::Railway.Inject(:dataset, override: true) => dataset_from, # {ctx[:dataset]} is private to {each_activity}.
|
176
|
+
}
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
if const_defined?(:Developer) # FIXME: how do you properly check for a gem?
|
181
|
+
Developer::Trace::Debugger.add_normalizer_step!(
|
182
|
+
Macro::Each.method(:compute_runtime_id),
|
183
|
+
id: "Each.runtime_id",
|
184
|
+
append: :runtime_id, # so that the following {#runtime_path} picks up those changes made here.
|
185
|
+
)
|
186
|
+
end
|
187
|
+
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::
|
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
|
28
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
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
|
-
[
|
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
|
-
|
32
|
-
}
|
37
|
+
)
|
33
38
|
end
|
34
39
|
|
35
40
|
# @private
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
+
# TaskWrap API.
|
60
|
+
def call(wrap_ctx, original_args)
|
61
|
+
(ctx, flow_options), original_circuit_options = original_args
|
59
62
|
|
60
|
-
|
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
|
-
#
|
64
|
-
#
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
end
|
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
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
92
|
-
|
93
|
-
|
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
|
-
|
96
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
165
|
+
original_flow_options = flow_options.slice(*(flow_options.keys - [:decision]))
|
104
166
|
|
105
|
-
|
106
|
-
|
107
|
-
end # ComputeLegacyReturnSignal
|
167
|
+
return nested_activity, [ctx, original_flow_options]
|
168
|
+
end
|
108
169
|
end
|
109
|
-
|
170
|
+
|
171
|
+
end # Nested
|
110
172
|
end
|
111
173
|
end
|
@@ -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
|