trailblazer-operation 0.0.13 → 0.1.1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -3
  3. data/CHANGES.md +44 -0
  4. data/Gemfile +13 -2
  5. data/Rakefile +8 -6
  6. data/lib/trailblazer/operation.rb +86 -12
  7. data/lib/trailblazer/operation/deprecated_macro.rb +19 -0
  8. data/lib/trailblazer/operation/inject.rb +34 -0
  9. data/lib/trailblazer/operation/inspect.rb +80 -0
  10. data/lib/trailblazer/operation/public_call.rb +62 -0
  11. data/lib/trailblazer/operation/railway.rb +32 -0
  12. data/lib/trailblazer/operation/railway/fast_track.rb +13 -0
  13. data/lib/trailblazer/operation/railway/normalizer.rb +73 -0
  14. data/lib/trailblazer/operation/railway/task_builder.rb +44 -0
  15. data/lib/trailblazer/operation/result.rb +6 -4
  16. data/lib/trailblazer/operation/skill.rb +8 -24
  17. data/lib/trailblazer/operation/task_wrap.rb +68 -0
  18. data/lib/trailblazer/operation/trace.rb +49 -0
  19. data/lib/trailblazer/operation/variable_mapping.rb +91 -0
  20. data/lib/trailblazer/operation/version.rb +1 -1
  21. data/test/call_test.rb +27 -8
  22. data/test/class_dependencies_test.rb +16 -0
  23. data/test/docs/doormat_test.rb +189 -0
  24. data/test/docs/wiring_test.rb +421 -0
  25. data/test/dry_container_test.rb +4 -0
  26. data/test/fast_track_test.rb +197 -0
  27. data/test/gemfiles/Gemfile.ruby-2.0 +1 -2
  28. data/test/gemfiles/Gemfile.ruby-2.0.lock +40 -0
  29. data/test/inheritance_test.rb +1 -1
  30. data/test/inspect_test.rb +43 -0
  31. data/test/introspect_test.rb +50 -0
  32. data/test/macro_test.rb +61 -0
  33. data/test/operation_test.rb +94 -0
  34. data/test/result_test.rb +14 -8
  35. data/test/ruby-2.0.0/operation_test.rb +73 -0
  36. data/test/ruby-2.0.0/step_test.rb +136 -0
  37. data/test/skill_test.rb +66 -48
  38. data/test/step_test.rb +228 -0
  39. data/test/task_wrap_test.rb +91 -0
  40. data/test/test_helper.rb +37 -0
  41. data/test/trace_test.rb +62 -0
  42. data/test/variable_mapping_test.rb +66 -0
  43. data/test/wire_test.rb +113 -0
  44. data/test/wiring/defaults_test.rb +197 -0
  45. data/test/wiring/subprocess_test.rb +70 -0
  46. data/trailblazer-operation.gemspec +3 -5
  47. metadata +62 -36
  48. data/lib/trailblazer/operation/1.9.3/option.rb +0 -36
  49. data/lib/trailblazer/operation/generic.rb +0 -12
  50. data/lib/trailblazer/operation/option.rb +0 -54
  51. data/lib/trailblazer/operation/pipetree.rb +0 -142
  52. data/lib/trailblazer/skill.rb +0 -70
  53. data/test/2.0.0-pipetree_test.rb +0 -100
  54. data/test/2.1.0-pipetree_test.rb +0 -100
  55. data/test/operation_skill_test.rb +0 -89
  56. data/test/pipetree_test.rb +0 -185
