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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +10 -7
  3. data/CHANGES.md +108 -0
  4. data/COMM-LICENSE +91 -0
  5. data/Gemfile +18 -4
  6. data/LICENSE.txt +7 -20
  7. data/README.md +55 -15
  8. data/Rakefile +21 -2
  9. data/draft-1.2.rb +7 -0
  10. data/lib/trailblazer.rb +17 -4
  11. data/lib/trailblazer/dsl.rb +47 -0
  12. data/lib/trailblazer/operation/auto_inject.rb +47 -0
  13. data/lib/trailblazer/operation/builder.rb +18 -18
  14. data/lib/trailblazer/operation/callback.rb +31 -38
  15. data/lib/trailblazer/operation/contract.rb +46 -0
  16. data/lib/trailblazer/operation/controller.rb +45 -27
  17. data/lib/trailblazer/operation/guard.rb +24 -0
  18. data/lib/trailblazer/operation/model.rb +41 -33
  19. data/lib/trailblazer/operation/nested.rb +43 -0
  20. data/lib/trailblazer/operation/params.rb +13 -0
  21. data/lib/trailblazer/operation/persist.rb +13 -0
  22. data/lib/trailblazer/operation/policy.rb +26 -72
  23. data/lib/trailblazer/operation/present.rb +19 -0
  24. data/lib/trailblazer/operation/procedural/contract.rb +15 -0
  25. data/lib/trailblazer/operation/procedural/validate.rb +22 -0
  26. data/lib/trailblazer/operation/pundit.rb +42 -0
  27. data/lib/trailblazer/operation/representer.rb +25 -92
  28. data/lib/trailblazer/operation/rescue.rb +23 -0
  29. data/lib/trailblazer/operation/resolver.rb +18 -24
  30. data/lib/trailblazer/operation/validate.rb +50 -0
  31. data/lib/trailblazer/operation/wrap.rb +37 -0
  32. data/lib/trailblazer/version.rb +1 -1
  33. data/test/{operation/controller_test.rb → controller_test.rb} +8 -4
  34. data/test/docs/auto_inject_test.rb +30 -0
  35. data/test/docs/contract_test.rb +429 -0
  36. data/test/docs/dry_test.rb +31 -0
  37. data/test/docs/guard_test.rb +143 -0
  38. data/test/docs/nested_test.rb +117 -0
  39. data/test/docs/policy_test.rb +2 -0
  40. data/test/docs/pundit_test.rb +109 -0
  41. data/test/docs/representer_test.rb +268 -0
  42. data/test/docs/rescue_test.rb +153 -0
  43. data/test/docs/wrap_test.rb +174 -0
  44. data/test/gemfiles/Gemfile.ruby-1.9 +3 -0
  45. data/test/gemfiles/Gemfile.ruby-2.0 +12 -0
  46. data/test/gemfiles/Gemfile.ruby-2.3 +12 -0
  47. data/test/module_test.rb +22 -15
  48. data/test/operation/builder_test.rb +66 -18
  49. data/test/operation/callback_test.rb +70 -0
  50. data/test/operation/contract_test.rb +385 -15
  51. data/test/operation/dsl/callback_test.rb +18 -30
  52. data/test/operation/dsl/contract_test.rb +209 -19
  53. data/test/operation/dsl/representer_test.rb +42 -15
  54. data/test/operation/guard_test.rb +1 -147
  55. data/test/operation/model_test.rb +105 -0
  56. data/test/operation/params_test.rb +36 -0
  57. data/test/operation/persist_test.rb +44 -0
  58. data/test/operation/pipedream_test.rb +59 -0
  59. data/test/operation/pipetree_test.rb +104 -0
  60. data/test/operation/present_test.rb +24 -0
  61. data/test/operation/pundit_test.rb +104 -0
  62. data/test/{representer_test.rb → operation/representer_test.rb} +58 -42
  63. data/test/operation/resolver_test.rb +34 -70
  64. data/test/operation_test.rb +57 -189
  65. data/test/test_helper.rb +23 -3
  66. data/trailblazer.gemspec +8 -7
  67. metadata +91 -59
  68. data/gemfiles/Gemfile.rails.lock +0 -130
  69. data/gemfiles/Gemfile.reform-2.0 +0 -6
  70. data/gemfiles/Gemfile.reform-2.1 +0 -7
  71. data/lib/trailblazer/autoloading.rb +0 -15
  72. data/lib/trailblazer/endpoint.rb +0 -31
  73. data/lib/trailblazer/operation.rb +0 -175
  74. data/lib/trailblazer/operation/collection.rb +0 -6
  75. data/lib/trailblazer/operation/dispatch.rb +0 -3
  76. data/lib/trailblazer/operation/model/dsl.rb +0 -29
  77. data/lib/trailblazer/operation/model/external.rb +0 -34
  78. data/lib/trailblazer/operation/policy/guard.rb +0 -35
  79. data/lib/trailblazer/operation/uploaded_file.rb +0 -77
  80. data/test/callback_test.rb +0 -104
  81. data/test/collection_test.rb +0 -57
  82. data/test/model_test.rb +0 -148
  83. data/test/operation/external_model_test.rb +0 -71
  84. data/test/operation/policy_test.rb +0 -97
  85. data/test/operation/reject_test.rb +0 -34
  86. data/test/rollback_test.rb +0 -47
