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