trailblazer-macro-contract 2.1.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in trailblazer.gemspec
4
+ gemspec
5
+
6
+ gem "reform-rails"
7
+ gem "activesupport"
8
+
9
+ gem "reform"
10
+ gem "multi_json"
11
+
12
+ gem "dry-auto_inject"
13
+ gem "dry-matcher"
14
+ gem "dry-validation"
15
+
16
+ # gem "trailblazer-operation", github: "trailblazer/trailblazer-operation"
17
+ # gem "trailblazer-macro", github: "trailblazer/trailblazer-macro"
18
+
19
+ gem "minitest-line"
20
+
21
+ gem "rubocop", require: false
@@ -0,0 +1,9 @@
1
+ Copyright (c) Nick Sutterer
2
+
3
+ Trailblazer is an Open Source project licensed under the terms of
4
+ the LGPLv3 license. Please see <http://www.gnu.org/licenses/lgpl-3.0.html>
5
+ for license text.
6
+
7
+ Trailblazer Enterprise has a commercial-friendly license allowing private forks
8
+ and modifications of Trailblazer. Please see http://trailblazer.to/enterprise/ for
9
+ more detail. You can find the commercial license terms in COMM-LICENSE.
@@ -0,0 +1,195 @@
1
+ # Trailblazer Macro Contract
2
+ The Contract Macros helps you defining contracts and assists with instantiating and validating data with those contracts at runtime.
3
+
4
+ ## Table of Contents
5
+ * [Installation](#installation)
6
+ * [Contract](#contract)
7
+ + [Build](#build)
8
+ + [Validation](#validation)
9
+ - [Key](#key)
10
+ + [Persist](#persist)
11
+ - [Name](#name)
12
+ + [Result Object](#result-object)
13
+
14
+ ## Installation
15
+ The obvious needs to be in your `Gemfile`.
16
+ ```ruby
17
+ gem "trailblazer-operation"
18
+ gem "reform"
19
+ gem "trailblazer-macro-contract"
20
+ ```
21
+ Note: you don't need to install anything if you're using the trailblazer gem itself.
22
+
23
+ ## Contract
24
+ The Contract Macro, covers the contracts for Trailblazer, they are basically Reform objects that you can define and validate inside an operation. Reform is a fantastic tool for deserializing and validating deeply nested hashes, and then, when valid, writing those to the database using your persistence layer such as ActiveRecord.
25
+
26
+ ```ruby
27
+ # app/concepts/song/contract/create.rb
28
+ module Song::Contract
29
+ class Create < Reform::Form
30
+ property :title
31
+ property :length
32
+
33
+ validates :title, length: 2..33
34
+ validates :length, numericality: true
35
+ end
36
+ end
37
+ ```
38
+
39
+ The Contract then gets hooked into the operation. using this Macro.
40
+ ```ruby
41
+ # app/concepts/song/operation/create.rb
42
+ class Song::Create < Trailblazer::Operation
43
+ step Model( Song, :new )
44
+ step Contract::Build( constant: Song::Contract::Create )
45
+ step Contract::Validate()
46
+ step Contract::Persist()
47
+ end
48
+ ```
49
+ As you can see, using contracts consists of five steps.
50
+
51
+ Define the contract class (or multiple of them) for the operation.
52
+ Plug the contract creation into the operation’s pipe using Contract::Build.
53
+ Run the contract’s validation for the params using Contract::Validate.
54
+ If successful, write the sane data to the model(s). This will usually happen in the Contract::Persist macro.
55
+ After the operation has been run, interpret the result. For instance, a controller calling an operation will render a erroring form for invalid input.
56
+
57
+ Here’s what the result would look like after running the Create operation with invalid data.
58
+ ```ruby
59
+ result = Song::Create.( title: "A" )
60
+ result.success? #=> false
61
+ result["contract.default"].errors.messages
62
+ #=> {:title=>["is too short (minimum is 2 characters)"], :length=>["is not a number"]}
63
+ ```
64
+
65
+ ### Build
66
+ The Contract::Build macro helps you to instantiate the contract. It is both helpful for a complete workflow, or to create the contract, only, without validating it, e.g. when presenting the form.
67
+ ```ruby
68
+ class Song::New < Trailblazer::Operation
69
+ step Model( Song, :new )
70
+ step Contract::Build( constant: Song::Contract::Create )
71
+ end
72
+ ```
73
+
74
+ This macro will grab the model from options["model"] and pass it into the contract’s constructor. The contract is then saved in options["contract.default"].
75
+ ```ruby
76
+ result = Song::New.()
77
+ result["model"] #=> #<struct Song title=nil, length=nil>
78
+ result["contract.default"]
79
+ #=> #<Song::Contract::Create model=#<struct Song title=nil, length=nil>>
80
+ ```
81
+ The Build macro accepts the :name option to change the name from default.
82
+
83
+ ### Validation
84
+ The Contract::Validate macro is responsible for validating the incoming params against its contract. That means you have to use Contract::Build beforehand, or create the contract yourself. The macro will then grab the params and throw then into the contract’s validate (or call) method.
85
+
86
+ ```ruby
87
+ class Song::ValidateOnly < Trailblazer::Operation
88
+ step Model( Song, :new )
89
+ step Contract::Build( constant: Song::Contract::Create )
90
+ step Contract::Validate()
91
+ end
92
+ ```
93
+ Depending on the outcome of the validation, it either stays on the right track, or deviates to left, skipping the remaining steps.
94
+ ```ruby
95
+ result = Song::ValidateOnly.({}) # empty params
96
+ result.success? #=> false
97
+ ```
98
+
99
+ Note that Validate really only validates the contract, nothing is written to the model, yet. You need to push data to the model manually, e.g. with Contract::Persist.
100
+ ```ruby
101
+ result = Song::ValidateOnly.({ title: "Rising Force", length: 13 })
102
+
103
+ result.success? #=> true
104
+ result["model"] #=> #<struct Song title=nil, length=nil>
105
+ result["contract.default"].title #=> "Rising Force"
106
+ ```
107
+
108
+ Validate will use options["params"] as the input. You can change the nesting with the :key option.
109
+
110
+ Internally, this macro will simply call Form#validate on the Reform object.
111
+
112
+ Note: Reform comes with sophisticated deserialization semantics for nested forms, it might be worth reading a bit about Reform to fully understand what you can do in the Validate step.
113
+
114
+ #### Key
115
+ Per default, Contract::Validate will use options["params"] as the data to be validated. Use the key: option if you want to validate a nested hash from the original params structure.
116
+ ```ruby
117
+ class Song::Create < Trailblazer::Operation
118
+ step Model( Song, :new )
119
+ step Contract::Build( constant: Song::Contract::Create )
120
+ step Contract::Validate( key: "song" )
121
+ step Contract::Persist( )
122
+ end
123
+ ```
124
+
125
+ This automatically extracts the nested "song" hash.
126
+ ```ruby
127
+ result = Song::Create.({ "song" => { title: "Rising Force", length: 13 } })
128
+ result.success? #=> true
129
+ ```
130
+
131
+ If that key isn’t present in the params hash, the operation fails before the actual validation.
132
+ ```ruby
133
+ result = Song::Create.({ title: "Rising Force", length: 13 })
134
+ result.success? #=> false
135
+ ```
136
+
137
+ Note: String vs. symbol do matter here since the operation will simply do a hash lookup using the key you provided.
138
+
139
+ ### Persist
140
+ To push validated data from the contract to the model(s), use Persist. Like Validate, this requires a contract to be set up beforehand.
141
+ ```ruby
142
+ class Song::Create < Trailblazer::Operation
143
+ step Model( Song, :new )
144
+ step Contract::Build( constant: Song::Contract::Create )
145
+ step Contract::Validate()
146
+ step Contract::Persist()
147
+ end
148
+ ```
149
+
150
+ After the step, the contract’s attribute values are written to the model, and the contract will call save on the model.
151
+ ```ruby
152
+ result = Song::Create.( title: "Rising Force", length: 13 )
153
+ result.success? #=> true
154
+ result["model"] #=> #<Song title="Rising Force", length=13>
155
+ ```
156
+
157
+ You can also configure the Persist step to call sync instead of Reform’s save.
158
+ ```ruby
159
+ step Persist( method: :sync )
160
+ ```
161
+ This will only write the contract’s data to the model without calling save on it.
162
+
163
+ #### Name
164
+ Explicit naming for the contract is possible, too.
165
+ ```ruby
166
+
167
+ class Song::Create < Trailblazer::Operation
168
+ step Model( Song, :new )
169
+ step Contract::Build( name: "form", constant: Song::Contract::Create )
170
+ step Contract::Validate( name: "form" )
171
+ step Contract::Persist( name: "form" )
172
+ end
173
+ ```
174
+
175
+ You have to use the name: option to tell each step what contract to use. The contract and its result will now use your name instead of default.
176
+ ```ruby
177
+ result = Song::Create.({ title: "A" })
178
+ result["contract.form"].errors.messages #=> {:title=>["is too short (minimum is 2 ch...
179
+ ```
180
+
181
+ Use this if your operation has multiple contracts.
182
+
183
+ ### Result Object
184
+ The operation will store the validation result for every contract in its own result object.
185
+
186
+ The path is result.contract.#{name}.
187
+ ```ruby
188
+ result = Create.({ length: "A" })
189
+
190
+ result["result.contract.default"].success? #=> false
191
+ result["result.contract.default"].errors #=> Errors object
192
+ result["result.contract.default"].errors.messages #=> {:length=>["is not a number"]}
193
+ ```
194
+
195
+ Each result object responds to success?, failure?, and errors, which is an Errors object. TODO: design/document Errors. WE ARE CURRENTLY WORKING ON A UNIFIED API FOR ERRORS (FOR DRY AND REFORM).
@@ -0,0 +1,17 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+ require "rubocop/rake_task"
4
+
5
+ task :default => %i[test rubocop]
6
+
7
+ Rake::TestTask.new(:test) do |test|
8
+ test.libs << 'test'
9
+ test.test_files = FileList['test/**/*_test.rb']
10
+ test.verbose = true
11
+ end
12
+
13
+ RuboCop::RakeTask.new(:rubocop) do |task|
14
+ task.patterns = ['lib/**/*.rb', 'test/**/*.rb']
15
+ task.options << "--display-cop-names"
16
+ task.fail_on_error = false
17
+ end
@@ -0,0 +1,5 @@
1
+ require "reform"
2
+ require "trailblazer/macro/contract/version"
3
+ require "trailblazer/operation/contract"
4
+ require "trailblazer/operation/validate"
5
+ require "trailblazer/operation/persist"
@@ -0,0 +1,7 @@
1
+ module Trailblazer
2
+ module Macro
3
+ module Contract
4
+ VERSION = "2.1.0.beta2".freeze
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,61 @@
1
+ module Trailblazer
2
+ class Operation
3
+ module Contract
4
+ def self.Build(name: "default", constant: nil, builder: nil)
5
+ task = lambda do |(options, flow_options), **circuit_options|
6
+ result = Build.(options, circuit_options, name: name, constant: constant, builder: builder)
7
+
8
+ return Activity::TaskBuilder::Binary.binary_direction_for( result, Activity::Right, Activity::Left ),
9
+ [options, flow_options]
10
+ end
11
+
12
+ { task: task, id: "contract.build" }
13
+ end
14
+
15
+ module Build
16
+ # Build contract at runtime.
17
+ def self.call(options, circuit_options, name: "default", constant: nil, builder: nil)
18
+ # TODO: we could probably clean this up a bit at some point.
19
+ contract_class = constant || options["contract.#{name}.class"] # DISCUSS: Injection possible here?
20
+ model = options[:model]
21
+ name = "contract.#{name}"
22
+
23
+ options[name] =
24
+ if builder
25
+ call_builder( options, circuit_options, builder: builder, constant: contract_class, name: name )
26
+ else
27
+ contract_class.new(model)
28
+ end
29
+ end
30
+
31
+ def self.call_builder(options, circuit_options, builder:raise, constant:raise, name:raise)
32
+ tmp_options = options.to_hash.merge(
33
+ constant: constant,
34
+ name: name
35
+ )
36
+ Trailblazer::Option(builder).( options, tmp_options, circuit_options )
37
+ end
38
+ end
39
+
40
+ module DSL
41
+ def self.extended(extender)
42
+ extender.extend(ClassDependencies)
43
+ warn "[Trailblazer] Using `contract do...end` is deprecated. Please use a form class and the Builder( constant: <Form> ) option."
44
+ end
45
+
46
+ # This is the class level DSL method.
47
+ # Op.contract #=> returns contract class
48
+ # Op.contract do .. end # defines contract
49
+ # Op.contract CommentForm # copies (and subclasses) external contract.
50
+ # Op.contract CommentForm do .. end # copies and extends contract.
51
+ def contract(name=:default, constant=nil, base: Reform::Form, &block)
52
+ heritage.record(:contract, name, constant, &block)
53
+
54
+ path, form_class = Trailblazer::DSL::Build.new.({ prefix: :contract, class: base, container: self }, name, constant, block)
55
+
56
+ self[path] = form_class
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,14 @@
1
+ module Trailblazer
2
+ class Operation
3
+ module Contract
4
+ def self.Persist(method: :save, name: "default")
5
+ path = "contract.#{name}"
6
+ step = ->(options, **) { options[path].send(method) }
7
+
8
+ task = Activity::TaskBuilder::Binary.( step )
9
+
10
+ { task: task, id: "persist.save" }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,74 @@
1
+ module Trailblazer
2
+ class Operation
3
+ module Contract
4
+ # result.contract = {..}
5
+ # result.contract.errors = {..}
6
+ # Deviate to left track if optional key is not found in params.
7
+ # Deviate to left if validation result falsey.
8
+ def self.Validate(skip_extract: false, name: "default", representer: false, key: nil) # DISCUSS: should we introduce something like Validate::Deserializer?
9
+ params_path = "contract.#{name}.params" # extract_params! save extracted params here.
10
+
11
+ extract = Validate::Extract.new( key: key, params_path: params_path ).freeze
12
+ validate = Validate.new( name: name, representer: representer, params_path: params_path ).freeze
13
+
14
+ # Build a simple Railway {Activity} for the internal flow.
15
+ activity = Module.new do
16
+ extend Activity::Railway(name: "Contract::Validate")
17
+
18
+ step extract, id: "#{params_path}_extract" unless skip_extract || representer
19
+ step validate, id: "contract.#{name}.call"
20
+ end
21
+
22
+ # activity, _ = activity.decompose
23
+
24
+ # DISCUSS: use Nested here?
25
+ { task: activity, id: "contract.#{name}.validate", outputs: activity.outputs }
26
+ end
27
+
28
+ class Validate
29
+ # Task: extract the contract's input from params by reading `:key`.
30
+ class Extract
31
+ def initialize(key:nil, params_path:nil)
32
+ @key, @params_path = key, params_path
33
+ end
34
+
35
+ def call( ctx, params:, ** )
36
+ ctx[@params_path] = @key ? params[@key] : params
37
+ end
38
+ end
39
+
40
+ def initialize(name:"default", representer:false, params_path:nil)
41
+ @name, @representer, @params_path = name, representer, params_path
42
+ end
43
+
44
+ # Task: Validates contract `:name`.
45
+ def call( ctx, ** )
46
+ validate!(
47
+ ctx,
48
+ representer: ctx["representer.#{@name}.class"] ||= @representer, # FIXME: maybe @representer should use DI.
49
+ params_path: @params_path
50
+ )
51
+ end
52
+
53
+ def validate!(options, representer:false, from: :document, params_path:nil)
54
+ path = "contract.#{@name}"
55
+ contract = options[path]
56
+
57
+ # this is for 1.1-style compatibility and should be removed once we have Deserializer in place:
58
+ options["result.#{path}"] = result =
59
+ if representer
60
+ # use :document as the body and let the representer deserialize to the contract.
61
+ # this will be simplified once we have Deserializer.
62
+ # translates to contract.("{document: bla}") { MyRepresenter.new(contract).from_json .. }
63
+ contract.(options[from]) { |document| representer.new(contract).parse(document) }
64
+ else
65
+ # let Reform handle the deserialization.
66
+ contract.(options[params_path])
67
+ end
68
+
69
+ result.success?
70
+ end
71
+ end
72
+ end
73
+ end # Operation
74
+ end
@@ -0,0 +1,439 @@
1
+ require "test_helper"
2
+
3
+ class DocsContractOverviewTest < Minitest::Spec
4
+ Song = Struct.new(:length, :title)
5
+
6
+ #:overv-reform
7
+ # app/concepts/song/create.rb
8
+ class Create < Trailblazer::Operation
9
+ #~contractonly
10
+ class MyContract < Reform::Form
11
+ property :title
12
+ property :length
13
+
14
+ validates :title, presence: true
15
+ validates :length, numericality: true
16
+ end
17
+ #~contractonly end
18
+
19
+ step Model( Song, :new )
20
+ step Contract::Build(constant: MyContract)
21
+ step Contract::Validate()
22
+ step Contract::Persist( method: :sync )
23
+ #~contractonly end
24
+ end
25
+ #:overv-reform end
26
+
27
+ puts Trailblazer::Operation::Inspect.(Create, style: :rows)
28
+
29
+ =begin
30
+ #:overv-reform-pipe
31
+ 0 ==========================&model.build
32
+ 1 =======================>contract.build
33
+ 2 ==============&validate.params.extract
34
+ 3 ====================&contract.validate
35
+ 4 =========================&persist.save
36
+ #:overv-reform-pipe end
37
+ =end
38
+
39
+ it do
40
+ assert Create.(params: {})["contract.default"].must_be_instance_of DocsContractOverviewTest::Create::MyContract
41
+ end
42
+
43
+ #- result
44
+ it do
45
+ #:result
46
+ result = Create.(params: { length: "A" })
47
+
48
+ result["result.contract.default"].success? #=> false
49
+ result["result.contract.default"].errors #=> Errors object
50
+ result["result.contract.default"].errors.messages #=> {:length=>["is not a number"]}
51
+
52
+ #:result end
53
+ result["result.contract.default"].success?.must_equal false
54
+ result["result.contract.default"].errors.messages.must_equal ({:title=>["can't be blank"], :length=>["is not a number"]})
55
+ end
56
+
57
+ it "shows 2-level tracing" do
58
+ result = Create.trace( params: { length: "A" } )
59
+ result.wtf.gsub(/0x\w+/, "").must_equal %{|-- #<Trailblazer::Activity::Start semantic=:default>
60
+ |-- model.build
61
+ |-- contract.build
62
+ |-- contract.default.validate
63
+ | |-- #<Trailblazer::Activity::Start semantic=:default>
64
+ | |-- contract.default.params_extract
65
+ | |-- contract.default.call
66
+ | `-- #<Trailblazer::Activity::End semantic=:failure>
67
+ `-- #<Trailblazer::Operation::Railway::End::Failure semantic=:failure>}
68
+ end
69
+ end
70
+ #---
71
+ # contract MyContract
72
+ class DocsContractExplicitTest < Minitest::Spec
73
+ Song = Struct.new(:length, :title)
74
+
75
+ #:reform-inline
76
+ class MyContract < Reform::Form
77
+ property :title
78
+ property :length
79
+
80
+ validates :title, presence: true
81
+ validates :length, numericality: true
82
+ end
83
+ #:reform-inline end
84
+
85
+ #:reform-inline-op
86
+ # app/concepts/comment/create.rb
87
+ class Create < Trailblazer::Operation
88
+ step Model( Song, :new )
89
+ step Contract::Build(constant: MyContract)
90
+ step Contract::Validate()
91
+ step Contract::Persist( method: :sync )
92
+ end
93
+ #:reform-inline-op end
94
+ end
95
+
96
+ #- Validate with manual key extraction
97
+ class DocsContractSeparateKeyTest < Minitest::Spec
98
+ Song = Struct.new(:id, :title)
99
+ #:key-extr
100
+ class Create < Trailblazer::Operation
101
+ class MyContract < Reform::Form
102
+ property :title
103
+ end
104
+
105
+ def type
106
+ "evergreen" # this is how you could do polymorphic lookups.
107
+ end
108
+
109
+ step Model( Song, :new )
110
+ step Contract::Build(constant: MyContract)
111
+ step :extract_params!
112
+ step Contract::Validate( skip_extract: true )
113
+ step Contract::Persist( method: :sync )
114
+
115
+ def extract_params!(options, **)
116
+ options["contract.default.params"] = options[:params][type]
117
+ end
118
+ end
119
+ #:key-extr end
120
+
121
+ it { Create.(params: { }).inspect(:model).must_equal %{<Result:false [#<struct DocsContractSeparateKeyTest::Song id=nil, title=nil>] >} }
122
+ it { Create.(params: {"evergreen" => { title: "SVG" }}).inspect(:model).must_equal %{<Result:true [#<struct DocsContractSeparateKeyTest::Song id=nil, title="SVG">] >} }
123
+ end
124
+
125
+ #---
126
+ #- Contract::Build( constant: XXX )
127
+ class ContractConstantTest < Minitest::Spec
128
+ Song = Struct.new(:title, :length) do
129
+ def save
130
+ true
131
+ end
132
+ end
133
+
134
+ #:constant-contract
135
+ # app/concepts/song/contract/create.rb
136
+ module Song::Contract
137
+ class Create < Reform::Form
138
+ property :title
139
+ property :length
140
+
141
+ validates :title, length: 2..33
142
+ validates :length, numericality: true
143
+ end
144
+ end
145
+ #:constant-contract end
146
+
147
+ #:constant
148
+ class Song::Create < Trailblazer::Operation
149
+ step Model( Song, :new )
150
+ step Contract::Build( constant: Song::Contract::Create )
151
+ step Contract::Validate()
152
+ step Contract::Persist()
153
+ end
154
+ #:constant end
155
+
156
+ it { Song::Create.(params: { title: "A" }).inspect(:model).must_equal %{<Result:false [#<struct ContractConstantTest::Song title=nil, length=nil>] >} }
157
+ it { Song::Create.(params: { title: "Anthony's Song", length: 12 }).inspect(:model).must_equal %{<Result:true [#<struct ContractConstantTest::Song title="Anthony's Song", length=12>] >} }
158
+ it do
159
+ #:constant-result
160
+ result = Song::Create.(params: { title: "A" })
161
+ result.success? #=> false
162
+ result["contract.default"].errors.messages
163
+ #=> {:title=>["is too short (minimum is 2 characters)"], :length=>["is not a number"]}
164
+ #:constant-result end
165
+
166
+ #:constant-result-true
167
+ result = Song::Create.(params: { title: "Rising Force", length: 13 })
168
+ result.success? #=> true
169
+ result["model"] #=> #<Song title="Rising Force", length=13>
170
+ #:constant-result-true end
171
+ end
172
+
173
+ #---
174
+ # Song::New
175
+ #:constant-new
176
+ class Song::New < Trailblazer::Operation
177
+ step Model( Song, :new )
178
+ step Contract::Build( constant: Song::Contract::Create )
179
+ end
180
+ #:constant-new end
181
+
182
+ it { Song::New.(params: {}).inspect(:model).must_equal %{<Result:true [#<struct ContractConstantTest::Song title=nil, length=nil>] >} }
183
+ it { Song::New.(params: {})["contract.default"].model.inspect.must_equal %{#<struct ContractConstantTest::Song title=nil, length=nil>} }
184
+ it do
185
+ #:constant-new-result
186
+ result = Song::New.(params: {})
187
+ result["model"] #=> #<struct Song title=nil, length=nil>
188
+ result["contract.default"]
189
+ #=> #<Song::Contract::Create model=#<struct Song title=nil, length=nil>>
190
+ #:constant-new-result end
191
+ end
192
+
193
+ #---
194
+ #:validate-only
195
+ class Song::ValidateOnly < Trailblazer::Operation
196
+ step Model( Song, :new )
197
+ step Contract::Build( constant: Song::Contract::Create )
198
+ step Contract::Validate()
199
+ end
200
+ #:validate-only end
201
+
202
+ it { Song::ValidateOnly.(params: {}).inspect(:model).must_equal %{<Result:false [#<struct ContractConstantTest::Song title=nil, length=nil>] >} }
203
+ it do
204
+ result = Song::ValidateOnly.(params: { title: "Rising Forse", length: 13 })
205
+ result.inspect(:model).must_equal %{<Result:true [#<struct ContractConstantTest::Song title=nil, length=nil>] >}
206
+ end
207
+
208
+ it do
209
+ #:validate-only-result-false
210
+ result = Song::ValidateOnly.(params: {}) # empty params
211
+ result.success? #=> false
212
+ #:validate-only-result-false end
213
+ end
214
+
215
+ it do
216
+ #:validate-only-result
217
+ result = Song::ValidateOnly.(params: { title: "Rising Force", length: 13 })
218
+
219
+ result.success? #=> true
220
+ result["model"] #=> #<struct Song title=nil, length=nil>
221
+ result["contract.default"].title #=> "Rising Force"
222
+ #:validate-only-result end
223
+ end
224
+ end
225
+
226
+ #---
227
+ #- Validate( key: :song )
228
+ class DocsContractKeyTest < Minitest::Spec
229
+ Song = Class.new(ContractConstantTest::Song)
230
+
231
+ module Song::Contract
232
+ Create = ContractConstantTest::Song::Contract::Create
233
+ end
234
+
235
+ #:key
236
+ class Song::Create < Trailblazer::Operation
237
+ step Model( Song, :new )
238
+ step Contract::Build( constant: Song::Contract::Create )
239
+ step Contract::Validate( key: "song" )
240
+ step Contract::Persist( )
241
+ end
242
+ #:key end
243
+
244
+ it { Song::Create.(params: {}).inspect(:model, "result.contract.default.extract").must_equal %{<Result:false [#<struct DocsContractKeyTest::Song title=nil, length=nil>, nil] >} }
245
+ it { Song::Create.(params: {"song" => { title: "SVG", length: 13 }}).inspect(:model).must_equal %{<Result:true [#<struct DocsContractKeyTest::Song title=\"SVG\", length=13>] >} }
246
+ it do
247
+ #:key-res
248
+ result = Song::Create.(params: { "song" => { title: "Rising Force", length: 13 } })
249
+ result.success? #=> true
250
+ #:key-res end
251
+
252
+ #:key-res-false
253
+ result = Song::Create.(params: { title: "Rising Force", length: 13 })
254
+ result.success? #=> false
255
+ #:key-res-false end
256
+ end
257
+ end
258
+
259
+ #- Contract::Build[ constant: XXX, name: AAA ]
260
+ class ContractNamedConstantTest < Minitest::Spec
261
+ Song = Class.new(ContractConstantTest::Song)
262
+
263
+ module Song::Contract
264
+ Create = ContractConstantTest::Song::Contract::Create
265
+ end
266
+
267
+ #:constant-name
268
+ class Song::Create < Trailblazer::Operation
269
+ step Model( Song, :new )
270
+ step Contract::Build( name: "form", constant: Song::Contract::Create )
271
+ step Contract::Validate( name: "form" )
272
+ step Contract::Persist( name: "form" )
273
+ end
274
+ #:constant-name end
275
+
276
+ it { Song::Create.(params: { title: "A" }).inspect(:model).must_equal %{<Result:false [#<struct ContractNamedConstantTest::Song title=nil, length=nil>] >} }
277
+ it { Song::Create.(params: { title: "Anthony's Song", length: 13 }).inspect(:model).must_equal %{<Result:true [#<struct ContractNamedConstantTest::Song title="Anthony's Song", length=13>] >} }
278
+
279
+ it do
280
+ #:name-res
281
+ result = Song::Create.(params: { title: "A" })
282
+ result["contract.form"].errors.messages #=> {:title=>["is too short (minimum is 2 ch...
283
+ #:name-res end
284
+ end
285
+ end
286
+
287
+ #---
288
+ #- dependency injection
289
+ #- contract class
290
+ class ContractInjectConstantTest < Minitest::Spec
291
+ Song = Struct.new(:id, :title)
292
+ #:di-constant-contract
293
+ class MyContract < Reform::Form
294
+ property :title
295
+ validates :title, length: 2..33
296
+ end
297
+ #:di-constant-contract end
298
+ #:di-constant
299
+ class Create < Trailblazer::Operation
300
+ step Model( Song, :new )
301
+ step Contract::Build()
302
+ step Contract::Validate()
303
+ step Contract::Persist( method: :sync )
304
+ end
305
+ #:di-constant end
306
+
307
+ it do
308
+ #:di-contract-call
309
+ Create.(
310
+ params: { title: "Anthony's Song" },
311
+ "contract.default.class" => MyContract
312
+ )
313
+ #:di-contract-call end
314
+ end
315
+ it { Create.(params: { title: "A" }, "contract.default.class" => MyContract).inspect(:model).must_equal %{<Result:false [#<struct ContractInjectConstantTest::Song id=nil, title=nil>] >} }
316
+ it { Create.(params: { title: "Anthony's Song" }, "contract.default.class" => MyContract).inspect(:model).must_equal %{<Result:true [#<struct ContractInjectConstantTest::Song id=nil, title="Anthony's Song">] >} }
317
+ end
318
+
319
+ class DryValidationContractTest < Minitest::Spec
320
+ Song = Struct.new(:id, :title)
321
+ #---
322
+ # DRY-validation with multiple validation sets,
323
+ #- result.path
324
+ #:dry-schema
325
+ require "reform/form/dry"
326
+ class Create < Trailblazer::Operation
327
+ # contract to verify params formally.
328
+ class MyContract < Reform::Form
329
+ feature Reform::Form::Dry
330
+ property :id
331
+ property :title
332
+
333
+ validation name: :default do
334
+ required(:id).filled
335
+ end
336
+
337
+ validation name: :extra, if: :default do
338
+ required(:title).filled(min_size?: 2)
339
+ end
340
+ end
341
+ #~form end
342
+
343
+ step Model( Song, :new ) # create the op's main model.
344
+ step Contract::Build( constant: MyContract ) # create the Reform contract.
345
+ step Contract::Validate() # validate the Reform contract.
346
+ step Contract::Persist( method: :sync) # persist the contract's data via the model.
347
+ #~form end
348
+ end
349
+ #:dry-schema end
350
+
351
+ puts "@@@@@ #{Trailblazer::Operation::Inspect.(Create, style: :rows)}"
352
+
353
+ it { Create.(params: {}).inspect("result.contract.default").must_include "Result:false"}
354
+ it { Create.(params: {}).inspect("result.contract.default").must_include "@errors={:id=>[\"must be filled\""}
355
+
356
+ it { Create.(params: { id: 1 }).inspect(:model, "result.contract.default").must_include "Result:false"}
357
+ it { Create.(params: { id: 1 }).inspect(:model, "result.contract.default").must_include "@errors={:title=>[\"must be filled\", \"size cannot be less than 2\"]}"}
358
+ it { Create.(params: { id: 1 }).inspect(:model, "result.contract.default").wont_include ":id=>[\"must be filled\""}
359
+
360
+ it { Create.(params: { id: 1, title: "" }).inspect(:model).must_equal %{<Result:false [#<struct DryValidationContractTest::Song id=nil, title=nil>] >} }
361
+ it { Create.(params: { id: 1, title: "Y" }).inspect(:model).must_equal %{<Result:false [#<struct DryValidationContractTest::Song id=nil, title=nil>] >} }
362
+ it { Create.(params: { id: 1, title: "Yo" }).inspect(:model).must_equal %{<Result:true [#<struct DryValidationContractTest::Song id=1, title="Yo">] >} }
363
+ end
364
+
365
+ class DocContractBuilderTest < Minitest::Spec
366
+ Song = Struct.new(:id, :title)
367
+ #---
368
+ #- builder:
369
+ #:builder-option
370
+ class Create < Trailblazer::Operation
371
+
372
+ class MyContract < Reform::Form
373
+ property :title
374
+ property :current_user, virtual: true
375
+
376
+ validate :current_user?
377
+ validates :title, presence: true
378
+
379
+ def current_user?
380
+ return true if defined?(current_user)
381
+ false
382
+ end
383
+ end
384
+
385
+ step Model( Song, :new )
386
+ step Contract::Build( constant: MyContract, builder: :default_contract! )
387
+ step Contract::Validate()
388
+ step Contract::Persist( method: :sync )
389
+
390
+ def default_contract!(options, constant:, model:, **)
391
+ constant.new(model, current_user: options [:current_user])
392
+ end
393
+ end
394
+ #:builder-option end
395
+
396
+ it { Create.(params: {}).inspect(:model).must_equal %{<Result:false [#<struct DocContractBuilderTest::Song id=nil, title=nil>] >} }
397
+ it { Create.(params: { title: "title"}, current_user: Module).inspect(:model).must_equal %{<Result:true [#<struct DocContractBuilderTest::Song id=nil, title="title">] >} }
398
+ end
399
+
400
+ class DocContractTest < Minitest::Spec
401
+ Song = Struct.new(:id, :title)
402
+ #---
403
+ # with contract block, and inheritance, the old way.
404
+ class Block < Trailblazer::Operation
405
+ class MyContract < Reform::Form
406
+ property :title
407
+ end
408
+
409
+ step Model( Song, :new )
410
+ step Contract::Build(constant: MyContract) # resolves to "contract.class.default" and is resolved at runtime.
411
+ step Contract::Validate()
412
+ step Contract::Persist( method: :sync )
413
+ end
414
+
415
+ it { Block.(params: {}).inspect(:model).must_equal %{<Result:true [#<struct DocContractTest::Song id=nil, title=nil>] >} }
416
+ it { Block.(params: { id:1, title: "Fame" }).inspect(:model).must_equal %{<Result:true [#<struct DocContractTest::Song id=nil, title="Fame">] >} }
417
+
418
+ class Breach < Block
419
+ class MyContract < MyContract
420
+ property :id
421
+ end
422
+
423
+ step Contract::Build(constant: MyContract), replace: "contract.build"
424
+ end
425
+
426
+ it { Breach.(params: { id:1, title: "Fame" }).inspect(:model).must_equal %{<Result:true [#<struct DocContractTest::Song id=1, title="Fame">] >} }
427
+
428
+ #-
429
+ # with constant.
430
+ class Break < Block
431
+ class MyContract < Reform::Form
432
+ property :id
433
+ end
434
+ # override the original block as if it's never been there.
435
+ step Contract::Build(constant: MyContract), replace: "contract.build"
436
+ end
437
+
438
+ it { Break.(params: { id:1, title: "Fame" }).inspect(:model).must_equal %{<Result:true [#<struct DocContractTest::Song id=1, title=nil>] >} }
439
+ end