trailblazer-activity-dsl-linear 0.5.0 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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,92 @@
1
+ module Trailblazer
2
+ class Activity
3
+ module DSL
4
+ module Linear
5
+ module Normalizer
6
+ # Normalizer pipeline for the {terminus} DSL method.
7
+ module Terminus
8
+ module_function
9
+
10
+ def Normalizer
11
+ normalizer_steps =
12
+ {
13
+ "activity.normalize_step_interface" => Normalizer.Task(Normalizer.method(:normalize_step_interface)), # first
14
+ "activity.merge_library_options" => Normalizer.Task(Normalizer.method(:merge_library_options)), # Merge "macro"/user options over library options.
15
+ "activity.normalize_for_macro" => Normalizer.Task(Normalizer.method(:merge_user_options)),
16
+ "activity.normalize_normalizer_options" => Normalizer.Task(Normalizer.method(:merge_normalizer_options)),
17
+ "activity.normalize_non_symbol_options" => Normalizer.Task(Normalizer.method(:normalize_non_symbol_options)),
18
+ "activity.normalize_context" => Normalizer.method(:normalize_context),
19
+ "terminus.normalize_task" => Normalizer.Task(Terminus.method(:normalize_task)),
20
+ "terminus.normalize_id" => Normalizer.Task(method(:normalize_id)),
21
+ "terminus.normalize_magnetic_to" => Normalizer.Task(Terminus.method(:normalize_magnetic_to)),
22
+ "terminus.append_end" => Normalizer.Task(Terminus.method(:append_end)),
23
+
24
+ "activity.compile_data" => Normalizer.Task(Normalizer.method(:compile_data)), # FIXME
25
+ "activity.create_row" => Normalizer.Task(Normalizer.method(:create_row)),
26
+ "activity.create_add" => Normalizer.Task(Normalizer.method(:create_add)),
27
+ "activity.create_adds" => Normalizer.Task(Normalizer.method(:create_adds)),
28
+ }
29
+
30
+ TaskWrap::Pipeline.new(normalizer_steps.to_a)
31
+ end
32
+
33
+ # @private
34
+ def normalize_id(ctx, id: nil, semantic:, **)
35
+ ctx.merge!(
36
+ id: id || Strategy.end_id(semantic: semantic)
37
+ )
38
+ end
39
+
40
+ # @private
41
+ # Set {:task} and {:semantic}.
42
+ def normalize_task(ctx, task:, **)
43
+ if task.kind_of?(Activity::End) # DISCUSS: do we want this check?
44
+ ctx = _normalize_task_for_end_event(ctx, **ctx)
45
+ else
46
+ # When used such as {terminus :found}, create the end event automatically.
47
+ ctx = _normalize_task_for_symbol(ctx, **ctx)
48
+ end
49
+ end
50
+
51
+ def _normalize_task_for_end_event(ctx, task:, **) # you cannot override using {:semantic}
52
+ ctx.merge!(
53
+ semantic: task.to_h[:semantic]
54
+ )
55
+ end
56
+
57
+ def _normalize_task_for_symbol(ctx, task:, semantic: task, **)
58
+ ctx.merge!(
59
+ task: Strategy.End(semantic),
60
+ semantic: semantic,
61
+ )
62
+ end
63
+
64
+ # @private
65
+ def normalize_magnetic_to(ctx, magnetic_to: nil, semantic:, **)
66
+ return if magnetic_to
67
+ ctx.merge!(magnetic_to: semantic)
68
+ end
69
+
70
+ # @private
71
+ def append_end(ctx, task:, semantic:, append_to: "End.success", **)
72
+ terminus_args = {
73
+ sequence_insert: [Activity::Adds::Insert.method(:Append), append_to],
74
+ stop_event: true
75
+ }
76
+
77
+ ctx.merge!(
78
+ wirings: [
79
+ Linear::Sequence::Search::Noop(
80
+ Activity::Output.new(task, semantic), # DISCUSS: do we really want to transport the semantic "in" the object?
81
+ )
82
+ ],
83
+ adds: [],
84
+ **terminus_args
85
+ )
86
+ end
87
+ end # Terminus
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -2,57 +2,119 @@ module Trailblazer
2
2
  class Activity