@@ -0,0 +1,13 @@
1
+ module Trailblazer
2
+ module Operation::Railway
3
+ def self.fail! ; Activity::Left end
4
+ def self.pass! ; Activity::Right end
5
+ def self.fail_fast!; Activity::Magnetic::Builder::FastTrack::FailFast end
6
+ def self.pass_fast!; Activity::Magnetic::Builder::FastTrack::PassFast end
7
+
8
+ module End
9
+ FailFast = Class.new(Operation::Railway::End::Failure)
10
+ PassFast = Class.new(Operation::Railway::End::Success)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,73 @@
1
+ module Trailblazer
2
+ module Operation::Railway
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} 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
+ module Normalizer
9
+ def self.call(task, options, unknown_options, sequence_options)
10
+ wrapped_task, options =
11
+ if task.is_a?(::Hash) # macro.
12
+ [
13
+ task[:task],
14
+ task.merge(options) # Note that the user options are merged over the macro options.
15
+ ]
16
+ elsif task.is_a?(Array) # TODO remove in 2.2
17
+ Operation::DeprecatedMacro.( *task )
18
+ else # user step
19
+ [
20
+ TaskBuilder.(task),
21
+ { id: task }.merge(options) # default :id
22
+ ]
23
+ end
24
+
25
+ options, unknown_options = deprecate_name(options, unknown_options) # TODO remove in 2.2
26
+
27
+ raise "No :id given for #{wrapped_task}" unless options[:id]
28
+
29
+ options = defaultize(task, options) # :plus_poles
30
+
31
+
32
+ options, locals, sequence_options = override(task, options, sequence_options) # :override
33
+
34
+ return wrapped_task, options, unknown_options, sequence_options
35
+ end
36
+
37
+ # Merge user options over defaults.
38
+ def self.defaultize(task, options)
39
+ {
40
+ plus_poles: InitialPlusPoles(),
41
+ }.merge(options)
42
+ end
43
+
44
+ # Handle the :override option which is specific to Operation.
45
+ def self.override(task, options, sequence_options)
46
+ options, locals = Activity::Magnetic::Builder.normalize(options, [:override])
47
+ sequence_options = sequence_options.merge( replace: options[:id] ) if locals[:override]
48
+
49
+ return options, locals, sequence_options
50
+ end
51
+
52
+ def self.InitialPlusPoles
53
+ Activity::Magnetic::DSL::PlusPoles.new.merge(
54
+ Activity.Output(Activity::Right, :success) => nil,
55
+ Activity.Output(Activity::Left, :failure) => nil,
56
+ )
57
+ end
58
+
59
+ def self.deprecate_name(options, unknown_options) # TODO remove in 2.2
60
+ unknown_options, deprecated_options = Activity::Magnetic::Builder.normalize(unknown_options, [:name])
61
+
62
+ options = options.merge( name: deprecated_options[:name] ) if deprecated_options[:name]
63
+
64
+ options, locals = Activity::Magnetic::Builder.normalize(options, [:name])
65
+ if locals[:name]
66
+ warn "[Trailblazer] The :name option for #step, #success and #failure has been renamed to :id."
67
+ options = options.merge(id: locals[:name])
68
+ end
69
+ return options, unknown_options
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,44 @@
1
+ module Trailblazer
2
+ module Operation::Railway
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 TaskBuilder
8
+ class Task < Proc
9
+ def initialize(source_location, &block)
10
+ @source_location = source_location
11
+ super &block
12
+ end
13
+
14
+ def to_s
15
+ "<Railway::Task{#{@source_location}}>"
16
+ end
17
+
18
+ def inspect
19
+ to_s
20
+ end
21
+ end
22
+
23
+ # TODO: make this class replaceable so @Mensfeld gets his own call style. :trollface:
24
+
25
+ def self.call(step, on_true=Activity::Right, on_false=Activity::Left)
26
+ Task.new step, &->( (options, *args), **circuit_args ) do
27
+ # Execute the user step with TRB's kw args.
28
+ result = Trailblazer::Option::KW(step).(options, **circuit_args) # circuit_args contains :exec_context.
29
+
30
+ # Return an appropriate signal which direction to go next.
31
+ direction = binary_direction_for(result, on_true, on_false)
32
+
33
+ [ direction, [ options, *args ], **circuit_args ]
34
+ end
35
+ end
36
+
37
+ # Translates the return value of the user step into a valid signal.
38
+ # Note that it passes through subclasses of {Signal}.
39
+ def self.binary_direction_for(result, on_true, on_false)
40
+ result.is_a?(Class) && result < Activity::Signal ? result : (result ? on_true : on_false)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,12 +1,11 @@
1
1
  class Trailblazer::Operation
