trailblazer-macro 2.1.0.beta1 → 2.1.0.beta2

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.
data/CHANGES.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 2.1.0.beta2
2
+
3
+ * Move all code related to DSL (`ClassDependencies`) back to the `trailblazer` gem.
4
+ * Configurable field key for the `Model()` macro
5
+
1
6
  # 2.1.0.beta1
2
7
 
3
8
  * First release into an unsuspecting world. Goal is to have this gem decoupled from any Representable and Reform dependencies.
data/Gemfile CHANGED
@@ -7,7 +7,8 @@ gemspec
7
7
  # gem "trailblazer-operation", path: "../operation"
8
8
  # gem "trailblazer-operation", github: "trailblazer/trailblazer-operation"
9
9
  gem "trailblazer-activity"#, github: "trailblazer/trailblazer-activity"
10
- gem "trailblazer-macro-contract", git: "https://github.com/trailblazer/trailblazer-macro-contract"
10
+ # gem "trailblazer-macro-contract", git: "https://github.com/trailblazer/trailblazer-macro-contract"
11
+ gem "trailblazer-macro-contract", path: "../trailblazer-macro-contract"
11
12
 
12
13
  gem "minitest-line"
13
14
  gem "rubocop", require: false
data/README.md CHANGED
@@ -2,4 +2,274 @@
2
2
  All common Macro's for Trailblazer::Operation, will come here
3
3
 
4
4
  ## TODO
