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,23 @@
1
+ class Trailblazer::Operation
2
+ module Rescue
3
+ def self.import!(_operation, import, *exceptions, handler:->(*){}, &block)
4
+ exceptions = [StandardError] unless exceptions.any?
5
+ handler = Pipetree::DSL::Option.(handler)
6
+
7
+ rescue_block = ->(options, operation, *, &nested_pipe) {
8
+ begin
9
+ res = nested_pipe.call
10
+ res.first == ::Pipetree::Flow::Right # FIXME.
11
+ rescue *exceptions => exception
12
+ handler.call(operation, exception, options)
13
+ false
14
+ end
15
+ }
16
+
17
+ Wrap.import! _operation, import, rescue_block, name: "Rescue:#{block.source_location.last}", &block
18
+ end
19
+ end
20
+
21
+ DSL.macro!(:Rescue, Rescue)
22
+ end
23
+
@@ -1,30 +1,24 @@
1
- require "trailblazer/operation/model/external"
2
- require "trailblazer/operation/policy"
3
-
4
1
  class Trailblazer::Operation
5
- # Provides builds-> (model, policy, params).
6
2
  module Resolver
7
- def self.included(includer)
8
- includer.class_eval do
9
- include Policy # ::build_policy
10
- include Model::External # ::build_operation_class
11
-
12
- extend BuildOperation # ::build_operation
13
- end
3
+ def self.import!(operation, import)
4
+ operation.extend Model::BuildMethods
5
+ operation.| operation.Builder(operation.builders)
14
6
  end
15
7
 
16
- module BuildOperation
17
- def build_operation(params, options={})
18
- model = model!(params)
19
- policy = policy_config.call(params[:current_user], model)
20
- build_operation_class(model, policy, params).
21
- new(params, options.merge(model: model, policy: policy))
22
- end
23
- end
8
+ # def self.included(includer)
9
+ # includer.class_eval do
10
+ # extend Model::DSL # ::model
11
+ # extend Model::BuildMethods # ::model!
12
+ # extend Policy::DSL # ::policy
13
+ # extend Policy::BuildPermission
14
+ # end
24
15
 
25
- def initialize(params, options)
26
- @policy = options[:policy]
27
- super
28
- end
16
+ # includer.> Model::Build, prepend: true
17
+ # includer.& Policy::Evaluate, after: Model::Build
18
+ # end
19
+ end
20
+
21
+ def self.Resolver(*args, &block)
22
+ [ Resolver, args, block ]
29
23
  end
30
- end
24
+ end
@@ -0,0 +1,50 @@
1
+ module Trailblazer::Operation::Contract
2
+ # result.contract = {..}
3
+ # result.contract.errors = {..}
4
+ # Deviate to left track if optional key is not found in params.
5
+ # Deviate to left if validation result falsey.
6
+ module Validate
7
+ def self.import!(operation, import, skip_extract:false, name: "default", representer:false, **args) # DISCUSS: should we introduce something like Validate::Deserializer?
8
+ if representer
9
+ skip_extract = true
10
+ operation["representer.#{name}.class"] = representer
11
+ end
12
+
13
+ import.(:&, ->(input, options) { extract_params!(input, options, **args) },
14
+ name: "validate.params.extract") unless skip_extract
15
+
16
+ # call the actual contract.validate(params)
17
+ # DISCUSS: should we pass the representer here, or do that in #validate! i'm still mulling over what's the best, most generic approach.
18
+ import.(:&, ->(operation, options) { validate!(operation, options, name: name, representer: options["representer.#{name}.class"], **args) },
19
+ name: "contract.validate")
20
+ end
21
+
22
+ def self.extract_params!(operation, options, key:nil, **)
23
+ # TODO: introduce nested pipes and pass composed input instead.
24
+ options["params.validate"] = key ? options["params"][key] : options["params"]
25
+ end
26
+
27
+ def self.validate!(operation, options, name: nil, representer:false, from: "document", **)
28
+ path = "contract.#{name}"
29
+ contract = operation[path]
30
+
31
+ # this is for 1.1-style compatibility and should be removed once we have Deserializer in place:
32
+ operation["result.#{path}"] = result =
33
+ if representer
34
+ # use "document" as the body and let the representer deserialize to the contract.
35
+ # this will be simplified once we have Deserializer.
36
+ # translates to contract.("{document: bla}") { MyRepresenter.new(contract).from_json .. }
37
+ contract.(options[from]) { |document| representer.new(contract).parse(document) }
38
+ else
39
+ # let Reform handle the deserialization.
40
+ contract.(options["params.validate"])
41
+ end
42
+
43
+ result.success?
44
+ end
45
+ end
46
+
47
+ def self.Validate(*args, &block)
48
+ [ Validate, args, block ]
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ class Trailblazer::Operation
2
+ module Wrap
3
+ def self.import!(operation, import, wrap, _options={}, &block)
4
+ pipe_api = API.new(operation, pipe = ::Pipetree::Flow[])
5
+
6
+ # DISCUSS: don't instance_exec when |pipe| given?
7
+ # yield pipe_api # create the nested pipe.
8
+ pipe_api.instance_exec(&block) # evaluate the nested pipe. this gets written onto `pipe`.
9
+
10
+ import.(:&, ->(input, options) { wrap.(options, input, pipe, & ->{ pipe.(input, options) }) }, _options)
11
+ end
12
+
13
+ class API
14
+ include Pipetree::DSL
15
+ include Pipetree::DSL::Macros
16
+
17
+ def initialize(target, pipe)
18
+ @target, @pipe = target, pipe
19
+ end
20
+
21
+ def _insert(operator, proc, options={}) # TODO: test me.
22
+ Pipetree::DSL.insert(@pipe, operator, proc, options, definer_name: @target.name)
23
+ end
24
+
25
+ def |(cfg, user_options={})
26
+ Pipetree::DSL.import(@target, @pipe, cfg, user_options)
27
+ end
28
+ alias_method :step, :| # DISCUSS: uhm...
29
+ end
30
+ end # Wrap
31
+
32
+ DSL.macro!(:Wrap, Wrap)
33
+ end
34
+
35
+
36
+ # (options, *) => (options, operation, bla)
37
+ # (*, params:, **) => (options, operation, bla, options)
@@ -1,3 +1,3 @@
1
1
  module Trailblazer
