trailblazer-activity 0.3.2 → 0.4.o

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +4 -3
  3. data/CHANGES.md +4 -0
  4. data/lib/trailblazer/activity.rb +82 -140
  5. data/lib/trailblazer/activity/config.rb +37 -0
  6. data/lib/trailblazer/activity/dsl/add_task.rb +22 -0
  7. data/lib/trailblazer/activity/dsl/helper.rb +49 -0
  8. data/lib/trailblazer/activity/implementation/build_state.rb +31 -0
  9. data/lib/trailblazer/activity/implementation/fast_track.rb +14 -0
  10. data/lib/trailblazer/activity/implementation/interface.rb +16 -0
  11. data/lib/trailblazer/activity/implementation/path.rb +55 -0
  12. data/lib/trailblazer/activity/implementation/railway.rb +18 -0
  13. data/lib/trailblazer/activity/{introspection.rb → introspect.rb} +33 -11
  14. data/lib/trailblazer/activity/magnetic.rb +7 -18
  15. data/lib/trailblazer/activity/magnetic/builder.rb +37 -92
  16. data/lib/trailblazer/activity/magnetic/builder/default_normalizer.rb +26 -0
  17. data/lib/trailblazer/activity/magnetic/builder/fast_track.rb +13 -15
  18. data/lib/trailblazer/activity/magnetic/builder/normalizer.rb +105 -0
  19. data/lib/trailblazer/activity/magnetic/builder/path.rb +14 -15
  20. data/lib/trailblazer/activity/magnetic/builder/railway.rb +8 -11
  21. data/lib/trailblazer/activity/magnetic/dsl.rb +4 -1
  22. data/lib/trailblazer/activity/magnetic/finalizer.rb +1 -1
  23. data/lib/trailblazer/activity/magnetic/merge.rb +18 -0
  24. data/lib/trailblazer/activity/present.rb +1 -1
  25. data/lib/trailblazer/activity/schema/dependencies.rb +6 -1
  26. data/lib/trailblazer/activity/state.rb +58 -0
  27. data/lib/trailblazer/activity/structures.rb +1 -2
  28. data/lib/trailblazer/activity/subprocess.rb +6 -3
  29. data/lib/trailblazer/activity/task_builder.rb +38 -0
  30. data/lib/trailblazer/activity/task_wrap.rb +46 -0
  31. data/lib/trailblazer/{wrap → activity/task_wrap}/call_task.rb +3 -3
  32. data/lib/trailblazer/activity/task_wrap/merge.rb +23 -0
  33. data/lib/trailblazer/{wrap → activity/task_wrap}/runner.rb +14 -16
  34. data/lib/trailblazer/{wrap → activity/task_wrap}/trace.rb +3 -3
  35. data/lib/trailblazer/activity/trace.rb +22 -28
  36. data/lib/trailblazer/activity/version.rb +2 -2
  37. data/lib/trailblazer/circuit.rb +7 -5
  38. data/trailblazer-activity.gemspec +2 -1
  39. metadata +39 -14
  40. data/lib/trailblazer/activity/heritage.rb +0 -30
  41. data/lib/trailblazer/activity/magnetic/builder/block.rb +0 -37
  42. data/lib/trailblazer/activity/process.rb +0 -16
  43. data/lib/trailblazer/activity/wrap.rb +0 -22