3
3
  module DSL
4
4
  module Linear
5
- # Normalizers are linear activities that process and normalize the options from a DSL call. They're
6
- # usually invoked from {Strategy#task_for}, which is called from {Path#step}, {Railway#pass}, etc.
5
+ # Normalizers are linear activities that process and normalize the options from a specific DSL call,
6
+ # such as `#step` or `#pass`. All defaulting should happen through the normalizer. An invoked
7
+ # normalizer produces an options hash that has to contain an [:adds] key with a ADDS structure usable
8
+ # for {Sequence.apply_adds}.
9
+ #
10
+ # They're usually invoked from {Strategy#invoke_normalizer_for!}, which is called from {Path#step},
11
+ # {Railway#pass}, etc.
7
12
  module Normalizer
13
+ # Container for all final normalizers of a specific Strategy.
14
+ class Normalizers
15
+ def initialize(**options)
16
+ @normalizers = options
17
+ end
18
+ # Execute the specific normalizer (step, fail, pass) for a particular option set provided
19
+ # by the DSL user. Usually invoked when you call {#step}.
20
+ def call(name, ctx)
21
+ normalizer = @normalizers.fetch(name)
22
+ wrap_ctx, _ = normalizer.(ctx, nil)
23
+ wrap_ctx
24
+ end
25
+ end
26
+
8
27
  module_function
9
28
 
10
- # Wrap {task} with {Trailblazer::Option} and execute it with kw args in {#call}.
11
- # Note that this instance always return {Right}.
29
+ # Helper for normalizers.
30
+ # To be applied on {Pipeline} instances.
31
+ def self.prepend_to(pipe, insertion_id, insertion)
32
+ adds =
33
+ insertion.collect do |id, task|
34
+ {insert: [Adds::Insert.method(:Prepend), insertion_id], row: Activity::TaskWrap::Pipeline.Row(id, task)}
35
+ end
36
+
37
+ Adds.apply_adds(pipe, insertion_id ? adds : adds.reverse)
38
+ end
39
+
40
+ # Helper for normalizers.
41
+ def self.replace(pipe, insertion_id, (id, task))
42
+ Adds.apply_adds(
43
+ pipe,
44
+ [{insert: [Adds::Insert.method(:Replace), insertion_id], row: Activity::TaskWrap::Pipeline.Row(id, task)}]
45
+ )
46
+ end
47
+
48
+ # Extend a particular normalizer with new steps and save it on the activity.
49
+ def self.extend!(activity_class, *step_methods)
50
+ activity_class.instance_variable_get(:@state).update!(:normalizers) do |normalizers|
51
+ hsh = normalizers.instance_variable_get(:@normalizers) # TODO: introduce {#to_h}.
52
+
53
+ new_normalizers = # {step: #<..>, pass: #<..>}
54
+ step_methods.collect do |name|
55
+ extended_normalizer = hsh.fetch(name) # grab existing normalizer.
56
+ new_normalizer = yield(extended_normalizer) # and let the user block change it.
57
+ [name, new_normalizer]
58
+ end.to_h
59
+
60
+
61
+ Normalizers.new(**hsh.merge(new_normalizers))
62
+ end
63
+ end
64
+
65
+ # @private
12
66
  class Task < TaskBuilder::Task
13
67
  def call(wrap_ctx, flow_options={})
14
- result = call_option(@task, [wrap_ctx, flow_options]) # DISCUSS: this mutates {ctx}.
68
+ _result = call_option(@task, [wrap_ctx, flow_options]) # DISCUSS: this mutates {ctx}.
15
69
 
16
70
  return wrap_ctx, flow_options
17
71
  end
18
72
  end
19
73
 
