trailblazer 1.1.2 → 2.0.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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +10 -7
  3. data/CHANGES.md +108 -0
  4. data/COMM-LICENSE +91 -0
  5. data/Gemfile +18 -4
  6. data/LICENSE.txt +7 -20
  7. data/README.md +55 -15
  8. data/Rakefile +21 -2
  9. data/draft-1.2.rb +7 -0
  10. data/lib/trailblazer.rb +17 -4
  11. data/lib/trailblazer/dsl.rb +47 -0
  12. data/lib/trailblazer/operation/auto_inject.rb +47 -0
  13. data/lib/trailblazer/operation/builder.rb +18 -18
  14. data/lib/trailblazer/operation/callback.rb +31 -38
  15. data/lib/trailblazer/operation/contract.rb +46 -0
  16. data/lib/trailblazer/operation/controller.rb +45 -27
  17. data/lib/trailblazer/operation/guard.rb +24 -0
  18. data/lib/trailblazer/operation/model.rb +41 -33
  19. data/lib/trailblazer/operation/nested.rb +43 -0
  20. data/lib/trailblazer/operation/params.rb +13 -0
  21. data/lib/trailblazer/operation/persist.rb +13 -0
  22. data/lib/trailblazer/operation/policy.rb +26 -72
  23. data/lib/trailblazer/operation/present.rb +19 -0
  24. data/lib/trailblazer/operation/procedural/contract.rb +15 -0
  25. data/lib/trailblazer/operation/procedural/validate.rb +22 -0
  26. data/lib/trailblazer/operation/pundit.rb +42 -0
  27. data/lib/trailblazer/operation/representer.rb +25 -92
  28. data/lib/trailblazer/operation/rescue.rb +23 -0
  29. data/lib/trailblazer/operation/resolver.rb +18 -24
  30. data/lib/trailblazer/operation/validate.rb +50 -0
  31. data/lib/trailblazer/operation/wrap.rb +37 -0
  32. data/lib/trailblazer/version.rb +1 -1
  33. data/test/{operation/controller_test.rb → controller_test.rb} +8 -4
  34. data/test/docs/auto_inject_test.rb +30 -0
  35. data/test/docs/contract_test.rb +429 -0
  36. data/test/docs/dry_test.rb +31 -0
  37. data/test/docs/guard_test.rb +143 -0
  38. data/test/docs/nested_test.rb +117 -0
  39. data/test/docs/policy_test.rb +2 -0
  40. data/test/docs/pundit_test.rb +109 -0
  41. data/test/docs/representer_test.rb +268 -0
  42. data/test/docs/rescue_test.rb +153 -0
  43. data/test/docs/wrap_test.rb +174 -0
  44. data/test/gemfiles/Gemfile.ruby-1.9 +3 -0
  45. data/test/gemfiles/Gemfile.ruby-2.0 +12 -0
  46. data/test/gemfiles/Gemfile.ruby-2.3 +12 -0
  47. data/test/module_test.rb +22 -15
  48. data/test/operation/builder_test.rb +66 -18
  49. data/test/operation/callback_test.rb +70 -0
  50. data/test/operation/contract_test.rb +385 -15
  51. data/test/operation/dsl/callback_test.rb +18 -30
  52. data/test/operation/dsl/contract_test.rb +209 -19
  53. data/test/operation/dsl/representer_test.rb +42 -15
  54. data/test/operation/guard_test.rb +1 -147
  55. data/test/operation/model_test.rb +105 -0
  56. data/test/operation/params_test.rb +36 -0
  57. data/test/operation/persist_test.rb +44 -0
  58. data/test/operation/pipedream_test.rb +59 -0
  59. data/test/operation/pipetree_test.rb +104 -0
  60. data/test/operation/present_test.rb +24 -0
  61. data/test/operation/pundit_test.rb +104 -0
  62. data/test/{representer_test.rb → operation/representer_test.rb} +58 -42
  63. data/test/operation/resolver_test.rb +34 -70
  64. data/test/operation_test.rb +57 -189
  65. data/test/test_helper.rb +23 -3
  66. data/trailblazer.gemspec +8 -7
  67. metadata +91 -59
  68. data/gemfiles/Gemfile.rails.lock +0 -130
  69. data/gemfiles/Gemfile.reform-2.0 +0 -6
  70. data/gemfiles/Gemfile.reform-2.1 +0 -7
  71. data/lib/trailblazer/autoloading.rb +0 -15
  72. data/lib/trailblazer/endpoint.rb +0 -31
  73. data/lib/trailblazer/operation.rb +0 -175
  74. data/lib/trailblazer/operation/collection.rb +0 -6
  75. data/lib/trailblazer/operation/dispatch.rb +0 -3
  76. data/lib/trailblazer/operation/model/dsl.rb +0 -29
  77. data/lib/trailblazer/operation/model/external.rb +0 -34
  78. data/lib/trailblazer/operation/policy/guard.rb +0 -35
  79. data/lib/trailblazer/operation/uploaded_file.rb +0 -77
  80. data/test/callback_test.rb +0 -104
  81. data/test/collection_test.rb +0 -57
  82. data/test/model_test.rb +0 -148
  83. data/test/operation/external_model_test.rb +0 -71
  84. data/test/operation/policy_test.rb +0 -97
  85. data/test/operation/reject_test.rb +0 -34
  86. data/test/rollback_test.rb +0 -47