2
2
  class Result
3
+ # @param success Boolean validity of the result object
4
+ # @param data Context
3
5
  def initialize(success, data)
4
- @success, @data = success, data # @data is a Skill instance.
6
+ @success, @data = success, data
5
7
  end
6
8
 
7
- extend Forwardable
8
- def_delegators :@data, :[] # DISCUSS: make it a real delegator? see Nested.
9
-
10
9
  def success?
11
10
  @success
12
11
  end
@@ -15,6 +14,9 @@ class Trailblazer::Operation
15
14
  ! success?
16
15
  end
17
16
 
17
+ extend Forwardable
18
+ def_delegators :@data, :[] # DISCUSS: make it a real delegator? see Nested.
19
+
18
20
  # DISCUSS: the two methods below are more for testing.
19
21
  def inspect(*slices)
20
22
  return "<Result:#{success?} #{slice(*slices).inspect} >" if slices.any?
@@ -1,11 +1,4 @@
1
- require "trailblazer/skill"
2
- # Dependency ("skill") management for Operation.
3
- # Op::[]
4
- # Op::[]=
5
- # Writing, even with an existing name, will never mutate a container.
6
- # Op#[]
7
- # Op#[]=
8
- # Op.(params, { "constructor" => competences })
1
+ # Dependencies can be defined on the operation. class level
9
2
  class Trailblazer::Operation
10
3
  module Skill
11
4
  # The class-level skill container: Operation::[], ::[]=.
@@ -18,24 +11,15 @@ class Trailblazer::Operation
18
11
  extend Forwardable
19
12
  def_delegators :skills, :[], :[]=
20
13
  end
14
+ end
21
15
 
22
- # Overrides Operation::call, creates the Skill hash and passes it to :call.
23
- module Call
24
- def call(options={}, *dependencies)
25
- super Trailblazer::Skill.new(options, *dependencies, self.skills)
26
- # DISCUSS: should this be: Trailblazer::Skill.new(runtime_options: [options, *dependencies], compiletime_options: [self.skills])
27
- end
28
- alias :_call :call
16
+ # The use of this module is not encouraged and it is only here for backward-compatibility.
17
+ # Instead, please pass dependencies via containers, locals, or macros into the respective steps.
18
+ module ClassDependencies
19
+ def __call__( (ctx, flow_options), **circuit_options )
20
+ @skills.each { |name, value| ctx[name] ||= value } # this resembles the behavior in 2.0. we didn't say we liked it.
29
21
 
30
- # It really sucks that Ruby doesn't have method overloading where we could simply have
31
- # two different implementations of ::call.
32
- # FIXME: that shouldn't be here in this namespace.
33
- module Positional
34
- def call(params={}, options={}, *dependencies)
35
- super(options.merge("params" => params), *dependencies)
36
- end
37
- end
22
+ super
38
23
  end
39
-
40
24
  end
41
25
  end