74
+ # Wrap user's normalizer task with {Trailblazer::Option} and thus execute it with
75
+ # convenient kw args.
76
+ #
77
+ # Example
78
+ # normalizer_task = Normalizer.Task(method(:normalize_id))
79
+ #
80
+ # # will call {normalizer_task} and pass ctx variables as kwargs, as follows
81
+ # def normalize_id(ctx, id: false, task:, **)
20
82
  def Task(user_proc)
21
83
  Normalizer::Task.new(Trailblazer::Option(user_proc), user_proc)
22
84
  end
23
85
 
24
- # activity_normalizer.([{options:, user_options:, normalizer_options: }])
25
- def activity_normalizer(pipeline)
26
- pipeline = TaskWrap::Pipeline.prepend(
27
- pipeline,
28
- nil, # this means, put it to the beginning.
86
+ # The generic normalizer not tied to `step` or friends.
87
+ def Normalizer
88
+ pipeline = TaskWrap::Pipeline.new(
29
89
  {
30
- "activity.normalize_step_interface" => Normalizer.Task(method(:normalize_step_interface)), # first
31
- "activity.normalize_for_macro" => Normalizer.Task(method(:merge_user_options)),
32
- "activity.normalize_normalizer_options" => Normalizer.Task(method(:merge_normalizer_options)),
33
- "activity.normalize_non_symbol_options" => Normalizer.Task(method(:normalize_non_symbol_options)),
34
- "activity.normalize_context" => method(:normalize_context),
35
- "activity.normalize_id" => Normalizer.Task(method(:normalize_id)),
36
- "activity.normalize_override" => Normalizer.Task(method(:normalize_override)),
37
- "activity.wrap_task_with_step_interface" => Normalizer.Task(method(:wrap_task_with_step_interface)), # last
38
- "activity.inherit_option" => Normalizer.Task(method(:inherit_option)),
39
- },
40
- )
41
-
42
- pipeline = TaskWrap::Pipeline.prepend(
43
- pipeline,
44
- "path.wirings",
45
- {
46
- "activity.normalize_outputs_from_dsl" => Normalizer.Task(method(:normalize_outputs_from_dsl)), # Output(Signal, :semantic) => Id()
47
- "activity.normalize_connections_from_dsl" => Normalizer.Task(method(:normalize_connections_from_dsl)),
48
- "activity.input_output_dsl" => Normalizer.Task(method(:input_output_dsl)),
49
- },
50
- )
51
-
52
- pipeline = TaskWrap::Pipeline.append(
53
- pipeline,
54
- nil,
55
- ["activity.cleanup_options", method(:cleanup_options)]
90
+ "activity.normalize_step_interface" => Normalizer.Task(method(:normalize_step_interface)), # Makes sure {:options} is always a hash.
91
+ "activity.merge_library_options" => Normalizer.Task(method(:merge_library_options)), # Merge "macro"/user options over library options.
92
+ "activity.normalize_for_macro" => Normalizer.Task(method(:merge_user_options)), # Merge user_options over "macro" options.
93
+ "activity.normalize_normalizer_options" => Normalizer.Task(method(:merge_normalizer_options)), # Merge user_options over normalizer_options.
94
+ "activity.normalize_non_symbol_options" => Normalizer.Task(method(:normalize_non_symbol_options)),
95
+ "activity.path_helper.forward_block" => Normalizer.Task(Helper::Path::Normalizer.method(:forward_block_for_path_branch)), # forward the "global" block
96
+ "activity.normalize_context" => method(:normalize_context),
97
+ "activity.normalize_id" => Normalizer.Task(method(:normalize_id)),
98
+ "activity.normalize_override" => Normalizer.Task(method(:normalize_override)),
99
+ "activity.wrap_task_with_step_interface" => Normalizer.Task(method(:wrap_task_with_step_interface)),
100
+
101
+ "activity.inherit_option" => Normalizer.Task(method(:inherit_option)),
102
+ "activity.sequence_insert" => Normalizer.Task(method(:normalize_sequence_insert)),
103
+ "activity.normalize_duplications" => Normalizer.Task(method(:normalize_duplications)),
104
+
105
+ "activity.path_helper.path_to_track" => Normalizer.Task(Helper::Path::Normalizer.method(:convert_paths_to_tracks)),
106
+ "activity.normalize_outputs_from_dsl" => Normalizer.Task(method(:normalize_outputs_from_dsl)), # Output(Signal, :semantic) => Id()
107
+ "activity.normalize_connections_from_dsl" => Normalizer.Task(method(:normalize_connections_from_dsl)),
108
+
109
+ "activity.wirings" => Normalizer.Task(method(:compile_wirings)),
110
+
111
+ # TODO: make this a "Subprocess":
112
+ "activity.compile_data" => Normalizer.Task(method(:compile_data)),
113
+ "activity.create_row" => Normalizer.Task(method(:create_row)),
114
+ "activity.create_add" => Normalizer.Task(method(:create_add)),
115
+ "activity.create_adds" => Normalizer.Task(method(:create_adds)),
116
+ }.
117
+ collect { |id, task| TaskWrap::Pipeline.Row(id, task) }
56
118
  )
