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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rubocop.yml +16 -0
- data/.rubocop_todo.yml +642 -0
- data/.travis.yml +15 -0
- data/CHANGES.md +3 -0
- data/COMM-LICENSE +91 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +9 -0
- data/README.md +5 -0
- data/Rakefile +13 -0
- data/lib/trailblazer/macro.rb +13 -0
- data/lib/trailblazer/macro/version.rb +5 -0
- data/lib/trailblazer/operation/guard.rb +18 -0
- data/lib/trailblazer/operation/input_output.rb +28 -0
- data/lib/trailblazer/operation/model.rb +52 -0
- data/lib/trailblazer/operation/nested.rb +90 -0
- data/lib/trailblazer/operation/policy.rb +44 -0
- data/lib/trailblazer/operation/pundit.rb +38 -0
- data/lib/trailblazer/operation/rescue.rb +42 -0
- data/lib/trailblazer/operation/wrap.rb +83 -0
- data/test/docs/guard_test.rb +162 -0
- data/test/docs/macro_test.rb +36 -0
- data/test/docs/model_test.rb +75 -0
- data/test/docs/nested_test.rb +113 -0
- data/test/docs/pundit_test.rb +133 -0
- data/test/docs/rescue_test.rb +126 -0
- data/test/docs/wrap_test.rb +274 -0
- data/test/lib/methods.rb +25 -0
- data/test/operation/model_test.rb +54 -0
- data/test/operation/nested_test.rb +293 -0
- data/test/operation/pundit_test.rb +106 -0
- data/test/test_helper.rb +39 -0
- data/trailblazer-macro.gemspec +36 -0
- metadata +243 -0
@@ -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
|