trailblazer 2.0.0 → 2.0.1
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 +4 -4
- data/CHANGES.md +13 -0
- data/COMM-LICENSE +21 -21
- data/Gemfile +3 -0
- data/README.md +15 -77
- data/Rakefile +1 -2
- data/doc/operation-2017.png +0 -0
- data/lib/trailblazer.rb +0 -1
- data/lib/trailblazer/operation/callback.rb +10 -19
- data/lib/trailblazer/operation/contract.rb +7 -8
- data/lib/trailblazer/operation/guard.rb +4 -13
- data/lib/trailblazer/operation/model.rb +30 -32
- data/lib/trailblazer/operation/nested.rb +21 -32
- data/lib/trailblazer/operation/persist.rb +4 -8
- data/lib/trailblazer/operation/policy.rb +5 -15
- data/lib/trailblazer/operation/pundit.rb +8 -17
- data/lib/trailblazer/operation/rescue.rb +17 -17
- data/lib/trailblazer/operation/validate.rb +34 -21
- data/lib/trailblazer/operation/wrap.rb +12 -25
- data/lib/trailblazer/version.rb +1 -1
- data/test/docs/dry_test.rb +4 -4
- data/test/docs/fast_test.rb +164 -0
- data/test/docs/guard_test.rb +2 -2
- data/test/docs/macro_test.rb +36 -0
- data/test/docs/model_test.rb +52 -0
- data/test/docs/nested_test.rb +101 -4
- data/test/docs/operation_test.rb +225 -1
- data/test/docs/pundit_test.rb +18 -17
- data/test/docs/rescue_test.rb +3 -3
- data/test/docs/wrap_test.rb +1 -0
- data/test/operation/callback_test.rb +5 -5
- data/test/operation/contract_test.rb +30 -19
- data/test/operation/dsl/callback_test.rb +2 -2
- data/test/operation/dsl/contract_test.rb +4 -4
- data/test/operation/model_test.rb +12 -57
- data/test/operation/persist_test.rb +7 -7
- data/test/operation/pipedream_test.rb +9 -9
- data/test/operation/pipetree_test.rb +5 -5
- data/test/operation/pundit_test.rb +7 -7
- data/test/operation/resolver_test.rb +47 -47
- data/trailblazer.gemspec +1 -1
- metadata +18 -11
- data/lib/trailblazer/operation/builder.rb +0 -24
- data/lib/trailblazer/operation/resolver.rb +0 -22
- data/test/controller_test.rb +0 -115
- data/test/operation/builder_test.rb +0 -89
data/test/docs/guard_test.rb
CHANGED
@@ -29,10 +29,10 @@ class DocsGuardProcTest < Minitest::Spec
|
|
29
29
|
#---
|
30
30
|
#- Guard inheritance
|
31
31
|
class New < Create
|
32
|
-
|
32
|
+
step Policy::Guard( ->(options) { options["current_user"] } ), override: true
|
33
33
|
end
|
34
34
|
|
35
|
-
it { New["pipetree"].inspect.must_equal %{[
|
35
|
+
it { New["pipetree"].inspect.must_equal %{[>operation.new,>policy.default.eval,>process]} }
|
36
36
|
end
|
37
37
|
|
38
38
|
#---
|
@@ -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 { Create["pipetree"].inspect.must_equal %{[>operation.new,>my_policy.manager]} }
|
32
|
+
end
|
33
|
+
|
34
|
+
# injectable option
|
35
|
+
# nested pipe
|
36
|
+
# using macros in macros
|
@@ -0,0 +1,52 @@
|
|
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
|
+
end
|
9
|
+
|
10
|
+
#:op
|
11
|
+
class Create < Trailblazer::Operation
|
12
|
+
step Model( Song, :new )
|
13
|
+
# ..
|
14
|
+
end
|
15
|
+
#:op end
|
16
|
+
|
17
|
+
it do
|
18
|
+
#:create
|
19
|
+
result = Create.({})
|
20
|
+
result["model"] #=> #<struct Song id=nil, title=nil>
|
21
|
+
#:create end
|
22
|
+
|
23
|
+
result["model"].inspect.must_equal %{#<struct DocsModelTest::Song id=nil, title=nil>}
|
24
|
+
end
|
25
|
+
|
26
|
+
#:update
|
27
|
+
class Update < Trailblazer::Operation
|
28
|
+
step Model( Song, :find_by )
|
29
|
+
# ..
|
30
|
+
end
|
31
|
+
#:update end
|
32
|
+
|
33
|
+
it do
|
34
|
+
#:update-ok
|
35
|
+
result = Update.({ id: 1 })
|
36
|
+
result["model"] #=> #<struct Song id=1, title="Roxanne">
|
37
|
+
#:update-ok end
|
38
|
+
|
39
|
+
result["model"].inspect.must_equal %{#<struct DocsModelTest::Song id=1, title=nil>}
|
40
|
+
end
|
41
|
+
|
42
|
+
it do
|
43
|
+
#:update-fail
|
44
|
+
result = Update.({})
|
45
|
+
result["model"] #=> nil
|
46
|
+
result.success? #=> false
|
47
|
+
#:update-fail end
|
48
|
+
|
49
|
+
result["model"].must_equal nil
|
50
|
+
result.success?.must_equal false
|
51
|
+
end
|
52
|
+
end
|
data/test/docs/nested_test.rb
CHANGED
@@ -73,15 +73,15 @@ class DocsNestedOperationTest < Minitest::Spec
|
|
73
73
|
|
74
74
|
#- shared data
|
75
75
|
class B < Trailblazer::Operation
|
76
|
-
|
77
|
-
|
78
|
-
|
76
|
+
success ->(options) { options["can.B.see.it?"] = options["this.should.not.be.visible.in.B"] }
|
77
|
+
success ->(options) { options["can.B.see.current_user?"] = options["current_user"] }
|
78
|
+
success ->(options) { options["can.B.see.A.class.data?"] = options["A.class.data"] }
|
79
79
|
end
|
80
80
|
|
81
81
|
class A < Trailblazer::Operation
|
82
82
|
self["A.class.data"] = true
|
83
83
|
|
84
|
-
|
84
|
+
success ->(options) { options["this.should.not.be.visible.in.B"] = true }
|
85
85
|
step Nested B
|
86
86
|
end
|
87
87
|
|
@@ -115,3 +115,100 @@ class NestedClassLevelTest < Minitest::Spec
|
|
115
115
|
it { Create.().inspect("x", "y").must_equal %{<Result:true [true, true] >} }
|
116
116
|
it { Create.(); Create["class"].must_equal nil }
|
117
117
|
end
|
118
|
+
|
119
|
+
class NestedWithCallableTest < Minitest::Spec
|
120
|
+
Song = Struct.new(:id, :title)
|
121
|
+
|
122
|
+
class X < Trailblazer::Operation
|
123
|
+
step ->(options) { options["x"] = true }
|
124
|
+
end
|
125
|
+
|
126
|
+
class Y < Trailblazer::Operation
|
127
|
+
step ->(options) { options["y"] = true }
|
128
|
+
end
|
129
|
+
|
130
|
+
class A < Trailblazer::Operation
|
131
|
+
step ->(options) { options["z"] = true }
|
132
|
+
step Nested( ->(options, *) { options["class"] } )
|
133
|
+
end
|
134
|
+
|
135
|
+
it { A.({}, "class" => X).inspect("x", "y", "z").must_equal "<Result:true [true, nil, true] >" }
|
136
|
+
it { A.({}, "class" => Y).inspect("x", "y", "z").must_equal "<Result:true [nil, true, true] >" }
|
137
|
+
# it { Create.({}).inspect("x", "y", "z").must_equal "<Result:true [nil, true, true] >" }
|
138
|
+
|
139
|
+
class Song
|
140
|
+
module Contract
|
141
|
+
class Create < Reform::Form
|
142
|
+
property :title
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
User = Struct.new(:is_admin) do
|
148
|
+
def admin?
|
149
|
+
!! is_admin
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class Create < Trailblazer::Operation
|
154
|
+
step Nested( ->(options, current_user:nil, **) { current_user.admin? ? Admin : NeedsModeration })
|
155
|
+
|
156
|
+
class NeedsModeration < Trailblazer::Operation
|
157
|
+
step Model( Song, :new )
|
158
|
+
step Contract::Build( constant: Song::Contract::Create )
|
159
|
+
step Contract::Validate()
|
160
|
+
step :notify_moderator!
|
161
|
+
|
162
|
+
def notify_moderator!(options, **)
|
163
|
+
#~noti
|
164
|
+
options["x"] = true
|
165
|
+
#~noti end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class Admin < Trailblazer::Operation # TODO: test if current_user is passed in.
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
let (:admin) { User.new(true) }
|
175
|
+
let (:anonymous) { User.new(false) }
|
176
|
+
|
177
|
+
it { Create.({}, "current_user" => anonymous).inspect("x").must_equal %{<Result:true [true] >} }
|
178
|
+
it { Create.({}, "current_user" => admin) .inspect("x").must_equal %{<Result:true [nil] >} }
|
179
|
+
|
180
|
+
#:method
|
181
|
+
class Update < Trailblazer::Operation
|
182
|
+
step Nested( :build! )
|
183
|
+
|
184
|
+
def build!(options, current_user:nil, **)
|
185
|
+
current_user.admin? ? Create::Admin : Create::NeedsModeration
|
186
|
+
end
|
187
|
+
end
|
188
|
+
#:method end
|
189
|
+
|
190
|
+
it { Update.({}, "current_user" => anonymous).inspect("x").must_equal %{<Result:true [true] >} }
|
191
|
+
it { Update.({}, "current_user" => admin) .inspect("x").must_equal %{<Result:true [nil] >} }
|
192
|
+
|
193
|
+
#:callable-builder
|
194
|
+
class MyBuilder
|
195
|
+
extend Uber::Callable
|
196
|
+
|
197
|
+
def self.call(options, current_user:nil, **)
|
198
|
+
current_user.admin? ? Create::Admin : Create::NeedsModeration
|
199
|
+
end
|
200
|
+
end
|
201
|
+
#:callable-builder end
|
202
|
+
|
203
|
+
#:callable
|
204
|
+
class Delete < Trailblazer::Operation
|
205
|
+
step Nested( MyBuilder )
|
206
|
+
# ..
|
207
|
+
end
|
208
|
+
#:callable end
|
209
|
+
|
210
|
+
it { Delete.({}, "current_user" => anonymous).inspect("x").must_equal %{<Result:true [true] >} }
|
211
|
+
it { Delete.({}, "current_user" => admin) .inspect("x").must_equal %{<Result:true [nil] >} }
|
212
|
+
end
|
213
|
+
|
214
|
+
# builder: Nested + deviate to left if nil / skip_track if true
|
data/test/docs/operation_test.rb
CHANGED
@@ -4,6 +4,32 @@ class DocsOperationExampleTest < Minitest::Spec
|
|
4
4
|
Song = Struct.new(:id, :title, :created_by) do
|
5
5
|
def save; true; end
|
6
6
|
end
|
7
|
+
User = Struct.new(:name)
|
8
|
+
|
9
|
+
#:invocation-dep
|
10
|
+
class Create < Trailblazer::Operation
|
11
|
+
step Model( Song, :new )
|
12
|
+
step :assign_current_user!
|
13
|
+
# ..
|
14
|
+
def assign_current_user!(options)
|
15
|
+
options["model"].created_by = options["current_user"]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
#:invocation-dep end
|
19
|
+
|
20
|
+
it do
|
21
|
+
current_user = User.new("Ema")
|
22
|
+
#:invocation-dep-call
|
23
|
+
result = Create.( { title: "Roxanne" }, "current_user" => current_user )
|
24
|
+
#:invocation-dep-call end
|
25
|
+
|
26
|
+
#:invocation-dep-res
|
27
|
+
result["current_user"] #=> #<User name="Ema">
|
28
|
+
result["model"] #=> #<Song id=nil, title=nil, created_by=#<User name="Ema">>
|
29
|
+
#:invocation-dep-res end
|
30
|
+
end
|
31
|
+
|
32
|
+
it { Create.({ title: "Roxanne" }, "current_user" => Module).inspect("model").must_equal %{<Result:true [#<struct DocsOperationExampleTest::Song id=nil, title=nil, created_by=Module>] >} }
|
7
33
|
|
8
34
|
#:op
|
9
35
|
class Song::Create < Trailblazer::Operation
|
@@ -41,7 +67,7 @@ class DndTest < Minitest::Spec
|
|
41
67
|
step :authorize!
|
42
68
|
failure :auth_err!
|
43
69
|
step :save!
|
44
|
-
|
70
|
+
failure Wrap
|
45
71
|
end
|
46
72
|
end
|
47
73
|
|
@@ -182,3 +208,201 @@ class DocsOperationAPIExampleTest < Minitest::Spec
|
|
182
208
|
it { Song::Create.({ }).inspect("model").must_equal %{<Result:false [#<struct DocsOperationAPIExampleTest::Song id=nil, title=nil, created_by=nil>] >} }
|
183
209
|
it { Song::Create.({ title: "Nothin'" }, "current_user"=>Module).inspect("model").must_equal %{<Result:true [#<struct DocsOperationAPIExampleTest::Song id=nil, title="Nothin'", created_by=Module>] >} }
|
184
210
|
end
|
211
|
+
|
212
|
+
|
213
|
+
class DocsOperationInheritanceTest < Minitest::Spec
|
214
|
+
Song = Struct.new(:id, :title, :created_by) do
|
215
|
+
def save; true; end
|
216
|
+
end
|
217
|
+
|
218
|
+
class MyContract < Reform::Form
|
219
|
+
property :title
|
220
|
+
validates :title, presence: true
|
221
|
+
end
|
222
|
+
|
223
|
+
#:inh-new
|
224
|
+
class New < Trailblazer::Operation
|
225
|
+
step Model( Song, :new )
|
226
|
+
step Contract::Build( constant: MyContract )
|
227
|
+
end
|
228
|
+
#:inh-new end
|
229
|
+
|
230
|
+
puts New["pipetree"].inspect(style: :row)
|
231
|
+
=begin
|
232
|
+
#:inh-new-pipe
|
233
|
+
0 =======================>>operation.new
|
234
|
+
1 ==========================&model.build
|
235
|
+
2 =======================>contract.build
|
236
|
+
#:inh-new-pipe end
|
237
|
+
=end
|
238
|
+
|
239
|
+
#:inh-create
|
240
|
+
class Create < New
|
241
|
+
step Contract::Validate()
|
242
|
+
step Contract::Persist()
|
243
|
+
end
|
244
|
+
#:inh-create end
|
245
|
+
|
246
|
+
puts Create["pipetree"].inspect(style: :row)
|
247
|
+
=begin
|
248
|
+
#:inh-create-pipe
|
249
|
+
0 =======================>>operation.new
|
250
|
+
1 ==========================&model.build
|
251
|
+
2 =======================>contract.build
|
252
|
+
3 ==============&contract.default.params
|
253
|
+
4 ============&contract.default.validate
|
254
|
+
5 =========================&persist.save
|
255
|
+
#:inh-create-pipe end
|
256
|
+
=end
|
257
|
+
|
258
|
+
module MyApp
|
259
|
+
end
|
260
|
+
|
261
|
+
#:override-app
|
262
|
+
module MyApp::Operation
|
263
|
+
class New < Trailblazer::Operation
|
264
|
+
extend Contract::DSL
|
265
|
+
|
266
|
+
contract do
|
267
|
+
property :title
|
268
|
+
end
|
269
|
+
|
270
|
+
step Model( nil, :new )
|
271
|
+
step Contract::Build()
|
272
|
+
end
|
273
|
+
end
|
274
|
+
#:override-app end
|
275
|
+
|
276
|
+
#:override-new
|
277
|
+
class Song::New < MyApp::Operation::New
|
278
|
+
step Model( Song, :new ), override: true
|
279
|
+
end
|
280
|
+
#:override-new end
|
281
|
+
|
282
|
+
puts Song::New["pipetree"].inspect(style: :row)
|
283
|
+
=begin
|
284
|
+
#:override-pipe
|
285
|
+
Song::New["pipetree"].inspect(style: :row)
|
286
|
+
0 =======================>>operation.new
|
287
|
+
1 ==========================&model.build
|
288
|
+
2 =======================>contract.build
|
289
|
+
#:override-pipe end
|
290
|
+
=end
|
291
|
+
|
292
|
+
it do
|
293
|
+
Song::New["pipetree"].inspect.must_equal %{[>operation.new,>model.build,>contract.build]}
|
294
|
+
Song::New.().inspect("model").must_equal %{<Result:true [#<struct DocsOperationInheritanceTest::Song id=nil, title=nil, created_by=nil>] >}
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
class DocsOperationStepOptionsTest < Minitest::Spec
|
299
|
+
Song = Struct.new(:title) do
|
300
|
+
def self.find_by(*)
|
301
|
+
nil
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
class AutomaticNameTest < Minitest::Spec
|
306
|
+
#:name-auto
|
307
|
+
class New < Trailblazer::Operation
|
308
|
+
step Model( Song, :new )
|
309
|
+
end
|
310
|
+
#:name-auto end
|
311
|
+
|
312
|
+
puts New["pipetree"].inspect(style: :row)
|
313
|
+
=begin
|
314
|
+
#:name-auto-pipe
|
315
|
+
0 =======================>>operation.new
|
316
|
+
1 ==========================&model.build
|
317
|
+
#:name-auto-pipe end
|
318
|
+
=end
|
319
|
+
|
320
|
+
#:replace-inh
|
321
|
+
class Update < New
|
322
|
+
step Model(Song, :find_by), replace: "model.build"
|
323
|
+
end
|
324
|
+
#:replace-inh end
|
325
|
+
|
326
|
+
puts Update["pipetree"].inspect(style: :row)
|
327
|
+
=begin
|
328
|
+
#:replace-inh-pipe
|
329
|
+
0 =======================>>operation.new
|
330
|
+
2 ==========================&model.build
|
331
|
+
#:replace-inh-pipe end
|
332
|
+
=end
|
333
|
+
|
334
|
+
it { Update.({}).inspect("model").must_equal %{<Result:false [nil] >} }
|
335
|
+
|
336
|
+
|
337
|
+
# #:delete-inh
|
338
|
+
# class Noop < New
|
339
|
+
# step nil, delete: "model.build"
|
340
|
+
# end
|
341
|
+
# #:delete-inh end
|
342
|
+
|
343
|
+
# puts "yo"
|
344
|
+
# puts Update["pipetree"].inspect(style: :row)
|
345
|
+
# =begin
|
346
|
+
# #:delete-inh-pipe
|
347
|
+
# 0 =======================>>operation.new
|
348
|
+
# 2 ==========================&model.build
|
349
|
+
# #:delete-inh-pipe end
|
350
|
+
# =end
|
351
|
+
|
352
|
+
# it { Noop.({}).inspect("model").must_equal %{<Result:false [nil] >} }
|
353
|
+
end
|
354
|
+
|
355
|
+
class ManualNameTest < Minitest::Spec
|
356
|
+
#:name-manu
|
357
|
+
class New < Trailblazer::Operation
|
358
|
+
step Model( Song, :new ), name: "build.song.model"
|
359
|
+
step :validate_params!, name: "my.params.validate"
|
360
|
+
# ..
|
361
|
+
end
|
362
|
+
#:name-manu end
|
363
|
+
|
364
|
+
puts New["pipetree"].inspect(style: :row)
|
365
|
+
=begin
|
366
|
+
#:name-manu-pipe
|
367
|
+
0 =======================>>operation.new
|
368
|
+
1 =====================&build.song.model
|
369
|
+
2 ===================&my.params.validate
|
370
|
+
#:name-manu-pipe end
|
371
|
+
=end
|
372
|
+
end
|
373
|
+
|
374
|
+
class BeforeTest < Minitest::Spec
|
375
|
+
#:pos-before
|
376
|
+
class New < Trailblazer::Operation
|
377
|
+
step Model( Song, :new )
|
378
|
+
step :validate_params!, before: "model.build"
|
379
|
+
# ..
|
380
|
+
end
|
381
|
+
#:pos-before end
|
382
|
+
|
383
|
+
puts New["pipetree"].inspect(style: :row)
|
384
|
+
=begin
|
385
|
+
#:pos-before-pipe
|
386
|
+
0 =======================>>operation.new
|
387
|
+
1 =====================&validate_params!
|
388
|
+
2 ==========================&model.build
|
389
|
+
#:pos-before-pipe end
|
390
|
+
=end
|
391
|
+
|
392
|
+
#:pos-inh
|
393
|
+
class Create < New
|
394
|
+
step :policy!, after: "operation.new"
|
395
|
+
end
|
396
|
+
#:pos-inh end
|
397
|
+
|
398
|
+
puts Create["pipetree"].inspect(style: :row)
|
399
|
+
=begin
|
400
|
+
#:pos-inh-pipe
|
401
|
+
0 =======================>>operation.new
|
402
|
+
1 ==============================&policy!
|
403
|
+
2 =====================&validate_params!
|
404
|
+
3 ==========================&model.build
|
405
|
+
#:pos-inh-pipe end
|
406
|
+
=end
|
407
|
+
end
|
408
|
+
end
|