@@ -0,0 +1,68 @@
1
+ module Trailblazer
2
+ module Operation::Railway
3
+ module TaskWrap
4
+ def self.included(includer)
5
+ includer.extend ClassMethods # ::__call__, ::inititalize_task_wraps!
6
+ includer.extend DSL
7
+
8
+ includer.initialize_task_wraps!
9
+ end
10
+
11
+ module ClassMethods
12
+ def initialize_task_wraps!
13
+ heritage.record :initialize_task_wraps!
14
+
15
+ # The map of task_wrap per step/task. Note that it defaults to Wrap.initial_activity.
16
+ # This gets extended at compile-time for particular tasks as the steps are created via the DSL.
17
+ self["__static_task_wraps__"] = ::Hash.new(Activity::Wrap.initial_activity)
18
+ end
19
+
20
+ # __call__ prepares `flow_options` and `static_wraps` for {TaskWrap::Runner}.
21
+ def __call__(args, **circuit_args)
22
+ args, _circuit_args = TaskWrap.arguments_for_call(self, args, **circuit_args)
23
+
24
+ super( args, circuit_args.merge(_circuit_args) ) # Railway::__call__
25
+ end
26
+ end
27
+
28
+ def self.arguments_for_call(operation, (options, flow_options), **circuit_args)
29
+ wrap_static = operation["__static_task_wraps__"]
30
+
31
+ circuit_args = {
32
+ runner: Activity::Wrap::Runner,
33
+ # FIXME: this sucks, why do we even need to pass an empty runtime there?
34
+ wrap_runtime: circuit_args[:wrap_runtime] || ::Hash.new([]), # FIXME:this sucks. (was:) this overwrites wrap_runtime from outside.
35
+ wrap_static: wrap_static,
36
+ }
37
+
38
+ return [ options, flow_options ], circuit_args
39
+ end
40
+
41
+ module DSL
42
+ # TODO: this override is hard to follow, we should have a pipeline circuit in DSL to add behavior.
43
+ # @private
44
+ def _task(*args)
45
+ returned = super # TODO: do this with a circuit :)
46
+ adds, (task, local_options) = returned
47
+
48
+ runner_options = local_options[:runner_options]
49
+
50
+ runner_options and apply_adds_from_runner_options!( task, runner_options )
51
+
52
+ returned
53
+ end
54
+
55
+ # Extend the static wrap for a specific task, at compile time.
56
+ def apply_adds_from_runner_options!(task, merge:raise, **ignored)
57
+ static_wrap = self["__static_task_wraps__"][task]
58
+
59
+ # macro might want to apply changes to the static task_wrap (e.g. Inject)
60
+ self["__static_task_wraps__"][task] = Activity::Magnetic::Builder.merge( static_wrap, merge )
61
+ end
62
+ end
63
+ end # TaskWrap
64
+ end
65
+ end
66
+
67
+
68
+ # |-- Railway::Call "insert.exec_context"
@@ -0,0 +1,49 @@
1
+ module Trailblazer
2
+ class Operation
3
+ module Trace
4
+ def self.call(operation, *args)
5
+ ctx = PublicCall.send(:options_for_public_call, *args)
6
+
7
+ # let Activity::Trace::call handle all parameters, just make sure it calls Operation.__call__
8
+ call_block = ->(operation, *args) { operation.__call__(*args) }
9
+
10
+ stack, direction, options, flow_options = Activity::Trace.(
11
+ operation,
12
+ ctx,
13
+ &call_block # instructs Trace to use __call__.
14
+ )
15
+
16
+ result = Railway::Result(direction, options)
17
+
18
+ Result.new(result, stack)
19
+ end
20
+
21
+ # `Operation::trace` is included for simple tracing of the flow.
22
+ # It simply forwards all arguments to `Trace.call`.
23
+ #
24
+ # @public
25
+ #
26
+ # Operation.trace(params, "current_user" => current_user).wtf
27
+ def trace(*args)
28
+ Trace.(self, *args)
29
+ end
30
+
31
+ # Presentation of the traced stack via the returned result object.
32
+ # This object is wrapped around the original result in {Trace.call}.
33
+ class Result < SimpleDelegator
34
+ def initialize(result, stack)
35
+ super(result)
36
+ @stack = stack
37
+ end
38
+
39
+ def wtf
40
+ Activity::Trace::Present.tree(@stack)
41
+ end
42
+
43
+ def wtf?
44
+ puts wtf
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,91 @@
1
+ class Trailblazer::Activity
2
+ module Wrap
3
+ # TaskWrap step to compute the incoming {Context} for the wrapped task.
4
+ # This allows renaming, filtering, hiding, of the options passed into the wrapped task.
5
+ #
6
+ # Both Input and Output are typically to be added before and after task_wrap.call_task.
7
+ #
8
+ # @note Assumption: we always have :input _and_ :output, where :input produces a Context and :output decomposes it.
9
+ class Input
10
+ def initialize(filter)
11
+ @filter = Trailblazer::Option(filter)
12
+ end
13
+
14
+ # `original_args` are the actual args passed to the wrapped task: [ [options, ..], circuit_options ]
15
+ #
16
+ def call( (wrap_ctx, original_args), **circuit_options )
17
+ # let user compute new ctx for the wrapped task.
18
+ input_ctx = apply_filter(*original_args) # FIXME: THIS SHOULD ALWAYS BE A _NEW_ Context.
19
+ # TODO: make this unnecessary.
20
+ # wrap user's hash in Context if it's not one, already (in case user used options.merge).
21
+ # DISCUSS: should we restrict user to .merge and options.Context?
22
+ input_ctx = Trailblazer.Context(input_ctx) if !input_ctx.instance_of?(Trailblazer::Context) || input_ctx==original_args[0][0]
23
+
24
+ wrap_ctx = wrap_ctx.merge( vm_original_args: original_args )
25
+
26
+ # decompose the original_args since we want to modify them.
27
+ (original_ctx, original_flow_options), original_circuit_options = original_args
28
+
29
+ # instead of the original Context, pass on the filtered `input_ctx` in the wrap.
30
+ return Trailblazer::Activity::Right, [ wrap_ctx, [[input_ctx, original_flow_options], original_circuit_options] ]
31
+ end
32
+
33
+ private
34
+
35
+ def apply_filter((original_ctx, original_flow_options), original_circuit_options)
36
+ @filter.( original_ctx, **original_circuit_options )
37
+ end
38
+ end
39
+
40
+ # TaskWrap step to compute the outgoing {Context} from the wrapped task.
41
+ # This allows renaming, filtering, hiding, of the options returned from the wrapped task.
42
+ class Output
43
+ def initialize(filter, strategy=CopyMutableToOriginal)
44
+ @filter = Trailblazer::Option(filter)
45
+ @strategy = strategy
46
+ end
47
+
48
+ # Runs the user filter and replaces the ctx in `wrap_ctx[:result_args]` with the filtered one.
49
+ def call( (wrap_ctx, original_args), **circuit_options )
50
+ (original_ctx, original_flow_options), original_circuit_options = original_args
51
+
52
+ returned_ctx, _ = wrap_ctx[:result_args] # this is the Context returned from `call`ing the task.
53
+
54
+ # returned_ctx is the Context object from the nested operation. In <=2.1, this might be a completely different one
55
+ # than "ours" we created in Input. We now need to compile a list of all added values. This is time-intensive and should
56
+ # be optimized by removing as many Context creations as possible (e.g. the one adding self[] stuff in Operation.__call__).
57
+ _, mutable_data = returned_ctx.decompose # DISCUSS: this is a weak assumption. What if the task returns a deeply nested Context?
58
+
59
+ # let user compute the output.
60
+ output = apply_filter(mutable_data, original_flow_options, original_circuit_options)
61
+
62
+ original_ctx = wrap_ctx[:vm_original_args][0][0]
63
+
64
+ new_ctx = @strategy.( original_ctx, output ) # here, we compute the "new" options {Context}.
65
+
66
+ wrap_ctx = wrap_ctx.merge( result_args: [new_ctx, original_flow_options] )
67
+
68
+ # and then pass on the "new" context.
69
+ return Trailblazer::Activity::Right, [ wrap_ctx, original_args ]
70
+ end
71
+
72
+ private
73
+
74
+ # @note API not stable
75
+ def apply_filter(mutable_data, original_flow_options, original_circuit_options)
76
+ @filter.(mutable_data, **original_circuit_options)
77
+ end
78
+
79
+ # "merge" Strategy
80
+ module CopyMutableToOriginal
81
+ # @param original Context
82
+ # @param options Context The object returned from a (nested) {Activity}.
83
+ def self.call(original, mutable)
84
+ mutable.each { |k,v| original[k] = v }
85
+
86
+ original
87
+ end
88
+ end
89
+ end
90
+ end # Wrap
91
+ end
@@ -1,5 +1,5 @@
1
1
  module Trailblazer
