rails_ops 1.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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +84 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +1216 -0
- data/RUBY_VERSION +1 -0
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/lib/rails_ops.rb +96 -0
- data/lib/rails_ops/authorization_backend/abstract.rb +7 -0
- data/lib/rails_ops/authorization_backend/can_can_can.rb +14 -0
- data/lib/rails_ops/configuration.rb +4 -0
- data/lib/rails_ops/context.rb +35 -0
- data/lib/rails_ops/controller_mixin.rb +105 -0
- data/lib/rails_ops/exceptions.rb +19 -0
- data/lib/rails_ops/hooked_job.rb +25 -0
- data/lib/rails_ops/hookup.rb +80 -0
- data/lib/rails_ops/hookup/dsl.rb +29 -0
- data/lib/rails_ops/hookup/dsl_validator.rb +45 -0
- data/lib/rails_ops/hookup/hook.rb +11 -0
- data/lib/rails_ops/log_subscriber.rb +24 -0
- data/lib/rails_ops/mixins.rb +2 -0
- data/lib/rails_ops/mixins/authorization.rb +83 -0
- data/lib/rails_ops/mixins/log_settings.rb +20 -0
- data/lib/rails_ops/mixins/model.rb +4 -0
- data/lib/rails_ops/mixins/model/authorization.rb +64 -0
- data/lib/rails_ops/mixins/model/nesting.rb +180 -0
- data/lib/rails_ops/mixins/policies.rb +42 -0
- data/lib/rails_ops/mixins/require_context.rb +33 -0
- data/lib/rails_ops/mixins/routes.rb +35 -0
- data/lib/rails_ops/mixins/schema_validation.rb +25 -0
- data/lib/rails_ops/mixins/sub_ops.rb +35 -0
- data/lib/rails_ops/model_casting.rb +17 -0
- data/lib/rails_ops/model_mixins.rb +12 -0
- data/lib/rails_ops/model_mixins/ar_extension.rb +20 -0
- data/lib/rails_ops/model_mixins/parent_op.rb +10 -0
- data/lib/rails_ops/model_mixins/protected_attributes.rb +78 -0
- data/lib/rails_ops/model_mixins/virtual_attributes.rb +24 -0
- data/lib/rails_ops/model_mixins/virtual_attributes/virtual_column_wrapper.rb +9 -0
- data/lib/rails_ops/model_mixins/virtual_has_one.rb +32 -0
- data/lib/rails_ops/operation.rb +215 -0
- data/lib/rails_ops/operation/model.rb +168 -0
- data/lib/rails_ops/operation/model/create.rb +35 -0
- data/lib/rails_ops/operation/model/destroy.rb +26 -0
- data/lib/rails_ops/operation/model/load.rb +72 -0
- data/lib/rails_ops/operation/model/update.rb +31 -0
- data/lib/rails_ops/patches/active_type_patch.rb +52 -0
- data/lib/rails_ops/profiler.rb +47 -0
- data/lib/rails_ops/profiler/node.rb +64 -0
- data/lib/rails_ops/railtie.rb +19 -0
- data/lib/rails_ops/scoped_env.rb +20 -0
- data/lib/rails_ops/virtual_model.rb +19 -0
- data/rails_ops.gemspec +58 -0
- data/test/test_helper.rb +3 -0
- metadata +252 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c520181f345d34d12b964e8da4b79764c3d352f2
|
4
|
+
data.tar.gz: daeceb586c2c8ed3571c5e997fc0af5a590bb908
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cc063fc63eac0e8252f098dd6b92245dea2128e84548eeb352cfd3655f866e069474b9fbc06ddef040f51e7147316f852f856cc6149f0739ad0aaaa8c331bd5b
|
7
|
+
data.tar.gz: b2e5f85b9c5e3830948028e56cb2a6bf243a4a605291c3087d1b41993b794c378b6f139dbc4f2a14fc73b960e9008df9809dd73bcd2e81fd96c9e6cbe156fe0f
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.3
|
3
|
+
|
4
|
+
Exclude:
|
5
|
+
- 'vendor/**/*'
|
6
|
+
- 'tmp/**/*'
|
7
|
+
- 'log/**/*'
|
8
|
+
- '*.gemspec'
|
9
|
+
|
10
|
+
DisplayCopNames: true
|
11
|
+
|
12
|
+
Style/FrozenStringLiteralComment:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Style/DoubleNegation:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Style/SignalException:
|
19
|
+
EnforcedStyle: only_fail
|
20
|
+
|
21
|
+
Style/ConditionalAssignment:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Style/IndentArray:
|
25
|
+
EnforcedStyle: consistent
|
26
|
+
|
27
|
+
Metrics/MethodLength:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
Metrics/ClassLength:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
Metrics/ModuleLength:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Metrics/ParameterLists:
|
37
|
+
Max: 5
|
38
|
+
CountKeywordArgs: false
|
39
|
+
|
40
|
+
Metrics/AbcSize:
|
41
|
+
Enabled: False
|
42
|
+
|
43
|
+
Metrics/CyclomaticComplexity:
|
44
|
+
Enabled: False
|
45
|
+
|
46
|
+
Metrics/PerceivedComplexity:
|
47
|
+
Enabled: False
|
48
|
+
|
49
|
+
Metrics/LineLength:
|
50
|
+
Max: 160
|
51
|
+
|
52
|
+
Metrics/BlockNesting:
|
53
|
+
Enabled: false
|
54
|
+
|
55
|
+
Metrics/BlockLength:
|
56
|
+
Enabled: false
|
57
|
+
|
58
|
+
Style/IfUnlessModifier:
|
59
|
+
Enabled: false
|
60
|
+
|
61
|
+
Style/Documentation:
|
62
|
+
Enabled: false
|
63
|
+
|
64
|
+
Style/RedundantReturn:
|
65
|
+
Enabled: false
|
66
|
+
|
67
|
+
Style/AsciiComments:
|
68
|
+
Enabled: false
|
69
|
+
|
70
|
+
Style/GuardClause:
|
71
|
+
Enabled: false
|
72
|
+
|
73
|
+
Style/ClassAndModuleChildren:
|
74
|
+
Enabled: false
|
75
|
+
EnforcedStyle: compact
|
76
|
+
SupportedStyles:
|
77
|
+
- nested
|
78
|
+
- compact
|
79
|
+
|
80
|
+
Style/NumericPredicate:
|
81
|
+
Enabled: false
|
82
|
+
|
83
|
+
Style/FormatString:
|
84
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Sitrox
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,1216 @@
|
|
1
|
+
rails_ops
|
2
|
+
=========
|
3
|
+
|
4
|
+
**This Gem is still under development and is not to be used in production yet.**
|
5
|
+
|
6
|
+
This Gem introduces an additional service layer for Rails: *Operations*. An
|
7
|
+
operation is in most cases a *business action* or *use case* and may or may not
|
8
|
+
involve one or multiple models. Rails Ops allow creating more modular
|
9
|
+
applications by splitting them up into its different operations. Each operation
|
10
|
+
is specified in a single, testable class.
|
11
|
+
|
12
|
+
To achieve this goal, this Gem provides the following building blocks:
|
13
|
+
|
14
|
+
- Various operation base classes for creating operations with a consistent
|
15
|
+
interface and no boilerplate code.
|
16
|
+
|
17
|
+
- A way of abstracting model classes for a specific business action.
|
18
|
+
|
19
|
+
Operation Basics
|
20
|
+
----------------
|
21
|
+
|
22
|
+
### Placing and naming operations
|
23
|
+
|
24
|
+
- Operations generally reside in `app/operations` and can be nested using various
|
25
|
+
subdirectories. They're all inside of the `Operations` namespace.
|
26
|
+
|
27
|
+
- Operations operating on a specific model should generally be namespaced with
|
28
|
+
the model's class name. So for instance, the operation `Create` for the `User`
|
29
|
+
model should generally live under `app/operations/user/create.rb` and therefore
|
30
|
+
be called `Operations::User::Create`.
|
31
|
+
|
32
|
+
- Operations inheriting from other operations should generally be nested within
|
33
|
+
their parent operation. See the next section for more details.
|
34
|
+
|
35
|
+
- Operation classes should always be named after an *action*, such as `Create`,
|
36
|
+
`MoveToPosition` and so on. Do not name an operation something like
|
37
|
+
`UserCreator` or `CreateUserOperation`.
|
38
|
+
|
39
|
+
#### Heads-up: Correct namespacing
|
40
|
+
|
41
|
+
As explained in the previous section, operations should be namespaced properly.
|
42
|
+
Operations can either live within a module or within a class. In most cases,
|
43
|
+
operations are placed in the `Operation` module or rather one of its
|
44
|
+
sub-modules. If, in some special case, operations are nested, they can reside
|
45
|
+
inside of another operation class (but not inside of its file) as well.
|
46
|
+
|
47
|
+
When declaring an operation within a namespace,
|
48
|
+
|
49
|
+
- Determine whether the namespace you're using is a module or a class. Make sure
|
50
|
+
you don't accidentally redefine a module as a class or vice-versa.
|
51
|
+
|
52
|
+
- If the operation resides within a module, make a module definition on the first
|
53
|
+
line and the operation class on the second. Example:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
module Operations::Frontend::Navigation
|
57
|
+
class DetermineActionsForStructureElement < RailsOps::Operation
|
58
|
+
...
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
- If the operation resides within a class, use a single-line definition:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class Operations::User::Create::FromApi < Operations::User::Create
|
67
|
+
...
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
Note that, when defining a namespace of which a segment is already known as a
|
72
|
+
(model) class, you cannot just use the model classes name to refer to it:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
module Operations::User
|
76
|
+
class Create < RailsOps::Operation
|
77
|
+
def perform
|
78
|
+
# This DOES NOT work as `User` in this case refers to the module of
|
79
|
+
# the same name defined on the first line of code.
|
80
|
+
User.create(params)
|
81
|
+
|
82
|
+
# This works as it takes an absolute namespace:
|
83
|
+
::User.create(params)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
### Basic operations
|
90
|
+
|
91
|
+
Every single operation follows a few basic principles:
|
92
|
+
|
93
|
+
- They inherit from {RailsOps::Operation}.
|
94
|
+
|
95
|
+
- They are called using the `run` or `run!` methods.
|
96
|
+
|
97
|
+
- They are parameterized using a `params` hash (and nothing else).
|
98
|
+
|
99
|
+
- They define a protected `perform` method which actually executes the
|
100
|
+
operation. This is usually overridden in each operation and called exclusively
|
101
|
+
by `run` or `run!`.
|
102
|
+
|
103
|
+
- They have a *Context*. See the respective chapter for more information.
|
104
|
+
|
105
|
+
So, an example of a very simple operation would be:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
class Operations::PrintHelloWorld < RailsOps::Operation
|
109
|
+
def perform
|
110
|
+
puts "Hello #{params[:name]}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
### Running operations manually
|
116
|
+
|
117
|
+
There are various ways of instantiating and running an operation. The most
|
118
|
+
basic way is the following:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
op = Operations::PrintHelloWorld.new(name: 'John Doe')
|
122
|
+
op.run
|
123
|
+
```
|
124
|
+
|
125
|
+
There is even a shortcut for this:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
Operations::PrintHelloWorld.run(name: 'John Doe')
|
129
|
+
```
|
130
|
+
|
131
|
+
### Validations, `run` and `run!`
|
132
|
+
|
133
|
+
As you have noticed, there are two methods for running operations: `run` and
|
134
|
+
`run!`. They behave exactly like `save` and `save!` of ActiveRecord: While the
|
135
|
+
`run!` method raises an exception if there is a validation error, `run` would
|
136
|
+
just return `false` (or `true` on success). As not every operation deals with
|
137
|
+
models or ActiveRecord models, `run` does not only catch the
|
138
|
+
`ActiveRecord::RecordInvalid` exception but also every exception that derives
|
139
|
+
from {RailsOps::Exceptions::ValidationFailed}.
|
140
|
+
|
141
|
+
#### Catching custom exceptions in `run`
|
142
|
+
|
143
|
+
If you'd like to catch a custom exception if the operation is called using
|
144
|
+
`run`, you can either derive this exception from
|
145
|
+
{RailsOps::Exceptions::ValidationFailed} or else override the
|
146
|
+
`validation_errors` method:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
class Operations::PrintHelloWorld < RailsOps::Operation
|
150
|
+
# Returns an array of exception classes that are considered as validation
|
151
|
+
# errors.
|
152
|
+
def validation_errors
|
153
|
+
super + [SomeCustomException]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
### Returning data from operations
|
159
|
+
|
160
|
+
All operations have the same call signatures: `run` always returns `true` or
|
161
|
+
`false` while `run!` always returns the operation instance (which allows easy
|
162
|
+
chaining). If you need to access data that has been generated / processed /
|
163
|
+
fetched in the operation, create custom accessor methods:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
class Operations::GenerateHelloWorld < RailsOps::Operation
|
167
|
+
attr_reader :result
|
168
|
+
|
169
|
+
def perform
|
170
|
+
@result = "Hello #{params[:name]}"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
puts Operations::GenerateHelloWorld.run!(name: 'John Doe').result
|
175
|
+
```
|
176
|
+
|
177
|
+
Params Handling
|
178
|
+
---------------
|
179
|
+
|
180
|
+
## Passing params to operations
|
181
|
+
|
182
|
+
Each single operation can take a `params` hash. Note that it does not have to be
|
183
|
+
in any relation with `ActionController`'s params - it's just a plain ruby hash
|
184
|
+
called `params`.
|
185
|
+
|
186
|
+
Params are assigned to the operation via their constructor:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
Operations::GenerateHelloWorld.new(foo: :bar)
|
190
|
+
```
|
191
|
+
|
192
|
+
If no params are given, an empty params hash will be used. If a
|
193
|
+
`ActionController::Parameters` object is passed, it will be permitted using
|
194
|
+
`permit!` and converted into a regular hash.
|
195
|
+
|
196
|
+
## Accessing params
|
197
|
+
|
198
|
+
For accessing params within an operation, you can use `params` or `osparams`.
|
199
|
+
While `params` directly returns the params hash, `osparams` converts them into
|
200
|
+
an `OpenStruct` first. This allows easy access using the 'dotted notation':
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
def perform
|
204
|
+
# Access a param using the `params` method
|
205
|
+
params[:foo]
|
206
|
+
|
207
|
+
# Access a param using the `osparams` method
|
208
|
+
osparams.foo
|
209
|
+
end
|
210
|
+
```
|
211
|
+
|
212
|
+
Note that both `params` and `osparams` return independent, deep duplicates of
|
213
|
+
the original `params` hash to the operation, so the hashes do not correspond.
|
214
|
+
|
215
|
+
The hash accessed via `params` is a always `Object::HashWithIndifferentAccess`.
|
216
|
+
|
217
|
+
## Validating params
|
218
|
+
|
219
|
+
You're strongly encouraged to perform a validation of the parameters passed to
|
220
|
+
an operation. This can be done in several ways:
|
221
|
+
|
222
|
+
- Manually using a *policy* (see chapter *Policies*):
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
class Operations::PrintHelloWorld < RailsOps::Operation
|
226
|
+
policy do
|
227
|
+
unless osparams.name && osparams.name.is_a?(String)
|
228
|
+
fail 'You must supply the "name" argument.'
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def perform
|
233
|
+
puts "Hello #{params[:name]}"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
238
|
+
- Using a [schemacop](https://github.com/sitrox/schemacop) schema:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
class Operations::PrintHelloWorld < RailsOps::Operation
|
242
|
+
schema do
|
243
|
+
req :name, :string
|
244
|
+
end
|
245
|
+
|
246
|
+
def perform
|
247
|
+
puts "Hello #{params[:name]}"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
This is the preferred way of performing basic params validation when not using
|
253
|
+
model validation (see next item).
|
254
|
+
|
255
|
+
See documentation of the Gem `schemacop` for more information on how to
|
256
|
+
specify schemata.
|
257
|
+
|
258
|
+
- Using a business model (see chapter *Model Operations*).
|
259
|
+
|
260
|
+
Policies
|
261
|
+
--------
|
262
|
+
|
263
|
+
Policies are nothing more than blocks of code that run either at operation
|
264
|
+
instantiation or before / after execution of the `perform` method and can be
|
265
|
+
used to check conditions such as params or permissions.
|
266
|
+
|
267
|
+
Policies are specified using the static method `policy`, inherited to any
|
268
|
+
sub-classes and executed in the order they were defined.
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
class Operations::PrintHelloWorld < RailsOps::Operation
|
272
|
+
policy do
|
273
|
+
puts 'This runs first'
|
274
|
+
end
|
275
|
+
|
276
|
+
policy do
|
277
|
+
puts 'This runs second'
|
278
|
+
end
|
279
|
+
|
280
|
+
def perform
|
281
|
+
puts 'This runs third'
|
282
|
+
puts 'Oh, and hello world'
|
283
|
+
end
|
284
|
+
end
|
285
|
+
```
|
286
|
+
|
287
|
+
The basic idea of policies is to validate input data (the `params` hash) or
|
288
|
+
other conditions such as authorizations or locks.
|
289
|
+
|
290
|
+
Some checks might still need to be performed directly within the `perform`
|
291
|
+
method. Use policies as much as possible though to keep things separated.
|
292
|
+
|
293
|
+
The return value of the policies is discarded. If a policy needs to fail, raise
|
294
|
+
an appropriate exception.
|
295
|
+
|
296
|
+
### Policy chains
|
297
|
+
|
298
|
+
As mentioned above, policies can be executed at 3 points in your operation's
|
299
|
+
lifecycle. This is possible using *policy chains*:
|
300
|
+
|
301
|
+
- `:on_init`
|
302
|
+
|
303
|
+
Policies in this chain run after the operation class is instantiated.
|
304
|
+
|
305
|
+
- `:before_perform`
|
306
|
+
|
307
|
+
Policies in this chain run immediately before the `perform` method is called.
|
308
|
+
Obviously this is never called if the operation is just instantiated and never
|
309
|
+
run. This is the default chain.
|
310
|
+
|
311
|
+
- `:after_perform`
|
312
|
+
|
313
|
+
Policies in this chain run immediately after the `perform` method is called.
|
314
|
+
Obviously this is never called if the operation is just instantiated and never
|
315
|
+
run.
|
316
|
+
|
317
|
+
The policy chain (default is `:before_perform`) can be specified as the first
|
318
|
+
argument of the `policy` class method:
|
319
|
+
|
320
|
+
```ruby
|
321
|
+
class MyOp
|
322
|
+
policy :on_init do
|
323
|
+
puts 'This is run once the operation has been instantiated.'
|
324
|
+
end
|
325
|
+
|
326
|
+
policy do
|
327
|
+
puts 'This is run before the operation is performed.'
|
328
|
+
end
|
329
|
+
end
|
330
|
+
```
|
331
|
+
|
332
|
+
Calling sub-operations
|
333
|
+
----------------------
|
334
|
+
|
335
|
+
It is possible and encouraged to call operations within operations if necessary.
|
336
|
+
As the basic principle is to create one operation per business action, there are
|
337
|
+
cases where nesting operations can be very beneficial.
|
338
|
+
|
339
|
+
Let's say we have an operation `User::Create` that creates a new user. The
|
340
|
+
operation should also assign the newly created user to a default `Group` after
|
341
|
+
creation. In this case, we basically have two separate operations that should
|
342
|
+
not be combined in one. For this case, use sub-operations:
|
343
|
+
|
344
|
+
```ruby
|
345
|
+
class Operations::User::Create < RailsOps::Operation
|
346
|
+
def perform
|
347
|
+
user = User.create(params)
|
348
|
+
run_sub! AssignToGroup, user: user, group: Group.default
|
349
|
+
end
|
350
|
+
end
|
351
|
+
```
|
352
|
+
|
353
|
+
Every operation offers the methods {RailsOps::Mixins::SubOps.run_sub},
|
354
|
+
{RailsOps::Mixins::SubOps.run_sub!} and {RailsOps::Mixins::SubOps.sub_op}. The
|
355
|
+
latter one just instantiates and returns a sub operation.
|
356
|
+
|
357
|
+
So why don't we just create and call the sub-operation directly? The reason lies
|
358
|
+
within the context that is automatically adapted and passed to the sub-operation
|
359
|
+
and enables to maintain the complete call stack and allows to pass on context
|
360
|
+
information such as the current user.
|
361
|
+
|
362
|
+
### A note on validations
|
363
|
+
|
364
|
+
As always when calling operations, you can decide whether an execution should
|
365
|
+
raise an exception on validation errors or else just return `false` by using the
|
366
|
+
bang or non-bang methods.
|
367
|
+
|
368
|
+
For nested operations, we must give this fact a little more thought. Consider
|
369
|
+
the following case:
|
370
|
+
|
371
|
+
- Operation *A* is called using `run`.
|
372
|
+
- Operation *A* calls operation *B* using `run_sub!`.
|
373
|
+
- Operation *B* throws a validation exception.
|
374
|
+
|
375
|
+
In this case, it is now expected that *A* returns non-gracefully, even though
|
376
|
+
it's called using the non-bang method. The reason is that *A* explicitly used
|
377
|
+
the bang-method for calling the sub-op.
|
378
|
+
|
379
|
+
However, as calling *A* catches any validation errors, it will also catch the
|
380
|
+
validation errors raised by a sub-operation. For this case, calling `run_sub!`
|
381
|
+
catches any validation errors and re-throws them as
|
382
|
+
{RailsOps::Exceptions::SubOpValidationFailed} which is not caught by the
|
383
|
+
surrounding op.
|
384
|
+
|
385
|
+
Contexts
|
386
|
+
--------
|
387
|
+
|
388
|
+
Most operations make use of generic parameters like the current user or an
|
389
|
+
authorization ability. Sure this could all be passed using the `params` hash,
|
390
|
+
but as this would have to be done for every single operation call, it would be
|
391
|
+
quite cumbersome.
|
392
|
+
|
393
|
+
For this reason Rails Ops provides a feature called *Contexts*. Contexts are
|
394
|
+
simple instances of {RailsOps::Context} that may or may not be passed to
|
395
|
+
operations. Contexts can include the following data:
|
396
|
+
|
397
|
+
- A user object
|
398
|
+
|
399
|
+
This is meant to be the user performing the operation. In a controller
|
400
|
+
context, this usually referred to as `current_user`.
|
401
|
+
|
402
|
+
- The session object
|
403
|
+
|
404
|
+
This is the rails `session` object (can be nil).
|
405
|
+
|
406
|
+
- An ability object
|
407
|
+
|
408
|
+
This is an ability object (i.e. cancan(can)) which holds the permissions
|
409
|
+
currently available. This is used for authorization within an operation.
|
410
|
+
|
411
|
+
- The operations chain
|
412
|
+
|
413
|
+
The operations chain contains the call stack of operations. This is
|
414
|
+
automatically generated when calling a sub-op or triggering an op using an
|
415
|
+
event (see chapter *Events* for more information on that).
|
416
|
+
|
417
|
+
TODO: This may induce memory issues. Is keeping the operation's chain worth
|
418
|
+
this trade-off?
|
419
|
+
|
420
|
+
- URL options
|
421
|
+
|
422
|
+
Rails uses a hash named `url_options` for generating URLs with correct prefix.
|
423
|
+
This information usually comes from a request and is automatically passed to
|
424
|
+
the operation context when calling an operation from a controller. This hash
|
425
|
+
is used by {RailsOps::Mixins::Routes}.
|
426
|
+
|
427
|
+
- Called via hook
|
428
|
+
|
429
|
+
`called_via_hook` is a boolean indicating whether or not this operation was
|
430
|
+
called by a hook (true) or by a regular method call (false). We will introduce
|
431
|
+
hooks below.
|
432
|
+
|
433
|
+
### Instantiating contexts
|
434
|
+
|
435
|
+
Contexts behave like a traditional model object and can be instantiated in
|
436
|
+
multiple ways:
|
437
|
+
|
438
|
+
```ruby
|
439
|
+
context = Context.new(user: current_user, params: { foo: bar })
|
440
|
+
|
441
|
+
# Another way
|
442
|
+
context = Context.new
|
443
|
+
context.user = current_user
|
444
|
+
```
|
445
|
+
|
446
|
+
### Feeding contexts to operations
|
447
|
+
|
448
|
+
Contexts are assigned to operations via the operation's constructor:
|
449
|
+
|
450
|
+
```ruby
|
451
|
+
my_context = RailsOps::Context.new
|
452
|
+
op = Operations::PrintHelloWorld.new(my_context, foo: :bar)
|
453
|
+
```
|
454
|
+
|
455
|
+
For your convenience, contexts also provide `run` and `run!` methods:
|
456
|
+
|
457
|
+
```ruby
|
458
|
+
my_context.run Operations::PrintHelloWorld, foo: :bar
|
459
|
+
```
|
460
|
+
|
461
|
+
### Sub-operations
|
462
|
+
|
463
|
+
When calling a sub-operation either using the corresponding sub-operation
|
464
|
+
methods or else using events, a new context is automatically created and
|
465
|
+
assigned to the sub-operation. This context includes all the data from the
|
466
|
+
original context. Also, the operations chain is automatically complemented with
|
467
|
+
the parent operation.
|
468
|
+
|
469
|
+
This is called *context spawning* and is performed using the
|
470
|
+
{RailsOps::Context.spawn} method.
|
471
|
+
|
472
|
+
Hooks
|
473
|
+
-----
|
474
|
+
|
475
|
+
In some cases, certain actions must be hooked in after execution of an
|
476
|
+
operation. While this can certainly be done with sub-operations, it is not
|
477
|
+
always desirable as the triggering operation should not always know of the
|
478
|
+
additional ones it's triggering:
|
479
|
+
|
480
|
+
- `Operations::User::Create` creates a user, but also creates a group object
|
481
|
+
using `Operations::Group::Create`. *This is an example for sub-ops*.
|
482
|
+
|
483
|
+
- `Operations::User::Create` creates a user. Whenever a user is created, another
|
484
|
+
part of the application needs to generate a TODO for the admin to approve this
|
485
|
+
user. *This would be an example for hooks*.
|
486
|
+
|
487
|
+
Hooks are pretty simple: Using the file `config/hookup.rb`, you can
|
488
|
+
specify which operations should be triggered after which operations. These
|
489
|
+
operations are then automatically triggered after the original operation's
|
490
|
+
`perform` (in the `run` method).
|
491
|
+
|
492
|
+
### Defining hooks
|
493
|
+
|
494
|
+
Hooks are defined in a file named `config/hookup.rb` in your local application.
|
495
|
+
In development mode, this file is automatically reloaded on each request so
|
496
|
+
there is no need to restart the application server for this while developing.
|
497
|
+
|
498
|
+
Defining hooks is as simple as defining a target operation and one or more
|
499
|
+
source operations.
|
500
|
+
|
501
|
+
```ruby
|
502
|
+
RailsOps.hookup.draw do
|
503
|
+
run Operations::Notifications::User::SendWelcomeEmail do
|
504
|
+
on Operations::User::Create
|
505
|
+
end
|
506
|
+
|
507
|
+
run Operations::Todos::GenerateUserApprovalTodo do
|
508
|
+
on Operations::User::Create
|
509
|
+
end
|
510
|
+
|
511
|
+
run Operations::Notification::SendTodoNotification do
|
512
|
+
on Operations::Todos::GenerateUserApprovalTodo
|
513
|
+
end
|
514
|
+
end
|
515
|
+
```
|
516
|
+
|
517
|
+
Operations hooks are always performed in the order they are defined.
|
518
|
+
|
519
|
+
### Events
|
520
|
+
|
521
|
+
Each operation can throw different *events*. The event `:after_run` is
|
522
|
+
automatically triggered after each operation's execution and should be
|
523
|
+
sufficient for most cases. However, it is also possible to trigger custom events
|
524
|
+
in the `perform` method:
|
525
|
+
|
526
|
+
```ruby
|
527
|
+
def perform
|
528
|
+
trigger :custom_event_name, { some: :params }
|
529
|
+
end
|
530
|
+
```
|
531
|
+
|
532
|
+
This can be hooked by specifying the custom event name in your hookup
|
533
|
+
configuration:
|
534
|
+
|
535
|
+
```ruby
|
536
|
+
on Operations::User::Create, :custom_event_name do
|
537
|
+
perform Operations::Notifications::User::SendWelcomeEmail
|
538
|
+
end
|
539
|
+
```
|
540
|
+
|
541
|
+
In most cases though, situations like these should rather be handled by
|
542
|
+
explicitly calling a sub-operation.
|
543
|
+
|
544
|
+
### Hook parameters
|
545
|
+
|
546
|
+
For each hook that is called, at set of parameters is passed to the respective
|
547
|
+
operations. When calling events manually (see section *Events*), you can
|
548
|
+
manually specify the parameters. For the default event `:after_run`, the set of
|
549
|
+
parameters is defined by the operation method `after_run_trigger_params`. In the
|
550
|
+
default case, this returns an empty array. Some operation base classes, like for
|
551
|
+
instance `RailsOps::Operation::Model`, override this method to supply a custom
|
552
|
+
set of parameters. See your respective base class for more information.
|
553
|
+
|
554
|
+
Be advised: It is not usually desirable to provide a very custom param set that
|
555
|
+
is tailored to one particular target operation. Trigger parameters should be as
|
556
|
+
generic as possible as specific cases should rather be handled using sub-ops.
|
557
|
+
|
558
|
+
Operations can be used to write adapters (*glue* operations) in order to hook
|
559
|
+
into an operation with incompatible parameters. Create a glue operation that
|
560
|
+
hooks into the source operation and prepares the params specifically for the
|
561
|
+
target operation, which is then called using a sub-operation or the hooking
|
562
|
+
system.
|
563
|
+
|
564
|
+
### Check if called via hook
|
565
|
+
|
566
|
+
You can determine whether your operation has been (directly) called via a hook
|
567
|
+
using the `called_via_hook` context method:
|
568
|
+
|
569
|
+
```ruby
|
570
|
+
def perform
|
571
|
+
puts 'Called via hook' if context.called_via_hook
|
572
|
+
end
|
573
|
+
```
|
574
|
+
|
575
|
+
Note that this property never propagates, so when calling a sub-operation from
|
576
|
+
an operation that has been called using a hook, `called_via_hook` of the
|
577
|
+
sub-operation is set to `false` again.
|
578
|
+
|
579
|
+
Authorization
|
580
|
+
-------------
|
581
|
+
|
582
|
+
Rails Ops offers backend-agnostic authorization using so-called
|
583
|
+
*authorization backends*.
|
584
|
+
|
585
|
+
Authorization basically happens by calling the method `authorize!` (or
|
586
|
+
`authorize_only!`, more on that later) within an operation. What exactly this
|
587
|
+
method does depends on the *authorization backend* specified.
|
588
|
+
|
589
|
+
### Authorization backends
|
590
|
+
|
591
|
+
Authorization backends are simple classes that supply the method `authorize!`.
|
592
|
+
This method, besides the operation instance, can take any number of arguments
|
593
|
+
and is supposed to perform authorization and raise if the authorization failed.
|
594
|
+
|
595
|
+
The authorization backend can be configured globally using the
|
596
|
+
`authorization_backend` configuration setting, which can be set to the name of
|
597
|
+
your backend class.
|
598
|
+
|
599
|
+
Example initializer:
|
600
|
+
|
601
|
+
```ruby
|
602
|
+
RailsOps.configure do |config|
|
603
|
+
config.authorization_backend = 'RailsOps::AuthorizationBackends::CanCanCan'
|
604
|
+
end
|
605
|
+
```
|
606
|
+
|
607
|
+
RailsOps ships with the following backend:
|
608
|
+
|
609
|
+
- `RailsOps::AutorizationBackend::Cancancan`
|
610
|
+
|
611
|
+
Offers integration of the `cancancan` Gem (which is a fork of the `cancan`
|
612
|
+
Gem).
|
613
|
+
|
614
|
+
### Performing authorization
|
615
|
+
|
616
|
+
Authorization is generally performed by calling `authorize!` in an operation.
|
617
|
+
The arguments, along with the operation instance, are passed on to the
|
618
|
+
`authorize!` method of your authorization backend. Basically, you can call
|
619
|
+
`authorize!` anywhere in your operation, but bear in mind that if your
|
620
|
+
authorization requires certain data (i.e. the `params` hash), your authorization
|
621
|
+
calls should occur *after* that certain data is available.
|
622
|
+
|
623
|
+
```ruby
|
624
|
+
class MyOp < RailsOps::Operation
|
625
|
+
def perform
|
626
|
+
authorize! :read, :some_area
|
627
|
+
end
|
628
|
+
end
|
629
|
+
```
|
630
|
+
|
631
|
+
Usually though, authorization, as other pre-conditions, are called within
|
632
|
+
policies:
|
633
|
+
|
634
|
+
```ruby
|
635
|
+
class MyOp < RailsOps::Operation
|
636
|
+
policy do
|
637
|
+
authorize! :read, :some_area
|
638
|
+
end
|
639
|
+
end
|
640
|
+
```
|
641
|
+
|
642
|
+
In many cases, you'd like the authorization to run no matter if the operation
|
643
|
+
ever runs. For this case, use the `:on_init` policy chain:
|
644
|
+
|
645
|
+
```ruby
|
646
|
+
class MyOp < RailsOps::Operation
|
647
|
+
policy :on_init do
|
648
|
+
authorize! :read, osparams.some_record
|
649
|
+
end
|
650
|
+
end
|
651
|
+
```
|
652
|
+
|
653
|
+
See section *Policy chains* for more information.
|
654
|
+
|
655
|
+
### Ensure that authorization has been performed
|
656
|
+
|
657
|
+
As it is a very common programming mistake to mistakenly omit calling
|
658
|
+
authorization, Rails Ops offers a solution for making sure that authorization
|
659
|
+
has been called in every operation.
|
660
|
+
|
661
|
+
This is done by calling `ensure_authorize_called!` on your operation. This will
|
662
|
+
raise an exception if no authorization has been performed. This method is
|
663
|
+
automatically called in `run` or `run!` after the execution of the `perform`
|
664
|
+
method.
|
665
|
+
|
666
|
+
This method only applies if authorization is currently enabled (see next
|
667
|
+
section), otherwise it does nothing.
|
668
|
+
|
669
|
+
It is implemented so that every call to `authorize!` sets an instance variable
|
670
|
+
of the respective operation to `true`, and `ensure_authorize_called!` checks
|
671
|
+
this instance variable on calling.
|
672
|
+
|
673
|
+
Sometimes you might want to call authorization that should not count for this
|
674
|
+
check, i.e. some base authorization that needs to be complemented with some
|
675
|
+
specific authorization code. In these cases, use `authorize_only!`:
|
676
|
+
|
677
|
+
```ruby
|
678
|
+
def perform
|
679
|
+
authorize_only! :foo, :bar
|
680
|
+
|
681
|
+
# The following will fail as authorize_only! calls do not count as authorized.
|
682
|
+
ensure_authorize_called!
|
683
|
+
end
|
684
|
+
```
|
685
|
+
|
686
|
+
This method otherwise does exactly the same as `authorize!` (in fact, it's the
|
687
|
+
underlying method used by it).
|
688
|
+
|
689
|
+
### Disabling authorization
|
690
|
+
|
691
|
+
Sometimes you don't want a specific operation to perform authorization, or you
|
692
|
+
don't want to perform any authorization at all.
|
693
|
+
|
694
|
+
For this reason, Rails Ops allows you to disable authorization globally, per
|
695
|
+
operation or per operation call (i.e. an operation should generally perform
|
696
|
+
authorization, but not in a specific case). If authorization is disabled, all
|
697
|
+
calls to `authorize!` won't have any effect and will never fail. Also, it is not
|
698
|
+
ensured that authorization has been performed as it would always fail (see
|
699
|
+
previous section).
|
700
|
+
|
701
|
+
Rails Ops offers multiple ways of disabling authorization:
|
702
|
+
|
703
|
+
- By not configuring any authorization backend.
|
704
|
+
|
705
|
+
- By calling the class method `without_authorization`:
|
706
|
+
|
707
|
+
```ruby
|
708
|
+
class MyOp < RailsOps::Operation
|
709
|
+
without_authorization
|
710
|
+
end
|
711
|
+
```
|
712
|
+
|
713
|
+
TODO: Explain why this can be useful even if there are no authorize! calls
|
714
|
+
in this operation.
|
715
|
+
|
716
|
+
- By invoking one or more operations in a `RailsOps.without_authorization`
|
717
|
+
block:
|
718
|
+
|
719
|
+
```ruby
|
720
|
+
RailsOps.without_authorization do
|
721
|
+
# Authorization will be disabled even if `SomeOperation` itself would
|
722
|
+
# otherwise perform authorization.
|
723
|
+
SomeOperation.run
|
724
|
+
end
|
725
|
+
```
|
726
|
+
|
727
|
+
Within operations, you can also use the instance method
|
728
|
+
`without_authorization` which does the same thing as the global one (it is
|
729
|
+
just a shortcut and can therefore be used interchangeably):
|
730
|
+
|
731
|
+
```ruby
|
732
|
+
class MyOp < RailsOps::Operation
|
733
|
+
def perform
|
734
|
+
without_authorization do
|
735
|
+
run_sub! SomeOtherOperation
|
736
|
+
end
|
737
|
+
end
|
738
|
+
end
|
739
|
+
```
|
740
|
+
|
741
|
+
Note that when calling `without_authorization` this does not only apply to
|
742
|
+
other operations called, but also to the operation you're currently in:
|
743
|
+
|
744
|
+
```ruby
|
745
|
+
class MyOp < RailsOps::Operation
|
746
|
+
def perform
|
747
|
+
without_authorization do
|
748
|
+
# The following line does nothing, as authorization is currently
|
749
|
+
# disabled.
|
750
|
+
authorize! :read, :some_area
|
751
|
+
end
|
752
|
+
end
|
753
|
+
end
|
754
|
+
```
|
755
|
+
|
756
|
+
Model Operations
|
757
|
+
----------------
|
758
|
+
|
759
|
+
One of the key features of RailsOps is model operations. RailsOps provides
|
760
|
+
multiple operation base classes which allow convenient manipulation of active
|
761
|
+
record models.
|
762
|
+
|
763
|
+
All of the model operation classes, including more specialized base classes,
|
764
|
+
inherit from {RailsOps::Operation::Model} (which in turn inherits from
|
765
|
+
{RailsOps::Operation} as every operation base class).
|
766
|
+
|
767
|
+
The key principle behind these model classes is to associate *one model class*
|
768
|
+
and *one model instance* with a particular operation.
|
769
|
+
|
770
|
+
## Setting a model class
|
771
|
+
|
772
|
+
Using the static method `model`, you can assign a model class that is used in
|
773
|
+
the scope of this operation.
|
774
|
+
|
775
|
+
```ruby
|
776
|
+
class SomeOperation < RailsOps::Operation::Model
|
777
|
+
model User
|
778
|
+
end
|
779
|
+
```
|
780
|
+
|
781
|
+
You can also directly extend this class by providing a block. If given, this
|
782
|
+
will automatically create a new, anonymous class that inherits from the given
|
783
|
+
base class and run the given block in the static context of this class:
|
784
|
+
|
785
|
+
```ruby
|
786
|
+
class SomeOperation < RailsOps::Operation::Model
|
787
|
+
model User do
|
788
|
+
# This code only runs in a dynamically created subclass of `User` and does
|
789
|
+
# not affect the original model class.
|
790
|
+
attr_accessible :name
|
791
|
+
end
|
792
|
+
end
|
793
|
+
```
|
794
|
+
|
795
|
+
You do not even have to specify a base class. In this case, the class returned
|
796
|
+
by the static method `default_model_class` (default: {ActiveType::Object}) will
|
797
|
+
be used as base class:
|
798
|
+
|
799
|
+
```ruby
|
800
|
+
class SomeOperation < RailsOps::Operation::Model
|
801
|
+
model do
|
802
|
+
# See ActiveType documentation for more information on virtual attributes.
|
803
|
+
attribute :name
|
804
|
+
end
|
805
|
+
end
|
806
|
+
```
|
807
|
+
|
808
|
+
## Obtaining a model instance
|
809
|
+
|
810
|
+
Model instances can be obtained using the *instance* method `model`, which is
|
811
|
+
not to be confused with the *class* method of the same name. Other than the
|
812
|
+
class method, the instance method instantiates and returns a model object with
|
813
|
+
the type / base class specified using the `model` class method:
|
814
|
+
|
815
|
+
```ruby
|
816
|
+
class SomeOperation < RailsOps::Operation::Model
|
817
|
+
model User
|
818
|
+
|
819
|
+
def perform
|
820
|
+
# This returns an instance of the 'User' class. To be precise: This example
|
821
|
+
# does not work out-of-the-box as this base class is abstract and does not
|
822
|
+
# implement the `build_model` method. But more on that later.
|
823
|
+
model
|
824
|
+
end
|
825
|
+
end
|
826
|
+
```
|
827
|
+
|
828
|
+
The instance method `model` only instantiates a model once and then caches it in
|
829
|
+
the instance variable `@model`. Therefore, you can call `model` multiple times
|
830
|
+
and always get back the same instance.
|
831
|
+
|
832
|
+
If no cached instance is found, one is built using the instance method
|
833
|
+
`build_model`. Note that this method is not provided by the `Model` base class
|
834
|
+
but only implemented in its subclasses. You can implement and override this
|
835
|
+
method to your liking though.
|
836
|
+
|
837
|
+
## Loading models
|
838
|
+
|
839
|
+
Using the base operation class {RailsOps::Operation::Model::Load}, a model can
|
840
|
+
be loaded. This is done by implementing the `build_model` mentioned above. In
|
841
|
+
this particular case, the `find` method of the statically assigned model class
|
842
|
+
is used in conjunction with an ID extracted from the operation's params.
|
843
|
+
|
844
|
+
```ruby
|
845
|
+
class Operations::User::Load < RailsOps::Operation::Model::Load
|
846
|
+
model User
|
847
|
+
end
|
848
|
+
|
849
|
+
op = Operations::User::Load.run!(id: 5)
|
850
|
+
op.model.id # => 5
|
851
|
+
```
|
852
|
+
|
853
|
+
Note that this base class is a bit of a special case: It does not provide a
|
854
|
+
`perform` method and does not need to be run at all in order to load a model.
|
855
|
+
This is very useful when, for example, displaying a form based on a model
|
856
|
+
instance without actually performing any particular action such as updating a
|
857
|
+
model.
|
858
|
+
|
859
|
+
Therefore, the above example would also work as follows:
|
860
|
+
|
861
|
+
```ruby
|
862
|
+
# The operation does not have to be performed to access the model instance.
|
863
|
+
op = Operations::User::Load.new(id: 5)
|
864
|
+
op.model.id # => 5
|
865
|
+
```
|
866
|
+
|
867
|
+
### Specifying ID field
|
868
|
+
|
869
|
+
Per default, the model instance is looked up using the field `id` and the ID
|
870
|
+
obtained from the method params using `params[:id]`. However, you can customize
|
871
|
+
this field name by overriding the method `model_id_field`:
|
872
|
+
|
873
|
+
```ruby
|
874
|
+
class Operations::User::Load < RailsOps::Operation::Model::Load
|
875
|
+
model User
|
876
|
+
|
877
|
+
def model_id_field
|
878
|
+
:some_other_id_field
|
879
|
+
end
|
880
|
+
end
|
881
|
+
```
|
882
|
+
|
883
|
+
### Locking
|
884
|
+
|
885
|
+
In most cases when you load a model, you might want to lock the corresponding
|
886
|
+
database record. RailsOps is configured to automatically perform this locking
|
887
|
+
at time of loading. However, you can override the default behavior using
|
888
|
+
the option {RailsOps.config.lock_models_at_build}.
|
889
|
+
|
890
|
+
This behavior can also be overwritten per operation using the
|
891
|
+
`lock_model_at_build` class method:
|
892
|
+
|
893
|
+
```ruby
|
894
|
+
class Operations::User::Load < RailsOps::Operation::Model::Load
|
895
|
+
model User
|
896
|
+
lock_model_at_build false # Takes `true` if no argument is passed
|
897
|
+
end
|
898
|
+
```
|
899
|
+
|
900
|
+
## Creating models
|
901
|
+
|
902
|
+
For creating models, you can use the base class
|
903
|
+
{RailsOps::Operation::Model::Create}.
|
904
|
+
|
905
|
+
This class mainly provides an implementation of the methods `build_model` and
|
906
|
+
`perform`.
|
907
|
+
|
908
|
+
The `build_model` method builds a new record using the operation's parameters.
|
909
|
+
See section *Parameter extraction for create and update* for more information on
|
910
|
+
that.
|
911
|
+
|
912
|
+
The `perform` method saves the record using `save!`.
|
913
|
+
|
914
|
+
As this base class is very minimalistic, it is recommended to fully read and
|
915
|
+
comprehend its source code.
|
916
|
+
|
917
|
+
## Updating models
|
918
|
+
|
919
|
+
For creating models, you can use the base class
|
920
|
+
{RailsOps::Operation::Model::Update} which is an extension of the `Load` base
|
921
|
+
class.
|
922
|
+
|
923
|
+
This class mainly provides an implementation of the methods `build_model` and
|
924
|
+
`perform`.
|
925
|
+
|
926
|
+
The `build_model` method updates a record using the operation's parameters. See
|
927
|
+
section *Parameter extraction for create and update* for more information on
|
928
|
+
that.
|
929
|
+
|
930
|
+
The `perform` method saves the record using `save!`.
|
931
|
+
|
932
|
+
As this base class is very minimalistic, it is recommended to fully read and
|
933
|
+
comprehend its source code.
|
934
|
+
|
935
|
+
## Destroying models
|
936
|
+
|
937
|
+
For destroying models, you can use the base class
|
938
|
+
{RailsOps::Operation::Model::Destroy} which is an extension of the `Load` base
|
939
|
+
class.
|
940
|
+
|
941
|
+
This class mainly provides an implementation of the method `perform`, which
|
942
|
+
destroys the model using its `destroy!` method.
|
943
|
+
|
944
|
+
As this base class is very minimalistic, it is recommended to fully read and
|
945
|
+
comprehend its source code.
|
946
|
+
|
947
|
+
## Parameter extraction for create and update
|
948
|
+
|
949
|
+
As mentioned before, the `Create` and `Update` base classes provide an
|
950
|
+
implementation of `build_model` that assigns parameters to a model.
|
951
|
+
|
952
|
+
The attributes are determined by the operation instance method
|
953
|
+
`extract_attributes_from_params` - the name being self-explaining. See its
|
954
|
+
source code for implementation details.
|
955
|
+
|
956
|
+
## Model authorization
|
957
|
+
|
958
|
+
While you can use the standard `authorize!` method (see chapter *Authorization*)
|
959
|
+
for authorizing models, RailsOps provides you a more convenient integration.
|
960
|
+
|
961
|
+
### Basic authorization
|
962
|
+
|
963
|
+
Model authorization can be performed via the operation instance methods
|
964
|
+
`authorize_model!` and `authorize_model_with_authorize_only!` (see chapter
|
965
|
+
*Authorization* for more information on the difference between these two).
|
966
|
+
|
967
|
+
There two methods provide a simple wrapper around `authorize!` and
|
968
|
+
`authorize_only!` that casts the given model class or instance to an active
|
969
|
+
record object. This is necessary if the given model class or instance is a
|
970
|
+
(possibly anonymous) extension of an active record class for certain
|
971
|
+
authorization backends to work. Therefore, use the specific model authorization
|
972
|
+
methods instead of the basic authorization methods for authorizing models.
|
973
|
+
|
974
|
+
If no model is given, the model authorization methods automatically obtain the
|
975
|
+
model from the instance method `model`.
|
976
|
+
|
977
|
+
### Automatic authorization
|
978
|
+
|
979
|
+
All model operation classes provide the operation instance method
|
980
|
+
`model_authorization` which is automatically run at model instantiation (this is
|
981
|
+
done using an `:on_init` policy). The purpose of this method is to perform an
|
982
|
+
authorization check based on this model.
|
983
|
+
|
984
|
+
While you can override this method to perform custom authorization, RailsOps
|
985
|
+
provides a base implementation. Using the class method
|
986
|
+
`model_authorization_action`, you can specify an action verb that is used for
|
987
|
+
authorizing your model.
|
988
|
+
|
989
|
+
```ruby
|
990
|
+
class Operations::User::Load < RailsOps::Operation::Model::Load
|
991
|
+
model User
|
992
|
+
|
993
|
+
# This automatically calls `authorize_model! :read` after operation
|
994
|
+
# instantiation.
|
995
|
+
model_authorization_action :read
|
996
|
+
end
|
997
|
+
```
|
998
|
+
|
999
|
+
Note that using the different model base classes, this is already set to a
|
1000
|
+
sensible default. See the respective class' source code for details.
|
1001
|
+
|
1002
|
+
## Model nesting
|
1003
|
+
|
1004
|
+
TODO
|
1005
|
+
|
1006
|
+
Record extension and virtual records
|
1007
|
+
------------------------------------
|
1008
|
+
|
1009
|
+
|
1010
|
+
Transactions
|
1011
|
+
------------
|
1012
|
+
|
1013
|
+
|
1014
|
+
|
1015
|
+
Controller Integration
|
1016
|
+
----------------------
|
1017
|
+
|
1018
|
+
While RailsOps certainly does not have to be used from a controller, it
|
1019
|
+
provides a mixin which extends controller classes with functionality that lets
|
1020
|
+
you easily instantiate and run operations.
|
1021
|
+
|
1022
|
+
## Installing
|
1023
|
+
|
1024
|
+
Controller integration is designed to be non-intrusive and therefore has to be
|
1025
|
+
installed manually. Add the following inclusion to the controllers in question
|
1026
|
+
(usually the `ApplicationController` base class):
|
1027
|
+
|
1028
|
+
```ruby
|
1029
|
+
class ApplicationController
|
1030
|
+
include RailsOps::ControllerMixin
|
1031
|
+
end
|
1032
|
+
```
|
1033
|
+
|
1034
|
+
## Basic usage
|
1035
|
+
|
1036
|
+
The basic concept behind controller integration is to instantiate and
|
1037
|
+
potentially run a single operation per request. Most of this guide refers to
|
1038
|
+
this particular use case. See section *Multiple operations per request* for more
|
1039
|
+
advanced solutions.
|
1040
|
+
|
1041
|
+
The following example shows the simplest way of setting and running an
|
1042
|
+
operation:
|
1043
|
+
|
1044
|
+
```ruby
|
1045
|
+
class SomeController < ApplicationController
|
1046
|
+
def some_action
|
1047
|
+
run! Operations::SomeOperation
|
1048
|
+
end
|
1049
|
+
end
|
1050
|
+
```
|
1051
|
+
|
1052
|
+
## Separating instantiation and execution
|
1053
|
+
|
1054
|
+
In the previous example, we instantiated and ran an operation in a single
|
1055
|
+
statement. While this might be feasible for some "fire-and-forget" controller
|
1056
|
+
actions, you might want to separate instantiation from actually running an
|
1057
|
+
operation.
|
1058
|
+
|
1059
|
+
For this reason, RailsOps' controller integration is designed to always use
|
1060
|
+
a two-step process: First the operation is instantiated and assigned to the
|
1061
|
+
controller instance variable `@op`, and then it's possibly executed.
|
1062
|
+
|
1063
|
+
In the following example, we do exactly the same thing as in the previous one,
|
1064
|
+
but with separate instantiation and execution:
|
1065
|
+
|
1066
|
+
```ruby
|
1067
|
+
class SomeController < ApplicationController
|
1068
|
+
def some_action
|
1069
|
+
# The following line instantiates the given operation and assigns the
|
1070
|
+
# instance to `@op`.
|
1071
|
+
op Operations::SomeOperation
|
1072
|
+
|
1073
|
+
# The following line runs the operation previously set using `op` using
|
1074
|
+
# the operations `run!` method. Note that `run` is available as well.
|
1075
|
+
run!
|
1076
|
+
end
|
1077
|
+
end
|
1078
|
+
```
|
1079
|
+
|
1080
|
+
The methods `run` and `run!` always require you to previously instantiate an
|
1081
|
+
operation using the `op` method.
|
1082
|
+
|
1083
|
+
This can be particularly useful for "combined" controller methods that either
|
1084
|
+
display a form or submit, i.e. based on the HTTP method used.
|
1085
|
+
|
1086
|
+
```ruby
|
1087
|
+
def update_username
|
1088
|
+
# As above operation extends RailsOps::Model, we can already access op.model
|
1089
|
+
# (i.e. in a form) without ever running the operation. Therefore, we
|
1090
|
+
# instantiate the operation even if it is a GET request.
|
1091
|
+
op Operations::User::UpdateUsername
|
1092
|
+
|
1093
|
+
# In this example, the operation is only run on POST requests.
|
1094
|
+
if request.post? && run
|
1095
|
+
redirect_to users_path
|
1096
|
+
end
|
1097
|
+
end
|
1098
|
+
```
|
1099
|
+
|
1100
|
+
## Checking for operations
|
1101
|
+
|
1102
|
+
Using the method `op?`, you can check whether an operation has already been
|
1103
|
+
instantiated (using `op`).
|
1104
|
+
|
1105
|
+
## Model shortcut
|
1106
|
+
|
1107
|
+
RailsOps conveniently provides you with a `model` instance method, which is a
|
1108
|
+
shortcut for `op.model`. This is particularly useful since this is available as
|
1109
|
+
a view helper method as well, see next section.
|
1110
|
+
|
1111
|
+
## View helper methods
|
1112
|
+
|
1113
|
+
The following controller methods are automatically provided as helper methods
|
1114
|
+
which can be used in views:
|
1115
|
+
|
1116
|
+
- `op`
|
1117
|
+
- `model`
|
1118
|
+
- `op?`
|
1119
|
+
|
1120
|
+
It is very common to use `model` for your forms:
|
1121
|
+
|
1122
|
+
```
|
1123
|
+
= form_for model do |f|
|
1124
|
+
- # Form code goes here
|
1125
|
+
```
|
1126
|
+
|
1127
|
+
## Parameters
|
1128
|
+
|
1129
|
+
As you've probably noticed in previous examples, we did not provide any
|
1130
|
+
parameters to the operation.
|
1131
|
+
|
1132
|
+
Per default, the `params` hash is automatically provided to the operation at
|
1133
|
+
instantiation. To be more precise: The params hash is filtered not to include
|
1134
|
+
certain fields (see {RailsOps::ControllerMixin::EXCEPT_PARAMS}) that are most
|
1135
|
+
commonly not used by operations (e.g. the `authenticity_token`).
|
1136
|
+
|
1137
|
+
This is achieved using the private `op_params` method. Overwrite it to your
|
1138
|
+
needs if you have to adapt it for the whole controller.
|
1139
|
+
|
1140
|
+
Alternatively, you can pass entirely custom params to an operation via the `op`
|
1141
|
+
method:
|
1142
|
+
|
1143
|
+
```ruby
|
1144
|
+
op SomeOperation, some_param: 'some_value'
|
1145
|
+
```
|
1146
|
+
|
1147
|
+
You can also combine these two approaches:
|
1148
|
+
|
1149
|
+
```ruby
|
1150
|
+
# This example takes the pre-filtered op_params hash and applies another, custom
|
1151
|
+
# filter before passing it to the operation.
|
1152
|
+
op SomeOperation, some_param: op_params.slice(:some_param, :some_other_param)
|
1153
|
+
```
|
1154
|
+
|
1155
|
+
## Authorization ensuring
|
1156
|
+
|
1157
|
+
For security reasons, RailsOps automatically checks after each action whether
|
1158
|
+
authorization has been performed. This is to avoid serving an action's response
|
1159
|
+
without ever authorizing.
|
1160
|
+
|
1161
|
+
The check is run in the `after_action` named
|
1162
|
+
`ensure_operation_authorize_called!` and only applies if an operation class has
|
1163
|
+
been set.
|
1164
|
+
|
1165
|
+
Note that this check also doesn't apply if the corresponding operation uses
|
1166
|
+
`without_authorization` (see section *Disabling authorization* for more
|
1167
|
+
information on this).
|
1168
|
+
|
1169
|
+
## Context
|
1170
|
+
|
1171
|
+
When using the `op` method to instantiate an operation, a context is
|
1172
|
+
automatically created. The following fields are set automatically:
|
1173
|
+
|
1174
|
+
- `params` (as described in subsection *Parameters*)
|
1175
|
+
- `user` (uses `current_user` controller method if available, otherwise `nil`)
|
1176
|
+
- `ability` (uses `current_ability` controller method if available, otherwise `nil`)
|
1177
|
+
- `session` (uses the `session` controller method)
|
1178
|
+
- `url_options` (uses the `url_options` controller method)
|
1179
|
+
|
1180
|
+
## Multiple operations per request
|
1181
|
+
|
1182
|
+
RailsOps does not currently support calling multiple operations in a single
|
1183
|
+
controller action out-of-the-box. You need to instantiate and run it manually.
|
1184
|
+
|
1185
|
+
Another approach is to create a parent operation which calls multiple
|
1186
|
+
sub-operations, see section *Calling sub-operations* for more information.
|
1187
|
+
|
1188
|
+
Operation Inheritance
|
1189
|
+
---------------------
|
1190
|
+
|
1191
|
+
Caveats
|
1192
|
+
-------
|
1193
|
+
|
1194
|
+
## Eager loading in development mode
|
1195
|
+
|
1196
|
+
Eager loading operation classes containing models with nested models or
|
1197
|
+
operations can be very slow in performance. In production mode, the same process
|
1198
|
+
is very fast and not an issue at all. To work around this problem, make sure you
|
1199
|
+
exclude your operation classes (i.e. `app/operations`) in your
|
1200
|
+
`config.eager_load_paths` of `development.rb`. Make sure not to touch this
|
1201
|
+
setting in production mode though.
|
1202
|
+
|
1203
|
+
Open points
|
1204
|
+
-----------
|
1205
|
+
|
1206
|
+
## Load model authorization
|
1207
|
+
|
1208
|
+
It is possible that we can eliminate the load model authorization functionality
|
1209
|
+
entirely. Operations using the `Load` base class can authorize using `:read`,
|
1210
|
+
while more specific operations can use `:update` or whatever. If we really need
|
1211
|
+
to check for `:read` when updating an object, it can be implemented in the
|
1212
|
+
ability class.
|
1213
|
+
|
1214
|
+
## Copyright
|
1215
|
+
|
1216
|
+
Copyright (c) 2017 Sitrox. See `LICENSE` for further details.
|