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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/CHANGES.md +52 -0
  4. data/Gemfile +3 -1
  5. data/README.md +9 -17
  6. data/lib/trailblazer/activity/dsl/linear/feature/merge.rb +36 -0
  7. data/lib/trailblazer/activity/dsl/linear/feature/patch.rb +40 -0
  8. data/lib/trailblazer/activity/dsl/linear/feature/variable_mapping/dsl.rb +281 -0
  9. data/lib/trailblazer/activity/dsl/linear/feature/variable_mapping/inherit.rb +38 -0
  10. data/lib/trailblazer/activity/dsl/linear/feature/variable_mapping.rb +298 -0
  11. data/lib/trailblazer/activity/dsl/linear/helper/path.rb +106 -0
  12. data/lib/trailblazer/activity/dsl/linear/helper.rb +54 -128
  13. data/lib/trailblazer/activity/dsl/linear/normalizer/terminus.rb +92 -0
  14. data/lib/trailblazer/activity/dsl/linear/normalizer.rb +194 -77
  15. data/lib/trailblazer/activity/dsl/linear/sequence/builder.rb +47 -0
  16. data/lib/trailblazer/activity/dsl/linear/sequence/compiler.rb +72 -0
  17. data/lib/trailblazer/activity/dsl/linear/sequence/search.rb +58 -0
  18. data/lib/trailblazer/activity/dsl/linear/sequence.rb +34 -0
  19. data/lib/trailblazer/activity/dsl/linear/strategy.rb +116 -71
  20. data/lib/trailblazer/activity/dsl/linear/version.rb +1 -1
  21. data/lib/trailblazer/activity/dsl/linear.rb +21 -177
  22. data/lib/trailblazer/activity/fast_track.rb +42 -53
  23. data/lib/trailblazer/activity/path.rb +52 -127
  24. data/lib/trailblazer/activity/railway.rb +48 -68
  25. data/trailblazer-activity-dsl-linear.gemspec +3 -2
  26. metadata +41 -13
  27. data/lib/trailblazer/activity/dsl/linear/compiler.rb +0 -70
  28. data/lib/trailblazer/activity/dsl/linear/state.rb +0 -63
  29. 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
- module Helper
6
- # @api private
7
- OutputSemantic = Struct.new(:value)
8
- Id = Struct.new(:value)
9
- Track = Struct.new(:color, :adds, :options)
10
- Extension = Struct.new(:callable) do
11
- def call(*args, &block)
12
- callable.(*args, &block)
13
- end
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
- def self.included(base)
17
- base.extend ClassMethods
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
- # Shortcut functions for the DSL.
21
- module ClassMethods
22
- # Output( Left, :failure )
23
- # Output( :failure ) #=> Output::Semantic
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
- # Connect the Output() => Track(path_track)
76
- return Track.new(track_color, adds, {})
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
- task: activity,
102
- outputs: Hash[activity.to_h[:outputs].collect { |output| [output.semantic, output] }]
103
- }
104
- end
36
+ def End(semantic)
37
+ Activity.End(semantic)
38
+ end
105
39
 
106
- module Patch
107
- module_function
40
+ def end_id(semantic:, **)
41
+ "End.#{semantic}" # TODO: use everywhere
42
+ end
108
43
 
109
- def customize(activity, options:)
110
- options = options.is_a?(Proc) ?
111
- { [] => options } : # hash-wrapping with empty path, for patching given activity itself
112
- options
44
+ def Track(color, wrap_around: false)
45
+ Track.new(color, [], wrap_around: wrap_around).freeze
46
+ end
113
47
 
114
- options.each do |path, patch|
115
- activity = call(activity, path, patch) # TODO: test if multiple patches works!
116
- end
48
+ def Id(id)
49
+ Id.new(id).freeze
50
+ end
117
51
 
118
- activity
119
- end
52
+ def Path(**options, &block)
53
+ options = options.merge(block: block) if block_given?
120
54
 
121
- def call(activity, path, customization)
122
- task_id, *path = path
55
+ Linear::PathBranch.new(options) # picked up by normalizer.
56
+ end
123
57
 
124
- patch =
125
- if task_id
126
- segment_activity = Introspect::Graph(activity).find(task_id).task
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
- # Replace the patched subprocess.
130
- -> { step Subprocess(patched_segment_activity), inherit: true, replace: task_id, id: task_id }
131
- else
132
- customization # apply the *actual* patch from the Subprocess() call.
133
- end
62
+ {
63
+ task: activity,
64
+ outputs: Hash[activity.to_h[:outputs].collect { |output| [output.semantic, output] }]
65
+ }
66
+ end
134
67
 
135
- patched_activity = Class.new(activity)
136
- patched_activity.class_exec(&patch)
137
- patched_activity
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
- def normalize(options, local_keys) # TODO: test me.
142
- locals = options.reject { |key, value| ! local_keys.include?(key) }
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