trailblazer-activity-dsl-linear 0.5.0 → 1.0.0.beta1
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/CHANGES.md +52 -0
- data/Gemfile +3 -1
- data/README.md +9 -17
- data/lib/trailblazer/activity/dsl/linear/feature/merge.rb +36 -0
- data/lib/trailblazer/activity/dsl/linear/feature/patch.rb +40 -0
- data/lib/trailblazer/activity/dsl/linear/feature/variable_mapping/dsl.rb +281 -0
- data/lib/trailblazer/activity/dsl/linear/feature/variable_mapping/inherit.rb +38 -0
- data/lib/trailblazer/activity/dsl/linear/feature/variable_mapping.rb +298 -0
- data/lib/trailblazer/activity/dsl/linear/helper/path.rb +106 -0
- data/lib/trailblazer/activity/dsl/linear/helper.rb +54 -128
- data/lib/trailblazer/activity/dsl/linear/normalizer/terminus.rb +92 -0
- data/lib/trailblazer/activity/dsl/linear/normalizer.rb +194 -77
- data/lib/trailblazer/activity/dsl/linear/sequence/builder.rb +47 -0
- data/lib/trailblazer/activity/dsl/linear/sequence/compiler.rb +72 -0
- data/lib/trailblazer/activity/dsl/linear/sequence/search.rb +58 -0
- data/lib/trailblazer/activity/dsl/linear/sequence.rb +34 -0
- data/lib/trailblazer/activity/dsl/linear/strategy.rb +116 -71
- data/lib/trailblazer/activity/dsl/linear/version.rb +1 -1
- data/lib/trailblazer/activity/dsl/linear.rb +21 -177
- data/lib/trailblazer/activity/fast_track.rb +42 -53
- data/lib/trailblazer/activity/path.rb +52 -127
- data/lib/trailblazer/activity/railway.rb +48 -68
- data/trailblazer-activity-dsl-linear.gemspec +3 -2
- metadata +41 -13
- data/lib/trailblazer/activity/dsl/linear/compiler.rb +0 -70
- data/lib/trailblazer/activity/dsl/linear/state.rb +0 -63
- data/lib/trailblazer/activity/dsl/linear/variable_mapping.rb +0 -240
@@ -0,0 +1,298 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Activity
|
3
|
+
module DSL
|
4
|
+
module Linear
|
5
|
+
# Normalizer-steps to implement {:input} and {:output}
|
6
|
+
# Returns an Extension instance to be thrown into the `step` DSL arguments.
|
7
|
+
def self.VariableMapping(input_id: "task_wrap.input", output_id: "task_wrap.output", **options)
|
8
|
+
input, output, normalizer_options, non_symbol_options = VariableMapping.merge_instructions_from_dsl(**options)
|
9
|
+
|
10
|
+
extension = VariableMapping.Extension(input, output)
|
11
|
+
|
12
|
+
return TaskWrap::Extension::WrapStatic.new(extension: extension), normalizer_options, non_symbol_options
|
13
|
+
end
|
14
|
+
|
15
|
+
module VariableMapping
|
16
|
+
# Add our normalizer steps to the strategy's normalizer.
|
17
|
+
def self.extend!(strategy, *step_methods) # DISCUSS: should this be implemented in Linear?
|
18
|
+
Linear::Normalizer.extend!(strategy, *step_methods) do |normalizer|
|
19
|
+
Linear::Normalizer.prepend_to(
|
20
|
+
normalizer,
|
21
|
+
"activity.wirings",
|
22
|
+
{
|
23
|
+
# In(), Out(), {:input}, Inject() feature
|
24
|
+
"activity.normalize_input_output_filters" => Linear::Normalizer.Task(VariableMapping::Normalizer.method(:normalize_input_output_filters)),
|
25
|
+
"activity.input_output_dsl" => Linear::Normalizer.Task(VariableMapping::Normalizer.method(:input_output_dsl)),
|
26
|
+
}
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.Extension(input, output, input_id: "task_wrap.input", output_id: "task_wrap.output")
|
32
|
+
TaskWrap.Extension(
|
33
|
+
[input, id: input_id, prepend: "task_wrap.call_task"],
|
34
|
+
[output, id: output_id, append: "task_wrap.call_task"]
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Steps that are added to the DSL normalizer.
|
39
|
+
module Normalizer
|
40
|
+
# Process {In() => [:model], Inject() => [:current_user], Out() => [:model]}
|
41
|
+
def self.normalize_input_output_filters(ctx, non_symbol_options:, **)
|
42
|
+
input_exts = non_symbol_options.find_all { |k,v| k.is_a?(VariableMapping::DSL::In) }
|
43
|
+
output_exts = non_symbol_options.find_all { |k,v| k.is_a?(VariableMapping::DSL::Out) }
|
44
|
+
inject_exts = non_symbol_options.find_all { |k,v| k.is_a?(VariableMapping::DSL::Inject) }
|
45
|
+
|
46
|
+
return unless input_exts.any? || output_exts.any? || inject_exts.any?
|
47
|
+
|
48
|
+
ctx[:inject_filters] = inject_exts
|
49
|
+
ctx[:in_filters] = input_exts
|
50
|
+
ctx[:out_filters] = output_exts
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.input_output_dsl(ctx, extensions: [], **options)
|
54
|
+
# no :input/:output/:inject/Input()/Output() passed.
|
55
|
+
return if (options.keys & [:input, :output, :inject, :inject_filters, :in_filters, :output_filters]).empty?
|
56
|
+
|
57
|
+
extension, normalizer_options, non_symbol_options = Linear.VariableMapping(**options)
|
58
|
+
|
59
|
+
ctx[:extensions] = extensions + [extension] # FIXME: allow {Extension() => extension}
|
60
|
+
ctx.merge!(**normalizer_options) # DISCUSS: is there another way of merging variables into ctx?
|
61
|
+
ctx[:non_symbol_options].merge!(non_symbol_options)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module_function
|
66
|
+
|
67
|
+
# For the input filter we
|
68
|
+
# 1. create a separate {Pipeline} instance {pipe}. Depending on the user's options, this might have up to four steps.
|
69
|
+
# 2. The {pipe} is run in a lamdba {input}, the lambda returns the pipe's ctx[:input_ctx].
|
70
|
+
# 3. The {input} filter in turn is wrapped into an {Activity::TaskWrap::Input} object via {#merge_instructions_for}.
|
71
|
+
# 4. The {TaskWrap::Input} instance is then finally placed into the taskWrap as {"task_wrap.input"}.
|
72
|
+
#
|
73
|
+
# @private
|
74
|
+
#
|
75
|
+
|
76
|
+
# default_input
|
77
|
+
# <or>
|
78
|
+
# oldway
|
79
|
+
# :input
|
80
|
+
# :inject
|
81
|
+
# newway(initial_input_pipeline)
|
82
|
+
# In,Inject
|
83
|
+
# => input_pipe
|
84
|
+
def merge_instructions_from_dsl(**options)
|
85
|
+
# The overriding {:input} option is set.
|
86
|
+
pipeline, has_mono_options, _ = DSL.pipe_for_mono_input(**options)
|
87
|
+
|
88
|
+
if ! has_mono_options
|
89
|
+
pipeline = DSL.pipe_for_composable_input(**options) # FIXME: rename filters consistently
|
90
|
+
end
|
91
|
+
|
92
|
+
# gets wrapped by {VariableMapping::Input} and called there.
|
93
|
+
# API: @filter.([ctx, original_flow_options], **original_circuit_options)
|
94
|
+
# input = Trailblazer::Option(->(original_ctx, **) { })
|
95
|
+
input = Pipe::Input.new(pipeline)
|
96
|
+
|
97
|
+
|
98
|
+
output_pipeline, has_mono_options, _ = DSL.pipe_for_mono_output(**options)
|
99
|
+
|
100
|
+
if ! has_mono_options
|
101
|
+
output_pipeline = DSL.pipe_for_composable_output(**options)
|
102
|
+
end
|
103
|
+
|
104
|
+
output = Pipe::Output.new(output_pipeline)
|
105
|
+
|
106
|
+
return input, output,
|
107
|
+
# normalizer_options:
|
108
|
+
{
|
109
|
+
variable_mapping_pipelines: [pipeline, output_pipeline],
|
110
|
+
},
|
111
|
+
# non_symbol_options:
|
112
|
+
{
|
113
|
+
Linear::Strategy.DataVariable() => :variable_mapping_pipelines # we want to store {:variable_mapping_pipelines} in {Row.data} for later reference.
|
114
|
+
}
|
115
|
+
# DISCUSS: should we remember the pure pipelines or get it from the compiled extension?
|
116
|
+
# store pipe in the extension (via TW::Extension.data)?
|
117
|
+
end
|
118
|
+
|
119
|
+
# < AddVariables
|
120
|
+
# Option
|
121
|
+
# filter
|
122
|
+
# merge_variables
|
123
|
+
|
124
|
+
# Runtime classes
|
125
|
+
Filter = Struct.new(:aggregate_step, :filter, :name, :add_variables_class)
|
126
|
+
|
127
|
+
# These objects are created via the DSL, keep all i/o steps in a Pipeline
|
128
|
+
# and run the latter when being `call`ed.
|
129
|
+
module Pipe
|
130
|
+
class Input
|
131
|
+
def initialize(pipe, id: :vm_original_ctx)
|
132
|
+
@pipe = pipe
|
133
|
+
@id = id # DISCUSS: untested.
|
134
|
+
end
|
135
|
+
|
136
|
+
def call(wrap_ctx, original_args)
|
137
|
+
(original_ctx, original_flow_options), original_circuit_options = original_args
|
138
|
+
|
139
|
+
# let user compute new ctx for the wrapped task.
|
140
|
+
pipe_ctx, _ = @pipe.({original_ctx: original_ctx}, [[original_ctx, original_flow_options], original_circuit_options])
|
141
|
+
input_ctx = pipe_ctx[:input_ctx]
|
142
|
+
|
143
|
+
wrap_ctx = wrap_ctx.merge(@id => original_ctx) # remember the original ctx under the key {:vm_original_ctx}.
|
144
|
+
|
145
|
+
# instead of the original Context, pass on the filtered `input_ctx` in the wrap.
|
146
|
+
return wrap_ctx, [[input_ctx, original_flow_options], original_circuit_options]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# API in VariableMapping::Output:
|
151
|
+
# output_ctx = @filter.(returned_ctx, [original_ctx, returned_flow_options], **original_circuit_options)
|
152
|
+
# Returns {output_ctx} that is used after taskWrap finished.
|
153
|
+
class Output < Input
|
154
|
+
# def call(returned_ctx, (original_ctx, returned_flow_options), **original_circuit_options)
|
155
|
+
def call(wrap_ctx, original_args)
|
156
|
+
returned_ctx, returned_flow_options = wrap_ctx[:return_args] # this is the Context returned from {call}ing the wrapped user task.
|
157
|
+
original_ctx = wrap_ctx[@id] # grab the original ctx from before which was set in the {:input} filter.
|
158
|
+
_, original_circuit_options = original_args
|
159
|
+
|
160
|
+
# let user compute the output.
|
161
|
+
pipe_ctx, _ = @pipe.({original_ctx: original_ctx, returned_ctx: returned_ctx}, [[original_ctx, returned_flow_options], original_circuit_options])
|
162
|
+
|
163
|
+
output_ctx = pipe_ctx[:aggregate]
|
164
|
+
|
165
|
+
wrap_ctx = wrap_ctx.merge(return_args: [output_ctx, returned_flow_options]) # DISCUSS: this won't allow tracing in the taskWrap as we're returning {returned_flow_options} from above.
|
166
|
+
|
167
|
+
return wrap_ctx, original_args
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# DISCUSS: improvable sections such as merge vs hash[]=
|
173
|
+
def initial_aggregate(wrap_ctx, original_args)
|
174
|
+
wrap_ctx = wrap_ctx.merge(aggregate: {})
|
175
|
+
|
176
|
+
return wrap_ctx, original_args
|
177
|
+
end
|
178
|
+
|
179
|
+
# Merge all original ctx variables into the new input_ctx.
|
180
|
+
# This happens when no {:input} is provided.
|
181
|
+
def default_input_ctx(wrap_ctx, original_args)
|
182
|
+
default_ctx = wrap_ctx[:original_ctx]
|
183
|
+
|
184
|
+
merge_variables(default_ctx, wrap_ctx, original_args)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Input/output Pipeline step that runs the user's {filter} and adds
|
188
|
+
# variables to the computed ctx.
|
189
|
+
#
|
190
|
+
# Basically implements {:input}.
|
191
|
+
#
|
192
|
+
# AddVariables: I call something with an Option-interface and run the return value through merge_variables().
|
193
|
+
# works on {:aggregate} by (usually) producing a hash fragment that is merged with the existing {:aggregate}
|
194
|
+
class AddVariables
|
195
|
+
def initialize(filter, user_filter)
|
196
|
+
@filter = filter # The users input/output filter.
|
197
|
+
@user_filter = user_filter # this is for introspection.
|
198
|
+
end
|
199
|
+
|
200
|
+
def call(wrap_ctx, original_args)
|
201
|
+
((original_ctx, _), circuit_options) = original_args
|
202
|
+
# puts "@@@@@ #{wrap_ctx[:returned_ctx].inspect}"
|
203
|
+
|
204
|
+
# this is the actual logic.
|
205
|
+
variables = call_filter(wrap_ctx, original_ctx, circuit_options, original_args)
|
206
|
+
|
207
|
+
VariableMapping.merge_variables(variables, wrap_ctx, original_args)
|
208
|
+
end
|
209
|
+
|
210
|
+
def call_filter(wrap_ctx, original_ctx, circuit_options, original_args)
|
211
|
+
_variables = @filter.(original_ctx, keyword_arguments: original_ctx.to_hash, **circuit_options)
|
212
|
+
end
|
213
|
+
|
214
|
+
class ReadFromAggregate < AddVariables # FIXME: REFACTOR
|
215
|
+
def call_filter(wrap_ctx, original_ctx, circuit_options, original_args)
|
216
|
+
new_ctx = wrap_ctx[:aggregate]
|
217
|
+
|
218
|
+
_variables = @filter.(new_ctx, keyword_arguments: new_ctx.to_hash, **circuit_options)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
class Output < AddVariables
|
223
|
+
def call_filter(wrap_ctx, original_ctx, circuit_options, original_args)
|
224
|
+
new_ctx = wrap_ctx[:returned_ctx]
|
225
|
+
|
226
|
+
@filter.(new_ctx, keyword_arguments: new_ctx.to_hash, **circuit_options)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Pass {inner_ctx, outer_ctx, **inner_ctx}
|
230
|
+
class WithOuterContext < Output
|
231
|
+
def call_filter(wrap_ctx, original_ctx, circuit_options, original_args)
|
232
|
+
new_ctx = wrap_ctx[:returned_ctx]
|
233
|
+
|
234
|
+
@filter.(new_ctx, original_ctx, keyword_arguments: new_ctx.to_hash, **circuit_options)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Always deletes from {:aggregate}.
|
239
|
+
class Delete < AddVariables
|
240
|
+
def call(wrap_ctx, original_args)
|
241
|
+
@filter.collect do |name|
|
242
|
+
wrap_ctx[:aggregate].delete(name) # FIXME: we're mutating a hash here!
|
243
|
+
end
|
244
|
+
|
245
|
+
return wrap_ctx, original_args
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Finally, create a new input ctx from all the
|
252
|
+
# collected input variables.
|
253
|
+
# This goes into the step/nested OP.
|
254
|
+
def scope(wrap_ctx, original_args)
|
255
|
+
((_, flow_options), _) = original_args
|
256
|
+
|
257
|
+
# this is the actual context passed into the step.
|
258
|
+
wrap_ctx[:input_ctx] = Trailblazer::Context(
|
259
|
+
wrap_ctx[:aggregate],
|
260
|
+
{}, # mutable variables
|
261
|
+
flow_options[:context_options]
|
262
|
+
)
|
263
|
+
|
264
|
+
return wrap_ctx, original_args
|
265
|
+
end
|
266
|
+
|
267
|
+
# Last call in every step. Currently replaces {:input_ctx} by adding variables using {#merge}.
|
268
|
+
# DISCUSS: improve here?
|
269
|
+
def merge_variables(variables, wrap_ctx, original_args)
|
270
|
+
wrap_ctx[:aggregate] = wrap_ctx[:aggregate].merge(variables)
|
271
|
+
|
272
|
+
return wrap_ctx, original_args
|
273
|
+
end
|
274
|
+
|
275
|
+
# @private
|
276
|
+
# The default {:output} filter only returns the "mutable" part of the inner ctx.
|
277
|
+
# This means only variables added using {inner_ctx[..]=} are merged on the outside.
|
278
|
+
def default_output_ctx(wrap_ctx, original_args)
|
279
|
+
new_ctx = wrap_ctx[:returned_ctx]
|
280
|
+
|
281
|
+
_wrapped, mutable = new_ctx.decompose # `_wrapped` is what the `:input` filter returned, `mutable` is what the task wrote to `scoped`.
|
282
|
+
|
283
|
+
merge_variables(mutable, wrap_ctx, original_args)
|
284
|
+
end
|
285
|
+
|
286
|
+
def merge_with_original(wrap_ctx, original_args)
|
287
|
+
original_ctx = wrap_ctx[:original_ctx] # outer ctx
|
288
|
+
output_variables = wrap_ctx[:aggregate]
|
289
|
+
|
290
|
+
wrap_ctx[:aggregate] = original_ctx.merge(output_variables) # FIXME: use merge_variables()
|
291
|
+
# pp wrap_ctx
|
292
|
+
return wrap_ctx, original_args
|
293
|
+
end
|
294
|
+
end # VariableMapping
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Trailblazer
|
2
|
+
class Activity
|
3
|
+
module DSL
|
4
|
+
module Linear
|
5
|
+
module Helper
|
6
|
+
# Normalizer logic for {Path() do end}.
|
7
|
+
#
|
8
|
+
# TODO: it would be cool to be able to connect an (empty) path to specific termini,
|
9
|
+
# this would work if we could add multiple magnetic_to.
|
10
|
+
module Path
|
11
|
+
# Normalizer steps to handle Path() macro.
|
12
|
+
module Normalizer
|
13
|
+
module_function
|
14
|
+
# Replace a block-expecting {PathBranch} instance with another one that's holding
|
15
|
+
# the global {:block} from {#step ... do end}.
|
16
|
+
def forward_block_for_path_branch(ctx, options:, normalizer_options:, library_options:, **)
|
17
|
+
block = options[:block]
|
18
|
+
non_symbol_options = options[:non_symbol_options]
|
19
|
+
|
20
|
+
return unless block
|
21
|
+
|
22
|
+
output, path_branch =
|
23
|
+
non_symbol_options.find { |output, cfg| cfg.kind_of?(Linear::PathBranch) }
|
24
|
+
|
25
|
+
path_branch_with_block = Linear::PathBranch.new(
|
26
|
+
normalizer_options.
|
27
|
+
merge(path_branch.options).
|
28
|
+
merge(block: block)
|
29
|
+
)
|
30
|
+
|
31
|
+
ctx[:options] = ctx[:options].merge(non_symbol_options: non_symbol_options.merge(output => path_branch_with_block))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convert all occurrences of Path() to a corresponding {Track}.
|
35
|
+
# The {Track} instance contains all additional {adds} steps and
|
36
|
+
# is picked up in {Normalizer.normalize_connections_from_dsl}.
|
37
|
+
def convert_paths_to_tracks(ctx, non_symbol_options:, block: false, **)
|
38
|
+
new_tracks = non_symbol_options.
|
39
|
+
find_all { |output, cfg| cfg.kind_of?(Linear::PathBranch) }.
|
40
|
+
collect { |output, cfg| [output, Path.convert_path_to_track(block: ctx[:block], **cfg.options)] }.
|
41
|
+
to_h
|
42
|
+
|
43
|
+
ctx[:non_symbol_options] = non_symbol_options.merge(new_tracks)
|
44
|
+
end
|
45
|
+
end # Normalizer
|
46
|
+
|
47
|
+
module_function
|
48
|
+
|
49
|
+
def convert_path_to_track(track_color: "track_#{rand}", connect_to: nil, before: false, block: nil, **options)
|
50
|
+
# DISCUSS: if anyone overrides `#step` in the "outer" activity, this won't be applied inside the branch.
|
51
|
+
|
52
|
+
# DISCUSS: use Path::Sequencer::Builder here instead?
|
53
|
+
path = Activity::Path(**options, track_name: track_color, &block)
|
54
|
+
|
55
|
+
seq = path.to_h[:sequence]
|
56
|
+
# Strip default ends `Start.default` and `End.success` (if present).
|
57
|
+
seq = seq[1..-1].reject{ |row| row[3][:stop_event] && row.id == 'End.success' }
|
58
|
+
|
59
|
+
if connect_to
|
60
|
+
seq = connect_for_sequence(seq, connect_to: connect_to)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Add the path elements before {End.success}.
|
64
|
+
# Termini (or :stop_event) are to be placed after {End.success}.
|
65
|
+
adds = seq.collect do |row|
|
66
|
+
options = row[3]
|
67
|
+
|
68
|
+
# the terminus of the path goes _after_ {End.success} into the "end group".
|
69
|
+
insert_method = options[:stop_event] ? Activity::Adds::Insert.method(:Append) : Activity::Adds::Insert.method(:Prepend)
|
70
|
+
|
71
|
+
insert_target = "End.success" # insert before/after
|
72
|
+
insert_target = before if before && connect_to.instance_of?(Trailblazer::Activity::DSL::Linear::Track) # FIXME: this is a bit hacky, of course!
|
73
|
+
|
74
|
+
{
|
75
|
+
row: row,
|
76
|
+
insert: [insert_method, insert_target]
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
# Connect the Output() => Track(path_track)
|
81
|
+
return Linear::Track.new(track_color, adds, {})
|
82
|
+
end
|
83
|
+
|
84
|
+
# Connect last row of the {sequence} to the given step via its {Id}
|
85
|
+
# Useful when steps needs to be inserted in between {Start} and {connect Id()}.
|
86
|
+
private def connect_for_sequence(sequence, connect_to:)
|
87
|
+
output, _ = sequence[-1][2][0].(sequence, sequence[-1]) # FIXME: the Forward() proc contains the row's Output, and the only current way to retrieve it is calling the search strategy. It should be Forward#to_h
|
88
|
+
|
89
|
+
# searches = [Search.ById(output, connect_to.value)]
|
90
|
+
searches = [Sequence::Search.ById(output, connect_to.value)] if connect_to.instance_of?(Trailblazer::Activity::DSL::Linear::Id)
|
91
|
+
searches = [Sequence::Search.Forward(output, connect_to.color)] if connect_to.instance_of?(Trailblazer::Activity::DSL::Linear::Track) # FIXME: use existing mapping logic!
|
92
|
+
|
93
|
+
row = sequence[-1]
|
94
|
+
row = row[0..1] + [searches] + [row[3]] # FIXME: not mutating an array is so hard: we only want to replace the "searches" element, index 2
|
95
|
+
row = Sequence::Row[*row]
|
96
|
+
|
97
|
+
sequence = sequence[0..-2] + [row]
|
98
|
+
|
99
|
+
sequence
|
100
|
+
end
|
101
|
+
end # Path
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -2,151 +2,77 @@ module Trailblazer
|
|
2
2
|
class Activity
|
3
3
|
module DSL
|
4
4
|
module Linear
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
5
|
+
# Data Structures used in the DSL. They're mostly created from helpers
|
6
|
+
# and then get processed in the normalizer.
|
7
|
+
#
|
8
|
+
# @private
|
9
|
+
OutputSemantic = Struct.new(:value)
|
10
|
+
Id = Struct.new(:value)
|
11
|
+
Track = Struct.new(:color, :adds, :options)
|
12
|
+
Extension = Struct.new(:callable) do
|
13
|
+
def call(*args, &block)
|
14
|
+
callable.(*args, &block)
|
14
15
|
end
|
16
|
+
end
|
17
|
+
PathBranch = Struct.new(:options)
|
18
|
+
DataVariableName = Class.new
|
15
19
|
|
16
|
-
|
17
|
-
|
20
|
+
# Shortcut functions for the DSL.
|
21
|
+
# Those are included in {Strategy} so they're available to all Strategy users such
|
22
|
+
# as {Railway} or {Operation}.
|
23
|
+
module Helper
|
24
|
+
# This is the namespace container for {Contract::}, {Policy::} and friends.
|
25
|
+
module Constants
|
18
26
|
end
|
19
27
|
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
def Output(signal, semantic=nil)
|
25
|
-
return OutputSemantic.new(signal) if semantic.nil?
|
26
|
-
|
27
|
-
Activity.Output(signal, semantic)
|
28
|
-
end
|
29
|
-
|
30
|
-
def End(semantic)
|
31
|
-
Activity.End(semantic)
|
32
|
-
end
|
33
|
-
|
34
|
-
def end_id(_end)
|
35
|
-
"End.#{_end.to_h[:semantic]}" # TODO: use everywhere
|
36
|
-
end
|
37
|
-
|
38
|
-
def Track(color, wrap_around: false)
|
39
|
-
Track.new(color, [], wrap_around: wrap_around).freeze
|
40
|
-
end
|
41
|
-
|
42
|
-
def Id(id)
|
43
|
-
Id.new(id).freeze
|
44
|
-
end
|
45
|
-
|
46
|
-
def Path(track_color: "track_#{rand}", connect_to: nil, before: false, **options, &block)
|
47
|
-
path = Activity::Path(track_name: track_color, **options)
|
48
|
-
activity = Class.new(path) { self.instance_exec(&block) }
|
49
|
-
|
50
|
-
seq = activity.instance_variable_get(:@state).to_h[:sequence] # TODO: fix @state interface
|
51
|
-
# Strip default ends `Start.default` and `End.success` (if present).
|
52
|
-
seq = seq[1..-1].reject{ |row| row[3][:stop_event] && row[3][:id] == 'End.success' }
|
53
|
-
|
54
|
-
if connect_to
|
55
|
-
seq = connect_for_sequence(seq, connect_to: connect_to)
|
56
|
-
end
|
57
|
-
|
58
|
-
# Add the path elements before {End.success}.
|
59
|
-
# Termini (or :stop_event) are to be placed after {End.success}.
|
60
|
-
adds = seq.collect do |row|
|
61
|
-
options = row[3]
|
62
|
-
|
63
|
-
# the terminus of the path goes _after_ {End.success} into the "end group".
|
64
|
-
insert_method = options[:stop_event] ? Insert.method(:Append) : Insert.method(:Prepend)
|
65
|
-
|
66
|
-
insert_target = "End.success" # insert before/after
|
67
|
-
insert_target = before if before && connect_to.instance_of?(Trailblazer::Activity::DSL::Linear::Helper::Track) # FIXME: this is a bit hacky, of course!
|
68
|
-
|
69
|
-
{
|
70
|
-
row: row,
|
71
|
-
insert: [insert_method, insert_target]
|
72
|
-
}
|
73
|
-
end
|
28
|
+
# Output( Left, :failure )
|
29
|
+
# Output( :failure ) #=> Output::Semantic
|
30
|
+
def Output(signal, semantic=nil)
|
31
|
+
return OutputSemantic.new(signal) if semantic.nil?
|
74
32
|
|
75
|
-
|
76
|
-
|
77
|
-
end
|
78
|
-
|
79
|
-
# Connect last row of the {sequence} to the given step via its {Id}
|
80
|
-
# Useful when steps needs to be inserted in between {Start} and {connect Id()}.
|
81
|
-
private def connect_for_sequence(sequence, connect_to:)
|
82
|
-
output, _ = sequence[-1][2][0].(sequence, sequence[-1]) # FIXME: the Forward() proc contains the row's Output, and the only current way to retrieve it is calling the search strategy. It should be Forward#to_h
|
83
|
-
|
84
|
-
# searches = [Search.ById(output, connect_to.value)]
|
85
|
-
searches = [Search.ById(output, connect_to.value)] if connect_to.instance_of?(Trailblazer::Activity::DSL::Linear::Helper::Id)
|
86
|
-
searches = [Search.Forward(output, connect_to.color)] if connect_to.instance_of?(Trailblazer::Activity::DSL::Linear::Helper::Track) # FIXME: use existing mapping logic!
|
87
|
-
|
88
|
-
row = sequence[-1]
|
89
|
-
row = row[0..1] + [searches] + [row[3]] # FIXME: not mutating an array is so hard: we only want to replace the "searches" element, index 2
|
90
|
-
|
91
|
-
sequence = sequence[0..-2] + [row]
|
92
|
-
|
93
|
-
sequence
|
94
|
-
end
|
95
|
-
|
96
|
-
# Computes the {:outputs} options for {activity}.
|
97
|
-
def Subprocess(activity, patch: {})
|
98
|
-
activity = Patch.customize(activity, options: patch)
|
33
|
+
Activity.Output(signal, semantic)
|
34
|
+
end
|
99
35
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
}
|
104
|
-
end
|
36
|
+
def End(semantic)
|
37
|
+
Activity.End(semantic)
|
38
|
+
end
|
105
39
|
|
106
|
-
|
107
|
-
|
40
|
+
def end_id(semantic:, **)
|
41
|
+
"End.#{semantic}" # TODO: use everywhere
|
42
|
+
end
|
108
43
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
options
|
44
|
+
def Track(color, wrap_around: false)
|
45
|
+
Track.new(color, [], wrap_around: wrap_around).freeze
|
46
|
+
end
|
113
47
|
|
114
|
-
|
115
|
-
|
116
|
-
|
48
|
+
def Id(id)
|
49
|
+
Id.new(id).freeze
|
50
|
+
end
|
117
51
|
|
118
|
-
|
119
|
-
|
52
|
+
def Path(**options, &block)
|
53
|
+
options = options.merge(block: block) if block_given?
|
120
54
|
|
121
|
-
|
122
|
-
|
55
|
+
Linear::PathBranch.new(options) # picked up by normalizer.
|
56
|
+
end
|
123
57
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
patched_segment_activity = call(segment_activity, path, customization)
|
58
|
+
# Computes the {:outputs} options for {activity}.
|
59
|
+
def Subprocess(activity, patch: {})
|
60
|
+
activity = Patch.customize(activity, options: patch)
|
128
61
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
62
|
+
{
|
63
|
+
task: activity,
|
64
|
+
outputs: Hash[activity.to_h[:outputs].collect { |output| [output.semantic, output] }]
|
65
|
+
}
|
66
|
+
end
|
134
67
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
end
|
139
|
-
end
|
68
|
+
def In(**kws); VariableMapping::DSL::In(**kws); end
|
69
|
+
def Out(**kws); VariableMapping::DSL::Out(**kws); end
|
70
|
+
def Inject(**kws); VariableMapping::DSL::Inject(**kws); end
|
140
71
|
|
141
|
-
|
142
|
-
|
143
|
-
foreign = options.reject { |key, value| local_keys.include?(key) }
|
144
|
-
return foreign, locals
|
145
|
-
end
|
72
|
+
def DataVariable
|
73
|
+
DataVariableName.new
|
146
74
|
end
|
147
75
|
end # Helper
|
148
|
-
|
149
|
-
include Helper # Introduce Helper constants in DSL::Linear scope
|
150
76
|
end # Linear
|
151
77
|
end # DSL
|
152
78
|
end # Activity
|