57
119
 
58
120
  pipeline
@@ -101,10 +163,15 @@ module Trailblazer
101
163
  ctx[:replace] = (id || raise)
102
164
  end
103
165
 
166
+ # {:library_options} such as :sequence, :dsl_track, etc.
167
+ def merge_library_options(ctx, options:, library_options:, **)
168
+ ctx[:options] = library_options.merge(options)
169
+ end
170
+
104
171
  # make ctx[:options] the actual ctx
105
- def merge_user_options(ctx, options:, **)
172
+ def merge_user_options(ctx, options:, user_options:, **)
106
173
  # {options} are either a <#task> or {} from macro
107
- ctx[:options] = options.merge(ctx[:user_options]) # Note that the user options are merged over the macro options.
174
+ ctx[:options] = options.merge(user_options) # Note that the user options are merged over the macro options.
108
175
  end
109
176
 
110
177
  # {:normalizer_options} such as {:track_name} get overridden by user/macro.
@@ -132,6 +199,48 @@ module Trailblazer
132
199
  end
133
200
  end
134
201
 
202
+ # Processes {:before,:after,:replace,:delete} options and
203
+ # defaults to {before: "End.success"} which, yeah.
204
+ def normalize_sequence_insert(ctx, end_id:, **)
205
+ insertion = ctx.keys & sequence_insert_options.keys
206
+ insertion = insertion[0] || :before
207
+ target = ctx[insertion] || end_id
208
+
209
+ insertion_method = sequence_insert_options[insertion]
210
+
211
+ ctx[:sequence_insert] = [Activity::Adds::Insert.method(insertion_method), target]
212
+ end
213
+
214
+ # @private
215
+ def sequence_insert_options
216
+ {
217
+ :before => :Prepend,
218
+ :after => :Append,
219
+ :replace => :Replace,
220
+ :delete => :Delete,
221
+ }
222
+ end
223
+
224
+ def normalize_duplications(ctx, replace: false, **)
225
+ return if replace
226
+
227
+ raise_on_duplicate_id(ctx, **ctx)
228
+ clone_duplicate_activity(ctx, **ctx) # DISCUSS: mutates {ctx}.
229
+ end
230
+
231
+ # @private
232
+ def raise_on_duplicate_id(ctx, id:, sequence:, **)
233
+ raise "ID #{id} is already taken. Please specify an `:id`." if sequence.find { |row| row.id == id }
234
+ end
235
+
236
+ # @private
237
+ def clone_duplicate_activity(ctx, task:, sequence:, **)
238
+ return unless task.is_a?(Class)
239
+
240
+ ctx[:task] = task.clone if sequence.find { |row| row[1] == task }
241
+ end
242
+
243
+
135
244
  # Move DSL user options such as {Output(:success) => Track(:found)} to
136
245
  # a new key {options[:non_symbol_options]}.
137
246
  # This allows using {options} as a {**ctx}-able hash in Ruby 2.6 and 3.0.
