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