@@ -0,0 +1,70 @@
1
+ require 'test_helper'
2
+
3
+ # callbacks are tested in Disposable::Callback::Group.
4
+ class OperationCallbackTest < MiniTest::Spec
5
+ Song = Struct.new(:name)
6
+
7
+ #---
8
+ # with contract and disposable semantics
9
+ class Create < Trailblazer::Operation
10
+ extend Contract::DSL
11
+
12
+ contract do
13
+ property :name
14
+ end
15
+
16
+ self.| Model( Song, :new )
17
+ self.| Contract::Build()
18
+ self.| Contract::Validate()
19
+ self.| Callback( :default )
20
+
21
+
22
+ extend Callback::DSL
23
+
24
+ callback do
25
+ on_change :notify_me!
26
+ on_change :notify_you!
27
+ end
28
+
29
+
30
+ # TODO: always dispatch, pass params.
31
+
32
+ def dispatched
33
+ self["dispatched"] ||= []
34
+ end
35
+
36
+ private
37
+ def notify_me!(*)
38
+ dispatched << :notify_me!
39
+ end
40
+
41
+ def notify_you!(*)
42
+ dispatched << :notify_you!
43
+ end
44
+ end
45
+
46
+
47
+ class Update < Create
48
+ # TODO: allow skipping groups.
49
+ # skip_dispatch :notify_me!
50
+
51
+ callback do
52
+ remove! :on_change, :notify_me!
53
+ end
54
+ end
55
+
56
+ #---
57
+ #- inheritance
58
+ it { Update["pipetree"].inspect.must_equal %{[>>operation.new,&model.build,>contract.build,&validate.params.extract,&contract.validate,&callback.default]} }
59
+
60
+
61
+ it "invokes all callbacks" do
62
+ res = Create.({"name"=>"Keep On Running"})
63
+ res["dispatched"].must_equal [:notify_me!, :notify_you!]
64
+ end
65
+
66
+ it "does not invoke removed callbacks" do
67
+ res = Update.({"name"=>"Keep On Running"})
68
+ res["dispatched"].must_equal [:notify_you!]
69
+ end
70
+ end
@@ -1,30 +1,400 @@
1
1
  require "test_helper"
2
+ require "trailblazer/operation/contract"
2
3
 
4
+ require "dry/validation"
3
5
 
4
- class OperationContractTest < MiniTest::Spec
6
+ class DryValidationTest < Minitest::Spec
7
+ class Create < Trailblazer::Operation
8
+ extend Contract::DSL
5
9
 