2
- VERSION = "1.1.2"
2
+ VERSION = "2.0.0.beta1"
3
3
  end
@@ -28,12 +28,15 @@ class ControllerTest < Minitest::Spec
28
28
  Comment = Struct.new(:body)
29
29
 
30
30
  class Comment::Update < Trailblazer::Operation
31
+ include Contract
32
+ include Present
33
+
31
34
  def model!(params)
32
35
  Comment.new(params[:body])
33
36
  end
34
37
 
35
38
  def inspect
36
- super.sub(/:0x\w+/, "")
39
+ "<Update: #{@params.inspect} #{self["model"].inspect}>"
37
40
  end
38
41
  end
39
42
 
@@ -46,7 +49,7 @@ class ControllerTest < Minitest::Spec
46
49
  end
47
50
 
48
51
  it do
49
- controller.show.inspect.must_equal "#<ControllerTest::Comment::Update @options={}, @valid=true, @params={:current_user=>#<struct ControllerTest::User role=:admin>}, @model=#<struct ControllerTest::Comment body=nil>>"
52
+ controller.show.inspect.must_equal "<Update: {:current_user=>#<struct ControllerTest::User role=:admin>} #<struct ControllerTest::Comment body=nil>>"
50
53
  end
51
54
  end
52
55
 
@@ -61,7 +64,7 @@ class ControllerTest < Minitest::Spec
61
64
  end
62
65
  end
63
66
 
64
- it { controller.show.inspect.must_equal "#<ControllerTest::Comment::Update @options={}, @valid=true, @params={:body=>\"Cool!\"}, @model=#<struct ControllerTest::Comment body=\"Cool!\">>" }
67
+ it { controller.show.inspect.must_equal "<Update: {:body=>\"Cool!\"} #<struct ControllerTest::Comment body=\"Cool!\">>" }
65
68
  end
66
69
 
67
70
  describe "#form" do
@@ -70,6 +73,7 @@ class ControllerTest < Minitest::Spec
70
73
  Comment.new
71
74
  end
72
75
 
76
+ include Contract
73
77
  contract do
74
78
  def prepopulate!(options)
75
79
  @options = options
@@ -108,4 +112,4 @@ class ControllerTest < Minitest::Spec
108
112
  it { controller(__body: "Great!").show.options.inspect.must_equal "{:admin=>true, :params=>{:__body=>\"Great!\", :user=>#<struct ControllerTest::User role=nil>}}" }
109
113
  end
110
114
  end
