trailblazer-operation 0.0.13 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +5 -3
  3. data/CHANGES.md +100 -0
  4. data/Gemfile +4 -2
  5. data/Rakefile +8 -6
  6. data/lib/trailblazer/operation.rb +80 -13
  7. data/lib/trailblazer/operation/callable.rb +42 -0
  8. data/lib/trailblazer/operation/class_dependencies.rb +25 -0
  9. data/lib/trailblazer/operation/deprecated_macro.rb +19 -0
  10. data/lib/trailblazer/operation/heritage.rb +30 -0
  11. data/lib/trailblazer/operation/inject.rb +36 -0
  12. data/lib/trailblazer/operation/inspect.rb +79 -0
  13. data/lib/trailblazer/operation/public_call.rb +55 -0
  14. data/lib/trailblazer/operation/railway.rb +32 -0
  15. data/lib/trailblazer/operation/railway/fast_track.rb +13 -0
  16. data/lib/trailblazer/operation/railway/macaroni.rb +23 -0
  17. data/lib/trailblazer/operation/railway/normalizer.rb +58 -0
  18. data/lib/trailblazer/operation/railway/task_builder.rb +37 -0
  19. data/lib/trailblazer/operation/result.rb +6 -4
  20. data/lib/trailblazer/operation/trace.rb +46 -0
  21. data/lib/trailblazer/operation/version.rb +1 -1
  22. data/test/call_test.rb +27 -8
  23. data/test/callable_test.rb +147 -0
  24. data/test/class_dependencies_test.rb +16 -0
  25. data/test/docs/doormat_test.rb +189 -0
  26. data/test/docs/macaroni_test.rb +33 -0
  27. data/test/docs/operation_test.rb +23 -0
  28. data/test/docs/wiring_test.rb +559 -0
  29. data/test/dry_container_test.rb +4 -0
  30. data/test/fast_track_test.rb +197 -0
  31. data/test/gemfiles/Gemfile.ruby-2.0 +1 -2
  32. data/test/gemfiles/Gemfile.ruby-2.0.lock +40 -0
  33. data/test/inheritance_test.rb +1 -1
  34. data/test/inspect_test.rb +43 -0
  35. data/test/introspect_test.rb +51 -0
  36. data/test/macro_test.rb +60 -0
  37. data/test/operation_test.rb +94 -0
  38. data/test/result_test.rb +14 -8
  39. data/test/ruby-2.0.0/operation_test.rb +61 -0
  40. data/test/ruby-2.0.0/step_test.rb +136 -0
  41. data/test/skill_test.rb +66 -48
  42. data/test/step_test.rb +228 -0
  43. data/test/task_wrap_test.rb +97 -0
  44. data/test/test_helper.rb +37 -0
  45. data/test/trace_test.rb +57 -0
  46. data/test/wire_test.rb +113 -0
  47. data/test/wiring/defaults_test.rb +197 -0
  48. data/test/wiring/subprocess_test.rb +70 -0
  49. data/trailblazer-operation.gemspec +3 -5
  50. metadata +68 -37
  51. data/lib/trailblazer/operation/1.9.3/option.rb +0 -36
  52. data/lib/trailblazer/operation/generic.rb +0 -12
  53. data/lib/trailblazer/operation/option.rb +0 -54
  54. data/lib/trailblazer/operation/pipetree.rb +0 -142
  55. data/lib/trailblazer/operation/skill.rb +0 -41
  56. data/lib/trailblazer/skill.rb +0 -70
  57. data/test/2.0.0-pipetree_test.rb +0 -100
  58. data/test/2.1.0-pipetree_test.rb +0 -100
  59. data/test/operation_skill_test.rb +0 -89
  60. data/test/pipetree_test.rb +0 -185