6
- class Operation < Trailblazer::Operation
10
+ contract "params", (Dry::Validation.Schema do
11
+ required(:id).filled
12
+ end)
13
+ # self["contract.params"] = Dry::Validation.Schema do
14
+ # required(:id).filled
15
+ # end
16
+
17
+ self.| :process
18
+
19
+ include Procedural::Validate
20
+
21
+ def process(options)
22
+ validate(options["params"], contract: self["contract.params"], path: "contract.params") { |f| puts f.inspect }
23
+ end
24
+ end
25
+
26
+ #- result object, contract
27
+ # success
28
+ it { Create.(id: 1)["result.contract.params"].success?.must_equal true }
29
+ it { Create.(id: 1)["result.contract.params"].errors.must_equal({}) }
30
+ # failure
31
+ it { Create.(id: nil)["result.contract.params"].success?.must_equal false }
32
+ it { Create.(id: nil)["result.contract.params"].errors.must_equal({:id=>["must be filled"]}) }
33
+
34
+ #---
35
+ # with Contract::Validate, but before op even gets instantiated.
36
+ class Update < Trailblazer::Operation #["contract"]
37
+ extend Contract::DSL
38
+
39
+ contract "params", (Dry::Validation.Schema do
40
+ required(:id).filled
41
+ end)
42
+
43
+ self.& ->(options) { options["contract.params"].(options["params"]).success? }, before: "operation.new"
44
+ end
45
+
46
+ it { Update.( id: 1 ).success?.must_equal true }
47
+ it { Update.( ).success?.must_equal false }
48
+ end
49
+
50
+ class ContractTest < Minitest::Spec
51
+ Song = Struct.new(:title)
52
+ # # generic form for testing.
53
+ # class Form
54
+ # def initialize(model, options={})
55
+ # @inspect = "#{self.class}: #{model} #{options.inspect}"
56
+ # end
57
+
58
+ # def validate
59
+ # @inspect
60
+ # end
61
+ # end
62
+
63
+ #---
64
+ # contract do..end (without constant)
65
+ #- explicit validate call
66
+ describe "contract do .. end" do
67
+ class Index < Trailblazer::Operation
68
+ extend Contract::DSL
69
+
70
+ contract do
71
+ property :title
72
+ end
73
+
74
+ self.> ->(options) { options["model"] = Song.new }
75
+ # self.| Model( Song, :new )
76
+ self.| Contract::Build()
77
+ self.| :process
78
+
79
+ include Procedural::Validate
80
+ # TODO: get model automatically in validate!
81
+
82
+ def process(options)
83
+ # validate(params, model: Song.new) { |f| self["x"] = f.to_nested_hash }
84
+ validate(options["params"]) { |f| self["x"] = f.to_nested_hash }
85
+ end
86
+ end
87
+
88
+ # will create a Reform::Form for us.
89
+ it { Index.(title: "Falling Down")["x"].must_equal({"title"=>"Falling Down"}) }
90
+ end
91
+
92
+ # # TODO: in all step tests.
93
+ # describe "dependency injection" do
94
+ # class Delete < Trailblazer::Operation
95
+ # include Contract::Step
96
+ # end
97
+
98
+ # class Follow < Trailblazer::Operation
99
+ # include Contract::Step
100
+ # end
101
+
102
+ # # inject contract instance via constructor.
103
+ # it { Delete.({}, "contract" => "contract/instance")["contract"].must_equal "contract/instance" }
104
+ # # inject contract class.
105
+ # it { Follow.({}, "contract.default.class" => Form)["contract"].class.must_equal Form }
106
+ # end
107
+
108
+
109
+ # # contract(model, [admin: true]).validate
110
+ # class Create < Trailblazer::Operation
111
+ # include Test::ReturnProcess
112
+ # include Contract::Explicit
113
+
114
+ # def call(options:false)
115
+ # return contract(model: Object, options: { admin: true }).validate if options
116
+ # contract(model: Object).validate
117
+ # end
118
+ # end
119
+
120
+ # # inject class, pass in model and options when constructing.
121
+ # # contract(model)
122
+ # it { Create.({}, "contract.default.class" => Form).must_equal "ContractTest::Form: Object {}" }
123
+ # # contract(model, options)
124
+ # it { Create.({ options: true }, "contract.default.class" => Form).must_equal "ContractTest::Form: Object {:admin=>true}" }
125
+
126
+ # # ::contract Form
127
+ # # contract(model).validate
128
+ # class Update < Trailblazer::Operation
129
+ # include Test::ReturnProcess
130
+ # include Contract::Explicit
131
+
132
+ # = Form
133
+
134
+ # def call(*)
135
+ # contract.validate
136
+ # end
137
+
138
+ # include Model( :Builder
139
+ # ) def model!(*)
140
+ # Object
141
+ # end
142
+ # end
143
+
144
+ # # use the class contract.
145
+ # it { Update.().must_equal "ContractTest::Form: Object {}" }
146
+ # # injected contract overrides class.
147
+ # it { Update.({}, "contract.default.class" => Injected = Class.new(Form)).must_equal "ContractTest::Injected: Object {}" }
148
+
149
+ # # passing Constant into #contract
150
+ # # contract(Object.new, { title: "Bad Feeling" }, Contract)
151
+ # class Operation < Trailblazer::Operation
152
+ # include Contract::Explicit
153
+
154
+ # class Contract < Reform::Form
155
+ # property :title, virtual: true
156
+ # end
157
+
158
+ # def process(params)
159
+ # contract(model: Object.new, options: { title: "Bad Feeling" }, contract_class: Contract)
160
+
161
+ # validate(params)
162
+ # end
163
+ # end
164
+
165
+ # # allow using #contract to inject model, options and class.
166
+ # it do
167
+ # contract = Operation.(id: 1)["contract"]
168
+ # contract.title.must_equal "Bad Feeling"
169
+ # contract.must_be_instance_of Operation::Contract
170
+ # end
171
+
172
+ # # allow using #contract before #validate.
173
+ # class Upsert < Trailblazer::Operation
174
+ # include Contract::Explicit
175
+
176
+ # contract do
177
+ # property :id
178
+ # property :title
179
+ # property :length
180
+ # end
181
+
182
+ # def process(params)
183
+ # self["model"] = Struct.new(:id, :title, :length).new
184
+ # contract.id = 1
185
+ # validate(params) { contract.length = 3 }
186
+ # end
187
+ # end
188
+
189
+ # it do
190
+ # contract = Upsert.(title: "Beethoven")["contract"]
191
+ # contract.id.must_equal 1
192
+ # contract.title.must_equal "Beethoven"
193
+ # contract.length.must_equal 3
194
+ # end
195
+ # end
196
+
197
+ #---
198
+ #- validate
199
+ class ValidateTest < Minitest::Spec
200
+ class Create < Trailblazer::Operation
201
+ extend Contract::DSL
7
202
  contract do