@@ -144,24 +253,23 @@ module Trailblazer
144
253
  end
145
254
 
146
255
  # Process {Output(:semantic) => target} and make them {:connections}.
147
- def normalize_connections_from_dsl(ctx, connections:, adds:, non_symbol_options:, **)
256
+ def normalize_connections_from_dsl(ctx, connections:, adds:, non_symbol_options:, sequence:, normalizers:, **)
148
257
  # Find all {Output() => Track()/Id()/End()}
149
- output_configs = non_symbol_options.find_all{ |k,v| k.kind_of?(Activity::DSL::Linear::OutputSemantic) }
258
+ output_configs = non_symbol_options.find_all{ |k,v| k.kind_of?(Linear::OutputSemantic) }
150
259
  return unless output_configs.any?
151
260
 
261
+ # DISCUSS: how could we add another magnetic_to to an end?
152
262
  output_configs.each do |output, cfg|
153
263
  new_connections, add =
154
- if cfg.is_a?(Activity::DSL::Linear::Track)
155
- [output_to_track(ctx, output, cfg), cfg.adds]
156
- elsif cfg.is_a?(Activity::DSL::Linear::Id)
264
+ if cfg.is_a?(Linear::Track)
265
+ [output_to_track(ctx, output, cfg), cfg.adds] # FIXME: why does Track have a {adds} field? we don't use it anywhere.
266
+ elsif cfg.is_a?(Linear::Id)
157
267
  [output_to_id(ctx, output, cfg.value), []]
158
268
  elsif cfg.is_a?(Activity::End)
159
- _adds = []
269
+ end_id = Activity::Railway.end_id(**cfg.to_h)
270
+ end_exists = Activity::Adds::Insert.find_index(ctx[:sequence], end_id)
160
271
 
161
- end_id = Linear.end_id(cfg)
162
- end_exists = Insert.find_index(ctx[:sequence], end_id)
163
-
164
- _adds = [add_end(cfg, magnetic_to: end_id, id: end_id)] unless end_exists
272
+ _adds = end_exists ? [] : add_terminus(cfg, id: end_id, sequence: sequence, normalizers: normalizers)
165
273
 
166
274
  [output_to_id(ctx, output, end_id), _adds]
167
275
  else
@@ -179,23 +287,18 @@ module Trailblazer
179
287
  def output_to_track(ctx, output, track)
180
288
  search_strategy = track.options[:wrap_around] ? :WrapAround : :Forward
181
289
 
182
- {output.value => [Linear::Search.method(search_strategy), track.color]}
290
+ {output.value => [Linear::Sequence::Search.method(search_strategy), track.color]}
183
291
  end
184
292
 
185
293
  def output_to_id(ctx, output, target)
186
- {output.value => [Linear::Search.method(:ById), target]}
294
+ {output.value => [Linear::Sequence::Search.method(:ById), target]}
187
295
  end
188
296
 
189
- # {#insert_task} options to add another end.
190
- def add_end(end_event, magnetic_to:, id:)
191
-
192
- options = Path::DSL.append_end_options(task: end_event, magnetic_to: magnetic_to, id: id)
193
- row = Linear::Sequence.create_row(**options)
297
+ # Returns ADDS for the new terminus.
298
+ def add_terminus(end_event, id:, sequence:, normalizers:)
299
+ step_options = Linear::Sequence::Builder.invoke_normalizer_for(:terminus, end_event, {id: id}, sequence: sequence, normalizer_options: {}, normalizers: normalizers)
194
300
 
195
- {
196
- row: row,
197
- insert: row[3][:sequence_insert]
198
- }
301
+ step_options[:adds]
199
302
  end
200
303
 
201
304
  # Output(Signal, :semantic) => Id()
@@ -207,30 +310,32 @@ module Trailblazer
207
310
 
208
311
  output_configs.collect do |output, cfg| # {cfg} = Track(:success)
209
312
  outputs = outputs.merge(output.semantic => output)
