trailblazer 2.0.7 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rubocop-https---raw-githubusercontent-com-trailblazer-meta-master-rubocop-yml +101 -0
- data/.rubocop.yml +20 -0
- data/.rubocop_todo.yml +556 -0
- data/.travis.yml +6 -10
- data/CHANGES.md +83 -1
- data/COMM-LICENSE +46 -75
- data/CONTRIBUTING.md +179 -0
- data/Gemfile +0 -27
- data/{LICENSE.txt → LICENSE} +4 -4
- data/README.md +39 -138
- data/Rakefile +2 -19
- data/lib/trailblazer.rb +3 -17
- data/lib/trailblazer/version.rb +3 -1
- data/test/test_helper.rb +12 -3
- data/trailblazer.gemspec +10 -14
- metadata +22 -147
- data/doc/Trb-The-Stack.png +0 -0
- data/doc/operation-2017.png +0 -0
- data/doc/trb.jpg +0 -0
- data/lib/trailblazer/dsl.rb +0 -47
- data/lib/trailblazer/operation/auto_inject.rb +0 -47
- data/lib/trailblazer/operation/callback.rb +0 -35
- data/lib/trailblazer/operation/contract.rb +0 -46
- data/lib/trailblazer/operation/guard.rb +0 -18
- data/lib/trailblazer/operation/model.rb +0 -60
- data/lib/trailblazer/operation/module.rb +0 -29
- data/lib/trailblazer/operation/nested.rb +0 -113
- data/lib/trailblazer/operation/persist.rb +0 -10
- data/lib/trailblazer/operation/policy.rb +0 -35
- data/lib/trailblazer/operation/procedural/contract.rb +0 -15
- data/lib/trailblazer/operation/procedural/validate.rb +0 -22
- data/lib/trailblazer/operation/pundit.rb +0 -38
- data/lib/trailblazer/operation/representer.rb +0 -31
- data/lib/trailblazer/operation/rescue.rb +0 -21
- data/lib/trailblazer/operation/test.rb +0 -17
- data/lib/trailblazer/operation/validate.rb +0 -68
- data/lib/trailblazer/operation/wrap.rb +0 -25
- data/test/docs/auto_inject_test.rb +0 -30
- data/test/docs/contract_test.rb +0 -525
- data/test/docs/dry_test.rb +0 -31
- data/test/docs/fast_test.rb +0 -164
- data/test/docs/guard_test.rb +0 -169
- data/test/docs/macro_test.rb +0 -36
- data/test/docs/model_test.rb +0 -75
- data/test/docs/nested_test.rb +0 -334
- data/test/docs/operation_test.rb +0 -408
- data/test/docs/policy_test.rb +0 -2
- data/test/docs/pundit_test.rb +0 -133
- data/test/docs/representer_test.rb +0 -268
- data/test/docs/rescue_test.rb +0 -154
- data/test/docs/wrap_test.rb +0 -183
- data/test/gemfiles/Gemfile.ruby-1.9 +0 -3
- data/test/gemfiles/Gemfile.ruby-2.0 +0 -12
- data/test/gemfiles/Gemfile.ruby-2.3 +0 -12
- data/test/module_test.rb +0 -100
- data/test/operation/callback_test.rb +0 -70
- data/test/operation/contract_test.rb +0 -420
- data/test/operation/dsl/callback_test.rb +0 -106
- data/test/operation/dsl/contract_test.rb +0 -294
- data/test/operation/dsl/representer_test.rb +0 -169
- data/test/operation/model_test.rb +0 -60
- data/test/operation/params_test.rb +0 -36
- data/test/operation/persist_test.rb +0 -44
- data/test/operation/pipedream_test.rb +0 -59
- data/test/operation/pipetree_test.rb +0 -104
- data/test/operation/present_test.rb +0 -24
- data/test/operation/pundit_test.rb +0 -104
- data/test/operation/representer_test.rb +0 -254
- data/test/operation/resolver_test.rb +0 -47
- data/test/operation_test.rb +0 -143
@@ -1,15 +0,0 @@
|
|
1
|
-
module Trailblazer::Operation::Procedural
|
2
|
-
# THIS IS UNTESTED, PRIVATE API AND WILL BE REMOVED SOON.
|
3
|
-
module Contract
|
4
|
-
# Instantiate the contract, either by using the user's contract passed into #validate
|
5
|
-
# or infer the Operation contract.
|
6
|
-
def contract_for(model:self["model"], options:{}, contract_class:self["contract.default.class"])
|
7
|
-
contract!(model: model, options: options, contract_class: contract_class)
|
8
|
-
end
|
9
|
-
|
10
|
-
# Override to construct your own contract.
|
11
|
-
def contract!(model:nil, options:{}, contract_class:nil)
|
12
|
-
contract_class.new(model, options)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,22 +0,0 @@
|
|
1
|
-
module Trailblazer::Operation::Procedural
|
2
|
-
module Validate
|
3
|
-
def validate(params, contract:self["contract.default"], path:"contract.default") # :params
|
4
|
-
# DISCUSS: should we only have path here and then look up contract ourselves?
|
5
|
-
result = validate_contract(contract, params) # run validation. # FIXME: must be overridable.
|
6
|
-
|
7
|
-
self["result.#{path}"] = result
|
8
|
-
|
9
|
-
if valid = result.success? # FIXME: to_bool or success?
|
10
|
-
yield result if block_given?
|
11
|
-
else
|
12
|
-
# self["errors.#{path}"] = result.errors # TODO: remove me
|
13
|
-
end
|
14
|
-
|
15
|
-
valid
|
16
|
-
end
|
17
|
-
|
18
|
-
def validate_contract(contract, params)
|
19
|
-
contract.(params)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
class Trailblazer::Operation
|
2
|
-
module Policy
|
3
|
-
def self.Pundit(policy_class, action, name: :default)
|
4
|
-
Policy.step(Pundit.build(policy_class, action), name: name)
|
5
|
-
end
|
6
|
-
|
7
|
-
module Pundit
|
8
|
-
def self.build(*args, &block)
|
9
|
-
Condition.new(*args, &block)
|
10
|
-
end
|
11
|
-
|
12
|
-
# Pundit::Condition is invoked at runtime when iterating the pipe.
|
13
|
-
class Condition
|
14
|
-
def initialize(policy_class, action)
|
15
|
-
@policy_class, @action = policy_class, action
|
16
|
-
end
|
17
|
-
|
18
|
-
# Instantiate the actual policy object, and call it.
|
19
|
-
def call(input, options)
|
20
|
-
policy = build_policy(options) # this translates to Pundit interface.
|
21
|
-
result!(policy.send(@action), policy)
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
def build_policy(options)
|
26
|
-
@policy_class.new(options["current_user"], options["model"])
|
27
|
-
end
|
28
|
-
|
29
|
-
def result!(success, policy)
|
30
|
-
data = { "policy" => policy }
|
31
|
-
data["message"] = "Breach" if !success # TODO: how to allow messages here?
|
32
|
-
|
33
|
-
Result.new(success, data)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
class Trailblazer::Operation
|
2
|
-
module Representer
|
3
|
-
def self.infer(contract_class, format:Representable::JSON)
|
4
|
-
Disposable::Rescheme.from(contract_class,
|
5
|
-
include: [format],
|
6
|
-
options_from: :deserializer, # use :instance etc. in deserializer.
|
7
|
-
superclass: Representable::Decorator,
|
8
|
-
definitions_from: lambda { |inline| inline.definitions },
|
9
|
-
exclude_options: [:default, :populator], # TODO: test with populator: in an operation.
|
10
|
-
exclude_properties: [:persisted?]
|
11
|
-
)
|
12
|
-
end
|
13
|
-
|
14
|
-
module DSL
|
15
|
-
def representer(name=:default, constant=nil, &block)
|
16
|
-
heritage.record(:representer, name, constant, &block)
|
17
|
-
|
18
|
-
# FIXME: make this nicer. we want to extend same-named callback groups.
|
19
|
-
# TODO: allow the same with contract, or better, test it!
|
20
|
-
path, representer_class = Trailblazer::DSL::Build.new.({ prefix: :representer, class: representer_base_class, container: self }, name, constant, block)
|
21
|
-
|
22
|
-
self[path] = representer_class
|
23
|
-
end
|
24
|
-
|
25
|
-
# TODO: make engine configurable?
|
26
|
-
def representer_base_class
|
27
|
-
Class.new(Representable::Decorator) { include Representable::JSON; self }
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
class Trailblazer::Operation
|
2
|
-
def self.Rescue(*exceptions, handler: lambda { |*| }, &block)
|
3
|
-
exceptions = [StandardError] unless exceptions.any?
|
4
|
-
handler = Option.(handler)
|
5
|
-
|
6
|
-
rescue_block = ->(options, operation, *, &nested_pipe) {
|
7
|
-
begin
|
8
|
-
res = nested_pipe.call
|
9
|
-
res.first == ::Pipetree::Railway::Right # FIXME.
|
10
|
-
rescue *exceptions => exception
|
11
|
-
handler.call(operation, exception, options)
|
12
|
-
false
|
13
|
-
end
|
14
|
-
}
|
15
|
-
|
16
|
-
step, _ = Wrap(rescue_block, &block)
|
17
|
-
|
18
|
-
[ step, name: "Rescue:#{block.source_location.last}" ]
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
@@ -1,17 +0,0 @@
|
|
1
|
-
module Trailblazer
|
2
|
-
module Test
|
3
|
-
module Run
|
4
|
-
# DISCUSS: use Endpoint here?
|
5
|
-
# DISCUSS: use Controller code here?
|
6
|
-
module_function
|
7
|
-
def run(operation_class, *args)
|
8
|
-
result = operation_class.(*args)
|
9
|
-
|
10
|
-
raise "[Trailblazer] #{operation_class} wasn't run successfully. #{result.inspect}" if result.failure?
|
11
|
-
|
12
|
-
result
|
13
|
-
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,68 +0,0 @@
|
|
1
|
-
class Trailblazer::Operation
|
2
|
-
module Contract
|
3
|
-
Railway = Pipetree::Railway
|
4
|
-
|
5
|
-
# result.contract = {..}
|
6
|
-
# result.contract.errors = {..}
|
7
|
-
# Deviate to left track if optional key is not found in params.
|
8
|
-
# Deviate to left if validation result falsey.
|
9
|
-
def self.Validate(skip_extract:false, name: "default", representer:false, key: nil) # DISCUSS: should we introduce something like Validate::Deserializer?
|
10
|
-
params_path = "contract.#{name}.params" # extract_params! save extracted params here.
|
11
|
-
|
12
|
-
return Validate::Call(name: name, representer: representer, params_path: params_path) if skip_extract || representer
|
13
|
-
|
14
|
-
extract_step, extract_options = Validate::Extract(key: key, params_path: params_path)
|
15
|
-
validate_step, validate_options = Validate::Call(name: name, representer: representer, params_path: params_path)
|
16
|
-
|
17
|
-
pipe = Railway.new # TODO: make nested pipes simpler.
|
18
|
-
.add(Railway::Right, Railway.&(extract_step), extract_options)
|
19
|
-
.add(Railway::Right, Railway.&(validate_step), validate_options)
|
20
|
-
|
21
|
-
step = ->(input, options) { pipe.(input, options).first <= Railway::Right }
|
22
|
-
|
23
|
-
[step, name: "contract.#{name}.validate"]
|
24
|
-
end
|
25
|
-
|
26
|
-
module Validate
|
27
|
-
# Macro: extract the contract's input from params by reading `:key`.
|
28
|
-
def self.Extract(key:nil, params_path:nil)
|
29
|
-
# TODO: introduce nested pipes and pass composed input instead.
|
30
|
-
step = ->(input, options) do
|
31
|
-
options[params_path] = key ? options["params"][key] : options["params"]
|
32
|
-
end
|
33
|
-
|
34
|
-
[ step, name: params_path ]
|
35
|
-
end
|
36
|
-
|
37
|
-
# Macro: Validates contract `:name`.
|
38
|
-
def self.Call(name:"default", representer:false, params_path:nil)
|
39
|
-
step = ->(input, options) {
|
40
|
-
validate!(options, name: name, representer: options["representer.#{name}.class"], params_path: params_path)
|
41
|
-
}
|
42
|
-
|
43
|
-
step = Pipetree::Step.new( step, "representer.#{name}.class" => representer )
|
44
|
-
|
45
|
-
[ step, name: "contract.#{name}.call" ]
|
46
|
-
end
|
47
|
-
|
48
|
-
def self.validate!(options, name:nil, representer:false, from: "document", params_path:nil)
|
49
|
-
path = "contract.#{name}"
|
50
|
-
contract = options[path]
|
51
|
-
|
52
|
-
# this is for 1.1-style compatibility and should be removed once we have Deserializer in place:
|
53
|
-
options["result.#{path}"] = result =
|
54
|
-
if representer
|
55
|
-
# use "document" as the body and let the representer deserialize to the contract.
|
56
|
-
# this will be simplified once we have Deserializer.
|
57
|
-
# translates to contract.("{document: bla}") { MyRepresenter.new(contract).from_json .. }
|
58
|
-
contract.(options[from]) { |document| representer.new(contract).parse(document) }
|
59
|
-
else
|
60
|
-
# let Reform handle the deserialization.
|
61
|
-
contract.(options[params_path])
|
62
|
-
end
|
63
|
-
|
64
|
-
result.success?
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
@@ -1,25 +0,0 @@
|
|
1
|
-
class Trailblazer::Operation
|
2
|
-
Base = self # TODO: we won't need this with 2.1.
|
3
|
-
|
4
|
-
def self.Wrap(wrap, &block)
|
5
|
-
operation = Class.new(Base)
|
6
|
-
# DISCUSS: don't instance_exec when |pipe| given?
|
7
|
-
operation.instance_exec(&block) # evaluate the nested pipe.
|
8
|
-
|
9
|
-
pipe = operation["pipetree"]
|
10
|
-
pipe.add(nil, nil, {delete: "operation.new"}) # TODO: make this a bit more elegant.
|
11
|
-
|
12
|
-
step = Wrap.for(wrap, pipe)
|
13
|
-
|
14
|
-
[ step, {} ]
|
15
|
-
end
|
16
|
-
|
17
|
-
module Wrap
|
18
|
-
def self.for(wrap, pipe)
|
19
|
-
->(input, options) { wrap.(options, input, pipe, & ->{ pipe.(input, options) }) }
|
20
|
-
end
|
21
|
-
end # Wrap
|
22
|
-
end
|
23
|
-
|
24
|
-
# (options, *) => (options, operation, bla)
|
25
|
-
# (*, params:, **) => (options, operation, bla, options)
|
@@ -1,30 +0,0 @@
|
|
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
|
data/test/docs/contract_test.rb
DELETED
@@ -1,525 +0,0 @@
|
|
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
|
-
#~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 Contract::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 Contract::Persist( method: :sync )
|
125
|
-
end
|
126
|
-
#:reform-inline-op end
|
127
|
-
end
|
128
|
-
|
129
|
-
#- Validate with manual key extraction
|
130
|
-
class DocsContractSeparateKeyTest < Minitest::Spec
|
131
|
-
Song = Struct.new(:id, :title)
|
132
|
-
#:key-extr
|
133
|
-
class Create < Trailblazer::Operation
|
134
|
-
extend Contract::DSL
|
135
|
-
|
136
|
-
contract do
|
137
|
-
property :title
|
138
|
-
end
|
139
|
-
|
140
|
-
def type
|
141
|
-
"evergreen" # this is how you could do polymorphic lookups.
|
142
|
-
end
|
143
|
-
|
144
|
-
step Model( Song, :new )
|
145
|
-
step Contract::Build()
|
146
|
-
step :extract_params!
|
147
|
-
step Contract::Validate( skip_extract: true )
|
148
|
-
step Contract::Persist( method: :sync )
|
149
|
-
|
150
|
-
def extract_params!(options)
|
151
|
-
options["contract.default.params"] = options["params"][type]
|
152
|
-
end
|
153
|
-
end
|
154
|
-
#:key-extr end
|
155
|
-
|
156
|
-
it { Create.({ }).inspect("model").must_equal %{<Result:false [#<struct DocsContractSeparateKeyTest::Song id=nil, title=nil>] >} }
|
157
|
-
it { Create.({"evergreen" => { title: "SVG" }}).inspect("model").must_equal %{<Result:true [#<struct DocsContractSeparateKeyTest::Song id=nil, title="SVG">] >} }
|
158
|
-
end
|
159
|
-
|
160
|
-
#---
|
161
|
-
#- Contract::Build( constant: XXX )
|
162
|
-
class ContractConstantTest < Minitest::Spec
|
163
|
-
Song = Struct.new(:title, :length) do
|
164
|
-
def save
|
165
|
-
true
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
#:constant-contract
|
170
|
-
# app/concepts/song/contract/create.rb
|
171
|
-
module Song::Contract
|
172
|
-
class Create < Reform::Form
|
173
|
-
property :title
|
174
|
-
property :length
|
175
|
-
|
176
|
-
validates :title, length: 2..33
|
177
|
-
validates :length, numericality: true
|
178
|
-
end
|
179
|
-
end
|
180
|
-
#:constant-contract end
|
181
|
-
|
182
|
-
#:constant
|
183
|
-
class Song::Create < Trailblazer::Operation
|
184
|
-
step Model( Song, :new )
|
185
|
-
step Contract::Build( constant: Song::Contract::Create )
|
186
|
-
step Contract::Validate()
|
187
|
-
step Contract::Persist()
|
188
|
-
end
|
189
|
-
#:constant end
|
190
|
-
|
191
|
-
it { Song::Create.({ title: "A" }).inspect("model").must_equal %{<Result:false [#<struct ContractConstantTest::Song title=nil, length=nil>] >} }
|
192
|
-
it { Song::Create.({ title: "Anthony's Song", length: 12 }).inspect("model").must_equal %{<Result:true [#<struct ContractConstantTest::Song title="Anthony's Song", length=12>] >} }
|
193
|
-
it do
|
194
|
-
#:constant-result
|
195
|
-
result = Song::Create.( title: "A" )
|
196
|
-
result.success? #=> false
|
197
|
-
result["contract.default"].errors.messages
|
198
|
-
#=> {:title=>["is too short (minimum is 2 characters)"], :length=>["is not a number"]}
|
199
|
-
#:constant-result end
|
200
|
-
|
201
|
-
#:constant-result-true
|
202
|
-
result = Song::Create.( title: "Rising Force", length: 13 )
|
203
|
-
result.success? #=> true
|
204
|
-
result["model"] #=> #<Song title="Rising Force", length=13>
|
205
|
-
#:constant-result-true end
|
206
|
-
end
|
207
|
-
|
208
|
-
#---
|
209
|
-
# Song::New
|
210
|
-
#:constant-new
|
211
|
-
class Song::New < Trailblazer::Operation
|
212
|
-
step Model( Song, :new )
|
213
|
-
step Contract::Build( constant: Song::Contract::Create )
|
214
|
-
end
|
215
|
-
#:constant-new end
|
216
|
-
|
217
|
-
it { Song::New.().inspect("model").must_equal %{<Result:true [#<struct ContractConstantTest::Song title=nil, length=nil>] >} }
|
218
|
-
it { Song::New.()["contract.default"].model.inspect.must_equal %{#<struct ContractConstantTest::Song title=nil, length=nil>} }
|
219
|
-
it do
|
220
|
-
#:constant-new-result
|
221
|
-
result = Song::New.()
|
222
|
-
result["model"] #=> #<struct Song title=nil, length=nil>
|
223
|
-
result["contract.default"]
|
224
|
-
#=> #<Song::Contract::Create model=#<struct Song title=nil, length=nil>>
|
225
|
-
#:constant-new-result end
|
226
|
-
end
|
227
|
-
|
228
|
-
#---
|
229
|
-
#:validate-only
|
230
|
-
class Song::ValidateOnly < Trailblazer::Operation
|
231
|
-
step Model( Song, :new )
|
232
|
-
step Contract::Build( constant: Song::Contract::Create )
|
233
|
-
step Contract::Validate()
|
234
|
-
end
|
235
|
-
#:validate-only end
|
236
|
-
|
237
|
-
it { Song::ValidateOnly.().inspect("model").must_equal %{<Result:false [#<struct ContractConstantTest::Song title=nil, length=nil>] >} }
|
238
|
-
it do
|
239
|
-
result = Song::ValidateOnly.({ title: "Rising Forse", length: 13 })
|
240
|
-
result.inspect("model").must_equal %{<Result:true [#<struct ContractConstantTest::Song title=nil, length=nil>] >}
|
241
|
-
end
|
242
|
-
|
243
|
-
it do
|
244
|
-
#:validate-only-result-false
|
245
|
-
result = Song::ValidateOnly.({}) # empty params
|
246
|
-
result.success? #=> false
|
247
|
-
#:validate-only-result-false end
|
248
|
-
end
|
249
|
-
|
250
|
-
it do
|
251
|
-
#:validate-only-result
|
252
|
-
result = Song::ValidateOnly.({ title: "Rising Force", length: 13 })
|
253
|
-
|
254
|
-
result.success? #=> true
|
255
|
-
result["model"] #=> #<struct Song title=nil, length=nil>
|
256
|
-
result["contract.default"].title #=> "Rising Force"
|
257
|
-
#:validate-only-result end
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
#---
|
262
|
-
#- Validate( key: :song )
|
263
|
-
class DocsContractKeyTest < Minitest::Spec
|
264
|
-
Song = Class.new(ContractConstantTest::Song)
|
265
|
-
|
266
|
-
module Song::Contract
|
267
|
-
Create = ContractConstantTest::Song::Contract::Create
|
268
|
-
end
|
269
|
-
|
270
|
-
#:key
|
271
|
-
class Song::Create < Trailblazer::Operation
|
272
|
-
step Model( Song, :new )
|
273
|
-
step Contract::Build( constant: Song::Contract::Create )
|
274
|
-
step Contract::Validate( key: "song" )
|
275
|
-
step Contract::Persist( )
|
276
|
-
end
|
277
|
-
#:key end
|
278
|
-
|
279
|
-
it { Song::Create.({}).inspect("model", "result.contract.default.extract").must_equal %{<Result:false [#<struct DocsContractKeyTest::Song title=nil, length=nil>, nil] >} }
|
280
|
-
it { Song::Create.({"song" => { title: "SVG", length: 13 }}).inspect("model").must_equal %{<Result:true [#<struct DocsContractKeyTest::Song title=\"SVG\", length=13>] >} }
|
281
|
-
it do
|
282
|
-
#:key-res
|
283
|
-
result = Song::Create.({ "song" => { title: "Rising Force", length: 13 } })
|
284
|
-
result.success? #=> true
|
285
|
-
#:key-res end
|
286
|
-
|
287
|
-
#:key-res-false
|
288
|
-
result = Song::Create.({ title: "Rising Force", length: 13 })
|
289
|
-
result.success? #=> false
|
290
|
-
#:key-res-false end
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
#- Contract::Build[ constant: XXX, name: AAA ]
|
295
|
-
class ContractNamedConstantTest < Minitest::Spec
|
296
|
-
Song = Class.new(ContractConstantTest::Song)
|
297
|
-
|
298
|
-
module Song::Contract
|
299
|
-
Create = ContractConstantTest::Song::Contract::Create
|
300
|
-
end
|
301
|
-
|
302
|
-
#:constant-name
|
303
|
-
class Song::Create < Trailblazer::Operation
|
304
|
-
step Model( Song, :new )
|
305
|
-
step Contract::Build( name: "form", constant: Song::Contract::Create )
|
306
|
-
step Contract::Validate( name: "form" )
|
307
|
-
step Contract::Persist( name: "form" )
|
308
|
-
end
|
309
|
-
#:constant-name end
|
310
|
-
|
311
|
-
it { Song::Create.({ title: "A" }).inspect("model").must_equal %{<Result:false [#<struct ContractNamedConstantTest::Song title=nil, length=nil>] >} }
|
312
|
-
it { Song::Create.({ title: "Anthony's Song", length: 13 }).inspect("model").must_equal %{<Result:true [#<struct ContractNamedConstantTest::Song title="Anthony's Song", length=13>] >} }
|
313
|
-
|
314
|
-
it do
|
315
|
-
#:name-res
|
316
|
-
result = Song::Create.({ title: "A" })
|
317
|
-
result["contract.form"].errors.messages #=> {:title=>["is too short (minimum is 2 ch...
|
318
|
-
#:name-res end
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
|
-
#---
|
323
|
-
#- dependency injection
|
324
|
-
#- contract class
|
325
|
-
class ContractInjectConstantTest < Minitest::Spec
|
326
|
-
Song = Struct.new(:id, :title)
|
327
|
-
#:di-constant-contract
|
328
|
-
class MyContract < Reform::Form
|
329
|
-
property :title
|
330
|
-
validates :title, length: 2..33
|
331
|
-
end
|
332
|
-
#:di-constant-contract end
|
333
|
-
#:di-constant
|
334
|
-
class Create < Trailblazer::Operation
|
335
|
-
step Model( Song, :new )
|
336
|
-
step Contract::Build()
|
337
|
-
step Contract::Validate()
|
338
|
-
step Contract::Persist( method: :sync )
|
339
|
-
end
|
340
|
-
#:di-constant end
|
341
|
-
|
342
|
-
it do
|
343
|
-
#:di-contract-call
|
344
|
-
Create.(
|
345
|
-
{ title: "Anthony's Song" },
|
346
|
-
"contract.default.class" => MyContract
|
347
|
-
)
|
348
|
-
#:di-contract-call end
|
349
|
-
end
|
350
|
-
it { Create.({ title: "A" }, "contract.default.class" => MyContract).inspect("model").must_equal %{<Result:false [#<struct ContractInjectConstantTest::Song id=nil, title=nil>] >} }
|
351
|
-
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">] >} }
|
352
|
-
end
|
353
|
-
|
354
|
-
class DryValidationContractTest < Minitest::Spec
|
355
|
-
Song = Struct.new(:id, :title)
|
356
|
-
#---
|
357
|
-
# DRY-validation params validation before op,
|
358
|
-
# plus main contract.
|
359
|
-
#- result.path
|
360
|
-
#:dry-schema
|
361
|
-
require "dry/validation"
|
362
|
-
class Create < Trailblazer::Operation
|
363
|
-
extend Contract::DSL
|
364
|
-
|
365
|
-
# contract to verify params formally.
|
366
|
-
contract "params", (Dry::Validation.Schema do
|
367
|
-
required(:id).filled
|
368
|
-
end)
|
369
|
-
#~form
|
370
|
-
# domain validations.
|
371
|
-
contract "form" do
|
372
|
-
property :title
|
373
|
-
validates :title, length: 1..3
|
374
|
-
end
|
375
|
-
#~form end
|
376
|
-
|
377
|
-
step Contract::Validate( name: "params" )
|
378
|
-
#~form
|
379
|
-
step Model( Song, :new ) # create the op's main model.
|
380
|
-
step Contract::Build( name: "form" ) # create the Reform contract.
|
381
|
-
step Contract::Validate( name: "form" ) # validate the Reform contract.
|
382
|
-
step Contract::Persist( method: :sync, name: "form" ) # persist the contract's data via the model.
|
383
|
-
#~form end
|
384
|
-
end
|
385
|
-
#:dry-schema end
|
386
|
-
|
387
|
-
puts "@@@@@ #{Create["pipetree"].inspect(style: :rows)}"
|
388
|
-
|
389
|
-
it { Create.({}).inspect("model", "result.contract.params").must_equal %{<Result:false [nil, #<Dry::Validation::Result output={} errors={:id=>[\"is missing\"]}>] >} }
|
390
|
-
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={}>] >} }
|
391
|
-
# it { Create.({ id: 1, title: "" }).inspect("model", "result.contract.form").must_equal %{<Result:false [#<struct DryValidationContractTest::Song id=nil, title=nil>] >} }
|
392
|
-
it { Create.({ id: 1, title: "" }).inspect("model").must_equal %{<Result:false [#<struct DryValidationContractTest::Song id=nil, title=nil>] >} }
|
393
|
-
it { Create.({ id: 1, title: "Yo" }).inspect("model").must_equal %{<Result:true [#<struct DryValidationContractTest::Song id=nil, title="Yo">] >} }
|
394
|
-
|
395
|
-
#:dry-schema-first
|
396
|
-
require "dry/validation"
|
397
|
-
class Delete < Trailblazer::Operation
|
398
|
-
extend Contract::DSL
|
399
|
-
|
400
|
-
contract "params", (Dry::Validation.Schema do
|
401
|
-
required(:id).filled
|
402
|
-
end)
|
403
|
-
|
404
|
-
step Contract::Validate( name: "params" ), before: "operation.new"
|
405
|
-
#~more
|
406
|
-
#~more end
|
407
|
-
end
|
408
|
-
#:dry-schema-first end
|
409
|
-
end
|
410
|
-
|
411
|
-
class DryExplicitSchemaTest < Minitest::Spec
|
412
|
-
#:dry-schema-explsch
|
413
|
-
# app/concepts/comment/contract/params.rb
|
414
|
-
require "dry/validation"
|
415
|
-
MySchema = Dry::Validation.Schema do
|
416
|
-
required(:id).filled
|
417
|
-
end
|
418
|
-
#:dry-schema-explsch end
|
419
|
-
|
420
|
-
#:dry-schema-expl
|
421
|
-
# app/concepts/comment/delete.rb
|
422
|
-
class Delete < Trailblazer::Operation
|
423
|
-
extend Contract::DSL
|
424
|
-
contract "params", MySchema
|
425
|
-
|
426
|
-
step Contract::Validate( name: "params" ), before: "operation.new"
|
427
|
-
end
|
428
|
-
#:dry-schema-expl end
|
429
|
-
end
|
430
|
-
|
431
|
-
class DocContractBuilderTest < Minitest::Spec
|
432
|
-
Song = Struct.new(:id, :title)
|
433
|
-
#---
|
434
|
-
#- builder:
|
435
|
-
#:builder-option
|
436
|
-
class Create < Trailblazer::Operation
|
437
|
-
extend Contract::DSL
|
438
|
-
|
439
|
-
contract do
|
440
|
-
property :title
|
441
|
-
property :current_user, virtual: true
|
442
|
-
validates :current_user, presence: true
|
443
|
-
end
|
444
|
-
|
445
|
-
step Model( Song, :new )
|
446
|
-
step Contract::Build( builder: :default_contract! )
|
447
|
-
step Contract::Validate()
|
448
|
-
step Contract::Persist( method: :sync )
|
449
|
-
|
450
|
-
def default_contract!(options, constant:, model:, **)
|
451
|
-
constant.new(model, current_user: self["current_user"])
|
452
|
-
end
|
453
|
-
end
|
454
|
-
#:builder-option end
|
455
|
-
|
456
|
-
it { Create.({}).inspect("model").must_equal %{<Result:false [#<struct DocContractBuilderTest::Song id=nil, title=nil>] >} }
|
457
|
-
it { Create.({ title: 1}, "current_user" => Module).inspect("model").must_equal %{<Result:true [#<struct DocContractBuilderTest::Song id=nil, title=1>] >} }
|
458
|
-
|
459
|
-
#- proc
|
460
|
-
class Update < Trailblazer::Operation
|
461
|
-
extend Contract::DSL
|
462
|
-
|
463
|
-
contract do
|
464
|
-
property :title
|
465
|
-
property :current_user, virtual: true
|
466
|
-
validates :current_user, presence: true
|
467
|
-
end
|
468
|
-
|
469
|
-
step Model( Song, :new )
|
470
|
-
#:builder-proc
|
471
|
-
step Contract::Build( builder: ->(options, constant:, model:, **) {
|
472
|
-
constant.new(model, current_user: options["current_user"])
|
473
|
-
})
|
474
|
-
#:builder-proc end
|
475
|
-
step Contract::Validate()
|
476
|
-
step Contract::Persist( method: :sync )
|
477
|
-
end
|
478
|
-
|
479
|
-
it { Update.({}).inspect("model").must_equal %{<Result:false [#<struct DocContractBuilderTest::Song id=nil, title=nil>] >} }
|
480
|
-
it { Update.({ title: 1}, "current_user" => Module).inspect("model").must_equal %{<Result:true [#<struct DocContractBuilderTest::Song id=nil, title=1>] >} }
|
481
|
-
end
|
482
|
-
|
483
|
-
class DocContractTest < Minitest::Spec
|
484
|
-
Song = Struct.new(:id, :title)
|
485
|
-
#---
|
486
|
-
# with contract block, and inheritance, the old way.
|
487
|
-
class Block < Trailblazer::Operation
|
488
|
-
extend Contract::DSL
|
489
|
-
contract do
|
490
|
-
property :title
|
491
|
-
end
|
492
|
-
|
493
|
-
step Model( Song, :new )
|
494
|
-
step Contract::Build() # resolves to "contract.class.default" and is resolved at runtime.
|
495
|
-
step Contract::Validate()
|
496
|
-
step Contract::Persist( method: :sync )
|
497
|
-
end
|
498
|
-
|
499
|
-
it { Block.({}).inspect("model").must_equal %{<Result:true [#<struct DocContractTest::Song id=nil, title=nil>] >} }
|
500
|
-
it { Block.({ id:1, title: "Fame" }).inspect("model").must_equal %{<Result:true [#<struct DocContractTest::Song id=nil, title="Fame">] >} }
|
501
|
-
|
502
|
-
class Breach < Block
|
503
|
-
contract do
|
504
|
-
property :id
|
505
|
-
end
|
506
|
-
end
|
507
|
-
|
508
|
-
it { Breach.({ id:1, title: "Fame" }).inspect("model").must_equal %{<Result:true [#<struct DocContractTest::Song id=1, title="Fame">] >} }
|
509
|
-
|
510
|
-
#-
|
511
|
-
# with constant.
|
512
|
-
class Break < Block
|
513
|
-
class MyContract < Reform::Form
|
514
|
-
property :id
|
515
|
-
end
|
516
|
-
# override the original block as if it's never been there.
|
517
|
-
contract MyContract
|
518
|
-
end
|
519
|
-
|
520
|
-
it { Break.({ id:1, title: "Fame" }).inspect("model").must_equal %{<Result:true [#<struct DocContractTest::Song id=1, title=nil>] >} }
|
521
|
-
end
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|