5
- Describe the Macro's that are included
5
+ Describe the following Macro's:
6
+ - Nested
7
+ - Rescue
8
+ - Wrap
9
+
10
+ ## Table of Contents
11
+ - [Model Macro](#model-macro)
12
+ - [Policy Macro](#policy-macro)
13
+ * [Policy::Pundit - Macro](#policy--pundit---macro)
14
+ + [Policy::Pundit - API](#policy--pundit---api)
15
+ + [Policy::Pundit - Name](#policy--pundit---name)
16
+ + [Policy::Pundit - Dependency Injection](#policy--pundit---dependency-injection)
17
+ * [Policy::Guard - Macro](#policy--guard---macro)
18
+ + [Policy::Guard - API](#policy--guard---api)
19
+ + [Policy::Guard - Callable](#policy--guard---callable)
20
+ + [Policy::Guard - Instance Method](#policy--guard---instance-method)
21
+ + [Policy::Guard - Name](#policy--guard---name)
22
+ + [Policy::Guard - Dependency Injection](#policy--guard---dependency-injection)
23
+ + [Policy::Guard - Position](#policy--guard---position)
24
+
25
+ ## Model Macro
26
+ Trailblazer also has a convenient Macro to handle model creation and basic finding by id. The Model macro literally does what our model! step did.
27
+
28
+ ```ruby
29
+ class Song::Create < Trailblazer::Operation
30
+ step Policy::Guard( :authorize! )
31
+ step Model( Song, :new )
32
+ end
33
+ ```
34
+
35
+ Note that Model is not designed for complex query logic - should you need that, you might want to use [Trailblazer Finder][trailblazer_finder_link] or simply write your own customized step.
36
+
37
+ Due to a lot of requests, we have adjusted the `:find_by` method so you can specify a key to find by.
38
+ ```ruby
39
+ class Song::Create < Trailblazer::Operation
40
+ step Policy::Guard( :authorize! )
41
+ step Model( Song, :find_by, :title )
42
+ end
43
+ ```
44
+ Not specifying the third parameter in the Model Macro for `:find_by`, will result in defaulting it back to `:id`.
45
+
46
+ [trailblazer-finder-link]: https://github.com/trailblazer/trailblazer-finder/
47
+
48
+ ## Policy Macro
49
+ An optional Policy Macro for Trailblazer Operations that blocks unauthorized users from running the operation.
50
+
51
+ You can abort running an operation using a policy. "Pundit-style" policy classes define the rules.
52
+ ```ruby
53
+ class Comment::Policy
54
+ def initialize(user, comment)
55
+ @user, @comment = user, comment
56
+ end
57
+
58
+ def create?
59
+ @user.admin?
60
+ end
61
+ end
62
+ ```
63
+
64
+ The rule is enabled via the ::policy call.
65
+ ```ruby
66
+ class Comment::Create < Trailblazer::Operation
67
+ step Policy( Comment::Policy, :create? )
68
+ end
69
+ ```
70
+
71
+ The policy is evaluated in #setup!, raises an exception if false and suppresses running #process.
72
+
73
+ ### Policy::Pundit - Macro
74
+ The Policy::Pundit Macro allows using Pundit-compatible policy classes in an operation.
75
+
76
+ A Pundit policy has various rule methods and a special constructor that receives the current user and the current model.
77
+ ```ruby
78
+ class MyPolicy
79
+ def initialize(user, model)
80
+ @user, @model = user, model
81
+ end
82
+
83
+ def create?
84
+ @user == Module && @model.id.nil?
85
+ end
86
+
87
+ def new?
88
+ @user == Class
89
+ end
90
+ end
91
+ ```
92
+
93
+ In pundit policies, it is a convention to have access to those objects at runtime and build rules on top of those.
94
+
95
+ You can plug this policy into your pipe at any point. However, this must be inserted after the "model" skill is available.
96
+ ```ruby
97
+ class Create < Trailblazer::Operation
98
+ step Model( Song, :new )
99
+ step Policy::Pundit( MyPolicy, :create? )
100
+ # ...
101
+ end
102
+ ```
103
+
104
+ Note that you don’t have to create the model via the Model macro - you can use any logic you want. The Pundit macro will grab the model from ["model"], though.
105
+
106
+ This policy will only pass when the operation is invoked as follows.
107
+ ```ruby
108
+ Create.( {}, "current_user" => User.find(1) )
109
+ ```
110
+
111
+ Any other call will cause a policy breach and stop the pipe from executing after the Policy::Pundit step.
112
+
113
+ #### Policy::Pundit - API
114
+ Add your polices using the Policy::Pundit macro. It accepts the policy class name, and the rule method to call.
115
+ ```ruby
116
+ class Create < Trailblazer::Operation
117
+ step Model( Song, :new )
118
+ step Policy::Pundit( MyPolicy, :create? )
119
+ # ...
120
+ end
121
+ ```
122
+
123
+ The step will create the policy instance automatically for you and passes the "model" and the "current_user" skill into the policies constructor. Just make sure those dependencies are available before the step is executed.
124
+
125
+ If the policy returns falsey, it deviates to the left track.
126
+
127
+ After running the Pundit step, its result is readable from the Result object.
128
+ ```ruby
129
+ result = Create.({}, "current_user" => Module)
130
+ result["result.policy.default"].success? #=> true
131
+ result["result.policy.default"]["policy"] #=> #<MyPolicy ...>
132
+ ```
133
+
134
+ Note that the actual policy instance is available via ["result.policy.#{name}"]["policy"] to be reinvoked with other rules (e.g. in the view layer).
135
+
136
+ #### Policy::Pundit - Name
137
+ You can add any number of Pundit policies to your pipe. Make sure to use name: to name them, though.
138
+ ```ruby
139
+ class Create < Trailblazer::Operation
140
+ step Model( Song, :new )
141
+ step Policy::Pundit( MyPolicy, :create?, name: "after_model" )
142
+ # ...
143
+ end
144
+ ```
145
+
146
+ The result will be stored in "result.policy.#{name}"
147
+ ```ruby
148
+ result = Create.({}, "current_user" => Module)
149
+ result["result.policy.after_model"].success? #=> true
150
+ ```
151
+
152
+ #### Policy::Pundit - Dependency Injection
153
+ Override a configured policy using dependency injection.
154
+ ```ruby
155
+ Create.({},
156
+ "current_user" => Module,
157
+ "policy.default.eval" => Trailblazer::Operation::Policy::Pundit.build(AnotherPolicy, :create?)
158
+ )
159
+ ```
160
+ You can inject it using "policy.#{name}.eval". It can be any object responding to call.
161
+
162
+ ### Policy::Guard - Macro
163
+ A guard is a step that helps you evaluating a condition and writing the result. If the condition was evaluated as falsey, the pipe won’t be further processed and a policy breach is reported in Result["result.policy.default"].
164
+
165
+ ```ruby
166
+ class Create < Trailblazer::Operation
167
+ step Policy::Guard( ->(options, params:, **) { params[:pass] } )
168
+ step :process
169
+
170
+ def process(*)
171
+ self["x"] = true
172
+ end
173
+ end
174
+ ```
175
+
176
+ The only way to make the above operation invoke the second step :process is as follows.
177
+ ```ruby
178
+ result = Create.({ pass: true })
179
+ result["x"] #=> true
180
+ ```
181
+
182
+ Any other input will result in an abortion of the pipe after the guard.
183
+ ```ruby
184
+ result = Create.()
185
+ result["x"] #=> nil
186
+ result["result.policy.default"].success? #=> false
187
+ ```
188
+
189
+ #### Policy::Guard - API
190
+ The Policy::Guard macro helps you inserting your guard logic. If not defined, it will be evaluated where you insert it.
191
+ ```ruby
192
+ class Create < Trailblazer::Operation
193
+ step Policy::Guard( ->(options, params:, **) { params[:pass] } )
194
+ # ...
195
+ end
196
+ ```
197
+ The options object is passed into the guard and allows you to read and inspect data like params or current_user. Please use kw args.
198
+
199
+ #### Policy::Guard - Callable
200
+ As always, the guard can also be a Callable-marked object.
201
+ ```ruby
202
+ class MyGuard
203
+ include Uber::Callable
204
+
205
+ def call(options, params:, **)
206
+ params[:pass]
207
+ end
208
+ end
209
+ ```
210
+
211
+ Insert the object instance via the Policy::Guard macro.
212
+ ```ruby
213
+ class Create < Trailblazer::Operation
214
+ step Policy::Guard( MyGuard.new )
215
+ # ...
216
+ end
217
+ ```
218
+
219
+ #### Policy::Guard - Instance Method
220
+ As always, you may also use an instance method to implement a guard.
221
+ ```ruby
222
+ class Create < Trailblazer::Operation
223
+ step Policy::Guard( :pass? )
224
+
225
+ def pass?(options, params:, **)
226
+ params[:pass]
227
+ end
228
+ # ...
229
+ end
230
+ ```
231
+
232
+ #### Policy::Guard - Name
233
+ The guard name defaults to default and can be set via name:. This allows having multiple guards.
234
+ ```ruby
235
+ class Create < Trailblazer::Operation
236
+ step Policy::Guard( ->(options, current_user:, **) { current_user }, name: :user )
237
+ # ...
238
+ end
239
+ ```
240
+
241
+ The result will sit in result.policy.#{name}.
242
+ ```ruby
243
+ result = Create.({}, "current_user" => true)
244
+ result["result.policy.user"].success? #=> true
245
+ ```
246
+
247
+ #### Policy::Guard - Dependency Injection
248
+ Instead of using the configured guard, you can inject any callable object that returns a Result object. Do so by overriding the policy.#{name}.eval path when calling the operation.
249
+ ```ruby
250
+ Create.({},
251
+ "current_user" => Module,
252
+ "policy.default.eval" => Trailblazer::Operation::Policy::Guard.build(->(options) { false })
253
+ )
254
+ ```
255
+ An easy way to let Trailblazer build a compatible object for you is using Guard.build.
256
+
257
+ This is helpful to override a certain policy for testing, or to invoke it with special rights, e.g. for an admin.
258
+
259
+ #### Policy::Guard - Position
260
+ You may specify a position.
261
+ ```ruby
262
+ class Create < Trailblazer::Operation
263
+ step :model!
264
+ step Policy::Guard( :authorize! ), before: :model!
265
+ end
266
+ ```
267
+
268
+ Resulting in the guard inserted before model!, even though it was added at a later point.
269
+ ```ruby
270
+ puts Create["pipetree"].inspect(style: :rows) #=>
271
+ # 0 ========================>operation.new
272
+ # 1 ==================>policy.default.eval
273
+ # 2 ===============================>model!
274
+ ```
275
+ This is helpful if you maintain modules for operations with generic steps.
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
3
  require "rubocop/rake_task"
4
4
 
5
- task :default => [:test]
5
+ task :default => %i[test rubocop]
6
6
 
7
7
  Rake::TestTask.new(:test) do |test|
8
8
  test.libs << 'test'
@@ -10,4 +10,8 @@ Rake::TestTask.new(:test) do |test|
10
10
  test.verbose = true
11
11
  end
12
12
 
13
- RuboCop::RakeTask.new
13
+ RuboCop::RakeTask.new(:rubocop) do |task|
14
+ task.patterns = ['lib/**/*.rb', 'test/**/*.rb']
15
+ task.options << "--display-cop-names"
16
+ task.fail_on_error = false
17
+ end
@@ -1,5 +1,5 @@
1
1
  module Trailblazer
2
2
  module Macro
3
- VERSION = "2.1.0.beta1".freeze
3
+ VERSION = "2.1.0.beta2"
4
4
  end
5
5
  end
@@ -1,11 +1,12 @@
1
1
  class Trailblazer::Operation
2
- def self.Model(model_class, action=nil)
2
+ def self.Model(model_class, action=nil, find_by_key=nil)
3
3
  task = Trailblazer::Activity::TaskBuilder::Binary.call(Model.new)
4
4
 
5
5
  extension = Trailblazer::Activity::TaskWrap::Merge.new(
6
6
  Wrap::Inject::Defaults(
7
- "model.class" => model_class,
8
- "model.action" => action
7
+ "model.class" => model_class,
8
+ "model.action" => action,
9
+ "model.find_by_key" => find_by_key
9
10
  )
10
11
  )
11
12
 
@@ -25,9 +26,10 @@ class Trailblazer::Operation
25
26
  def call(options, params)
26
27
  action = options["model.action"] || :new
27
28
  model_class = options["model.class"]
29
+ find_by_key = options["model.find_by_key"] || :id
28
30
  action = :pass_through unless %i[new find_by find].include?(action)
29
31
 
30
- send("#{action}!", model_class, params, options["model.action"])
32
+ send("#{action}!", model_class, params, options["model.action"], find_by_key)
31
33
  end
32
34
 
33
35
  def new!(model_class, params, *)
@@ -39,12 +41,12 @@ class Trailblazer::Operation
39
41
  end
40
42
 
41
43
  # Doesn't throw an exception and will return false to divert to Left.
42
- def find_by!(model_class, params, *)
43
- model_class.find_by(id: params[:id])
44
+ def find_by!(model_class, params, action, find_by_key, *)
45
+ model_class.find_by(find_by_key.to_sym => params[find_by_key])
44
46
  end
45
47
 
46
48
  # Call any method on the model class and pass :id.
47
- def pass_through!(model_class, params, action)
49
+ def pass_through!(model_class, params, action, *)
48
50
  model_class.send(action, params[:id])
49
51
  end
50
52
  end
@@ -2,8 +2,11 @@ require "test_helper"
2
2
 
3
3
  class DocsModelTest < Minitest::Spec
4
4
  Song = Struct.new(:id, :title) do
5
- def self.find_by(id:nil)
6
- id.nil? ? nil : new(id)
5
+ def self.find_by(args)
6
+ key, value = args.flatten
7
+ return nil if value.nil?
8
+ return new(value) if key == :id
9
+ new(2, value) if key == :title
7
10
  end
8
11
 
9
12
  def self.[](id)
@@ -27,6 +30,7 @@ class DocsModelTest < Minitest::Spec
27
30
  result[:model].inspect.must_equal %{#<struct DocsModelTest::Song id=nil, title=nil>}
28
31
  end
29
32
 
33
+
30
34
  #:update
31
35
  class Update < Trailblazer::Operation
32
36
  step Model( Song, :find_by )
@@ -34,22 +38,47 @@ class DocsModelTest < Minitest::Spec
34
38
  end
35
39
  #:update end
36
40
 
41
+ #:update-with-find-by-key
42
+ class UpdateWithFindByKey < Trailblazer::Operation
43
+ step Model( Song, :find_by, :title )
44
+ # ..
45
+ end
46
+ #:update-with-find-by-key end
47
+
37
48
  it do
38
49
  #:update-ok
39
50
  result = Update.(params: { id: 1 })
40
- result[:model] #=> #<struct Song id=1, title="Roxanne">
51
+ result[:model] #=> #<struct Song id=1, title="nil">
41
52
  #:update-ok end
42
53
 
43
54
  result[:model].inspect.must_equal %{#<struct DocsModelTest::Song id=1, title=nil>}
44
55
  end
45
56
 
57
+ it do
58
+ #:update-with-find-by-key-ok
59
+ result = UpdateWithFindByKey.(params: { title: "Test" } )
60
+ result[:model] #=> #<struct Song id=2, title="Test">
61
+ #:update-with-find-by-key-ok end
62
+
63
+ result[:model].inspect.must_equal %{#<struct DocsModelTest::Song id=2, title="Test">}
64
+ end
65
+
46
66
  it do
47
67
  #:update-fail
48
68
  result = Update.(params: {})
49
69
  result[:model] #=> nil
50
70
  result.success? #=> false
51
71
  #:update-fail end
72
+ result[:model].must_be_nil
73
+ result.success?.must_equal false
74
+ end
52
75
 
76
+ it do
77
+ #:update-with-find-by-key-fail
78
+ result = UpdateWithFindByKey.(params: {title: nil})
79
+ result[:model] #=> nil
80
+ result.success? #=> false
81
+ #:update-with-find-by-key-fail end
53
82
  result[:model].must_be_nil
54
83
  result.success?.must_equal false
55
84
  end
@@ -1,9 +1,14 @@
1
1
  require "test_helper"
2
2
 
3
3
  class ModelTest < Minitest::Spec
4
- Song = Struct.new(:id) do
4
+ Song = Struct.new(:id, :title) do
5
5
  def self.find(id); new(id) end
6
- def self.find_by(id:nil); id.nil? ? nil : new(id) end
6
+ def self.find_by(args)
7
+ key, value = args.flatten
8
+ return nil if value.nil?
9
+ return new(value) if key == :id
10
+ new(2, value) if key == :title
11
+ end
7
12
  end
8
13
 
9
14
  #---
@@ -13,7 +18,7 @@ class ModelTest < Minitest::Spec
13
18
  end
14
19
 
15
20
  # :new new.
16
- it { Create.(params: {})[:model].inspect.must_equal %{#<struct ModelTest::Song id=nil>} }
21
+ it { Create.(params: {})[:model].inspect.must_equal %{#<struct ModelTest::Song id=nil, title=nil>} }
17
22
 
18
23
  class Update < Create
19
24
  step Model( Song, :find ), override: true
@@ -23,7 +28,7 @@ class ModelTest < Minitest::Spec
23
28
  #- inheritance
24
29
 
25
30
  # :find it
26
- it { Update.(params: { id: 1 })[:model].inspect.must_equal %{#<struct ModelTest::Song id=1>} }
31
+ it { Update.(params: { id: 1 })[:model].inspect.must_equal %{#<struct ModelTest::Song id=1, title=nil>} }
27
32
 
28
33
  # inherited inspect is ok
29
34
  it { Trailblazer::Operation::Inspect.(Update).must_equal %{[>model.build]} }
@@ -37,6 +42,14 @@ class ModelTest < Minitest::Spec
37
42
  def process(options, **); options["x"] = true end
38
43
  end
39
44
 
45
+ # :find_by, exceptionless.
46
+ class FindByKey < Trailblazer::Operation
47
+ step Model( Song, :find_by, :title )
48
+ step :process
49
+
50
+ def process(options, **); options["x"] = true end
51
+ end
52
+
40
53
  # can't find model.
41
54
  #- result object, model
42
55
  it do
@@ -49,6 +62,21 @@ class ModelTest < Minitest::Spec
49
62
  it do
50
63
  Find.(params: {id: 9})["result.model"].success?.must_equal true
51
64
  Find.(params: {id: 9})["x"].must_equal true
52
- Find.(params: {id: 9})[:model].inspect.must_equal %{#<struct ModelTest::Song id=9>}
65
+ Find.(params: {id: 9})[:model].inspect.must_equal %{#<struct ModelTest::Song id=9, title=nil>}
66
+ end
67
+
68
+ # can't find model by title.
69
+ #- result object, model
70
+ it do
71
+ FindByKey.(params: {title: nil})["result.model"].failure?.must_equal true
72
+ FindByKey.(params: {title: nil})["x"].must_be_nil
73
+ FindByKey.(params: {title: nil}).failure?.must_equal true
74
+ end
75
+
76
+ #- result object, model by title
77
+ it do
78
+ FindByKey.(params: {title: "Test"})["result.model"].success?.must_equal true
79
+ FindByKey.(params: {title: "Test"})["x"].must_equal true
80
+ FindByKey.(params: {title: "Test"})[:model].inspect.must_equal %{#<struct ModelTest::Song id=2, title="Test">}
53
81
  end
54
82
  end