210
- dsl_options = dsl_options.merge(Linear.Output(output.semantic) => cfg)
313
+ dsl_options = dsl_options.merge(Linear::Strategy.Output(output.semantic) => cfg)
211
314
  end
212
315
 
213
316
  ctx[:outputs] = outputs
214
317
  ctx[:non_symbol_options] = non_symbol_options.merge(dsl_options)
215
318
  end
216
319
 
217
- def input_output_dsl(ctx, extensions: [], **)
218
- config = ctx.select { |k,v| [:input, :output, :output_with_outer_ctx, :inject].include?(k) } # TODO: optimize this, we don't have to go through the entire hash.
219
- return unless config.any? # no :input/:output/:inject passed.
220
-
221
- ctx[:extensions] = extensions + [Linear.VariableMapping(**config)]
222
- end
223
-
224
320
  # Currently, the {:inherit} option copies over {:connections} from the original step
225
321
  # and merges them with the (prolly) connections passed from the user.
226
322
  def inherit_option(ctx, inherit: false, sequence:, id:, extensions: [], **)
227
- return unless inherit
323
+ return unless inherit === true
228
324
 
229
- index = Linear::Insert.find_index(sequence, id)
230
- row = sequence[index] # from this row we're inheriting options.
325
+ row = InheritOption.find_row(sequence, id) # from this row we're inheriting options.
326
+
327
+ inherited_connections = row.data[:connections]
328
+ inherited_extensions = row.data[:extensions]
329
+
330
+ ctx[:connections] = get_inheritable_connections(ctx, inherited_connections)
331
+ ctx[:extensions] = Array(inherited_extensions) + Array(extensions)
332
+ end
231
333
 
232
- ctx[:connections] = get_inheritable_connections(ctx, row[3][:connections])
233
- ctx[:extensions] = Array(row[3][:extensions]) + Array(extensions)
334
+ module InheritOption # TODO: move all inherit methods in here!
335
+ def self.find_row(sequence, id)
336
+ index = Activity::Adds::Insert.find_index(sequence, id)
337
+ sequence[index]
338
+ end
234
339
  end
235
340
 
236
341
  # return connections from {parent} step which are supported by current step
@@ -240,12 +345,24 @@ module Trailblazer
240
345
  parent_connections.slice(*ctx[:outputs].keys)
241
346
  end
242
347
 
243
- # TODO: make this extendable!
244
- def cleanup_options(ctx, flow_options)
245
- # new_ctx = ctx.reject { |k, v| [:connections, :outputs, :end_id, :step_interface_builder, :failure_end, :track_name, :sequence].include?(k) }
246
- new_ctx = ctx.reject { |k, v| [:outputs, :end_id, :step_interface_builder, :failure_end, :track_name, :sequence, :non_symbol_options].include?(k) }
348
+ def create_row(ctx, task:, wirings:, magnetic_to:, data:, **)
349
+ ctx[:row] = Sequence.create_row(task: task, magnetic_to: magnetic_to, wirings: wirings, **data)
350
+ end
351
+
352
+ def create_add(ctx, row:, sequence_insert:, **)
353
+ ctx[:add] = {row: row, insert: sequence_insert}
354
+ end
355
+
356
+ def create_adds(ctx, add:, adds:, **)
357
+ ctx[:adds] = [add] + adds
358
+ end
359
+
360
+ # TODO: document DataVariable() => :name
361
+ # Compile data that goes into the sequence row.
362
+ def compile_data(ctx, default_variables_for_data: [:id, :dsl_track, :connections, :extensions, :stop_event], non_symbol_options:, **)
363
+ variables_for_data = non_symbol_options.find_all { |k,v| k.instance_of?(Linear::DataVariableName) }.collect { |k,v| Array(v) }.flatten
247
364
 
248
- return new_ctx, flow_options
365
+ ctx[:data] = (default_variables_for_data + variables_for_data).collect { |key| [key, ctx[key]] }.to_h
249
366
  end
250
367
  end
251
368
 
