trailblazer 1.1.2 → 2.0.0.beta1

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