@@ -0,0 +1,105 @@
1
+ module Trailblazer
2
+ module Activity::Magnetic
3
+ # The {Normalizer} is called for every DSL call (step/pass/fail etc.) and normalizes/defaults
4
+ # the user options, such as setting `:id`, connecting the task's outputs or wrapping the user's
5
+ # task via {TaskBuilder::Binary} in order to translate true/false to `Right` or `Left`.
6
+ #
7
+ # The Normalizer sits in the `@builder`, which receives all DSL calls from the Operation subclass.
8
+ class Normalizer
9
+ def self.build(task_builder: Activity::TaskBuilder::Binary, default_plus_poles: Normalizer.InitialPlusPoles(), pipeline: Pipeline, extension:[], **options)
10
+ return new(
11
+ default_plus_poles: default_plus_poles,
12
+ extension: extension,
13
+ task_builder: task_builder,
14
+ pipeline: pipeline,
15
+ ), options
16
+ end
17
+
18
+ # @private Might be removed.
19
+ def self.InitialPlusPoles
20
+ Activity::Magnetic::DSL::PlusPoles.new.merge(
21
+ Activity.Output(Activity::Right, :success) => nil,
22
+ Activity.Output(Activity::Left, :failure) => nil,
23
+ )
24
+ end
25
+
26
+ def initialize(task_builder:, default_plus_poles:, pipeline:, **options)
27
+ @task_builder = task_builder
28
+ @default_plus_poles = default_plus_poles
29
+ @pipeline = pipeline # TODO: test me.
30
+ freeze
31
+ end
32
+
33
+ def call(task, options)
34
+ ctx = {
35
+ task: task, options: options,
36
+ task_builder: @task_builder,
37
+ default_plus_poles: @default_plus_poles,
38
+ }
39
+
40
+ signal, (ctx, ) = @pipeline.( [ctx] )
41
+
42
+ return ctx[:options][:task], ctx[:local_options], ctx[:connection_options], ctx[:sequence_options]
43
+ end
44
+
45
+ # needs the basic Normalizer
46
+
47
+ # :default_plus_poles is an injectable option.
48
+ module Pipeline
49
+ extend Trailblazer::Activity::Path( normalizer_class: DefaultNormalizer, plus_poles: Builder::Path.default_plus_poles )
50
+
51
+ def self.split_options( ctx, task:, options:, ** )
52
+ keywords = extract_dsl_keywords(options)
53
+
54
+ # sort through the "original" user DSL options.
55
+ options, local_options = Options.normalize( options, keywords ) # DISCUSS:
56
+ local_options, sequence_options = Options.normalize( local_options, Activity::Schema::Dependencies.sequence_keywords )
57
+
58
+ ctx[:local_options],
59
+ ctx[:connection_options],
60
+ ctx[:sequence_options] = local_options, options, sequence_options
61
+ end
62
+
63
+ # Filter out connections, e.g. `Output(:fail_fast) => :success` and return only the keywords like `:id` or `:replace`.
64
+ def self.extract_dsl_keywords(options, connection_classes = [Activity::Output, DSL::Output::Semantic])
65
+ options.keys - options.keys.find_all { |k| connection_classes.include?( k.class ) }
66
+ end
67
+
68
+ # FIXME; why don't we use the extensions passed into the initializer?
69
+ def self.normalize_extension_option( ctx, local_options:, ** )
70
+ local_options[:extension] = (local_options[:extension]||[]) + [ Activity::Introspect.method(:add_introspection) ] # fixme: this sucks
71
+ end
72
+
73
+ # Normalizes ctx[:options]
74
+ def self.normalize_for_macro( ctx, task:, options:, task_builder:, ** )
75
+ ctx[:options] =
76
+ if task.is_a?(::Hash) # macro.
77
+ options = options.merge(extension: (options[:extension]||[])+(task[:extension]||[]) ) # FIXME.
78
+
79
+ task.merge(options) # Note that the user options are merged over the macro options.
80
+ else # user step
81
+ { id: task }
82
+ .merge(options) # default :id
83
+ .merge( task: task_builder.(task) )
84
+ end
85
+ end
86
+
87
+ # Merge user options over defaults.
88
+ def self.defaultize( ctx, local_options:, default_plus_poles:, ** ) # TODO: test :default_plus_poles
89
+ ctx[:local_options] =
90
+ {
91
+ plus_poles: default_plus_poles,
92
+ }
93
+ .merge(local_options)
94
+ end
95
+
96
+ task Activity::TaskBuilder::Binary.( method(:normalize_for_macro) ), id: "normalize_for_macro"
97
+ task Activity::TaskBuilder::Binary.( method(:split_options) ), id: "split_options"
98
+ task Activity::TaskBuilder::Binary.( method(:normalize_extension_option) ), id: "normalize_extension_option"
99
+ task Activity::TaskBuilder::Binary.( method(:defaultize) ), id: "defaultize"
100
+ # task ->((ctx, _), **) { pp ctx; [Activity::Right, [ctx, _]] }
101
+ end
102
+ end # Normalizer
103
+
104
+ end
105
+ end
@@ -14,14 +14,10 @@ module Trailblazer
14
14
  )
15
15
  end
16
16
 
17
- def self.plan(options={}, normalizer=DefaultNormalizer.new(plus_poles: default_plus_poles), &block)
18
- plan_for( *Path.for(normalizer, options), &block )
19
- end
20
-
21
17
  def task(task, options={}, &block)
22
18
  polarizations = Path.TaskPolarizations( @builder_options.merge( type: options[:type] ) ) # DISCUSS: handle :type here? Really?
23
19
 
24
- insert_element( Path, polarizations, task, options, &block )
20
+ return Path, polarizations, task, options, block
25
21
  end
26
22
 
27
23
 