@@ -0,0 +1,47 @@
1
+ module Trailblazer
2
+ class Activity
3
+ module DSL
4
+ module Linear
5
+ class Sequence
6
+ # Implements a DSL Builder pattern.
7
+ module Builder
8
+ # @return Sequence
9
+ def self.call(method, argument, options, **kws, &block)
10
+ update_sequence_for(method, argument, options, **kws, &block)
11
+ end
12
+
13
+ # @private
14
+ # Run a specific normalizer (e.g. for `#step`), apply the adds to the sequence and return the latter.
15
+ # DISCUSS: where does this method belong? Sequence + Normalizers?
16
+ def self.update_sequence_for(type, task, options={}, sequence:, **kws, &block)
17
+ step_options = invoke_normalizer_for(type, task, options, sequence: sequence, **kws, &block)
18
+
19
+ _sequence = Activity::Adds.apply_adds(sequence, step_options[:adds])
20
+ end
21
+
22
+ # @private
23
+ # DISCUSS: used in {Normalizer#add_terminus}, too.
24
+ def self.invoke_normalizer_for(type, task, options, normalizers:, normalizer_options:, sequence:, &block)
25
+ # These options represent direct configuration of the very method call that causes the normalizer to be run.
26
+ library_options = {
27
+ dsl_track: type,
28
+ block: block,
29
+
30
+ # DISCUSS: for some reason I am not entirely happy with those variables being here. Maybe this will change.
31
+ normalizers: normalizers,
32
+ sequence: sequence,
33
+ }
34
+
35
+ _step_options = normalizers.(type,
36
+ normalizer_options: normalizer_options, # class-level Strategy configuration, such as :step_interface_builder
37
+ options: task, # macro-options
38
+ user_options: options, # user-specified options from the DSL method
39
+ library_options: library_options # see above, "runtime" options (from compile-time, haha).
40
+ )
41
+ end
42
+ end # Builder
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,72 @@
1
+ module Trailblazer
2
+ class Activity
3
+ module DSL
4
+ module Linear
5
+ class Sequence
6
+ # Compile a {Schema} by computing {implementations} and {intermediate} from a {Sequence}.
7
+ module Compiler
8
+ module_function
9
+
10
+ # Default strategy to find out what's a stop event is to inspect the TaskRef's {data[:stop_event]}.
11
+ def find_stop_task_ids(intermediate_wiring)
12
+ intermediate_wiring.collect { |task_ref, outs| task_ref.data[:stop_event] ? task_ref.id : nil }.compact
13
+ end
14
+
15
+ # The first task in the wiring is the default start task.
16
+ def find_start_task_ids(intermediate_wiring)
17
+ [intermediate_wiring.first.first.id]
18
+ end
19
+
20
+ def call(sequence, find_stops: method(:find_stop_task_ids), find_start: method(:find_start_task_ids))
21
+ _implementations, intermediate_wiring =
22
+ sequence.inject([[], []]) do |(implementations, intermediates), seq_row|
23
+ _magnetic_to, task, connections, data = seq_row
24
+ id = data[:id]
25
+
26
+ # execute all {Search}s for one sequence row.
27
+ connections = find_connections(seq_row, connections, sequence)
28
+
29
+ # FIXME: {:extensions} should be initialized
30
+ implementations += [[id, Schema::Implementation::Task(task, connections.collect { |output, _| output }, data[:extensions] || []) ]]
31
+
32
+ intermediates += [
33
+ [
34
+ Schema::Intermediate::TaskRef(id, data),
35
+ # Compute outputs.
36
+ connections.collect { |output, target_id| Schema::Intermediate::Out(output.semantic, target_id) }
37
+ ]
38
+ ]
39
+
40
+ [implementations, intermediates]
41
+ end
42
+
43
+ start_task_ids = find_start.(intermediate_wiring)
44
+ stop_task_refs = find_stops.(intermediate_wiring)
45
+
46
+ intermediate = Schema::Intermediate.new(Hash[intermediate_wiring], stop_task_refs, start_task_ids)
47
+ implementation = Hash[_implementations]
48
+
49
+ Schema::Intermediate.(intermediate, implementation) # implemented in the generic {trailblazer-activity} gem.
50
+ end
51
+
52
+ # private
53
+
54
+ def find_connections(seq_row, strategies, sequence)
55
+ strategies.collect do |search|
56
+ output, target_seq_row = search.(sequence, seq_row) # invoke the node's "connection search" strategy.
57
+
58
+ target_seq_row = sequence[-1] if target_seq_row.nil? # connect to an End if target unknown. # DISCUSS: make this configurable, maybe?
59
+
60
+ [
61
+ output, # implementation
62
+ target_seq_row[3][:id], # intermediate # FIXME. this sucks.
63
+ target_seq_row # DISCUSS: needed?
64
+ ]
65
+ end.compact
66
+ end
67
+ end # Compiler
68
+ end # Sequence
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,58 @@
1
+ class Trailblazer::Activity
2
+ module DSL
3
+ module Linear
4
+ # Search strategies are part of the {wirings}, they find the next step
5
+ # for an output.
6
+ class Sequence
7
+ module Search
8
+ module_function
9
+
10
+ # From this task onwards, find the next task that's "magnetic to" {target_color}.
11
+ # Note that we only go forward, no back-references are done here.
12
+ def Forward(output, target_color)
13
+ ->(sequence, me) do
14
+ target_seq_row = find_in_range(sequence[sequence.index(me)+1..-1], target_color)
15
+
16
+ return output, target_seq_row
17
+ end
18
+ end
19
+
20
+ # Tries to find a track colored step by doing a Forward-search, first, then wraps around going
21
+ # through all steps from sequence start to self.
22
+ def WrapAround(output, target_color)
23
+ ->(sequence, me) do
24
+ my_index = sequence.index(me)
25
+ # First, try all elements after me, then go through the elements preceding myself.
26
+ wrapped_range = sequence[my_index+1..-1] + sequence[0..my_index-1]
27
+
28
+ target_seq_row = find_in_range(wrapped_range, target_color)
29
+
30
+ return output, target_seq_row
31
+ end
32
+ end
33
+
34
+ def Noop(output)
35
+ ->(sequence, me) do
36
+ return output, [nil,nil,nil,{}] # FIXME
37
+ end
38
+ end
39
+
40
+ # Find the seq_row with {id} and connect the current node to it.
41
+ def ById(output, id)
42
+ ->(sequence, me) do
43
+ index = Adds::Insert.find_index(sequence, id) or return output, sequence[0] # FIXME # or raise "Couldn't find {#{id}}"
44
+ target_seq_row = sequence[index]
45
+
46
+ return output, target_seq_row
47
+ end
48
+ end
49
+
50
+ # @private
51
+ def find_in_range(range, target_color)
52
+ _target_seq_row = range.find { |seq_row| seq_row[0] == target_color }
53
+ end
54
+ end # Search
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ class Trailblazer::Activity
2
+ module DSL
3
+ module Linear
4
+ # A {Sequence} consists of rows, each row represents one step (or task) of an activity
5
+ # and its incoming and outgoing connections.
6
+ # {Sequence row} consisting of {[magnetic_to, task, connections_searches, data]}.
7
+ # A Sequence is compiled into an activity using {Compiler}.
8
+ #
9
+ # Complies with the Adds interface (#to_a).
10
+ class Sequence < Array
11
+ # Row interface is part of the ADDs specification.
12
+ class Row < Array
13
+ def id
14
+ data[:id]
15
+ end
16
+
17
+ def data
18
+ self[3]
19
+ end
20
+ end
21
+
22
+ # Return {Sequence row} consisting of {[magnetic_to, task, connections_searches, data]}.
23
+ def self.create_row(task:, magnetic_to:, wirings:, **data)
24
+ Row[
25
+ magnetic_to,
26
+ task,
27
+ wirings,
28
+ data # {id: "Start.success"}
29
+ ]
30
+ end
31
+ end # Sequence
32
+ end # Linear
33
+ end
34
+ end