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,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