2
2
  class Operation
3
- VERSION = "0.0.13"
3
+ VERSION = "0.1.1"
4
4
  end
5
5
  end
data/test/call_test.rb CHANGED
@@ -1,28 +1,47 @@
1
1
  require "test_helper"
2
2
 
3
- # DISCUSS: do we need this test?
4
3
  class CallTest < Minitest::Spec
5
4
  describe "::call" do
6
5
  class Create < Trailblazer::Operation
6
+ step ->(*) { true }
7
7
  def inspect
8
8
  "#{@skills.inspect}"
9
9
  end
10
10
  end
11
11
 
12
- it { Create.().must_be_instance_of Trailblazer::Operation::Result }
12
+ it { Create.().must_be_instance_of Trailblazer::Operation::Railway::Result }
13
13
 
14
- it { Create.({}).inspect.must_equal %{<Result:true <Skill {} {\"params\"=>{}} {\"pipetree\"=>[>operation.new]}> >} }
15
- it { Create.(name: "Jacob").inspect.must_equal %{<Result:true <Skill {} {\"params\"=>{:name=>\"Jacob\"}} {\"pipetree\"=>[>operation.new]}> >} }
16
- it { Create.({ name: "Jacob" }, { policy: Object }).inspect.must_equal %{<Result:true <Skill {} {:policy=>Object, \"params\"=>{:name=>\"Jacob\"}} {\"pipetree\"=>[>operation.new]}> >} }
14
+ # it { Create.({}).inspect.must_equal %{<Result:true <Skill {} {\"params\"=>{}} {\"pipetree\"=>[>operation.new]}> >} }
15
+ # it { Create.(name: "Jacob").inspect.must_equal %{<Result:true <Skill {} {\"params\"=>{:name=>\"Jacob\"}} {\"pipetree\"=>[>operation.new]}> >} }
16
+ # it { Create.({ name: "Jacob" }, { policy: Object }).inspect.must_equal %{<Result:true <Skill {} {:policy=>Object, \"params\"=>{:name=>\"Jacob\"}} {\"pipetree\"=>[>operation.new]}> >} }
17
17
 
18
18
  #---
19
19
  # success?
20
20
  class Update < Trailblazer::Operation
21
- step ->(options) { options["params"] }, after: "operation.new"
21
+ step ->(options, **) { options[:result] }
22
+ end
23
+
24
+ # operation success
25
+ it do
26
+ result = Update.(result: true)
27
+
28
+ result.success?.must_equal true
29
+
30
+ result.event.must_be_instance_of Trailblazer::Operation::Railway::End::Success
31
+ result.event.must_equal Update.outputs[:success].signal
32
+ end
33
+
34
+ # operation failure
35
+ it do
36
+ result = Update.(result: false)
37
+
38
+ result.success?.must_equal false
39
+ result.failure?.must_equal true
40
+
41
+ result.event.must_be_instance_of Trailblazer::Operation::Railway::End::Failure
42
+ result.event.must_equal Update.outputs[:failure].signal
22
43
  end
23
44
 
24
- it { Update.(true).success?.must_equal true }
25
- it { Update.(false).success?.must_equal false }
26
45
  end
27
46
  end
28
47