@@ -32,35 +28,38 @@ module Trailblazer
32
28
  end
33
29
 
34
30
  # @return [Adds] list of Adds instances that can be chained or added to an existing sequence.
35
- def self.InitialAdds(track_color:raise, end_semantic:raise, default_plus_poles: self.default_plus_poles, track_end: Activity.End(track_color, end_semantic), **o)
31
+ def self.InitialAdds(track_color:raise, end_semantic:raise, default_plus_poles: self.default_plus_poles, track_end: Activity.End(track_color, end_semantic), **)
36
32
 
37
33
  builder_options={ track_color: track_color, end_semantic: end_semantic }
38
34
 
39
35
  start_adds = adds(
40
- "Start.default", Activity::Start.new(:default),
36
+ Activity::Start.new(:default),
41
37
 
42
- default_plus_poles,
43
38
  TaskPolarizations(builder_options),
44
- [],
45
39
 
46
40
  {}, { group: :start },
47
- [] # magnetic_to
41
+
42
+ id: "Start.default",
43
+ magnetic_to: [],
44
+ plus_poles: default_plus_poles
48
45
  )
49
46
 
50
47
  end_adds = adds(
51
- "End.#{track_color}", track_end,
48
+ track_end,
52
49
 
53
- {}, # plus_poles
54
50
  TaskPolarizations(builder_options.merge( type: :End )),
55
- [],
56
51
 
57
- {}, { group: :end }
52
+ {}, { group: :end },
53
+
54
+ id: "End.#{track_color}",
55
+ plus_poles: {},
56
+ magnetic_to: nil,
58
57
  )
59
58
 
60
59
  start_adds + end_adds
61
60
  end
62
61
 
63
- def self.TaskPolarizations(track_color:raise, type: :task, **o)
62
+ def self.TaskPolarizations(track_color:raise, type: :task, **)
64
63
  return [EndPolarization.new( track_color: track_color )] if type == :End # DISCUSS: should this dispatch be here?
65
64
 
66
65
  [TaskPolarization.new( track_color: track_color )]
@@ -10,20 +10,16 @@ module Trailblazer
10
10
  )
11
11
  end
12
12
 
13
- def self.plan(options={}, normalizer=DefaultNormalizer.new(plus_poles: default_plus_poles), &block)
14
- plan_for( *Railway.for(normalizer, options), &block )
15
- end
16
-
17
13
  def step(task, options={}, &block)
18
- insert_element( Railway, Railway.StepPolarizations(@builder_options), task, options, &block )
14
+ return Railway, Railway.StepPolarizations(@builder_options), task, options, block
19
15
  end
20
16
 
21
17
  def fail(task, options={}, &block)
22
- insert_element( Railway, Railway.FailPolarizations(@builder_options), task, options, &block )
18
+ return Railway, Railway.FailPolarizations(@builder_options), task, options, block
23
19
  end
24
20
 
25
21
  def pass(task, options={}, &block)
26
- insert_element( Railway, Railway.PassPolarizations(@builder_options), task, options, &block )
22
+ return Railway, Railway.PassPolarizations(@builder_options), task, options, block
27
23
  end
28
24
 
29
25
  def self.default_plus_poles
@@ -39,15 +35,16 @@ module Trailblazer
39
35
  path_adds = Path.InitialAdds(**builder_options)
40
36
 
41
37
  end_adds = adds(
42
- "End.#{failure_color}", failure_end,
38
+ failure_end,
43
39
 
44
- {}, # plus_poles
45
40
  Path::TaskPolarizations(builder_options.merge( type: :End )),
46
- [],
47
41
 
48
42
  {},
49
43
  { group: :end },
50
- [failure_color]
44
+
45
+ magnetic_to: [failure_color],
46
+ id: "End.#{failure_color}",
47
+ plus_poles: {},
51
48
  )
52
49
 
53
50
  path_adds + end_adds
@@ -59,11 +59,14 @@ module Trailblazer
59
59
  ]
60
60
  # procs come from DSL calls such as `Path() do ... end`.
61
61
  elsif task.is_a?(Proc)
62
- start_color, adds = task.(block)
62
+ start_color, activity = task.(block)
63
+
64
+ adds = activity.decompose[:adds]
63
65
 
64
66
  [
65
67
  Polarization.new( output: output, color: start_color ),
66
68
  # TODO: this is a pseudo-"merge" and should be public API at some point.
69
+ # TODO: we also need to merge all the other states such as debug.
67
70
  adds[1..-1] # drop start
68
71
  ]
69
72
  else # An additional plus polarization. Example: Output => :success