8
- property :id
9
203
  property :title
10
- property :length
204
+ validates :title, presence: true
11
205
  end
12
206
 
13
- def process(params)
14
- @model = Struct.new(:id, :title, :length).new
15
207
 
16
- contract.id = 1
17
- validate(params) do
18
- contract.length = 3
208
+ include Procedural::Validate
209
+ def process(options)
210
+ if validate(options["params"])
211
+ self["x"] = "works!"
212
+ true
213
+ else
214
+ self["x"] = "try again"
215
+ false
19
216
  end
20
217
  end
218
+
219
+ self.| Model( Song, :new ) # FIXME.
220
+ self.| Contract::Build()
221
+ self.& :process
222
+ end
223
+
224
+ # validate returns the #validate result
225
+ it do
226
+ Create.(title: nil)["x"].must_equal "try again"
227
+ Create.(title: nil).success?.must_equal false
228
+ end
229
+ it { Create.(title: "SVG")["x"].must_equal "works!" }
230
+ it { Create.(title: "SVG").success?.must_equal true }
231
+
232
+ # result object from validation.
233
+ #- result object, contract
234
+ it { Create.(title: 1)["result.contract.default"].success?.must_equal true }
235
+ it { Create.(title: 1)["result.contract.default"].errors.messages.must_equal({}) } # FIXME: change API with Fran.
236
+ it { Create.(title: nil)["result.contract.default"].success?.must_equal false }
237
+ it { Create.(title: nil)["result.contract.default"].errors.messages.must_equal({:title=>["can't be blank"]}) } # FIXME: change API with Fran.
238
+ # #---
239
+ # # validate with block returns result.
240
+ # class Update < Trailblazer::Operation
241
+ # include Contract::Explicit
242
+ # contract Form
243
+
244
+ # def process(params)
245
+ # self["x"] = validate(params) { }
246
+ # end
247
+ # end
248
+
249
+ # it { Update.(false)["x"].must_equal false}
250
+ # it { Update.(true)["x"]. must_equal true}
251
+
252
+ #---
253
+ # Contract::Validate[]
254
+ class Update < Trailblazer::Operation
255
+ extend Contract::DSL
256
+ contract do
257
+ property :title
258
+ validates :title, presence: true
259
+ end
260
+
261
+ self.| Model( Song, :new ) # FIXME.
262
+ self.| Contract::Build()
263
+ self.| Contract::Validate() # generic validate call for you.
264
+
265
+ # include Procedural::Validate
266
+ ->(*) { validate(options["params"][:song]) } # <-- TODO
21
267
  end
22
268
 
23
- # allow using #contract before #validate.
269
+ # success
24
270
  it do
