trailblazer 1.1.2 → 2.0.0.beta1

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