111
- end
115
+ end
@@ -0,0 +1,30 @@
1
+ require "test_helper"
2
+ require "dry/container"
3
+ require "trailblazer/operation/auto_inject"
4
+
5
+ class DryAutoInjectTest < Minitest::Spec
6
+ my_container = Dry::Container.new
7
+ my_container.register(:user_repository, -> { Object })
8
+
9
+ AutoInject = Trailblazer::Operation::AutoInject(my_container)
10
+
11
+ class Create < Trailblazer::Operation
12
+ include AutoInject[:user_repository]
13
+ end
14
+
15
+ it "auto-injects user_repository" do
16
+ res = Create.({})
17
+ res[:user_repository].must_equal Object
18
+ end
19
+
20
+ it "allows dependency injection via ::call" do
21
+ Create.({}, user_repository: String)[:user_repository].must_equal String
22
+ end
23
+
24
+ describe "inheritance" do
25
+ class Update < Create
26
+ end
27
+
28
+ it { Update.()[:user_repository].must_equal Object }
29
+ end
30
+ end
@@ -0,0 +1,429 @@
1
+ require "test_helper"
2
+
3
+ class DocsContractOverviewTest < Minitest::Spec
4
+ Song = Struct.new(:length, :title)
5
+
6
+ #:overv-reform
7
+ # app/concepts/comment/create.rb
8
+ class Create < Trailblazer::Operation
9
+ #~bla
10
+ extend Contract::DSL
11
+
12
+ contract do
13
+ property :title
14
+ #~contractonly
15
+ property :length
16
+
17
+ validates :title, presence: true
18
+ validates :length, numericality: true
19
+ #~contractonly end
20
+ end
21
+ #~contractonly
22
+
23
+ #~bla end
24
+ step Model( Song, :new )
25
+ step Contract::Build()
26
+ step Contract::Validate()
27
+ step Persist( method: :sync )
28
+ #~contractonly end
29
+ end
30
+ #:overv-reform end
31
+
32
+ puts Create["pipetree"].inspect(style: :rows)
33
+
34
+ =begin
35
+ #:overv-reform-pipe
36
+ 0 =======================>>operation.new
37
+ 1 ==========================&model.build
38
+ 2 =======================>contract.build
39
+ 3 ==============&validate.params.extract
40
+ 4 ====================&contract.validate
41
+ 5 =========================&persist.save
42
+ #:overv-reform-pipe end
43
+ =end
44
+
45
+ it do
46
+ assert Create["contract.default.class"] < Reform::Form
47
+ end
48
+
49
+ #- result
50
+ it do
51
+ #:result
52
+ result = Create.({ length: "A" })
53
+
54
+ result["result.contract.default"].success? #=> false
55
+ result["result.contract.default"].errors #=> Errors object
56
+ result["result.contract.default"].errors.messages #=> {:length=>["is not a number"]}
57
+
58
+ #:result end
59
+ result["result.contract.default"].success?.must_equal false
60
+ result["result.contract.default"].errors.messages.must_equal ({:title=>["can't be blank"], :length=>["is not a number"]})
61
+ end
62
+ end
63
+
64
+ class DocsContractNameTest < Minitest::Spec
65
+ Song = Struct.new(:length, :title)
66
+
67
+ #:contract-name
68
+ # app/concepts/comment/update.rb
69
+ class Update < Trailblazer::Operation
70
+ #~contract
71
+ extend Contract::DSL
72
+
73
+ contract :params do
74
+ property :id
75
+ validates :id, presence: true
76
+ end
77
+ #~contract end
78
+ #~pipe
79
+ step Model( Song, :new )
80
+ step Contract::Build( name: "params" )
81
+ step Contract::Validate( name: "params" )
82
+ #~pipe end
83
+ end
84
+ #:contract-name end
85
+ end
86
+
87
+ class DocsContractReferenceTest < Minitest::Spec
88
+ MyContract = Class.new
89
+ #:contract-ref
90
+ # app/concepts/comment/update.rb
91
+ class Update < Trailblazer::Operation
92
+ #~contract
93
+ extend Contract::DSL
94
+ contract :user, MyContract
95
+ end
96
+ #:contract-ref end
97
+ end
98
+
99
+ #---
100
+ # contract MyContract
101
+ class DocsContractExplicitTest < Minitest::Spec
102
+ Song = Struct.new(:length, :title)
103
+
104
+ #:reform-inline
105
+ class MyContract < Reform::Form
106
+ property :title
107
+ property :length
108
+
109
+ validates :title, presence: true
110
+ validates :length, numericality: true
111
+ end
112
+ #:reform-inline end
113
+
114
+ #:reform-inline-op
115
+ # app/concepts/comment/create.rb
116
+ class Create < Trailblazer::Operation
117
+ extend Contract::DSL
118
+
119
+ contract MyContract
120
+
121
+ step Model( Song, :new )
122
+ step Contract::Build()
123
+ step Contract::Validate()
124
+ step Persist( method: :sync )
125
+ end
126
+ #:reform-inline-op end
127
+ end
128
+
129
+ #---
130
+ #- Validate[key: :song]
131
+ class DocsContractKeyTest < Minitest::Spec
132
+ Song = Struct.new(:id, :title)
133
+ #:key
134
+ class Create < Trailblazer::Operation
135
+ extend Contract::DSL
136
+
137
+ contract do
138
+ property :title
139
+ end
140
+
141
+ step Model( Song, :new )
142
+ step Contract::Build()
143
+ step Contract::Validate( key: "song" )
144
+ step Persist( method: :sync )
145
+ end
146
+ #:key end
147
+
148
+ it { Create.({}).inspect("model").must_equal %{<Result:false [#<struct DocsContractKeyTest::Song id=nil, title=nil>] >} }
149
+ it { Create.({"song" => { title: "SVG" }}).inspect("model").must_equal %{<Result:true [#<struct DocsContractKeyTest::Song id=nil, title="SVG">] >} }
150
+ end
151
+
152
+ #- Validate with manual key extraction
153
+ class DocsContractSeparateKeyTest < Minitest::Spec
154
+ Song = Struct.new(:id, :title)
155
+ #:key-extr
156
+ class Create < Trailblazer::Operation
157
+ extend Contract::DSL
158
+
159
+ contract do
160
+ property :title
161
+ end
162
+
163
+ def type
164
+ "evergreen" # this is how you could do polymorphic lookups.
165
+ end
166
+
167
+ step Model( Song, :new )
168
+ step Contract::Build()
169
+ consider :extract_params!
170
+ step Contract::Validate( skip_extract: true )
171
+ step Persist( method: :sync )
172
+
173
+ def extract_params!(options)
174
+ options["params.validate"] = options["params"][type]
175
+ end
176
+ end
177
+ #:key-extr end
178
+
179
+ it { Create.({ }).inspect("model").must_equal %{<Result:false [#<struct DocsContractSeparateKeyTest::Song id=nil, title=nil>] >} }
180
+ it { Create.({"evergreen" => { title: "SVG" }}).inspect("model").must_equal %{<Result:true [#<struct DocsContractSeparateKeyTest::Song id=nil, title="SVG">] >} }
181
+ end
182
+
183
+ #---
184
+ #- Contract::Build[ constant: XXX ]
185
+ class ContractConstantTest < Minitest::Spec
186
+ Song = Struct.new(:id, :title)
187
+ #:constant
188
+ class Create < Trailblazer::Operation
189
+ class MyContract < Reform::Form
190
+ property :title
191
+ validates :title, length: 2..33
192
+ end
193
+
194
+
195
+
196
+ step Model( Song, :new )
197
+ step Contract::Build( constant: MyContract )
198
+ step Contract::Validate()
199
+ step Persist( method: :sync )
200
+ end
201
+ #:constant end
202
+
203
+ it { Create.({ title: "A" }).inspect("model").must_equal %{<Result:false [#<struct ContractConstantTest::Song id=nil, title=nil>] >} }
204
+ it { Create.({ title: "Anthony's Song" }).inspect("model").must_equal %{<Result:true [#<struct ContractConstantTest::Song id=nil, title="Anthony's Song">] >} }
205
+ end
206
+
207
+ #- Contract::Build[ constant: XXX, name: AAA ]
208
+ class ContractNamedConstantTest < Minitest::Spec
209
+ Song = Struct.new(:id, :title)
210
+ #:constant-name
211
+ class Create < Trailblazer::Operation
212
+ class MyContract < Reform::Form
213
+ property :title
214
+ validates :title, length: 2..33
215
+ end
216
+
217
+ step Model( Song, :new )
218
+ step Contract::Build( constant: MyContract, name: "form" )
219
+ step Contract::Validate( name: "form" )
220
+ step Persist( method: :sync, name: "contract.form" )
221
+ end
222
+ #:constant-name end
223
+
224
+ it { Create.({ title: "A" }).inspect("model").must_equal %{<Result:false [#<struct ContractNamedConstantTest::Song id=nil, title=nil>] >} }
225
+ it { Create.({ title: "Anthony's Song" }).inspect("model").must_equal %{<Result:true [#<struct ContractNamedConstantTest::Song id=nil, title="Anthony's Song">] >} }
226
+ end
227
+
228
+ #---
229
+ #- dependency injection
230
+ #- contract class
231
+ class ContractInjectConstantTest < Minitest::Spec
232
+ Song = Struct.new(:id, :title)
233
+ #:di-constant-contract
234
+ class MyContract < Reform::Form
235
+ property :title
236
+ validates :title, length: 2..33
237
+ end
238
+ #:di-constant-contract end
239
+ #:di-constant
240
+ class Create < Trailblazer::Operation
241
+ step Model( Song, :new )
242
+ step Contract::Build()
243
+ step Contract::Validate()
244
+ step Persist( method: :sync )
245
+ end
246
+ #:di-constant end
247
+
248
+ it do
249
+ #:di-contract-call
250
+ Create.(
251
+ { title: "Anthony's Song" },
252
+ "contract.default.class" => MyContract
253
+ )
254
+ #:di-contract-call end
255
+ end
256
+ it { Create.({ title: "A" }, "contract.default.class" => MyContract).inspect("model").must_equal %{<Result:false [#<struct ContractInjectConstantTest::Song id=nil, title=nil>] >} }
257
+ it { Create.({ title: "Anthony's Song" }, "contract.default.class" => MyContract).inspect("model").must_equal %{<Result:true [#<struct ContractInjectConstantTest::Song id=nil, title="Anthony's Song">] >} }
258
+ end
259
+
260
+ class DryValidationContractTest < Minitest::Spec
261
+ Song = Struct.new(:id, :title)
262
+ #---
263
+ # DRY-validation params validation before op,
264
+ # plus main contract.
265
+ #- result.path
266
+ #:dry-schema
267
+ require "dry/validation"
268
+ class Create < Trailblazer::Operation
269
+ extend Contract::DSL
270
+
271
+ # contract to verify params formally.
272
+ contract "params", (Dry::Validation.Schema do
273
+ required(:id).filled
274
+ end)
275
+ #~form
276
+ # domain validations.
277
+ contract "form" do
278
+ property :title
279
+ validates :title, length: 1..3
280
+ end
281
+ #~form end
282
+
283
+ step Contract::Validate( name: "params" )
284
+ #~form
285
+ step Model( Song, :new ) # create the op's main model.
286
+ step Contract::Build( name: "form" ) # create the Reform contract.
287
+ step Contract::Validate( name: "form" ) # validate the Reform contract.
288
+ step Persist( method: :sync, name: "contract.form" ) # persist the contract's data via the model.
289
+ #~form end
290
+ end
291
+ #:dry-schema end
292
+
293
+ it { Create.({}).inspect("model", "result.contract.params").must_equal %{<Result:false [nil, #<Dry::Validation::Result output={} errors={:id=>[\"is missing\"]}>] >} }
294
+ it { Create.({ id: 1 }).inspect("model", "result.contract.params").must_equal %{<Result:false [#<struct DryValidationContractTest::Song id=nil, title=nil>, #<Dry::Validation::Result output={:id=>1} errors={}>] >} }
295
+ # it { Create.({ id: 1, title: "" }).inspect("model", "result.contract.form").must_equal %{<Result:false [#<struct DryValidationContractTest::Song id=nil, title=nil>] >} }
296
+ it { Create.({ id: 1, title: "" }).inspect("model").must_equal %{<Result:false [#<struct DryValidationContractTest::Song id=nil, title=nil>] >} }
297
+ it { Create.({ id: 1, title: "Yo" }).inspect("model").must_equal %{<Result:true [#<struct DryValidationContractTest::Song id=nil, title="Yo">] >} }
298
+
299
+ #:dry-schema-first
300
+ require "dry/validation"
301
+ class Delete < Trailblazer::Operation
302
+ extend Contract::DSL
303
+
304
+ contract "params", (Dry::Validation.Schema do
305
+ required(:id).filled
306
+ end)
307
+
308
+ step Contract::Validate( name: "params" ), before: "operation.new"
309
+ #~more
310
+ #~more end
311
+ end
312
+ #:dry-schema-first end
313
+ end
314
+
315
+ class DryExplicitSchemaTest < Minitest::Spec
316
+ #:dry-schema-explsch
317
+ # app/concepts/comment/contract/params.rb
318
+ require "dry/validation"
319
+ MySchema = Dry::Validation.Schema do
320
+ required(:id).filled
321
+ end
322
+ #:dry-schema-explsch end
323
+
324
+ #:dry-schema-expl
325
+ # app/concepts/comment/delete.rb
326
+ class Delete < Trailblazer::Operation
327
+ extend Contract::DSL
328
+ contract "params", MySchema
329
+
330
+ step Contract::Validate( name: "params" ), before: "operation.new"
331
+ end
332
+ #:dry-schema-expl end
333
+ end
334
+
335
+ class DocContractBuilderTest < Minitest::Spec
336
+ Song = Struct.new(:id, :title)
337
+ #---
338
+ #- builder:
339
+ #:builder-option
340
+ class Create < Trailblazer::Operation
341
+ extend Contract::DSL
342
+
343
+ contract do
344
+ property :title
345
+ property :current_user, virtual: true
346
+ validates :current_user, presence: true
347
+ end
348
+
349
+ step Model( Song, :new )
350
+ step Contract::Build( builder: :default_contract! )
351
+ step Contract::Validate()
352
+ step Persist( method: :sync )
353
+
354
+ def default_contract!(constant:, model:)
355
+ constant.new(model, current_user: self["current_user"])
356
+ end
357
+ end
358
+ #:builder-option end
359
+
360
+ it { Create.({}).inspect("model").must_equal %{<Result:false [#<struct DocContractBuilderTest::Song id=nil, title=nil>] >} }
361
+ it { Create.({ title: 1}, "current_user" => Module).inspect("model").must_equal %{<Result:true [#<struct DocContractBuilderTest::Song id=nil, title=1>] >} }
362
+
363
+ #- proc
364
+ class Update < Trailblazer::Operation
365
+ extend Contract::DSL
366
+
367
+ contract do
368
+ property :title
369
+ property :current_user, virtual: true
370
+ validates :current_user, presence: true
371
+ end
372
+
373
+ step Model( Song, :new )
374
+ #:builder-proc
375
+ step Contract::Build( builder: ->(operation, constant:, model:) {
376
+ constant.new(model, current_user: operation["current_user"])
377
+ })
378
+ #:builder-proc end
379
+ step Contract::Validate()
380
+ step Persist( method: :sync )
381
+ end
382
+
383
+ it { Update.({}).inspect("model").must_equal %{<Result:false [#<struct DocContractBuilderTest::Song id=nil, title=nil>] >} }
384
+ it { Update.({ title: 1}, "current_user" => Module).inspect("model").must_equal %{<Result:true [#<struct DocContractBuilderTest::Song id=nil, title=1>] >} }
385
+ end
386
+
387
+ class DocContractTest < Minitest::Spec
388
+ Song = Struct.new(:id, :title)
389
+ #---
390
+ # with contract block, and inheritance, the old way.
391
+ class Block < Trailblazer::Operation
392
+ extend Contract::DSL
393
+ contract do
394
+ property :title
395
+ end
396
+
397
+ step Model( Song, :new )
398
+ step Contract::Build() # resolves to "contract.class.default" and is resolved at runtime.
399
+ step Contract::Validate()
400
+ step Persist( method: :sync )
401
+ end
402
+
403
+ it { Block.({}).inspect("model").must_equal %{<Result:true [#<struct DocContractTest::Song id=nil, title=nil>] >} }
404
+ it { Block.({ id:1, title: "Fame" }).inspect("model").must_equal %{<Result:true [#<struct DocContractTest::Song id=nil, title="Fame">] >} }
405
+
406
+ class Breach < Block
407
+ contract do
408
+ property :id
409
+ end
410
+ end
411
+
412
+ it { Breach.({ id:1, title: "Fame" }).inspect("model").must_equal %{<Result:true [#<struct DocContractTest::Song id=1, title="Fame">] >} }
413
+
414
+ #-
415
+ # with constant.
416
+ class Break < Block
417
+ class MyContract < Reform::Form
418
+ property :id
419
+ end
420
+ # override the original block as if it's never been there.
421
+ contract MyContract
422
+ end
423
+
424
+ it { Break.({ id:1, title: "Fame" }).inspect("model").must_equal %{<Result:true [#<struct DocContractTest::Song id=1, title=nil>] >} }
425
+ end
426
+
427
+
428
+
429
+