@@ -0,0 +1,55 @@
1
+ module Trailblazer
2
+ module Operation::PublicCall
3
+ # This is the outer-most public `call` method that gets invoked when calling `Create.()`.
4
+ # The signature of this is `params, options, *containers`. This was a mistake, as the
5
+ # first argument could've been part of `options` hash in the first place.
6
+ #
7
+ # Create.(params, runtime_data, *containers)
8
+ # #=> Result<Context...>
9
+ #
10
+ # In workflows/Nested compositions, this method is not used anymore and it might probably
11
+ # get removed in future versions of TRB. Currently, we use Operation::__call__ as an alternative.
12
+ #
13
+ #
14
+ # @note Do not override this method as it will be removed in future versions. Also, you will break tracing.
15
+ # @return Operation::Railway::Result binary result object
16
+ def call(*args)
17
+ return call_with_circuit_interface(*args) if args.any? && args[0].is_a?(Array) # This is kind of a hack that could be well hidden if Ruby had method overloading. Goal is to simplify the call/__call__ thing as we're fading out Operation::call anyway.
18
+ call_with_public_interface(*args)
19
+ end
20
+
21
+ def call_with_public_interface(*args)
22
+ ctx = Operation::PublicCall.options_for_public_call(*args)
23
+
24
+ # call the activity.
25
+ # This will result in invoking {::call_with_circuit_interface}.
26
+ last_signal, (options, flow_options) = Activity::TaskWrap.invoke(self, [ctx, {}], {})
27
+
28
+ # Result is successful if the activity ended with an End event derived from Railway::End::Success.
29
+ Operation::Railway::Result(last_signal, options, flow_options)
30
+ end
31
+
32
+ # This interface is used for all nested OPs (and the outer-most, too).
33
+ def call_with_circuit_interface(args, circuit_options)
34
+ @activity.(
35
+ args,
36
+ circuit_options.merge(
37
+ exec_context: new
38
+ )
39
+ )
40
+ end
41
+
42
+ # Compile a Context object to be passed into the Activity::call.
43
+ # @private
44
+ def self.options_for_public_call(options={}, *containers)
45
+ # generate the skill hash that embraces runtime options plus potential containers, the so called Runtime options.
46
+ # This wrapping is supposed to happen once in the entire system.
47
+
48
+ hash_transformer = ->(containers) { containers[0].to_hash } # FIXME: don't transform any containers into kw args.
49
+
50
+ immutable_options = Trailblazer::Context::ContainerChain.new( [options, *containers], to_hash: hash_transformer ) # Runtime options, immutable.
51
+
52
+ ctx = Trailblazer::Context(immutable_options)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,32 @@
1
+ module Trailblazer
2
+ # Operations is simply a thin API to define, inherit and run circuits by passing the options object.
3
+ # It encourages the linear railway style (http://trb.to/gems/workflow/circuit.html#operation) but can
4
+ # easily be extend for more complex workflows.
5
+ class Operation
6
+ # End event: All subclasses of End:::Success are interpreted as "success".
7
+ module Railway
8
+ # @param options Context
9
+ # @param end_event The last emitted signal in a circuit is usually the end event.
10
+ def self.Result(end_event, options, *)
11
+ Result.new(end_event.kind_of?(End::Success), options, end_event)
12
+ end
13
+
14
+ # The Railway::Result knows about its binary state, the context (data), and the last event in the circuit.
15
+ class Result < Result # Operation::Result
16
+ def initialize(success, data, event)
17
+ super(success, data)
18
+
19
+ @event = event
20
+ end
21
+
22
+ attr_reader :event
23
+ end
24
+
25
+ module End
26
+ class Success < Activity::End; end
27
+ class Failure < Activity::End; end
28
+ end
29
+
30
+ end # Railway
31
+ end
32
+ end
@@ -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::FastTrack::FailFast end
6
+ def self.pass_fast!; Activity::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,23 @@
1
+ module Trailblazer
2
+ module Operation::Railway
3
+ # Call the user's steps with a differing API (inspired by Maciej Mensfeld) that
4
+ # only receives keyword args. The `options` keyword is the stateful context object
5
+ #
6
+ # def my_step( params:, ** )
7
+ # def my_step( params:, options:, ** )
8
+ module Macaroni
9
+ def self.call(user_proc)
10
+ Activity::TaskBuilder::Task.new( Trailblazer::Option.build( Macaroni::Option, user_proc ), user_proc )
11
+ end
12
+
13
+ class Option < Trailblazer::Option
14
+ # The Option#call! method prepares the arguments.
15
+ def self.call!(proc, options, *)
16
+ proc.( **options.to_hash.merge( options: options ) )
17
+ end
18
+ end
19
+ end
20
+
21
+ KwSignature = Macaroni
22
+ end
23
+ end
@@ -0,0 +1,58 @@
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
+ Pipeline = Activity::Magnetic::Normalizer::Pipeline.clone
10
+
11
+ Pipeline.module_eval do
12
+ # Handle the :override option which is specific to Operation.
13
+ def self.override(ctx, task:, options:, sequence_options:, **)
14
+ options, locals = Activity::Magnetic::Options.normalize(options, [:override])
15
+ sequence_options = sequence_options.merge( replace: options[:id] ) if locals[:override]
16
+
17
+ ctx[:options], ctx[:sequence_options] = options, sequence_options
18
+ end
19
+
20
+ # TODO remove in 2.2
21
+ def self.deprecate_macro_with_two_args(ctx, task:, **)
22
+ return true unless task.is_a?(Array) # TODO remove in 2.2
23
+
24
+ ctx[:options] = Operation::DeprecatedMacro.( *task )
25
+ end
26
+
27
+ # TODO remove in 2.2
28
+ def self.deprecate_name(ctx, local_options:, connection_options:, **)
29
+ connection_options, deprecated_options = Activity::Magnetic::Options.normalize(connection_options, [:name])
30
+ local_options, _deprecated_options = Activity::Magnetic::Options.normalize(local_options, [:name])
31
+
32
+ deprecated_options = deprecated_options.merge(_deprecated_options)
33
+
34
+ local_options = local_options.merge( name: deprecated_options[:name] ) if deprecated_options[:name]
35
+
36
+ local_options, locals = Activity::Magnetic::Options.normalize(local_options, [:name])
37
+ if locals[:name]
38
+ warn "[Trailblazer] The :name option for #step, #success and #failure has been renamed to :id."
39
+ local_options = local_options.merge(id: locals[:name])
40
+ end
41
+
42
+ ctx[:local_options], ctx[:connection_options] = local_options, connection_options
43
+ end
44
+
45
+ def self.raise_on_missing_id(ctx, local_options:, **)
46
+ raise "No :id given for #{local_options[:task]}" unless local_options[:id]
47
+ true
48
+ end
49
+
50
+ # add more normalization tasks to the existing Magnetic::Normalizer::Pipeline
51
+ task Activity::TaskBuilder::Binary( method(:deprecate_macro_with_two_args) ), before: "split_options"
52
+ task Activity::TaskBuilder::Binary( method(:deprecate_name) )
53
+ task Activity::TaskBuilder::Binary( method(:override) )
54
+ task Activity::TaskBuilder::Binary( method(:raise_on_missing_id) )
55
+ end
56
+ end # Normalizer
57
+ end
58
+ end
@@ -0,0 +1,37 @@
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
+ 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
+ freeze
24
+ end
25
+
26
+ def call( (options, *args), **circuit_args )
27
+ # Execute the user step with TRB's kw args.
28
+ result = @task.( options, **circuit_args ) # circuit_args contains :exec_context.
29
+
30
+ # Return an appropriate signal which direction to go next.
31
+ direction = TaskBuilder.binary_direction_for( result, Activity::Right, Activity::Left )
32
+
33
+ [ direction, [ options, *args ], **circuit_args ]
34
+ end
35
+ end
36
+ end
37
+ 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?
@@ -0,0 +1,46 @@
1
+ module Trailblazer
2
+ class Operation
3
+ module Trace
4
+ # @note The problem in this method is, we have redundancy with Operation::PublicCall
5
+ def self.call(operation, *args)
6
+ ctx = PublicCall.options_for_public_call(*args) # redundant with PublicCall::call.
7
+
8
+ # Prepare the tracing-specific arguments. This is only run once for the entire circuit!
9
+ operation, *args = Trailblazer::Activity::Trace.arguments_for_call( operation, [ctx, {}], {} )
10
+
11
+ last_signal, (ctx, flow_options) = Activity::TaskWrap.invoke(operation, *args )
12
+
13
+ result = Railway::Result(last_signal, ctx) # redundant with PublicCall::call.
14
+
15
+ Result.new(result, flow_options[:stack].to_a)
16
+ end
17
+
18
+ # `Operation::trace` is included for simple tracing of the flow.
19
+ # It simply forwards all arguments to `Trace.call`.
20
+ #
21
+ # @public
22
+ #
23
+ # Operation.trace(params, "current_user" => current_user).wtf
24
+ def trace(*args)
25
+ Trace.(self, *args)
26
+ end
27
+
28
+ # Presentation of the traced stack via the returned result object.
29
+ # This object is wrapped around the original result in {Trace.call}.
30
+ class Result < SimpleDelegator
31
+ def initialize(result, stack)
32
+ super(result)
33
+ @stack = stack
34
+ end
35
+
36
+ def wtf
37
+ Trailblazer::Activity::Trace::Present.(@stack)
38
+ end
39
+
40
+ def wtf?
41
+ puts wtf
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,5 +1,5 @@
1
1
  module Trailblazer
