trailblazer 0.3.3 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGES.md +12 -0
  4. data/Gemfile +2 -1
  5. data/README.md +73 -38
  6. data/Rakefile +1 -1
  7. data/lib/trailblazer/autoloading.rb +4 -1
  8. data/lib/trailblazer/endpoint.rb +7 -13
  9. data/lib/trailblazer/operation.rb +54 -40
  10. data/lib/trailblazer/operation/builder.rb +26 -0
  11. data/lib/trailblazer/operation/collection.rb +1 -2
  12. data/lib/trailblazer/operation/controller.rb +36 -48
  13. data/lib/trailblazer/operation/dispatch.rb +11 -11
  14. data/lib/trailblazer/operation/model.rb +50 -0
  15. data/lib/trailblazer/operation/model/dsl.rb +29 -0
  16. data/lib/trailblazer/operation/model/external.rb +34 -0
  17. data/lib/trailblazer/operation/policy.rb +87 -0
  18. data/lib/trailblazer/operation/policy/guard.rb +34 -0
  19. data/lib/trailblazer/operation/representer.rb +33 -12
  20. data/lib/trailblazer/operation/resolver.rb +30 -0
  21. data/lib/trailblazer/operation/responder.rb +0 -1
  22. data/lib/trailblazer/operation/worker.rb +24 -7
  23. data/lib/trailblazer/version.rb +1 -1
  24. data/test/collection_test.rb +2 -1
  25. data/test/{crud_test.rb → model_test.rb} +17 -35
  26. data/test/operation/builder_test.rb +41 -0
  27. data/test/operation/dsl/callback_test.rb +108 -0
  28. data/test/operation/dsl/contract_test.rb +104 -0
  29. data/test/operation/dsl/representer_test.rb +143 -0
  30. data/test/operation/external_model_test.rb +71 -0
  31. data/test/operation/guard_test.rb +97 -0
  32. data/test/operation/policy_test.rb +97 -0
  33. data/test/operation/resolver_test.rb +83 -0
  34. data/test/operation_test.rb +7 -75
  35. data/test/rails/__respond_test.rb +20 -0
  36. data/test/rails/controller_test.rb +4 -102
  37. data/test/rails/endpoint_test.rb +7 -47
  38. data/test/rails/fake_app/controllers.rb +16 -21
  39. data/test/rails/fake_app/rails_app.rb +5 -0
  40. data/test/rails/fake_app/song/operations.rb +11 -4
  41. data/test/rails/respond_test.rb +95 -0
  42. data/test/responder_test.rb +6 -6
  43. data/test/rollback_test.rb +2 -2
  44. data/test/worker_test.rb +13 -9
  45. data/trailblazer.gemspec +2 -2
  46. metadata +38 -15
  47. data/lib/trailblazer/operation/crud.rb +0 -82
  48. data/lib/trailblazer/rails/railtie.rb +0 -34
  49. 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