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.
- checksums.yaml +4 -4
- data/.travis.yml +10 -7
- data/CHANGES.md +108 -0
- data/COMM-LICENSE +91 -0
- data/Gemfile +18 -4
- data/LICENSE.txt +7 -20
- data/README.md +55 -15
- data/Rakefile +21 -2
- data/draft-1.2.rb +7 -0
- data/lib/trailblazer.rb +17 -4
- data/lib/trailblazer/dsl.rb +47 -0
- data/lib/trailblazer/operation/auto_inject.rb +47 -0
- data/lib/trailblazer/operation/builder.rb +18 -18
- data/lib/trailblazer/operation/callback.rb +31 -38
- data/lib/trailblazer/operation/contract.rb +46 -0
- data/lib/trailblazer/operation/controller.rb +45 -27
- data/lib/trailblazer/operation/guard.rb +24 -0
- data/lib/trailblazer/operation/model.rb +41 -33
- data/lib/trailblazer/operation/nested.rb +43 -0
- data/lib/trailblazer/operation/params.rb +13 -0
- data/lib/trailblazer/operation/persist.rb +13 -0
- data/lib/trailblazer/operation/policy.rb +26 -72
- data/lib/trailblazer/operation/present.rb +19 -0
- data/lib/trailblazer/operation/procedural/contract.rb +15 -0
- data/lib/trailblazer/operation/procedural/validate.rb +22 -0
- data/lib/trailblazer/operation/pundit.rb +42 -0
- data/lib/trailblazer/operation/representer.rb +25 -92
- data/lib/trailblazer/operation/rescue.rb +23 -0
- data/lib/trailblazer/operation/resolver.rb +18 -24
- data/lib/trailblazer/operation/validate.rb +50 -0
- data/lib/trailblazer/operation/wrap.rb +37 -0
- data/lib/trailblazer/version.rb +1 -1
- data/test/{operation/controller_test.rb → controller_test.rb} +8 -4
- data/test/docs/auto_inject_test.rb +30 -0
- data/test/docs/contract_test.rb +429 -0
- data/test/docs/dry_test.rb +31 -0
- data/test/docs/guard_test.rb +143 -0
- data/test/docs/nested_test.rb +117 -0
- data/test/docs/policy_test.rb +2 -0
- data/test/docs/pundit_test.rb +109 -0
- data/test/docs/representer_test.rb +268 -0
- data/test/docs/rescue_test.rb +153 -0
- data/test/docs/wrap_test.rb +174 -0
- data/test/gemfiles/Gemfile.ruby-1.9 +3 -0
- data/test/gemfiles/Gemfile.ruby-2.0 +12 -0
- data/test/gemfiles/Gemfile.ruby-2.3 +12 -0
- data/test/module_test.rb +22 -15
- data/test/operation/builder_test.rb +66 -18
- data/test/operation/callback_test.rb +70 -0
- data/test/operation/contract_test.rb +385 -15
- data/test/operation/dsl/callback_test.rb +18 -30
- data/test/operation/dsl/contract_test.rb +209 -19
- data/test/operation/dsl/representer_test.rb +42 -15
- data/test/operation/guard_test.rb +1 -147
- data/test/operation/model_test.rb +105 -0
- data/test/operation/params_test.rb +36 -0
- data/test/operation/persist_test.rb +44 -0
- data/test/operation/pipedream_test.rb +59 -0
- data/test/operation/pipetree_test.rb +104 -0
- data/test/operation/present_test.rb +24 -0
- data/test/operation/pundit_test.rb +104 -0
- data/test/{representer_test.rb → operation/representer_test.rb} +58 -42
- data/test/operation/resolver_test.rb +34 -70
- data/test/operation_test.rb +57 -189
- data/test/test_helper.rb +23 -3
- data/trailblazer.gemspec +8 -7
- metadata +91 -59
- data/gemfiles/Gemfile.rails.lock +0 -130
- data/gemfiles/Gemfile.reform-2.0 +0 -6
- data/gemfiles/Gemfile.reform-2.1 +0 -7
- data/lib/trailblazer/autoloading.rb +0 -15
- data/lib/trailblazer/endpoint.rb +0 -31
- data/lib/trailblazer/operation.rb +0 -175
- data/lib/trailblazer/operation/collection.rb +0 -6
- data/lib/trailblazer/operation/dispatch.rb +0 -3
- data/lib/trailblazer/operation/model/dsl.rb +0 -29
- data/lib/trailblazer/operation/model/external.rb +0 -34
- data/lib/trailblazer/operation/policy/guard.rb +0 -35
- data/lib/trailblazer/operation/uploaded_file.rb +0 -77
- data/test/callback_test.rb +0 -104
- data/test/collection_test.rb +0 -57
- data/test/model_test.rb +0 -148
- data/test/operation/external_model_test.rb +0 -71
- data/test/operation/policy_test.rb +0 -97
- data/test/operation/reject_test.rb +0 -34
- 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
|
6
|
+
class DryValidationTest < Minitest::Spec
|
7
|
+
class Create < Trailblazer::Operation
|
8
|
+
extend Contract::DSL
|
5
9
|
|
6
|
-
|
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
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
-
#
|
269
|
+
# success
|
24
270
|
it do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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!
|