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