trailblazer-macro 2.1.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.
@@ -0,0 +1,42 @@
1
+ class Trailblazer::Operation
2
+ NoopHandler = lambda { |*| }
3
+
4
+ def self.Rescue(*exceptions, handler: NoopHandler, &block)
5
+ exceptions = [StandardError] unless exceptions.any?
6
+
7
+ handler = Rescue.deprecate_positional_handler_signature(handler)
8
+ handler = Trailblazer::Option(handler)
9
+
10
+ # This block is evaluated by {Wrap}.
11
+ rescue_block = ->((ctx, flow_options), **circuit_options, &nested_activity) do
12
+ begin
13
+ nested_activity.call
14
+ rescue *exceptions => exception
15
+ # DISCUSS: should we deprecate this signature and rather apply the Task API here?
16
+ handler.call(exception, ctx, **circuit_options) # FIXME: when there's an error here, it shows the wrong exception!
17
+
18
+ [ Trailblazer::Operation::Railway.fail!, [ctx, flow_options] ]
19
+ end
20
+ end
21
+
22
+ Wrap( rescue_block, id: "Rescue(#{rand(100)})", &block )
23
+ # FIXME: name
24
+ # [ step, name: "Rescue:#{block.source_location.last}" ]
25
+ end
26
+
27
+ # TODO: remove me in 2.2.
28
+ module Rescue
29
+ def self.deprecate_positional_handler_signature(handler)
30
+ return handler if handler.is_a?(Symbol) # can't do nutting about this.
31
+
32
+ arity = handler.is_a?(Class) ? handler.method(:call).arity : handler.arity
33
+
34
+ return handler if arity != 2 # means (exception, (ctx, flow_options), *, &block), "new style"
35
+
36
+ ->(exception, (ctx, flow_options), **circuit_options, &block) do
37
+ warn "[Trailblazer] Rescue handlers have a new signature: (exception, *, &block)"
38
+ handler.(exception, ctx, &block)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,83 @@
1
+ class Trailblazer::Operation
2
+ def self.Wrap(user_wrap, id: "Wrap/#{rand(100)}", &block)
3
+ operation_class = Wrap.create_operation(block)
4
+ wrapped = Wrap::Wrapped.new(operation_class, user_wrap)
5
+
6
+ { task: wrapped, id: id, outputs: operation_class.outputs }
7
+ end
8
+
9
+ module Wrap
10
+ def self.create_operation(block)
11
+ Class.new( Nested.operation_class, &block ) # Usually resolves to Trailblazer::Operation.
12
+ end
13
+
14
+ # behaves like an operation so it plays with Nested and simply calls the operation in the user-provided block.
15
+ class Wrapped #< Trailblazer::Operation # FIXME: the inheritance is only to satisfy Nested( Wrapped.new )
16
+ include Trailblazer::Activity::Interface
17
+
18
+ private def deprecate_positional_wrap_signature(user_wrap)
19
+ parameters = user_wrap.is_a?(Class) ? user_wrap.method(:call).parameters : user_wrap.parameters
20
+
21
+ return user_wrap if parameters[0] == [:req] # means ((ctx, flow_options), *, &block), "new style"
22
+
23
+ ->((ctx, flow_options), **circuit_options, &block) do
24
+ warn "[Trailblazer] Wrap handlers have a new signature: ((ctx), *, &block)"
25
+ user_wrap.(ctx, &block)
26
+ end
27
+ end
28
+
29
+ def initialize(operation, user_wrap)
30
+ user_wrap = deprecate_positional_wrap_signature(user_wrap)
31
+
32
+ @operation = operation
33
+ @user_wrap = user_wrap
34
+
35
+ # Since in the user block, you can return Railway.pass! etc, we need to map
36
+ # those to the actual wrapped operation's end.
37
+ outputs = @operation.outputs
38
+ @signal_to_output = {
39
+ Railway.pass! => outputs[:success].signal,
40
+ Railway.fail! => outputs[:failure].signal,
41
+ Railway.pass_fast! => outputs[:pass_fast].signal,
42
+ Railway.fail_fast! => outputs[:fail_fast].signal,
43
+ true => outputs[:success].signal,
44
+ false => outputs[:failure].signal,
45
+ nil => outputs[:failure].signal,
46
+ }
47
+ end
48
+
49
+ def call( (ctx, flow_options), **circuit_options )
50
+ block_calling_wrapped = -> {
51
+ activity = @operation.to_h[:activity]
52
+
53
+ activity.( [ctx, flow_options], **circuit_options )
54
+ }
55
+
56
+ # call the user's Wrap {} block in the operation.
57
+ # This will invoke block_calling_wrapped above if the user block yields.
58
+ returned = @user_wrap.( [ctx, flow_options], **circuit_options, &block_calling_wrapped )
59
+
60
+ # {returned} can be
61
+ # 1. {circuit interface return} from the begin block, because the wrapped OP passed
62
+ # 2. {task interface return} because the user block returns "customized" signals, true of fale
63
+
64
+ if returned.is_a?(Array) # 1. {circuit interface return}, new style.
65
+ signal, (ctx, flow_options) = returned
66
+ else # 2. {task interface return}, only a signal (or true/false)
67
+ # TODO: deprecate this?
68
+ signal = returned
69
+ end
70
+
71
+ # Use the original {signal} if there's no mapping.
72
+ # This usually means signal is an End instance or a custom signal.
73
+ signal = @signal_to_output.fetch(signal, signal)
74
+
75
+ return signal, [ctx, flow_options]
76
+ end
77
+
78
+ def outputs
79
+ @operation.outputs
80
+ end
81
+ end
82
+ end # Wrap
83
+ end
@@ -0,0 +1,162 @@
1
+ require "test_helper"
2
+
3
+ #--
4
+ # with proc
5
+ class DocsGuardProcTest < Minitest::Spec
6
+ #:proc
7
+ class Create < Trailblazer::Operation
8
+ step Policy::Guard(->(options, pass:, **) { pass })
9
+ #~pipeonly
10
+ step :process
11
+
12
+ def process(options, **)
13
+ options["x"] = true
14
+ end
15
+ #~pipeonly end
16
+ end
17
+ #:proc end
18
+
19
+ it { Create.(pass: false)["x"].must_be_nil }
20
+ it { Create.(pass: true)["x"].must_equal true }
21
+
22
+ #- result object, guard
23
+ it { Create.(pass: true)["result.policy.default"].success?.must_equal true }
24
+ it { Create.(pass: false)["result.policy.default"].success?.must_equal false }
25
+
26
+ #---
27
+ #- Guard inheritance
28
+ class New < Create
29
+ step Policy::Guard( ->(options, current_user:, **) { current_user } ), override: true
30
+ end
31
+
32
+ it { Trailblazer::Operation::Inspect.(New).must_equal %{[>policy.default.eval,>process]} }
33
+ end
34
+
35
+ #---
36
+ # with Callable
37
+ class DocsGuardTest < Minitest::Spec
38
+ #:callable
39
+ class MyGuard
40
+ include Uber::Callable
41
+
42
+ def call(options, pass:, **)
43
+ pass
44
+ end
45
+ end
46
+ #:callable end
47
+
48
+ #:callable-op
49
+ class Create < Trailblazer::Operation
50
+ step Policy::Guard( MyGuard.new )
51
+ #~pipe-only
52
+ step :process
53
+
54
+ def process(options, **)
55
+ options[:x] = true
56
+ end
57
+ #~pipe-only end
58
+ end
59
+ #:callable-op end
60
+
61
+ it { Create.(pass: false)[:x].must_be_nil }
62
+ it { Create.(pass: true)[:x].must_equal true }
63
+ end
64
+
65
+ #---
66
+ # with method
67
+ class DocsGuardMethodTest < Minitest::Spec
68
+ #:method
69
+ class Create < Trailblazer::Operation
70
+ step Policy::Guard( :pass? )
71
+
72
+ def pass?(options, pass:, **)
73
+ pass
74
+ end
75
+ #~pipe-onlyy
76
+ step :process
77
+
78
+ def process(options, **)
79
+ options["x"] = true
80
+ end
81
+ #~pipe-onlyy end
82
+ end
83
+ #:method end
84
+
85
+ it { Create.(pass: false).inspect("x").must_equal %{<Result:false [nil] >} }
86
+ it { Create.(pass: true).inspect("x").must_equal %{<Result:true [true] >} }
87
+ end
88
+
89
+ #---
90
+ # with name:
91
+ class DocsGuardNamedTest < Minitest::Spec
92
+ #:name
93
+ class Create < Trailblazer::Operation
94
+ step Policy::Guard( ->(options, current_user:, **) { current_user }, name: :user )
95
+ # ...
96
+ end
97
+ #:name end
98
+
99
+ it { Create.(:current_user => nil )["result.policy.user"].success?.must_equal false }
100
+ it { Create.(:current_user => Module)["result.policy.user"].success?.must_equal true }
101
+
102
+ it {
103
+ #:name-result
104
+ result = Create.(:current_user => true)
105
+ result["result.policy.user"].success? #=> true
106
+ #:name-result end
107
+ }
108
+ end
109
+
110
+ #---
111
+ # dependency injection
112
+ class DocsGuardInjectionTest < Minitest::Spec
113
+ #:di-op
114
+ class Create < Trailblazer::Operation
115
+ step Policy::Guard( ->(options, current_user:, **) { current_user == Module } )
116
+ end
117
+ #:di-op end
118
+
119
+ it { Create.(:current_user => Module).inspect("").must_equal %{<Result:true [nil] >} }
120
+ it {
121
+ result =
122
+ #:di-call
123
+ Create.({},
124
+ :current_user => Module,
125
+ "policy.default.eval" => Trailblazer::Operation::Policy::Guard.build(->(options, **) { false })
126
+ )
127
+ #:di-call end
128
+ result.inspect("").must_equal %{<Result:false [nil] >} }
129
+ end
130
+
131
+ #---
132
+ # missing current_user throws exception
133
+ class DocsGuardMissingKeywordTest < Minitest::Spec
134
+ class Create < Trailblazer::Operation
135
+ step Policy::Guard( ->(options, current_user:, **) { current_user == Module } )
136
+ end
137
+
138
+ it { assert_raises(ArgumentError) { Create.() } }
139
+ it { Create.(:current_user => Module).success?.must_equal true }
140
+ end
141
+
142
+ #---
143
+ # before:
144
+ class DocsGuardPositionTest < Minitest::Spec
145
+ #:before
146
+ class Create < Trailblazer::Operation
147
+ step :model!
148
+ step Policy::Guard( :authorize! ),
149
+ before: :model!
150
+ end
151
+ #:before end
152
+
153
+ it { Trailblazer::Operation::Inspect.(Create).must_equal %{[>policy.default.eval,>model!]} }
154
+ it do
155
+ #:before-pipe
156
+ Trailblazer::Operation::Inspect.(Create, style: :rows) #=>
157
+ # 0 ========================>operation.new
158
+ # 1 ==================>policy.default.eval
159
+ # 2 ===============================>model!
160
+ #:before-pipe end
161
+ end
162
+ end
@@ -0,0 +1,36 @@
1
+ require "test_helper"
2
+
3
+ class DocsMacroTest < Minitest::Spec
4
+ #:simple
5
+ module Macro
6
+ def self.MyPolicy(allowed_role: "admin")
7
+ step = ->(input, options) { options["current_user"].type == allowed_role }
8
+
9
+ [ step, name: "my_policy.#{allowed_role}" ] # :before, :replace, etc. work, too.
10
+ end
11
+ end
12
+ #:simple end
13
+
14
+ #:simple-op
15
+ class Create < Trailblazer::Operation
16
+ step Macro::MyPolicy( allowed_role: "manager" )
17
+ # ..
18
+ end
19
+ #:simple-op end
20
+
21
+ =begin
22
+ it do
23
+ #:simple-pipe
24
+ puts Create["pipetree"].inspect(style: :rows) #=>
25
+ 0 ========================>operation.new
26
+ 1 ====================>my_policy.manager
27
+ #:simple-pipe end
28
+ end
29
+ =end
30
+
31
+ it { Operation::Inspect.(Create).must_equal %{[>my_policy.manager]} }
32
+ end
33
+
34
+ # injectable option
35
+ # nested pipe
36
+ # using macros in macros
@@ -0,0 +1,75 @@
1
+ require "test_helper"
2
+
3
+ class DocsModelTest < Minitest::Spec
4
+ Song = Struct.new(:id, :title) do
5
+ def self.find_by(id:nil)
6
+ id.nil? ? nil : new(id)
7
+ end
8
+
9
+ def self.[](id)
10
+ id.nil? ? nil : new(id+99)
11
+ end
12
+ end
13
+
14
+ #:op
15
+ class Create < Trailblazer::Operation
16
+ step Model( Song, :new )
17
+ # ..
18
+ end
19
+ #:op end
20
+
21
+ it do
22
+ #:create
23
+ result = Create.(params: {})
24
+ result[:model] #=> #<struct Song id=nil, title=nil>
25
+ #:create end
26
+
27
+ result[:model].inspect.must_equal %{#<struct DocsModelTest::Song id=nil, title=nil>}
28
+ end
29
+
30
+ #:update
31
+ class Update < Trailblazer::Operation
32
+ step Model( Song, :find_by )
33
+ # ..
34
+ end
35
+ #:update end
36
+
37
+ it do
38
+ #:update-ok
39
+ result = Update.(params: { id: 1 })
40
+ result[:model] #=> #<struct Song id=1, title="Roxanne">
41
+ #:update-ok end
42
+
43
+ result[:model].inspect.must_equal %{#<struct DocsModelTest::Song id=1, title=nil>}
44
+ end
45
+
46
+ it do
47
+ #:update-fail
48
+ result = Update.(params: {})
49
+ result[:model] #=> nil
50
+ result.success? #=> false
51
+ #:update-fail end
52
+
53
+ result[:model].must_be_nil
54
+ result.success?.must_equal false
55
+ end
56
+
57
+ #:show
58
+ class Show < Trailblazer::Operation
59
+ step Model( Song, :[] )
60
+ # ..
61
+ end
62
+ #:show end
63
+
64
+ it do
65
+ result = Show.(params: { id: 1 })
66
+
67
+ #:show-ok
68
+ result = Show.(params: { id: 1 })
69
+ result[:model] #=> #<struct Song id=1, title="Roxanne">
70
+ #:show-ok end
71
+
72
+ result.success?.must_equal true
73
+ result[:model].inspect.must_equal %{#<struct DocsModelTest::Song id=100, title=nil>}
74
+ end
75
+ end
@@ -0,0 +1,113 @@
1
+ require "test_helper"
2
+
3
+ class NestedInput < Minitest::Spec
4
+ #:input-multiply
5
+ class Multiplier < Trailblazer::Operation
6
+ step ->(options, x:, y:, **) { options["product"] = x*y }
7
+ end
8
+ #:input-multiply end
9
+
10
+ #:input-pi
11
+ class MultiplyByPi < Trailblazer::Operation
12
+ step ->(options, **) { options["pi_constant"] = 3.14159 }
13
+ step Nested( Multiplier, input: ->(options, **) do
14
+ { "y" => options["pi_constant"],
15
+ "x" => options["x"]
16
+ }
17
+ end )
18
+ end
19
+ #:input-pi end
20
+
21
+ it { MultiplyByPi.("x" => 9).inspect("product").must_equal %{<Result:true [28.27431] >} }
22
+
23
+ it do
24
+ #:input-result
25
+ result = MultiplyByPi.("x" => 9)
26
+ result["product"] #=> [28.27431]
27
+ #:input-result end
28
+ end
29
+ end
30
+
31
+ class NestedInputCallable < Minitest::Spec
32
+ Multiplier = NestedInput::Multiplier
33
+
34
+ #:input-callable
35
+ class MyInput
36
+ def self.call(options, **)
37
+ {
38
+ "y" => options["pi_constant"],
39
+ "x" => options["x"]
40
+ }
41
+ end
42
+ end
43
+ #:input-callable end
44
+
45
+ #:input-callable-op
46
+ class MultiplyByPi < Trailblazer::Operation
47
+ step ->(options, **) { options["pi_constant"] = 3.14159 }
48
+ step Nested( Multiplier, input: MyInput )
49
+ end
50
+ #:input-callable-op end
51
+
52
+ it { MultiplyByPi.("x" => 9).inspect("product").must_equal %{<Result:true [28.27431] >} }
53
+ end
54
+
55
+ class NestedWithCallableAndInputTest < Minitest::Spec
56
+ Memo = Struct.new(:title, :text, :created_by)
57
+
58
+ class Memo::Upsert < Trailblazer::Operation
59
+ step Nested( :operation_class, input: :input_for_create )
60
+
61
+ def operation_class( ctx, ** )
62
+ ctx[:id] ? Update : Create
63
+ end
64
+
65
+ # only let :title pass through.
66
+ def input_for_create( ctx )
67
+ { title: ctx[:title] }
68
+ end
69
+
70
+ class Create < Trailblazer::Operation
71
+ step :create_memo
72
+
73
+ def create_memo( ctx, ** )
74
+ ctx[:model] = Memo.new(ctx[:title], ctx[:text], :create)
75
+ end
76
+ end
77
+
78
+ class Update < Trailblazer::Operation
79
+ step :find_by_title
80
+
81
+ def find_by_title( ctx, ** )
82
+ ctx[:model] = Memo.new(ctx[:title], ctx[:text], :update)
83
+ end
84
+ end
85
+ end
86
+
87
+ it "runs Create without :id" do
88
+ Memo::Upsert.( title: "Yay!" ).inspect(:model).
89
+ must_equal %{<Result:true [#<struct NestedWithCallableAndInputTest::Memo title=\"Yay!\", text=nil, created_by=:create>] >}
90
+ end
91
+
92
+ it "runs Update without :id" do
93
+ Memo::Upsert.( id: 1, title: "Yay!" ).inspect(:model).
94
+ must_equal %{<Result:true [#<struct NestedWithCallableAndInputTest::Memo title=\"Yay!\", text=nil, created_by=:update>] >}
95
+ end
96
+ end
97
+
98
+ # builder: Nested + deviate to left if nil / skip_track if true
99
+
100
+ #---
101
+ # automatic :name
102
+ class NestedNameTest < Minitest::Spec
103
+ class Create < Trailblazer::Operation
104
+ class Present < Trailblazer::Operation
105
+ # ...
106
+ end
107
+
108
+ step Nested( Present )
109
+ # ...
110
+ end
111
+
112
+ it { Operation::Inspect.(Create).must_equal %{[>Nested(NestedNameTest::Create::Present)]} }
113
+ end