trailblazer 0.3.3 → 1.0.0.rc1
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 +1 -1
- data/CHANGES.md +12 -0
- data/Gemfile +2 -1
- data/README.md +73 -38
- data/Rakefile +1 -1
- data/lib/trailblazer/autoloading.rb +4 -1
- data/lib/trailblazer/endpoint.rb +7 -13
- data/lib/trailblazer/operation.rb +54 -40
- data/lib/trailblazer/operation/builder.rb +26 -0
- data/lib/trailblazer/operation/collection.rb +1 -2
- data/lib/trailblazer/operation/controller.rb +36 -48
- data/lib/trailblazer/operation/dispatch.rb +11 -11
- data/lib/trailblazer/operation/model.rb +50 -0
- data/lib/trailblazer/operation/model/dsl.rb +29 -0
- data/lib/trailblazer/operation/model/external.rb +34 -0
- data/lib/trailblazer/operation/policy.rb +87 -0
- data/lib/trailblazer/operation/policy/guard.rb +34 -0
- data/lib/trailblazer/operation/representer.rb +33 -12
- data/lib/trailblazer/operation/resolver.rb +30 -0
- data/lib/trailblazer/operation/responder.rb +0 -1
- data/lib/trailblazer/operation/worker.rb +24 -7
- data/lib/trailblazer/version.rb +1 -1
- data/test/collection_test.rb +2 -1
- data/test/{crud_test.rb → model_test.rb} +17 -35
- data/test/operation/builder_test.rb +41 -0
- data/test/operation/dsl/callback_test.rb +108 -0
- data/test/operation/dsl/contract_test.rb +104 -0
- data/test/operation/dsl/representer_test.rb +143 -0
- data/test/operation/external_model_test.rb +71 -0
- data/test/operation/guard_test.rb +97 -0
- data/test/operation/policy_test.rb +97 -0
- data/test/operation/resolver_test.rb +83 -0
- data/test/operation_test.rb +7 -75
- data/test/rails/__respond_test.rb +20 -0
- data/test/rails/controller_test.rb +4 -102
- data/test/rails/endpoint_test.rb +7 -47
- data/test/rails/fake_app/controllers.rb +16 -21
- data/test/rails/fake_app/rails_app.rb +5 -0
- data/test/rails/fake_app/song/operations.rb +11 -4
- data/test/rails/respond_test.rb +95 -0
- data/test/responder_test.rb +6 -6
- data/test/rollback_test.rb +2 -2
- data/test/worker_test.rb +13 -9
- data/trailblazer.gemspec +2 -2
- metadata +38 -15
- data/lib/trailblazer/operation/crud.rb +0 -82
- data/lib/trailblazer/rails/railtie.rb +0 -34
- data/test/rails/fake_app/views/bands/show.html.erb +0 -1
@@ -0,0 +1,143 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "representable/json"
|
3
|
+
require "trailblazer/operation/representer"
|
4
|
+
|
5
|
+
class DslRepresenterTest < MiniTest::Spec
|
6
|
+
module SongProcess
|
7
|
+
def process(params)
|
8
|
+
@model = OpenStruct.new(params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def represented
|
12
|
+
model
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "inheritance across operations" do
|
17
|
+
class Operation < Trailblazer::Operation
|
18
|
+
include Representer
|
19
|
+
include Responder
|
20
|
+
include SongProcess
|
21
|
+
|
22
|
+
representer do
|
23
|
+
property :title
|
24
|
+
end
|
25
|
+
|
26
|
+
class JSON < self
|
27
|
+
representer do
|
28
|
+
property :band
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it { Operation.(title: "Nothing To Lose", band: "Gary Moore").to_json.must_equal %{{"title":"Nothing To Lose"}} }
|
34
|
+
# only the subclass must have the `band` field, even though it's set in the original operation.
|
35
|
+
it { Operation::JSON.(title: "Nothing To Lose", band: "Gary Moore").to_json.must_equal %{{"title":"Nothing To Lose","band":"Gary Moore"}} }
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "Op.representer" do
|
39
|
+
it { Operation.representer.must_equal Operation.representer_class }
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "Op.representer CommentRepresenter" do
|
43
|
+
class SongRepresenter < Representable::Decorator
|
44
|
+
include Representable::JSON
|
45
|
+
property :songTitle
|
46
|
+
end
|
47
|
+
|
48
|
+
class OpWithExternalRepresenter < Trailblazer::Operation
|
49
|
+
include Representer
|
50
|
+
include SongProcess
|
51
|
+
representer SongRepresenter
|
52
|
+
end
|
53
|
+
|
54
|
+
it { OpWithExternalRepresenter.("songTitle"=>"Listen To Your Heartbeat").to_json.must_equal %{{"songTitle":"Listen To Your Heartbeat"}} }
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "Op.representer CommentRepresenter do .. end" do
|
58
|
+
class HitRepresenter < Representable::Decorator
|
59
|
+
include Representable::JSON
|
60
|
+
property :title
|
61
|
+
end
|
62
|
+
|
63
|
+
class OpNotExtendingRepresenter < Trailblazer::Operation
|
64
|
+
include Representer
|
65
|
+
include SongProcess
|
66
|
+
representer HitRepresenter
|
67
|
+
end
|
68
|
+
|
69
|
+
class OpExtendingRepresenter < Trailblazer::Operation
|
70
|
+
include Representer
|
71
|
+
include SongProcess
|
72
|
+
representer HitRepresenter do
|
73
|
+
property :genre
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# this operation copies HitRepresenter and shouldn't have `genre`.
|
78
|
+
it do
|
79
|
+
OpNotExtendingRepresenter.("title"=>"Monsterparty", "genre"=>"Punk").to_json.must_equal %{{"title":"Monsterparty"}}
|
80
|
+
end
|
81
|
+
|
82
|
+
# # this operation copies HitRepresenter and extends it with the property `genre`.
|
83
|
+
it do
|
84
|
+
OpExtendingRepresenter.("title"=>"Monsterparty", "genre"=>"Punk").to_json.must_equal %{{"title":"Monsterparty","genre":"Punk"}}
|
85
|
+
end
|
86
|
+
|
87
|
+
# # of course, the original representer wasn't modified, either.
|
88
|
+
it do
|
89
|
+
HitRepresenter.new(OpenStruct.new(title: "Monsterparty", genre: "Punk")).to_json.must_equal %{{"title":"Monsterparty"}}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "Op.representer (inferring)" do
|
94
|
+
class OpWithContract < Trailblazer::Operation
|
95
|
+
include Representer
|
96
|
+
include SongProcess
|
97
|
+
|
98
|
+
contract do
|
99
|
+
property :songTitle
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class OpWithContract2 < Trailblazer::Operation
|
104
|
+
include Representer
|
105
|
+
include SongProcess
|
106
|
+
|
107
|
+
contract OpWithContract.contract
|
108
|
+
representer do
|
109
|
+
property :genre
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
it { OpWithContract.("songTitle"=>"Monsterparty", "genre"=>"Punk").to_json.must_equal %{{"songTitle":"Monsterparty"}} }
|
114
|
+
# this representer block extends the inferred from contract.
|
115
|
+
it { OpWithContract2.("songTitle"=>"Monsterparty", "genre"=>"Punk").to_json.must_equal %{{"songTitle":"Monsterparty","genre":"Punk"}} }
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "Op.representer_class" do
|
119
|
+
class PlayRepresenter < Representable::Decorator
|
120
|
+
include Representable::JSON
|
121
|
+
property :title
|
122
|
+
end
|
123
|
+
|
124
|
+
class OpSettingRepresenter < Trailblazer::Operation
|
125
|
+
include Representer
|
126
|
+
include SongProcess
|
127
|
+
self.representer_class= PlayRepresenter
|
128
|
+
end
|
129
|
+
|
130
|
+
class OpExtendRepresenter < Trailblazer::Operation
|
131
|
+
include Representer
|
132
|
+
include SongProcess
|
133
|
+
self.representer_class= PlayRepresenter
|
134
|
+
representer do
|
135
|
+
property :genre
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# both operations produce the same as the representer is shared, not copied.
|
140
|
+
it { OpSettingRepresenter.("title"=>"Monsterparty", "genre"=>"Punk").to_json.must_equal %{{"title":"Monsterparty","genre":"Punk"}} }
|
141
|
+
it { OpExtendRepresenter.("title"=>"Monsterparty", "genre"=>"Punk").to_json.must_equal %{{"title":"Monsterparty","genre":"Punk"}} }
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "trailblazer/operation/model/external"
|
3
|
+
|
4
|
+
class ExternalModelTest < MiniTest::Spec
|
5
|
+
Song = Struct.new(:title, :id) do
|
6
|
+
class << self
|
7
|
+
attr_accessor :find_result # TODO: eventually, replace with AR test.
|
8
|
+
attr_accessor :all_records
|
9
|
+
|
10
|
+
def find(id)
|
11
|
+
find_result.tap do |song|
|
12
|
+
song.id = id
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end # FIXME: use from CrudTest.
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
class Bla < Trailblazer::Operation
|
22
|
+
include Model::External
|
23
|
+
model Song, :update
|
24
|
+
|
25
|
+
def process(params)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
let (:song) { Song.new("Numbers") }
|
30
|
+
|
31
|
+
before do
|
32
|
+
Song.find_result = song
|
33
|
+
end
|
34
|
+
|
35
|
+
# ::model!
|
36
|
+
it do
|
37
|
+
Bla.model!(id: 1).must_equal song
|
38
|
+
song.id.must_equal 1
|
39
|
+
end
|
40
|
+
|
41
|
+
# call style.
|
42
|
+
it do
|
43
|
+
Bla.(id: 2).model.must_equal song
|
44
|
+
song.id.must_equal 2
|
45
|
+
end
|
46
|
+
|
47
|
+
# #present.
|
48
|
+
it do
|
49
|
+
Bla.present({}).model.must_equal song
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
class OpWithBuilder < Bla
|
54
|
+
class A < self
|
55
|
+
end
|
56
|
+
|
57
|
+
builds -> (model, params) do
|
58
|
+
return A if model.id == 1 and params[:user] == 2
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "::builds args" do
|
63
|
+
it do
|
64
|
+
OpWithBuilder.(id: 1, user: "different").must_be_instance_of OpWithBuilder
|
65
|
+
end
|
66
|
+
|
67
|
+
it do
|
68
|
+
OpWithBuilder.(id: 1, user: 2).must_be_instance_of OpWithBuilder::A
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "trailblazer/operation/policy"
|
3
|
+
|
4
|
+
class OpPolicyGuardTest < MiniTest::Spec
|
5
|
+
Song = Struct.new(:name)
|
6
|
+
|
7
|
+
class Create < Trailblazer::Operation
|
8
|
+
include Policy::Guard
|
9
|
+
|
10
|
+
def model!(*)
|
11
|
+
Song.new
|
12
|
+
end
|
13
|
+
|
14
|
+
policy do |params|
|
15
|
+
model.is_a?(Song) and params[:valid]
|
16
|
+
end
|
17
|
+
|
18
|
+
def process(*)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# valid.
|
23
|
+
it do
|
24
|
+
op = Create.(valid: true)
|
25
|
+
end
|
26
|
+
|
27
|
+
# invalid.
|
28
|
+
it do
|
29
|
+
assert_raises Trailblazer::NotAuthorizedError do
|
30
|
+
op = Create.(valid: false)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
describe "inheritance" do
|
36
|
+
class Update < Create
|
37
|
+
policy do |params|
|
38
|
+
params[:valid] == "correct"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Delete < Create
|
43
|
+
end
|
44
|
+
|
45
|
+
it do
|
46
|
+
Create.(valid: true).wont_equal nil
|
47
|
+
Delete.(valid: true).wont_equal nil
|
48
|
+
Update.(valid: "correct").wont_equal nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
describe "no policy defined, but included" do
|
54
|
+
class Show < Trailblazer::Operation
|
55
|
+
include Policy::Guard
|
56
|
+
|
57
|
+
def process(*)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
it { Show.({}).wont_equal nil }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
class OpBuilderDenyTest < MiniTest::Spec
|
67
|
+
Song = Struct.new(:name)
|
68
|
+
|
69
|
+
class Create < Trailblazer::Operation
|
70
|
+
include Deny
|
71
|
+
|
72
|
+
builds do |params|
|
73
|
+
deny! unless params[:valid]
|
74
|
+
end
|
75
|
+
|
76
|
+
def process(params)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Update < Create
|
81
|
+
builds -> (params) do
|
82
|
+
deny! unless params[:valid]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# valid.
|
87
|
+
it do
|
88
|
+
op = Create.(valid: true)
|
89
|
+
end
|
90
|
+
|
91
|
+
# invalid.
|
92
|
+
it do
|
93
|
+
assert_raises Trailblazer::NotAuthorizedError do
|
94
|
+
op = Create.(valid: false)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "trailblazer/operation/policy"
|
3
|
+
|
4
|
+
|
5
|
+
class OpPunditPolicyTest < MiniTest::Spec
|
6
|
+
Song = Struct.new(:name)
|
7
|
+
User = Struct.new(:name)
|
8
|
+
|
9
|
+
class BlaPolicy
|
10
|
+
def initialize(user, song)
|
11
|
+
@user = user
|
12
|
+
@song = song
|
13
|
+
end
|
14
|
+
|
15
|
+
def create?
|
16
|
+
@user.is_a?(User) and @song.is_a?(Song)
|
17
|
+
end
|
18
|
+
|
19
|
+
def edit?
|
20
|
+
"yepp"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class BlaOperation < Trailblazer::Operation
|
25
|
+
include Policy
|
26
|
+
policy BlaPolicy, :create?
|
27
|
+
|
28
|
+
def model!(*)
|
29
|
+
Song.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def process(*)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# valid.
|
37
|
+
it do
|
38
|
+
op = BlaOperation.({current_user: User.new})
|
39
|
+
|
40
|
+
# #policy provides the Policy instance.
|
41
|
+
op.policy.edit?.must_equal "yepp"
|
42
|
+
end
|
43
|
+
|
44
|
+
# invalid.
|
45
|
+
it do
|
46
|
+
assert_raises Trailblazer::NotAuthorizedError do
|
47
|
+
op = BlaOperation.({current_user: nil})
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
# no policy set
|
53
|
+
class NoPolicyOperation < Trailblazer::Operation
|
54
|
+
include Policy
|
55
|
+
# no policy.
|
56
|
+
|
57
|
+
def process(*)
|
58
|
+
@model = Song.new
|
59
|
+
end
|
60
|
+
|
61
|
+
class Delete < self
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
class LocalPolicy
|
66
|
+
def initialize(user, song)
|
67
|
+
@user = user
|
68
|
+
@song = song
|
69
|
+
end
|
70
|
+
|
71
|
+
def update?; false end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Update < self
|
75
|
+
policy LocalPolicy, :update?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# valid.
|
80
|
+
it do
|
81
|
+
op = NoPolicyOperation.({})
|
82
|
+
op.model.must_be_instance_of Song
|
83
|
+
end
|
84
|
+
|
85
|
+
# inherited without config works.
|
86
|
+
it do
|
87
|
+
op = NoPolicyOperation::Delete.({})
|
88
|
+
op.model.must_be_instance_of Song
|
89
|
+
end
|
90
|
+
|
91
|
+
# inherited can override.
|
92
|
+
it do
|
93
|
+
assert_raises Trailblazer::NotAuthorizedError do
|
94
|
+
op = NoPolicyOperation::Update.({})
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "trailblazer/operation/resolver"
|
3
|
+
|
4
|
+
class ResolverTest < MiniTest::Spec
|
5
|
+
Song = Struct.new(:title)
|
6
|
+
User = Struct.new(:name)
|
7
|
+
|
8
|
+
class MyKitchenRules
|
9
|
+
def initialize(user, song)
|
10
|
+
@user = user
|
11
|
+
@song = song
|
12
|
+
end
|
13
|
+
|
14
|
+
def create?
|
15
|
+
@user.is_a?(User) and @song.is_a?(Song)
|
16
|
+
end
|
17
|
+
|
18
|
+
def admin?
|
19
|
+
@user && @user.name == "admin" && @song.is_a?(Song)
|
20
|
+
end
|
21
|
+
|
22
|
+
def true?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Create < Trailblazer::Operation
|
28
|
+
include Resolver
|
29
|
+
model Song, :create
|
30
|
+
policy MyKitchenRules, :create?
|
31
|
+
|
32
|
+
builds-> (model, policy, params) do
|
33
|
+
return ForGaryMoore if model.title == "Friday On My Mind"
|
34
|
+
return Admin if policy.admin?
|
35
|
+
return SignedIn if params[:current_user] && params[:current_user].name
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.model!(params)
|
39
|
+
Song.new(params[:title])
|
40
|
+
end
|
41
|
+
|
42
|
+
def process(*)
|
43
|
+
end
|
44
|
+
|
45
|
+
class Admin < self
|
46
|
+
end
|
47
|
+
class SignedIn < self
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# valid.
|
52
|
+
it { Create.({current_user: User.new}).must_be_instance_of Create }
|
53
|
+
it { Create.({current_user: User.new("admin")}).must_be_instance_of Create::Admin }
|
54
|
+
it { Create.({current_user: User.new("kenneth")}).must_be_instance_of Create::SignedIn }
|
55
|
+
|
56
|
+
# invalid.
|
57
|
+
it do
|
58
|
+
assert_raises Trailblazer::NotAuthorizedError do
|
59
|
+
Create.({})
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
describe "passes policy into operation" do
|
65
|
+
class Update < Trailblazer::Operation
|
66
|
+
include Resolver
|
67
|
+
model Song, :create
|
68
|
+
policy MyKitchenRules, :true?
|
69
|
+
|
70
|
+
builds-> (model, policy, params) do
|
71
|
+
policy.instance_eval { def whoami; "me!" end }
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def process(*)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it do
|
80
|
+
Update.({}).policy.whoami.must_equal "me!"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|