trailblazer-macro-contract 2.1.0.beta2

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.
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