@@ -34,7 +34,7 @@ module Trailblazer
34
34
  def self.circuit_hash_to_process(circuit_hash)
35
35
  end_events = end_events_for(circuit_hash)
36
36
 
37
- return Activity::Process.new( circuit_hash, end_events ), end_events
37
+ return Circuit.new(circuit_hash, end_events), end_events
38
38
  end
39
39
 
40
40
  # Filters out unconnected ends, e.g. the standard end in nested tracks that weren't used.
@@ -0,0 +1,18 @@
1
+ class Trailblazer::Activity < Module
2
+ module Magnetic
3
+ module Merge
4
+ # THIS IS HIGHLY EXPERIMENTAL AS WE'RE NOT MERGING taskWrap etc.
5
+ def merge!(merged)
6
+ merged_adds = Builder.merge(self[:adds], merged[:adds])
7
+ # TODO: MERGE DEBUG, TASK_WRAP
8
+ builder, adds, circuit, outputs, = State.recompile(self[:builder], merged_adds)
9
+
10
+ self[:adds] = adds
11
+ self[:circuit] = circuit
12
+ self[:outputs] = outputs
13
+
14
+ self
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,7 +1,7 @@
1
1
  require "hirb"
2
2
 
3
3
  module Trailblazer
4
- class Activity
4
+ class Activity < Module
5
5
  module Trace
6
6
  # TODO: make this simpler.
7
7
  module Present
@@ -29,13 +29,18 @@ module Trailblazer
29
29
  @order.collect{ |name| @groups[name].to_a }.flatten(1)
30
30
  end
31
31
 
32
- # private
32
+ # @api private
33
33
  def find(id)
34
34
  @groups.find do |name, group|
35
35
  index = group.send( :find_index, id )
36
36
  return group, index if index
37
37
  end
38
38
  end
39
+
40
+ # @api private
41
+ def self.sequence_keywords
42
+ [ :group, :before, :after, :replace, :delete ]
43
+ end
39
44
  end
40
45
  end
41
46
  end
@@ -0,0 +1,58 @@
1
+ module Trailblazer
2
+ class Activity < Module # @private
3
+ # Maintain Builder/Adds/Process/Outputs as immutable objects.
4
+ module State
5
+ def self.build(builder_class, normalizer, builder_options)
6
+ builder, adds = builder_class.for(normalizer, builder_options) # e.g. Path.for(...) which creates a Builder::Path instance.
7
+
8
+ recompile(builder.freeze, adds.freeze)
9
+ end
10
+
11
+ def self.add(builder, adds, name, *args, &block)
12
+ new_adds, *returned_options = builder.insert(name, *args, &block) # builder.task
13
+
14
+ adds = adds + new_adds
15
+
16
+ recompile(builder, adds.freeze, returned_options)
17
+ end
18
+
19
+ private
20
+
21
+ # @return {builder, Adds, Process, outputs}, returned_options
22
+ def self.recompile(builder, adds, *args)
23
+ circuit, outputs = recompile_circuit(adds)
24
+
25
+ return builder, adds, circuit.freeze, outputs.freeze, *args
26
+ end
27
+
28
+ def self.recompile_circuit(adds)
29
+ circuit, outputs = Recompile.( adds )
30
+ end
31
+
32
+ module Recompile
33
+ # Recompile the circuit and outputs from the {ADDS} instance that collects circuit tasks and connections.
34
+ #
35
+ # @return [Process, Hash] The {Process} instance and its outputs hash.
36
+ def self.call(adds)
37
+ circuit, end_events = Magnetic::Builder::Finalizer.(adds)
38
+ outputs = recompile_outputs(end_events)
39
+
40
+ return circuit, outputs
41
+ end
42
+
43
+ private
44
+
45
+ def self.recompile_outputs(end_events)
46
+ ary = end_events.collect do |evt|
47
+ [
48
+ semantic = evt.instance_variable_get(:@options)[:semantic], # DISCUSS: better API here?
49
+ Activity::Output(evt, semantic)
50
+ ]
51
+ end
52
+
53
+ ::Hash[ ary ]
54
+ end
55
+ end # Recompile
56
+ end # State
57
+ end
58
+ end
@@ -1,6 +1,5 @@
1
1
  module Trailblazer
2
- class Activity
3
- # End event is just another callable task.
2
+ class Activity < Module # End event is just another callable task.
4
3
  # Any instance of subclass of End will halt the circuit's execution when hit.
5
4
  class End
6
5
  def initialize(name, options={})
@@ -1,6 +1,5 @@
1
1
  module Trailblazer
