trailblazer-macro 2.1.0.beta1

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