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,31 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "dry/container"
|
3
|
+
|
4
|
+
class DryContainerTest < Minitest::Spec
|
5
|
+
Song = Struct.new(:id, :title)
|
6
|
+
|
7
|
+
class MyContract < Reform::Form
|
8
|
+
property :title
|
9
|
+
validates :title, length: 2..33
|
10
|
+
end
|
11
|
+
|
12
|
+
my_container = Dry::Container.new
|
13
|
+
my_container.register("contract.default.class", MyContract)
|
14
|
+
# my_container.namespace("contract") do
|
15
|
+
# register("create") { Array }
|
16
|
+
# end
|
17
|
+
|
18
|
+
#---
|
19
|
+
#- dependency injection
|
20
|
+
#- with Dry-container
|
21
|
+
class Create < Trailblazer::Operation
|
22
|
+
self.| Model( Song, :new )
|
23
|
+
self.| Contract::Build()
|
24
|
+
self.| Contract::Validate()
|
25
|
+
self.| Persist( method: :sync )
|
26
|
+
end
|
27
|
+
#:key end
|
28
|
+
|
29
|
+
it { Create.({ title: "A" }, {}, my_container).inspect("model").must_equal %{<Result:false [#<struct DryContainerTest::Song id=nil, title=nil>] >} }
|
30
|
+
it { Create.({ title: "Anthony's Song" }, {}, my_container).inspect("model").must_equal %{<Result:true [#<struct DryContainerTest::Song id=nil, title="Anthony's Song">] >} }
|
31
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
#--
|
4
|
+
# with proc
|
5
|
+
class DocsGuardProcTest < Minitest::Spec
|
6
|
+
#:proc
|
7
|
+
class Create < Trailblazer::Operation
|
8
|
+
step Policy::Guard( ->(options) { options["params"][:pass] } )
|
9
|
+
step :process
|
10
|
+
#~pipeonly
|
11
|
+
|
12
|
+
def process(*)
|
13
|
+
self["x"] = true
|
14
|
+
end
|
15
|
+
#~pipeonly end
|
16
|
+
end
|
17
|
+
#:proc end
|
18
|
+
|
19
|
+
it { Create.(pass: false)["x"].must_equal nil }
|
20
|
+
it { Create.(pass: true)["x"].must_equal true }
|
21
|
+
|
22
|
+
#- result object, guard
|
23
|
+
it { Create.(pass: true)["result.policy.default"].success?.must_equal true }
|
24
|
+
it { Create.(pass: false)["result.policy.default"].success?.must_equal false }
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
#---
|
30
|
+
#- Guard inheritance
|
31
|
+
class New < Create
|
32
|
+
end
|
33
|
+
|
34
|
+
it { New["pipetree"].inspect.must_equal %{[>>operation.new,&policy.default.eval,>process]} }
|
35
|
+
end
|
36
|
+
|
37
|
+
#---
|
38
|
+
# with Callable
|
39
|
+
class DocsGuardTest < Minitest::Spec
|
40
|
+
#:callable
|
41
|
+
class MyGuard
|
42
|
+
include Uber::Callable
|
43
|
+
|
44
|
+
def call(options)
|
45
|
+
options["params"][:pass]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
#:callable end
|
49
|
+
|
50
|
+
#:callable-op
|
51
|
+
class Create < Trailblazer::Operation
|
52
|
+
step Policy::Guard( MyGuard.new )
|
53
|
+
step :process
|
54
|
+
#~pipe-only
|
55
|
+
def process(*); self[:x] = true; end
|
56
|
+
#~pipe-only end
|
57
|
+
end
|
58
|
+
#:callable-op end
|
59
|
+
|
60
|
+
it { Create.(pass: false)[:x].must_equal nil }
|
61
|
+
it { Create.(pass: true)[:x].must_equal true }
|
62
|
+
end
|
63
|
+
|
64
|
+
#---
|
65
|
+
# with method
|
66
|
+
# class DocsGuardMethodTest < Minitest::Spec
|
67
|
+
# #:method
|
68
|
+
# class Create < Trailblazer::Operation
|
69
|
+
# step Policy::Guard[ : ]
|
70
|
+
# step :process
|
71
|
+
# #~pipe-only
|
72
|
+
# def process(*); self[:x] = true; end
|
73
|
+
# #~pipe-only end
|
74
|
+
# end
|
75
|
+
# #:method end
|
76
|
+
|
77
|
+
# it { Create.(pass: false)[:x].must_equal nil }
|
78
|
+
# it { Create.(pass: true)[:x].must_equal true }
|
79
|
+
# end
|
80
|
+
|
81
|
+
#---
|
82
|
+
# with name:
|
83
|
+
class DocsGuardNamedTest < Minitest::Spec
|
84
|
+
#:name
|
85
|
+
class Create < Trailblazer::Operation
|
86
|
+
step Policy::Guard( ->(options) { options["current_user"] }, name: :user )
|
87
|
+
# ...
|
88
|
+
end
|
89
|
+
#:name end
|
90
|
+
|
91
|
+
it { Create.()["result.policy.user"].success?.must_equal false }
|
92
|
+
it { Create.({}, "current_user" => Module)["result.policy.user"].success?.must_equal true }
|
93
|
+
|
94
|
+
it {
|
95
|
+
#:name-result
|
96
|
+
result = Create.({}, "current_user" => true)
|
97
|
+
result["result.policy.user"].success? #=> true
|
98
|
+
#:name-result end
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
#---
|
103
|
+
# class-level guard
|
104
|
+
class DocsGuardClassLevelTest < Minitest::Spec
|
105
|
+
#:class-level
|
106
|
+
class Create < Trailblazer::Operation
|
107
|
+
step Policy::Guard( ->(options) { options["current_user"] == Module } ),
|
108
|
+
before: "operation.new"
|
109
|
+
#~pipe--only
|
110
|
+
step ->(options) { options["x"] = true }
|
111
|
+
#~pipe--only end
|
112
|
+
end
|
113
|
+
#:class-level end
|
114
|
+
|
115
|
+
it { Create.(); Create["result.policy"].must_equal nil }
|
116
|
+
it { Create.({}, "current_user" => Module)["x"].must_equal true }
|
117
|
+
it { Create.({} )["x"].must_equal nil }
|
118
|
+
end
|
119
|
+
|
120
|
+
#---
|
121
|
+
# dependency injection
|
122
|
+
class DocsGuardInjectionTest < Minitest::Spec
|
123
|
+
#:di-op
|
124
|
+
class Create < Trailblazer::Operation
|
125
|
+
step Policy::Guard( ->(options) { options["current_user"] == Module } )
|
126
|
+
end
|
127
|
+
#:di-op end
|
128
|
+
|
129
|
+
it { Create.({}, "current_user" => Module).inspect("").must_equal %{<Result:true [nil] >} }
|
130
|
+
it {
|
131
|
+
result =
|
132
|
+
#:di-call
|
133
|
+
Create.({},
|
134
|
+
"current_user" => Module,
|
135
|
+
"policy.default.eval" => Trailblazer::Operation::Policy::Guard.build(->(options) { false })
|
136
|
+
)
|
137
|
+
#:di-call end
|
138
|
+
result.inspect("").must_equal %{<Result:false [nil] >} }
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
# TODO:
|
143
|
+
#policy.default
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class DocsNestedOperationTest < Minitest::Spec
|
4
|
+
Song = Struct.new(:id, :title) do
|
5
|
+
def self.find(id)
|
6
|
+
return new(1, "Bristol") if id == 1
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
#---
|
11
|
+
#- nested operations
|
12
|
+
#:edit
|
13
|
+
class Edit < Trailblazer::Operation
|
14
|
+
extend Contract::DSL
|
15
|
+
|
16
|
+
contract do
|
17
|
+
property :title
|
18
|
+
end
|
19
|
+
|
20
|
+
step Model( Song, :find )
|
21
|
+
step Contract::Build()
|
22
|
+
end
|
23
|
+
#:edit end
|
24
|
+
|
25
|
+
# step Nested( Edit ) #, "policy.default" => self["policy.create"]
|
26
|
+
#:update
|
27
|
+
class Update < Trailblazer::Operation
|
28
|
+
step Nested( Edit )
|
29
|
+
step Contract::Validate()
|
30
|
+
step Persist( method: :sync )
|
31
|
+
end
|
32
|
+
#:update end
|
33
|
+
|
34
|
+
puts Update["pipetree"].inspect(style: :rows)
|
35
|
+
|
36
|
+
#-
|
37
|
+
# Edit allows grabbing model and contract
|
38
|
+
it do
|
39
|
+
#:edit-call
|
40
|
+
result = Edit.(id: 1)
|
41
|
+
|
42
|
+
result["model"] #=> #<Song id=1, title=\"Bristol\">
|
43
|
+
result["contract.default"] #=> #<Reform::Form ..>
|
44
|
+
#:edit-call end
|
45
|
+
result.inspect("model").must_equal %{<Result:true [#<struct DocsNestedOperationTest::Song id=1, title=\"Bristol\">] >}
|
46
|
+
result["contract.default"].model.must_equal result["model"]
|
47
|
+
end
|
48
|
+
|
49
|
+
#-
|
50
|
+
# Update also allows grabbing model and contract
|
51
|
+
it do
|
52
|
+
#:update-call
|
53
|
+
result = Update.(id: 1, title: "Call It A Night")
|
54
|
+
|
55
|
+
result["model"] #=> #<Song id=1 , title=\"Call It A Night\">
|
56
|
+
result["contract.default"] #=> #<Reform::Form ..>
|
57
|
+
#:update-call end
|
58
|
+
result.inspect("model").must_equal %{<Result:true [#<struct DocsNestedOperationTest::Song id=1, title=\"Call It A Night\">] >}
|
59
|
+
result["contract.default"].model.must_equal result["model"]
|
60
|
+
end
|
61
|
+
|
62
|
+
#-
|
63
|
+
# Edit is successful.
|
64
|
+
it do
|
65
|
+
result = Update.({ id: 1, title: "Miami" }, "current_user" => Module)
|
66
|
+
result.inspect("model").must_equal %{<Result:true [#<struct DocsNestedOperationTest::Song id=1, title="Miami">] >}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Edit fails
|
70
|
+
it do
|
71
|
+
Update.(id: 2).inspect("model").must_equal %{<Result:false [nil] >}
|
72
|
+
end
|
73
|
+
|
74
|
+
#- shared data
|
75
|
+
class B < Trailblazer::Operation
|
76
|
+
self.> ->(options) { options["can.B.see.it?"] = options["this.should.not.be.visible.in.B"] }
|
77
|
+
self.> ->(options) { options["can.B.see.current_user?"] = options["current_user"] }
|
78
|
+
self.> ->(options) { options["can.B.see.A.class.data?"] = options["A.class.data"] }
|
79
|
+
end
|
80
|
+
|
81
|
+
class A < Trailblazer::Operation
|
82
|
+
self["A.class.data"] = true
|
83
|
+
|
84
|
+
self.> ->(options) { options["this.should.not.be.visible.in.B"] = true }
|
85
|
+
step Nested B
|
86
|
+
end
|
87
|
+
|
88
|
+
# mutual data from A doesn't bleed into B.
|
89
|
+
it { A.()["can.B.see.it?"].must_equal nil }
|
90
|
+
it { A.()["this.should.not.be.visible.in.B"].must_equal true }
|
91
|
+
# runtime dependencies are visible in B.
|
92
|
+
it { A.({}, "current_user" => Module)["can.B.see.current_user?"].must_equal Module }
|
93
|
+
# class data from A doesn't bleed into B.
|
94
|
+
it { A.()["can.B.see.A.class.data?"].must_equal nil }
|
95
|
+
|
96
|
+
|
97
|
+
# cr_result = Create.({}, "result" => result)
|
98
|
+
# puts cr_result["model"]
|
99
|
+
# puts cr_result["contract.default"]
|
100
|
+
end
|
101
|
+
|
102
|
+
class NestedClassLevelTest < Minitest::Spec
|
103
|
+
#:class-level
|
104
|
+
class New < Trailblazer::Operation
|
105
|
+
step ->(options) { options["class"] = true }, before: "operation.new"
|
106
|
+
step ->(options) { options["x"] = true }
|
107
|
+
end
|
108
|
+
|
109
|
+
class Create < Trailblazer::Operation
|
110
|
+
step Nested New
|
111
|
+
step ->(options) { options["y"] = true }
|
112
|
+
end
|
113
|
+
#:class-level end
|
114
|
+
|
115
|
+
it { Create.().inspect("x", "y").must_equal %{<Result:true [true, true] >} }
|
116
|
+
it { Create.(); Create["class"].must_equal nil }
|
117
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
#:policy
|
4
|
+
class MyPolicy
|
5
|
+
def initialize(user, model)
|
6
|
+
@user, @model = user, model
|
7
|
+
end
|
8
|
+
|
9
|
+
def create?
|
10
|
+
@user == Module && @model.id.nil?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
#:policy end
|
14
|
+
|
15
|
+
#--
|
16
|
+
# with policy
|
17
|
+
class DocsPunditProcTest < Minitest::Spec
|
18
|
+
Song = Struct.new(:id)
|
19
|
+
|
20
|
+
#:pundit
|
21
|
+
class Create < Trailblazer::Operation
|
22
|
+
step Model( Song, :new )
|
23
|
+
step Policy::Pundit( MyPolicy, :create? )
|
24
|
+
# ...
|
25
|
+
end
|
26
|
+
#:pundit end
|
27
|
+
|
28
|
+
it { Create.({}, "current_user" => Module).inspect("model").must_equal %{<Result:true [#<struct DocsPunditProcTest::Song id=nil>] >} }
|
29
|
+
it { Create.({} ).inspect("model").must_equal %{<Result:false [#<struct DocsPunditProcTest::Song id=nil>] >} }
|
30
|
+
|
31
|
+
it do
|
32
|
+
#:pundit-result
|
33
|
+
result = Create.({}, "current_user" => Module)
|
34
|
+
result["result.policy.default"].success? #=> true
|
35
|
+
result["result.policy.default"]["policy"] #=> #<MyPolicy ...>
|
36
|
+
#:pundit-result end
|
37
|
+
result["result.policy.default"].success?.must_equal true
|
38
|
+
result["result.policy.default"]["policy"].is_a?(MyPolicy).must_equal true
|
39
|
+
end
|
40
|
+
|
41
|
+
#---
|
42
|
+
#- Guard inheritance
|
43
|
+
class New < Create
|
44
|
+
end
|
45
|
+
|
46
|
+
it { New["pipetree"].inspect.must_equal %{[>>operation.new,&model.build,&policy.default.eval]} }
|
47
|
+
|
48
|
+
#---
|
49
|
+
# dependency injection
|
50
|
+
class AnotherPolicy < MyPolicy
|
51
|
+
def create?
|
52
|
+
true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it {
|
57
|
+
result =
|
58
|
+
#:di-call
|
59
|
+
Create.({},
|
60
|
+
"current_user" => Module,
|
61
|
+
"policy.default.eval" => Trailblazer::Operation::Policy::Pundit.build(AnotherPolicy, :create?)
|
62
|
+
)
|
63
|
+
#:di-call end
|
64
|
+
result.inspect("").must_equal %{<Result:true [nil] >} }
|
65
|
+
end
|
66
|
+
|
67
|
+
#-
|
68
|
+
# with name:
|
69
|
+
class PunditWithNameTest < Minitest::Spec
|
70
|
+
Song = Struct.new(:id)
|
71
|
+
|
72
|
+
#:name
|
73
|
+
class Create < Trailblazer::Operation
|
74
|
+
step Model( Song, :new )
|
75
|
+
step Policy::Pundit( MyPolicy, :create?, name: "after_model" )
|
76
|
+
# ...
|
77
|
+
end
|
78
|
+
#:name end
|
79
|
+
|
80
|
+
it {
|
81
|
+
#:name-call
|
82
|
+
result = Create.({}, "current_user" => Module)
|
83
|
+
result["result.policy.after_model"].success? #=> true
|
84
|
+
#:name-call end
|
85
|
+
result["result.policy.after_model"].success?.must_equal true }
|
86
|
+
end
|
87
|
+
|
88
|
+
#---
|
89
|
+
# class-level guard
|
90
|
+
# class DocsGuardClassLevelTest < Minitest::Spec
|
91
|
+
# #:class-level
|
92
|
+
# class Create < Trailblazer::Operation
|
93
|
+
# step Policy::Guard[ ->(options) { options["current_user"] == Module } ],
|
94
|
+
# before: "operation.new"
|
95
|
+
# #~pipe--only
|
96
|
+
# step ->(options) { options["x"] = true }
|
97
|
+
# #~pipe--only end
|
98
|
+
# end
|
99
|
+
# #:class-level end
|
100
|
+
|
101
|
+
# it { Create.(); Create["result.policy"].must_equal nil }
|
102
|
+
# it { Create.({}, "current_user" => Module)["x"].must_equal true }
|
103
|
+
# it { Create.({} )["x"].must_equal nil }
|
104
|
+
# end
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
# TODO:
|
109
|
+
#policy.default
|
@@ -0,0 +1,268 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "representable/json"
|
3
|
+
|
4
|
+
#---
|
5
|
+
# infer
|
6
|
+
class DocsRepresenterInferTest < Minitest::Spec
|
7
|
+
Song = Struct.new(:id, :title)
|
8
|
+
|
9
|
+
#:infer
|
10
|
+
class Create < Trailblazer::Operation
|
11
|
+
class MyContract < Reform::Form
|
12
|
+
property :id
|
13
|
+
end
|
14
|
+
|
15
|
+
step Model( Song, :new )
|
16
|
+
step Contract::Build( constant: MyContract )
|
17
|
+
step Contract::Validate( representer: Representer.infer(MyContract, format: Representable::JSON) )
|
18
|
+
step Persist( method: :sync )
|
19
|
+
end
|
20
|
+
#:infer end
|
21
|
+
|
22
|
+
let (:json) { MultiJson.dump(id: 1) }
|
23
|
+
it { Create.({}, "document" => json).inspect("model").must_equal %{<Result:true [#<struct DocsRepresenterInferTest::Song id=1, title=nil>] >} }
|
24
|
+
end
|
25
|
+
|
26
|
+
#---
|
27
|
+
# explicit
|
28
|
+
class DocsRepresenterExplicitTest < Minitest::Spec
|
29
|
+
Song = Struct.new(:id, :title)
|
30
|
+
|
31
|
+
#:explicit-rep
|
32
|
+
class MyRepresenter < Representable::Decorator
|
33
|
+
include Representable::JSON
|
34
|
+
property :id
|
35
|
+
end
|
36
|
+
#:explicit-rep end
|
37
|
+
|
38
|
+
#:explicit-op
|
39
|
+
class Create < Trailblazer::Operation
|
40
|
+
class MyContract < Reform::Form
|
41
|
+
property :id
|
42
|
+
end
|
43
|
+
|
44
|
+
step Model( Song, :new )
|
45
|
+
step Contract::Build( constant: MyContract )
|
46
|
+
step Contract::Validate( representer: MyRepresenter ) # :representer
|
47
|
+
step Persist( method: :sync )
|
48
|
+
end
|
49
|
+
#:explicit-op end
|
50
|
+
|
51
|
+
let (:json) { MultiJson.dump(id: 1) }
|
52
|
+
it { Create.({}, "document" => json).inspect("model").must_equal %{<Result:true [#<struct DocsRepresenterExplicitTest::Song id=1, title=nil>] >} }
|
53
|
+
it do
|
54
|
+
#:explicit-call
|
55
|
+
Create.({}, "document" => '{"id": 1}')
|
56
|
+
#:explicit-call end
|
57
|
+
end
|
58
|
+
|
59
|
+
#- render
|
60
|
+
it do
|
61
|
+
#:render
|
62
|
+
result = Create.({}, "document" => '{"id": 1}')
|
63
|
+
json = result["representer.default.class"].new(result["model"]).to_json
|
64
|
+
json #=> '{"id":1}'
|
65
|
+
#:render end
|
66
|
+
json.must_equal '{"id":1}'
|
67
|
+
end
|
68
|
+
|
69
|
+
#-
|
70
|
+
# with dependency injection
|
71
|
+
# overriding the JSON representer with an XML one.
|
72
|
+
#:di-rep
|
73
|
+
require "representable/xml"
|
74
|
+
|
75
|
+
class MyXMLRepresenter < Representable::Decorator
|
76
|
+
include Representable::XML
|
77
|
+
property :id
|
78
|
+
end
|
79
|
+
#:di-rep end
|
80
|
+
|
81
|
+
let (:xml) { %{<body><id>1</id></body>} }
|
82
|
+
it do
|
83
|
+
#:di-call
|
84
|
+
result = Create.({},
|
85
|
+
"document" => '<body><id>1</id></body>',
|
86
|
+
"representer.default.class" => MyXMLRepresenter # injection
|
87
|
+
)
|
88
|
+
#:di-call end
|
89
|
+
result.inspect("model").must_equal %{<Result:true [#<struct DocsRepresenterExplicitTest::Song id="1", title=nil>] >}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
#---
|
94
|
+
# dependency injection
|
95
|
+
class DocsRepresenterDITest < Minitest::Spec
|
96
|
+
Song = Struct.new(:id, :title)
|
97
|
+
|
98
|
+
class MyRepresenter < Representable::Decorator
|
99
|
+
include Representable::JSON
|
100
|
+
property :id
|
101
|
+
end
|
102
|
+
|
103
|
+
class Create < Trailblazer::Operation
|
104
|
+
class MyContract < Reform::Form
|
105
|
+
property :id
|
106
|
+
end
|
107
|
+
|
108
|
+
step Model( Song, :new )
|
109
|
+
step Contract::Build( constant: MyContract )
|
110
|
+
step Contract::Validate()
|
111
|
+
step Persist( method: :sync )
|
112
|
+
end
|
113
|
+
|
114
|
+
let (:json) { MultiJson.dump(id: 1) }
|
115
|
+
it { Create.({}, "document" => json,
|
116
|
+
"representer.default.class" => MyRepresenter).inspect("model").must_equal %{<Result:true [#<struct DocsRepresenterDITest::Song id=1, title=nil>] >} }
|
117
|
+
end
|
118
|
+
|
119
|
+
#---
|
120
|
+
# inline
|
121
|
+
class DocsRepresenterInlineTest < Minitest::Spec
|
122
|
+
Song = Struct.new(:id, :title)
|
123
|
+
|
124
|
+
#:inline
|
125
|
+
class Create < Trailblazer::Operation
|
126
|
+
class MyContract < Reform::Form
|
127
|
+
property :id
|
128
|
+
end
|
129
|
+
|
130
|
+
extend Representer::DSL
|
131
|
+
|
132
|
+
representer do
|
133
|
+
property :id
|
134
|
+
end
|
135
|
+
|
136
|
+
step Model( Song, :new )
|
137
|
+
step Contract::Build( constant: MyContract )
|
138
|
+
step Contract::Validate( representer: self["representer.default.class"] )
|
139
|
+
step Persist( method: :sync )
|
140
|
+
end
|
141
|
+
#:inline end
|
142
|
+
|
143
|
+
let (:json) { MultiJson.dump(id: 1) }
|
144
|
+
it { Create.({}, "document" => json).inspect("model").must_equal %{<Result:true [#<struct DocsRepresenterInlineTest::Song id=1, title=nil>] >} }
|
145
|
+
end
|
146
|
+
|
147
|
+
#---
|
148
|
+
# rendering
|
149
|
+
class DocsRepresenterManualRenderTest < Minitest::Spec
|
150
|
+
Song = Struct.new(:id, :title) do
|
151
|
+
def self.find(id)
|
152
|
+
new(id)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class Show < Trailblazer::Operation
|
157
|
+
extend Representer::DSL
|
158
|
+
representer do
|
159
|
+
property :id
|
160
|
+
end
|
161
|
+
|
162
|
+
step Model( Song, :find )
|
163
|
+
end
|
164
|
+
|
165
|
+
it do
|
166
|
+
result =Show.({ id: 1 })
|
167
|
+
json = result["representer.default.class"].new(result["model"]).to_json
|
168
|
+
json.must_equal %{{"id":1}}
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
#---
|
173
|
+
# naming
|
174
|
+
|
175
|
+
class DocsRepresenterNamingTest < Minitest::Spec
|
176
|
+
MyRepresenter = Object
|
177
|
+
|
178
|
+
#:naming
|
179
|
+
class Create < Trailblazer::Operation
|
180
|
+
extend Representer::DSL
|
181
|
+
representer MyRepresenter
|
182
|
+
end
|
183
|
+
|
184
|
+
Create["representer.default.class"] #=> MyRepresenter
|
185
|
+
#:naming end
|
186
|
+
it { Create["representer.default.class"].must_be_kind_of MyRepresenter }
|
187
|
+
end
|
188
|
+
|
189
|
+
#---
|
190
|
+
# rendering
|
191
|
+
require "roar/json/hal"
|
192
|
+
|
193
|
+
class DocsRepresenterFullExampleTest < Minitest::Spec
|
194
|
+
Song = Struct.new(:id, :title) do
|
195
|
+
def initialize(*)
|
196
|
+
self.id = 1
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
#:errors-rep
|
201
|
+
class ErrorsRepresenter < Representable::Decorator
|
202
|
+
include Representable::JSON
|
203
|
+
collection :errors
|
204
|
+
end
|
205
|
+
#:errors-rep end
|
206
|
+
|
207
|
+
#:full
|
208
|
+
class Create < Trailblazer::Operation
|
209
|
+
extend Contract::DSL
|
210
|
+
extend Representer::DSL
|
211
|
+
|
212
|
+
contract do
|
213
|
+
property :title
|
214
|
+
validates :title, presence: true
|
215
|
+
end
|
216
|
+
|
217
|
+
representer :parse do
|
218
|
+
property :title
|
219
|
+
end
|
220
|
+
|
221
|
+
representer :render do
|
222
|
+
include Roar::JSON::HAL
|
223
|
+
|
224
|
+
property :id
|
225
|
+
property :title
|
226
|
+
link(:self) { "/songs/#{represented.id}" }
|
227
|
+
end
|
228
|
+
|
229
|
+
representer :errors, ErrorsRepresenter # explicit reference.
|
230
|
+
|
231
|
+
step Model( Song, :new )
|
232
|
+
step Contract::Build()
|
233
|
+
step Contract::Validate( representer: self["representer.parse.class"] )
|
234
|
+
step Persist( method: :sync )
|
235
|
+
end
|
236
|
+
#:full end
|
237
|
+
|
238
|
+
it do
|
239
|
+
result =Create.({}, "document" => '{"title": "Tested"}')
|
240
|
+
|
241
|
+
json = result["representer.render.class"].new(result["model"]).to_json
|
242
|
+
|
243
|
+
json.must_equal %{{"id":1,"title":"Tested","_links":{"self":{"href":"/songs/1"}}}}
|
244
|
+
|
245
|
+
|
246
|
+
#:full-call
|
247
|
+
def create
|
248
|
+
result = Create.(params, "document" => request.body.read)
|
249
|
+
|
250
|
+
if result.success?
|
251
|
+
result["representer.render.class"].new(result["model"]).to_json
|
252
|
+
else
|
253
|
+
result["representer.errors.class"].new(result["result.contract.default"]).to_json
|
254
|
+
end
|
255
|
+
end
|
256
|
+
#:full-call end
|
257
|
+
end
|
258
|
+
|
259
|
+
it do
|
260
|
+
result =Create.({}, "document" => '{"title": ""}')
|
261
|
+
|
262
|
+
if result.failure?
|
263
|
+
json = result["representer.errors.class"].new(result["result.contract.default"]).to_json
|
264
|
+
end
|
265
|
+
|
266
|
+
json.must_equal %{{"errors":[["title","can't be blank"]]}}
|
267
|
+
end
|
268
|
+
end
|