2
- class Activity
3
- # A {Subprocess} is an instance of an abstract {Activity} that can be `call`ed.
2
+ class Activity < Module # A {Subprocess} is an instance of an abstract {Activity} that can be `call`ed.
4
3
  # It is the runtime instance that runs from a specific start event.
5
4
  def self.Subprocess(*args)
6
5
  Subprocess.new(*args)
@@ -13,7 +12,7 @@ module Trailblazer
13
12
 
14
13
  def initialize(activity, call: :call, **options)
15
14
  @activity = activity
16
- @options = options
15
+ @options = options
17
16
  @call = call
18
17
  end
19
18
 
@@ -29,6 +28,10 @@ module Trailblazer
29
28
  def debug
30
29
  @activity.debug
31
30
  end
31
+
32
+ def to_s
33
+ %{#<Trailblazer::Activity::Subprocess activity=#{@activity}>}
34
+ end
32
35
  end
33
36
  end
34
37
  end
@@ -0,0 +1,38 @@
1
+ module Trailblazer
2
+ module Activity::TaskBuilder
3
+ # every step is wrapped by this proc/decider. this is executed in the circuit as the actual task.
4
+ # Step calls step.(options, **options, flow_options)
5
+ # Output direction binary: true=>Right, false=>Left.
6
+ # Passes through all subclasses of Direction.~~~~~~~~~~~~~~~~~
7
+ module Binary
8
+ def self.call(user_proc)
9
+ Task.new( Trailblazer::Option::KW( user_proc ), user_proc )
10
+ end
11
+
12
+ # Translates the return value of the user step into a valid signal.
13
+ # Note that it passes through subclasses of {Signal}.
14
+ def self.binary_direction_for(result, on_true, on_false)
15
+ result.is_a?(Class) && result < Activity::Signal ? result : (result ? on_true : on_false)
16
+ end
17
+ end
18
+
19
+ class Task
20
+ def initialize(task, user_proc)
21
+ @task = task
22
+ @user_proc = user_proc
23
+
24
+ freeze
25
+ end
26
+
27
+ def call( (options, *args), **circuit_args )
28
+ # Execute the user step with TRB's kw args.
29
+ result = @task.( options, **circuit_args ) # circuit_args contains :exec_context.
30
+
31
+ # Return an appropriate signal which direction to go next.
32
+ direction = Binary.binary_direction_for( result, Activity::Right, Activity::Left )
33
+
34
+ [ direction, [ options, *args ], **circuit_args ]
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ class Trailblazer::Activity < Module
2
+ #
3
+ # Example with tracing:
4
+ #
5
+ # Call the task_wrap circuit:
6
+ # |-- Start
7
+ # |-- Trace.capture_args [optional]
8
+ # |-- Call (call actual task) id: "task_wrap.call_task"
9
+ # |-- Trace.capture_return [optional]
10
+ # |-- Wrap::End
11
+ module TaskWrap
12
+ # The actual activity that implements the taskWrap.
13
+ def self.initial_activity
14
+ Module.new do
15
+ extend Trailblazer::Activity::Path( name: "taskWrap", normalizer_class: Magnetic::DefaultNormalizer )
16
+
17
+ task TaskWrap.method(:call_task), id: "task_wrap.call_task" # ::call_task is defined in task_wrap/call_task.
18
+ end
19
+ end
20
+
21
+ # Compute runtime arguments necessary to execute a taskWrap per task of the activity.
22
+ def self.arguments_for_call(activity, (options, flow_options), **circuit_options)
23
+ circuit_options = circuit_options.merge(
24
+ runner: TaskWrap::Runner,
25
+ wrap_runtime: circuit_options[:wrap_runtime] || {},
26
+ wrap_static: activity[:wrap_static] || {},
27
+ )
28
+
29
+ return activity, [ options, flow_options ], circuit_options
30
+ end
31
+
32
+ module NonStatic
33
+ def self.arguments_for_call(activity, (options, flow_options), **circuit_options)
34
+ circuit_options = circuit_options.merge(
35
+ runner: TaskWrap::Runner,
36
+ wrap_runtime: circuit_options[:wrap_runtime] || {}, # FIXME:this sucks. (was:) this overwrites wrap_runtime from outside.
37
+ wrap_static: ::Hash.new(TaskWrap.initial_activity), # add a default static wrap.
38
+ )
39
+
40
+ return activity, [ options, flow_options ], circuit_options
41
+ end
42
+ end
43
+
44
+ # better: MyClass < Activity(TaskWrap, ...)
45
+ end
46
+ end