25
- op = Operation.(title: "Beethoven")
26
- op.contract.id.must_equal 1
27
- op.contract.title.must_equal "Beethoven"
28
- op.contract.length.must_equal 3
271
+ result = Update.(title: "SVG")
272
+ result.success?.must_equal true
273
+ result["result.contract.default"].success?.must_equal true
274
+ result["result.contract.default"].errors.messages.must_equal({})
275
+ end
276
+
277
+ # failure
278
+ it do
279
+ result = Update.(title: nil)
280
+ result.success?.must_equal false
281
+ result["result.contract.default"].success?.must_equal false
282
+ result["result.contract.default"].errors.messages.must_equal({:title=>["can't be blank"]})
283
+ end
284
+
285
+ #---
286
+ # Contract::Validate[key: :song]
287
+ class Upsert < Trailblazer::Operation
288
+ extend Contract::DSL
289
+ contract do
290
+ property :title
291
+ validates :title, presence: true
292
+ end
293
+
294
+ self.| Model( Song, :new ) # FIXME.
295
+ self.| Contract::Build()
296
+ self.| Contract::Validate( key: :song) # generic validate call for you.
297
+ # ->(*) { validate(options["params"][:song]) } # <-- TODO
298
+ end
299
+
300
+ # success
301
+ it { Upsert.(song: { title: "SVG" }).success?.must_equal true }
302
+ # failure
303
+ it { Upsert.(song: { title: nil }).success?.must_equal false }
304
+ # key not found
305
+ it { Upsert.().success?.must_equal false }
306
+
307
+ #---
308
+ # params.validate gets set (TODO: change in 2.1)
309
+ it { Upsert.(song: { title: "SVG" })["params"].must_equal({:song=>{:title=>"SVG"}}) }
310
+ it { Upsert.(song: { title: "SVG" })["params.validate"].must_equal({:title=>"SVG"}) }
311
+
312
+ #---
313
+ #- inheritance
314
+ class New < Upsert
29
315
  end
30
- end
316
+
317
+ it { New["pipetree"].inspect.must_equal %{[>>operation.new,&model.build,>contract.build,&validate.params.extract,&contract.validate]} }
318
+ end
319
+
320
+ # #---
321
+ # # allow using #contract to inject model and arguments.
322
+ # class OperationContractWithOptionsTest < Minitest::Spec
323
+ # # contract(model, title: "Bad Feeling")
324
+ # class Operation < Trailblazer::Operation
325
+ # include Contract::Explicit
326
+ # contract do
327
+ # property :id
328
+ # property :title, virtual: true
329
+ # end
330
+
331
+ # def process(params)
332
+ # model = Struct.new(:id).new
333
+
334
+ # contract(model: model, options: { title: "Bad Feeling" })
335
+
336
+ # validate(params)
337
+ # end
338
+ # end
339
+
340
+ # it do
341
+ # op = Operation.(id: 1)
342
+ # op["contract"].id.must_equal 1
343
+ # op["contract"].title.must_equal "Bad Feeling"
344
+ # end
345
+
346
+ # # contract({ song: song, album: album }, title: "Medicine Balls")
347
+ # class CompositionOperation < Trailblazer::Operation
348
+ # include Contract::Explicit
349
+ # contract do
350
+ # include Reform::Form::Composition
351
+ # property :song_id, on: :song
352
+ # property :album_name, on: :album
353
+ # property :title, virtual: true
354
+ # end
355
+
356
+ # def process(params)
357
+ # song = Struct.new(:song_id).new(1)
358
+ # album = Struct.new(:album_name).new("Forever Malcom Young")
359
+
360
+ # contract(model: { song: song, album: album }, options: { title: "Medicine Balls" })
361
+
362
+ # validate(params)
363
+ # end
364
+ # end
365
+
366
+ # it do
367
+ # contract = CompositionOperation.({})["contract"]
368
+ # contract.song_id.must_equal 1
369
+ # contract.album_name.must_equal "Forever Malcom Young"
370
+ # contract.title.must_equal "Medicine Balls"
371
+ # end
372
+
373
+ # # validate(params, { song: song, album: album }, title: "Medicine Balls")
374
+ # class CompositionValidateOperation < Trailblazer::Operation
375
+ # include Contract::Explicit
376
+ # contract do
377
+ # include Reform::Form::Composition
378
+ # property :song_id, on: :song
379
+ # property :album_name, on: :album
380
+ # property :title, virtual: true
381
+ # end
382
+
383
+ # def process(params)
384
+ # song = Struct.new(:song_id).new(1)
385
+ # album = Struct.new(:album_name).new("Forever Malcom Young")
386
+
387
+ # validate(params, model: { song: song, album: album }, options: { title: "Medicine Balls" })
388
+ # end
389
+ # end
390
+
391
+ # it do
392
+ # contract = CompositionValidateOperation.({})["contract"]
393
+ # contract.song_id.must_equal 1
394
+ # contract.album_name.must_equal "Forever Malcom Young"
395
+ # contract.title.must_equal "Medicine Balls"
396
+ # end
397
+ end
398
+
399
+ # TODO: full stack test with validate, process, save, etc.
400
+ # with model!