trailblazer 1.1.2 → 2.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|