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,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
+