trailblazer-operation 0.0.13 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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