2
2
  class Operation
3
- VERSION = "0.0.13"
3
+ VERSION = "0.4.1"
4
4
  end
5
5
  end
@@ -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
 
@@ -0,0 +1,147 @@
1
+ require "test_helper"
2
+
3
+ class CallableHelper < Minitest::Spec
4
+ Operation = Trailblazer::Operation
5
+ Activity = Trailblazer::Activity
6
+
7
+ module Blog
8
+ Read = ->((options, *args), *) { options["Read"] = 1; [ Activity::Right, [options, *args] ] }
9
+ Next = ->((options, *args), *) { options["NextPage"] = []; [ options["return"], [options, *args] ] }
10
+ Comment = ->((options, *args), *) { options["Comment"] = 2; [ Activity::Right, [options, *args] ] }
11
+ end
12
+
13
+ module User
14
+ Relax = ->((options, *args), *) { options["Relax"]=true; [ Activity::Right, [options, *args] ] }
15
+ end
16
+
17
+ ### Callable( )
18
+ ###
19
+ describe "circuit with 1 level of nesting" do # TODO: test this kind of configuration in dsl_tests somewhere.
20
+ let(:blog) do
21
+ Module.new do
22
+ extend Activity::Path()
23
+
24
+ task task: Blog::Read
25
+ task task: Blog::Next, Output(Activity::Right, :done) => "End.success", Output(Activity::Left, :success) => Track(:success)
26
+ task task: Blog::Comment
27
+ end
28
+ end
29
+
30
+ let(:user) do
31
+ _blog = blog
32
+
33
+ Module.new do
34
+ extend Activity::Path()
35
+
36
+ task task: _blog, _blog.outputs[:success] => Track(:success)
37
+ task task: User::Relax
38
+ end
39
+ end
40
+
41
+ it "ends before comment, on next_page" do
42
+ user.( [options = { "return" => Activity::Right }] ).must_equal(
43
+ [user.outputs[:success].signal, [{"return"=>Trailblazer::Activity::Right, "Read"=>1, "NextPage"=>[], "Relax"=>true}]]
44
+ )
45
+
46
+ options.must_equal({"return"=>Trailblazer::Activity::Right, "Read"=>1, "NextPage"=>[], "Relax"=>true})
47
+ end
48
+ end
49
+
50
+ ### Callable( End1, End2 )
51
+ ###
52
+ describe "circuit with 2 end events in the nested process" do
53
+ let(:blog) do
54
+ Module.new do
55
+ extend Activity::Path()
56
+
57
+ task task: Blog::Read
58
+ task task: Blog::Next, Output(Activity::Right, :success___) => :__success, Output(Activity::Left, :retry___) => _retry=End(:retry)
59
+ end
60
+ end
61
+
62
+ let(:user) do
63
+ _blog = blog
64
+
65
+ Module.new do
66
+ extend Activity::Path()
67
+
68
+ task task: _blog, _blog.outputs[:success] => Track(:success), _blog.outputs[:retry] => "End.success"
69
+ task task: User::Relax
70
+ end
71
+ end
72
+
73
+ it "runs from Callable->default to Relax" do
74
+ user.( [ options = { "return" => Activity::Right } ] ).must_equal [
75
+ user.outputs[:success].signal,
76
+ [ {"return"=>Activity::Right, "Read"=>1, "NextPage"=>[], "Relax"=>true} ]
77
+ ]
78
+
79
+ options.must_equal({"return"=>Activity::Right, "Read"=>1, "NextPage"=>[], "Relax"=>true})
80
+ end
81
+
82
+ it "runs from other Callable end" do
83
+ user.( [ options = { "return" => Activity::Left } ] ).must_equal [
84
+ user.outputs[:success].signal,
85
+ [ {"return"=>Activity::Left, "Read"=>1, "NextPage"=>[]} ]
86
+ ]
87
+
88
+ options.must_equal({"return"=>Activity::Left, "Read"=>1, "NextPage"=>[]})
89
+ end
90
+
91
+ #---
92
+ #- Callable( activity, start_at )
93
+ let(:with_nested_and_start_at) do
94
+ _blog = blog
95
+
96
+ Module.new do
97
+ extend Activity::Path()
98
+
99
+ task task: Operation::Callable( _blog, start_task: Blog::Next ), _blog.outputs[:success] => Track(:success)
100
+ task task: User::Relax
101
+ end
102
+ end
103
+
104
+ it "runs Callable from alternative start" do
105
+ with_nested_and_start_at.( [options = { "return" => Activity::Right }] ).
106
+ must_equal [
107
+ with_nested_and_start_at.outputs[:success].signal,
108
+ [ {"return"=>Activity::Right, "NextPage"=>[], "Relax"=>true} ]
109
+ ]
110
+
111
+ options.must_equal({"return"=>Activity::Right, "NextPage"=>[], "Relax"=>true})
112
+ end
113
+
114
+ #---
115
+ #- Callable( activity, call: :__call__ ) { ... }
116
+ describe "Callable with :call option" do
117
+ let(:process) do
118
+ class Workout
119
+ def self.__call__((options, *args), *)
120
+ options[:workout] = 9
121
+
122
+ return Activity::Right, [options, *args]
123
+ end
124
+ end
125
+
126
+ subprocess = Operation::Callable( Workout, call: :__call__ )
127
+
128
+ Module.new do
129
+ extend Activity::Path()
130
+
131
+ task task: subprocess
132
+ task task: User::Relax
133
+ end
134
+ end
135
+
136
+ it "runs Callable process with __call__" do
137
+ process.( [options = { "return" => Activity::Right }] ).
138
+ must_equal [
139
+ process.outputs[:success].signal,
140
+ [{"return"=>Activity::Right, :workout=>9, "Relax"=>true}]
141
+ ]
142
+
143
+ options.must_equal({"return"=>Activity::Right, :workout=>9, "Relax"=>true})
144
+ end
145
+ end
146
+ end
147
+ end