@@ -0,0 +1,24 @@
1
+ require "test_helper"
2
+
3
+ require "trailblazer/operation/present"
4
+
5
+ class PresentTest < Minitest::Spec
6
+ class Create < Trailblazer::Operation
7
+ include Test::ReturnCall
8
+ include Present
9
+
10
+ include Model::Builder
11
+ def model!(*); Object end
12
+
13
+ def call(params)
14
+ "#call run!"
15
+ end
16
+ end
17
+
18
+ it do
19
+ result = Create.present
20
+ result["model"].must_equal Object
21
+ end
22
+
23
+ it { Create.().must_equal "#call run!" }
24
+ end
@@ -0,0 +1,104 @@
1
+ require "test_helper"
2
+ require "trailblazer/operation/policy"
3
+
4
+ class PolicyTest < Minitest::Spec
5
+ Song = Struct.new(:id) do
6
+ def self.find(id); new(id) end
7
+ end
8
+
9
+ class Auth
10
+ def initialize(user, model); @user, @model = user, model end
11
+ def only_user?; @user == Module && @model.nil? end
12
+ def user_object?; @user == Object end
13
+ def user_and_model?; @user == Module && @model.class == Song end
14
+ def inspect; "<Auth: user:#{@user.inspect}, model:#{@model.inspect}>" end
15
+ end
16
+
17
+ #---
18
+ # Instance-level: Only policy, no model
19
+ class Create < Trailblazer::Operation
20
+ self.| Policy::Pundit( Auth, :only_user? )
21
+ self.| :process
22
+
23
+ def process(*)
24
+ self["process"] = true
25
+ end
26
+ end
27
+
28
+ # successful.
29
+ it do
30
+ result = Create.({}, "current_user" => Module)
31
+ result["process"].must_equal true
32
+ #- result object, policy
33
+ result["result.policy.default"].success?.must_equal true
34
+ result["result.policy.default"]["message"].must_equal nil
35
+ # result[:valid].must_equal nil
36
+ result["policy.default"].inspect.must_equal %{<Auth: user:Module, model:nil>}
37
+ end
38
+ # breach.
39
+ it do
40
+ result = Create.({}, "current_user" => nil)
41
+ result["process"].must_equal nil
42
+ #- result object, policy
43
+ result["result.policy.default"].success?.must_equal false
44
+ result["result.policy.default"]["message"].must_equal "Breach"
45
+ end
46
+ # inject different policy.Condition it { Create.({}, "current_user" => Object, "policy.default.eval" => Trailblazer::Operation::Policy::Pundit::Condition.new(Auth, :user_object?))["process"].must_equal true }
47
+ it { Create.({}, "current_user" => Module, "policy.default.eval" => Trailblazer::Operation::Policy::Pundit::Condition.new(Auth, :user_object?))["process"].must_equal nil }
48
+
49
+
50
+ #---
51
+ # inheritance, adding Model
52
+ class Show < Create
53
+ self.| Model( Song, :new ), before: "policy.default.eval"
54
+ end
55
+
56
+ it { Show["pipetree"].inspect.must_equal %{[>>operation.new,&model.build,&policy.default.eval,>process]} }
57
+
58
+ # invalid because user AND model.
59
+ it do
60
+ result = Show.({}, "current_user" => Module)
61
+ result["process"].must_equal nil
62
+ result["model"].inspect.must_equal %{#<struct PolicyTest::Song id=nil>}
63
+ # result["policy"].inspect.must_equal %{#<struct PolicyTest::Song id=nil>}
64
+ end
65
+
66
+ # valid because new policy.
67
+ it do
68
+ # puts Show["pipetree"].inspect
69
+ result = Show.({}, "current_user" => Module, "policy.default.eval" => Trailblazer::Operation::Policy::Pundit::Condition.new(Auth, :user_and_model?))
70
+ result["process"].must_equal true
71
+ result["model"].inspect.must_equal %{#<struct PolicyTest::Song id=nil>}
72
+ result["policy.default"].inspect.must_equal %{<Auth: user:Module, model:#<struct PolicyTest::Song id=nil>>}
73
+ end
74
+
75
+ ##--
76
+ # TOOOODOOO: Policy and Model before Build ("External" or almost Resolver)
77
+ class Edit < Trailblazer::Operation
78
+ self.| Model Song, :update
79
+ self.| Policy::Pundit( Auth, :user_and_model? )
80
+ self.| :process
81
+
82
+ def process(*); self["process"] = true end
83
+ end
84
+
85
+ # successful.
86
+ it do
87
+ result = Edit.({ id: 1 }, "current_user" => Module)
88
+ result["process"].must_equal true
89
+ result["model"].inspect.must_equal %{#<struct PolicyTest::Song id=1>}
90
+ result["result.policy.default"].success?.must_equal true
91
+ result["result.policy.default"]["message"].must_equal nil
92
+ # result[:valid].must_equal nil
93
+ result["policy.default"].inspect.must_equal %{<Auth: user:Module, model:#<struct PolicyTest::Song id=1>>}
94
+ end
95
+
96
+ # breach.
97
+ it do
98
+ result = Edit.({ id: 4 }, "current_user" => nil)
99
+ result["model"].inspect.must_equal %{#<struct PolicyTest::Song id=4>}
100
+ result["process"].must_equal nil
101
+ result["result.policy.default"].success?.must_equal false
102
+ result["result.policy.default"]["message"].must_equal "Breach"
103
+ end
104
+ end
@@ -1,5 +1,4 @@
1
1
  require "test_helper"
2
-
3
2
  require "representable/json"
4
3
 
5
4
  class RepresenterTest < MiniTest::Spec
@@ -7,8 +6,10 @@ class RepresenterTest < MiniTest::Spec
7
6
  Artist = Struct.new(:name)
8
7
 
9
8
  class Create < Trailblazer::Operation
10
- require "trailblazer/operation/representer"
9
+ include Contract::Explicit
11
10
  include Representer
11
+ include Representer::InferFromContract
12
+ attr_reader :model # FIXME: all we want is #model.
12
13
 
13
14
  contract do
14
15
  property :title
@@ -19,18 +20,19 @@ class RepresenterTest < MiniTest::Spec
19
20
  end
20
21
  end
21
22
 
22
- def process(params)
23
- @model = Album.new # NO artist!!!
24
- validate(params[:album], @model)
23
+ def call(params)
24
+ self["model"] = Album.new # NO artist!!!
25
+ validate(params[:album], model: self["model"])
26
+ self
25
27
  end
26
28
  end
27
29
 
28
30
 
29
31
  # Infers representer from contract, no customization.
30
32
  class Show < Create
31
- def process(params)
32
- @model = Album.new("After The War", Artist.new("Gary Moore"))
33
- @contract = @model
33
+ def call(params)
34
+ self["model"] = Album.new("After The War", Artist.new("Gary Moore"))
35
+ self
34
36
  end
35
37
  end
36
38
 
@@ -46,36 +48,32 @@ class RepresenterTest < MiniTest::Spec
46
48
  end
47
49
 
48
50
  class HypermediaShow < HypermediaCreate
49
- def process(params)
50
- @model = Album.new("After The War", Artist.new("Gary Moore"))
51
- @contract = @model
51
+ def call(params)
52
+ self["model"] = Album.new("After The War", Artist.new("Gary Moore"))
53
+ self
52
54
  end
53
55
  end
54
56
 
55
57
 
56
58
  # rendering
57
59
  # generic contract -> representer
58
- it do
59
- res, op = Show.run({})
60
- op.to_json.must_equal %{{"title":"After The War","artist":{"name":"Gary Moore"}}}
61
- end
60
+ it { Show.().to_json.must_equal %{{"title":"After The War","artist":{"name":"Gary Moore"}}} }
62
61
 
63
62
  # contract -> representer with hypermedia
64
63
  it do
65
- res, op = HypermediaShow.run({})
66
- op.to_json.must_equal %{{"title":"After The War","artist":{"name":"Gary Moore"},"_links":{"self":{"href":"//album/After The War"}}}}
64
+ HypermediaShow.().to_json.must_equal %{{"title":"After The War","artist":{"name":"Gary Moore"},"_links":{"self":{"href":"//album/After The War"}}}}
67
65
  end
68
66
 
69
67
 
70
68
  # parsing
71
69
  it do
72
- res, op = Create.run(album: %{{"title":"Run For Cover","artist":{"name":"Gary Moore"}}})
70
+ op = Create.(album: %{{"title":"Run For Cover","artist":{"name":"Gary Moore"}}})
73
71
  op.contract.title.must_equal "Run For Cover"
74
72
  op.contract.artist.name.must_equal "Gary Moore"
75
73
  end
76
74
 
77
75
  it do
78
- res, op = HypermediaCreate.run(album: %{{"title":"After The War","artist":{"name":"Gary Moore"},"_links":{"self":{"href":"//album/After The War"}}}})
76
+ op = HypermediaCreate.(album: %{{"title":"After The War","artist":{"name":"Gary Moore"},"_links":{"self":{"href":"//album/After The War"}}}})
79
77
  op.contract.title.must_equal "After The War"
80
78
  op.contract.artist.name.must_equal "Gary Moore"
81
79
  end
@@ -87,7 +85,9 @@ class RepresenterTest < MiniTest::Spec
87
85
  # explicit representer set with ::representer_class=.
88
86
  require "roar/decorator"
89
87
  class JsonApiCreate < Trailblazer::Operation
88
+ include Contract::Explicit
90
89
  include Representer
90
+ attr_reader :model
91
91
 
92
92
  contract do # we still need contract as the representer writes to the contract twin.
93
93
  property :title
@@ -97,31 +97,33 @@ class RepresenterTest < MiniTest::Spec
97
97
  include Roar::JSON
98
98
  property :title
99
99
  end
100
- self.representer_class = AlbumRepresenter
101
100
 
102
- def process(params)
103
- @model = Album.new # NO artist!!!
104
- validate(params[:album], @model)
101
+ # FIXME: this won't inherit, of course.
102
+ # self["representer.class"] = AlbumRepresenter
103
+ representer AlbumRepresenter
104
+
105
+ def call(params)
106
+ self["model"] = Album.new # NO artist!!!
107
+ validate(params[:album], model: self["model"])
108
+ self
105
109
  end
106
110
  end
107
111
 
108
112
  class JsonApiShow < JsonApiCreate
109
- def process(params)
110
- @model = Album.new("After The War", Artist.new("Gary Moore"))
111
- @contract = @model
113
+ def call(params)
114
+ self["model"] = Album.new("After The War", Artist.new("Gary Moore"))
115
+ self
112
116
  end
113
117
  end
114
118
 
115
119
  # render.
116
120
  it do
117
- res, op = JsonApiShow.run({})
118
- op.to_json.must_equal %{{"title":"After The War"}}
121
+ JsonApiShow.().to_json.must_equal %{{"title":"After The War"}}
119
122
  end
120
123
 
121
124
  # parse.
122
125
  it do
123
- res, op = JsonApiCreate.run(album: %{{"title":"Run For Cover"}})
124
- op.contract.title.must_equal "Run For Cover"
126
+ JsonApiCreate.(album: %{{"title":"Run For Cover"}}).contract.title.must_equal "Run For Cover"
125
127
  end
126
128
  end
127
129
 
@@ -130,26 +132,35 @@ class InternalRepresenterAPITest < MiniTest::Spec
130
132
 
131
133
  describe "#represented" do
132
134
  class Show < Trailblazer::Operation
135
+ include Contract::Explicit
133
136
  include Representer, Model
134
137
  model Song, :create
135
138
 
136
139
  representer do
137
140
  property :class
138
141
  end
142
+
143
+ def call(*)
144
+ self
145
+ end
146
+
147
+ def model # FIXME.
148
+ self["model"]
149
+ end
139
150
  end
140
151
 
141
152
  it "uses #model as represented, per default" do
142
- Show.present({}).to_json.must_equal '{"class":"InternalRepresenterAPITest::Song"}'
153
+ Show.({}).to_json.must_equal '{"class":"InternalRepresenterAPITest::Song"}'
143
154
  end
144
155
 
145
156
  class ShowContract < Show
146
157
  def represented
147
- contract
158
+ "Object"
148
159
  end
149
160
  end
150
161
 
151
162
  it "can be overriden to use the contract" do
152
- ShowContract.present({}).to_json.must_equal %{{"class":"#{ShowContract.contract_class}"}}
163
+ ShowContract.({}).to_json.must_equal %{{"class":"String"}}
153
164
  end
154
165
  end
155
166
 
@@ -163,16 +174,17 @@ class InternalRepresenterAPITest < MiniTest::Spec
163
174
  end
164
175
 
165
176
  def to_json(*)
166
- super(@params)
177
+ super(self["params"])
167
178
  end
168
179
 
169
- def model!(params)
180
+ include Model::Builder
181
+ def model!(*)
170
182
  Song.new(1)
171
183
  end
172
184
  end
173
185
 
174
186
  it "allows to pass options to #to_json" do
175
- OptionsShow.present(include: [:id]).to_json.must_equal '{"id":1}'
187
+ OptionsShow.(include: [:id]).to_json.must_equal %{{"id":1}}
176
188
  end
177
189
  end
178
190
  end
@@ -182,6 +194,7 @@ class DifferentParseAndRenderingRepresenterTest < MiniTest::Spec
182
194
 
183
195
  # rendering
184
196
  class Create < Trailblazer::Operation
197
+ include Contract::Explicit
185
198
  extend Representer::DSL
186
199
  include Representer::Rendering # no Deserializer::Hash here or anything.
187
200
 
@@ -193,11 +206,12 @@ class DifferentParseAndRenderingRepresenterTest < MiniTest::Spec
193
206
  property :title, as: :Title
194
207
  end
195
208
 
196
- def process(params)
197
- @model = Album.new
209
+ def call(params)
210
+ self["model"] = Album.new
198
211
  validate(params) do
199
212
  contract.sync
200
213
  end
214
+ self
201
215
  end
202
216
  end
203
217
 
@@ -207,6 +221,7 @@ class DifferentParseAndRenderingRepresenterTest < MiniTest::Spec
207
221
 
208
222
  # parsing
209
223
  class Update < Trailblazer::Operation
224
+ include Contract::Explicit
210
225
  extend Representer::DSL
211
226
  include Representer::Deserializer::Hash # no Rendering.
212
227
 
@@ -218,17 +233,18 @@ class DifferentParseAndRenderingRepresenterTest < MiniTest::Spec
218
233
  property :title
219
234
  end
220
235
 
221
-
222
- def process(params)
223
- @model = Album.new
236
+ def call(params)
237
+ self["model"] = Album.new
224
238
 
225
239
  validate(params) do
226
240
  contract.sync
227
241
  end
242
+
243
+ self
228
244
  end
229
245
 
230
246
  def to_json(*)
231
- %{{"title": "#{model.title}"}}
247
+ %{{"title": "#{self["model"].title}"}}
232
248
  end
233
249
  end
234
250
 
@@ -1,83 +1,47 @@
1
1
  require "test_helper"
2
- require "trailblazer/operation/resolver"
3
2
 
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
3
+ class ResolverTest < Minitest::Spec
4
+ Song = Struct.new(:id) do
5
+ def self.find(id); new(id) end
25
6
  end
26
7
 
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
8
+ class Auth
9
+ def initialize(*args); @user, @model = *args end
10
+ def only_user?; @user == Module && @model.nil? end
11
+ def user_object?; @user == Object end
12
+ def user_and_model?; @user == Module && @model.class == Song end
13
+ def inspect; "<Auth: user:#{@user.inspect}, model:#{@model.inspect}>" end
14
+ end
37
15
 
38
- def self.model!(params)
39
- Song.new(params[:title])
40
- end
16
+ class A < Trailblazer::Operation
17
+ extend Builder::DSL
18
+ builds ->(options) {
19
+ return P if options["params"] == { some: "params", id:1 }
20
+ return B if options["policy.default"].inspect == %{<Auth: user:Module, model:#<struct ResolverTest::Song id=3>>} # both user and model:id are set!
21
+ return M if options["model"].inspect == %{#<struct ResolverTest::Song id=9>}
22
+ }
41
23
 
42
- def process(*)
43
- end
24
+ self.| Model( Song, :update ), before: "operation.new"
25
+ self.| Policy::Pundit( Auth, :user_and_model? ), before: "operation.new"
26
+ require "trailblazer/operation/resolver"
27
+ self.| Resolver(), before: "operation.new"
44
28
 
45
- class Admin < self
46
- end
47
- class SignedIn < self
48
- end
49
- end
29
+ self.| :process
50
30
 
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 }
31
+ class P < self; end
32
+ class B < self; end
33
+ class M < self; end
55
34
 
56
- # invalid.
57
- it do
58
- assert_raises Trailblazer::NotAuthorizedError do
59
- Create.({})
60
- end
35
+ def process(*); self["x"] = self.class end
61
36
  end
62
37
 
38
+ it { A["pipetree"].inspect.must_equal %{[&model.build,&policy.default.eval,>>builder.call,>>operation.new,>process]} }
63
39
 
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
40
+ it { r=A.({ some: "params", id: 1 }, { "current_user" => Module })
41
+ puts r.inspect
74
42
 
75
- def process(*)
76
- end
77
- end
78
-
79
- it do
80
- Update.({}).policy.whoami.must_equal "me!"
81
- end
82
- end
83
- end
43
+ }
44
+ it { A.({ some: "params", id: 1 }, { "current_user" => Module })["x"].must_equal A::P }
45
+ it { A.({ id: 3 }, { "current_user" => Module })["x"].must_equal A::B }
46
+ it { A.({ id: 9 }, { "current_user" => Module })["x"].must_equal A::M }
47
+ end