rails_ops 1.7.3 → 1.7.5

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/README.md CHANGED
@@ -2,8 +2,7 @@
2
2
  [![Rubocop check](https://github.com/sitrox/rails_ops/actions/workflows/rubocop.yml/badge.svg)](https://github.com/sitrox/rails_ops/actions/workflows/rubocop.yml)
3
3
  [![Gem Version](https://badge.fury.io/rb/rails_ops.svg)](https://badge.fury.io/rb/rails_ops)
4
4
 
5
- rails_ops
6
- =========
5
+ # rails_ops
7
6
 
8
7
  This Gem introduces an additional service layer for Rails: *Operations*. An
9
8
  operation is in most cases a *business action* or *use case* and may or may not
@@ -18,8 +17,7 @@ To achieve this goal, this Gem provides the following building blocks:
18
17
 
19
18
  - A way of abstracting model classes for a specific business action.
20
19
 
21
- Requirements & Installation
22
- ---------------------------
20
+ ## Requirements & Installation
23
21
 
24
22
  ### Requirements
25
23
 
@@ -90,10 +88,9 @@ Requirements & Installation
90
88
 
91
89
  Taken from [this github issues comment](https://github.com/rails/rails/issues/40126#issuecomment-816275285).
92
90
 
93
- Operation Basics
94
- ----------------
91
+ ## Operation Basics
95
92
 
96
- ### Placing and naming operations
93
+ ### Placing and Naming Operations
97
94
 
98
95
  - Operations generally reside in `app/operations` and can be nested using
99
96
  various subdirectories. They're all inside of the `Operations` namespace.
@@ -110,7 +107,7 @@ Operation Basics
110
107
  `MoveToPosition` and so on. Do not name an operation something like
111
108
  `UserCreator` or `CreateUserOperation`.
112
109
 
113
- #### Heads-up: Correct namespacing
110
+ #### Heads-up: Correct Namespacing
114
111
 
115
112
  As explained in the previous section, operations should be namespaced properly.
116
113
  Operations can either live within a module or within a class. In most cases,
@@ -160,7 +157,7 @@ Note that, when defining a namespace of which a segment is already known as a
160
157
  end
161
158
  ```
162
159
 
163
- ### Basic operations
160
+ ### Basic Operations
164
161
 
165
162
  Every single operation follows a few basic principles:
166
163
 
@@ -186,7 +183,7 @@ class Operations::PrintHelloWorld < RailsOps::Operation
186
183
  end
187
184
  ```
188
185
 
189
- ### Running operations manually
186
+ ### Running Operations Manually
190
187
 
191
188
  There are various ways of instantiating and running an operation. The most
192
189
  basic way is the following:
@@ -212,7 +209,7 @@ models or ActiveRecord models, `run` does not only catch the
212
209
  `ActiveRecord::RecordInvalid` exception but also every exception that derives
213
210
  from {RailsOps::Exceptions::ValidationFailed}.
214
211
 
215
- #### Catching custom exceptions in `run`
212
+ #### Catching Custom Exceptions in `run`
216
213
 
217
214
  If you'd like to catch a custom exception if the operation is called using
218
215
  `run`, you can either derive this exception from
@@ -229,7 +226,7 @@ class Operations::PrintHelloWorld < RailsOps::Operation
229
226
  end
230
227
  ```
231
228
 
232
- ### Returning data from operations
229
+ ### Returning Data from Operations
233
230
 
234
231
  All operations have the same call signatures: `run` always returns `true` or
235
232
  `false` while `run!` always returns the operation instance (which allows easy
@@ -248,10 +245,9 @@ end
248
245
  puts Operations::GenerateHelloWorld.run!(name: 'John Doe').result
249
246
  ```
250
247
 
251
- Params Handling
252
- ---------------
248
+ ## Params Handling
253
249
 
254
- ## Passing params to operations
250
+ ### Passing Params to Operations
255
251
 
256
252
  Each single operation can take a `params` hash. Note that it does not have to be
257
253
  in any relation with `ActionController`'s params - it's just a plain ruby hash
@@ -268,7 +264,7 @@ If no params are given, an empty params hash will be used. If a
268
264
  `ActionController::Parameters` object is passed, it will be permitted using
269
265
  `permit!` and converted into a regular hash.
270
266
 
271
- ## Accessing params
267
+ ### Accessing Params
272
268
 
273
269
  For accessing params within an operation, you can use `params` or `osparams`.
274
270
  While `params` directly returns the params hash, `osparams` converts them into
@@ -287,9 +283,9 @@ end
287
283
  Note that both `params` and `osparams` return independent, deep duplicates of
288
284
  the original `params` hash to the operation, so the hashes do not correspond.
289
285
 
290
- The hash accessed via `params` is a always `Object::HashWithIndifferentAccess`.
286
+ The hash accessed via `params` is always an `Object::HashWithIndifferentAccess`.
291
287
 
292
- ## Validating params
288
+ ### Validating Params
293
289
 
294
290
  You're strongly encouraged to perform a validation of the parameters passed to
295
291
  an operation, as unvalidated params pose a security threat. This can be done in several ways:
@@ -310,7 +306,7 @@ an operation, as unvalidated params pose a security threat. This can be done in
310
306
 
311
307
  This is the recommended way of performing basic params validation. Please see the next section *Schema best practices* for more information.
312
308
 
313
- See documentation of the Gem `schemacop` for more information on how to
309
+ See documentation of the gem `schemacop` for more information on how to
314
310
  specify schemata.
315
311
 
316
312
 
@@ -332,7 +328,7 @@ an operation, as unvalidated params pose a security threat. This can be done in
332
328
 
333
329
  - Using a business model (see chapter *Model Operations*).
334
330
 
335
- ### Schema best practices
331
+ ### Schema Best Practices
336
332
 
337
333
  As previously mentioned, using schema from the `schemacop` gem is the recommended way to validate params passed in to an operation. In general, it's recommended to use version 3 of schemacop, i.e. either use `schema3` to specify the schema, or set the default schema version to 3:
338
334
 
@@ -343,7 +339,7 @@ RailsOps.configure do |config|
343
339
  end
344
340
  ```
345
341
 
346
- #### Internal code
342
+ #### Internal Code
347
343
 
348
344
  When writing a schema for an operation which is only used internally (e.g. called from another operation, or called from a part of the code where you control the params, e.g. a rake task), it's recommended to specify the types of all items, as this will catch any mismatched data. For example:
349
345
 
@@ -360,7 +356,7 @@ class Operations::PrintHelloWorldWithId < RailsOps::Operation
360
356
  end
361
357
  ```
362
358
 
363
- #### Called within controllers
359
+ #### Called Within Controllers
364
360
 
365
361
  On the other hand, operations which are called within controllers (e.g. to encapsulate an update operation of a model) should not assume any types, and instead use model validations (if applicable) to validate the correctness of the data. In this case, the schema should only be used to filter the params. As such, it's recommended to use `obj` to specify params which are not strings, as this will allow anything (but only the specified values). An example would be:
366
362
 
@@ -381,7 +377,7 @@ end
381
377
 
382
378
  Validating that `age` is an integer and `is_active` then should be done with a validation in the `User` model, as this will also populate the model errors, which in turn will display the error in the form. If you were to validate the type of the data here, it would raise a `Schemacop::Exceptions::ValidationError` exception, which you would need to handle seperately.
383
379
 
384
- Finally, when additional, obsolete params are supplied, the schema validation would also fail. To have a similar behavious to the strong params from Rails, which drop non-whitelisted params without raising an exception, you can use the `ignore_obsolete_properties` option. This will simply ignore and drop any params which are not explicitly whitelisted:
380
+ Finally, when additional, obsolete params are supplied, the schema validation would also fail. To have a similar behaviour to the strong params from Rails, which drop non-whitelisted params without raising an exception, you can use the `ignore_obsolete_properties` option. This will simply ignore and drop any params which are not explicitly whitelisted:
385
381
 
386
382
  ```ruby
387
383
  module Operations::User
@@ -398,9 +394,9 @@ module Operations::User
398
394
  end
399
395
  ```
400
396
 
401
- ### Catching schema validation errors
397
+ ### Catching Schema Validation Errors
402
398
 
403
- When an operation is called from a controller (via the `run` or `run!` method) and a schema validation excaption occurs, the controller will respond with an empty body and a status code `400` (bad request). This behaviour is enabled by default, but can be disabled with the `rescue_validation_error_in_controller` config option:
399
+ When an operation is called from a controller (via the `run` or `run!` method) and a schema validation exception occurs, the controller will respond with an empty body and a status code `400` (bad request). This behaviour is enabled by default, but can be disabled with the `rescue_validation_error_in_controller` config option:
404
400
 
405
401
  ```ruby
406
402
  # config/initializers/rails_ops.rb
@@ -413,8 +409,7 @@ Generally, this should be left enabled, as sending invalid data to the controlle
413
409
 
414
410
  Please note that this behaviour is disabled in development mode, as the full exception messages are useful for debugging purposes.
415
411
 
416
- Policies
417
- --------
412
+ ## Policies
418
413
 
419
414
  Policies are nothing more than blocks of code that run either at operation
420
415
  instantiation or before / after execution of the `perform` method and can be
@@ -449,7 +444,7 @@ method. Use policies as much as possible though to keep things separated.
449
444
  The return value of the policies is discarded. If a policy needs to fail, raise
450
445
  an appropriate exception.
451
446
 
452
- ### Policy chains
447
+ ### Policy Chains
453
448
 
454
449
  As mentioned above, policies can be executed at various points in your
455
450
  operation's lifecycle. This is possible using *policy chains*:
@@ -484,6 +479,17 @@ operation's lifecycle. This is possible using *policy chains*:
484
479
  its descendants. Policies in this chain run after nested model operations are
485
480
  performed before performing any nested model operations.
486
481
 
482
+ - `:before_model_validation`
483
+
484
+ This only applies to operations deriving from `RailsOps::Operation::Model`
485
+ and its descendants. Policies in this chain run right before
486
+ `model.validate!` is called inside `perform_nested_model_ops!`. This is
487
+ the correct place for attribute cleanup and sanitization — for example,
488
+ nilling out attributes that are irrelevant based on another attribute's
489
+ value (e.g. role-dependent fields after a form reload). At this point
490
+ the model's attributes are already assigned, so you can inspect and
491
+ modify them before validation runs.
492
+
487
493
  - `:after_perform`
488
494
 
489
495
  Policies in this chain run immediately after the `perform` method is called.
@@ -521,8 +527,7 @@ It is also important to note, that this block is
521
527
  not guaranteed to be run first in the chain, if multiple blocks have set `:prepend_action` to true.
522
528
 
523
529
 
524
- Calling sub-operations
525
- ----------------------
530
+ ## Calling Sub-Operations
526
531
 
527
532
  It is possible and encouraged to call operations within operations if necessary.
528
533
  As the basic principle is to create one operation per business action, there are
@@ -551,7 +556,7 @@ within the context that is automatically adapted and passed to the sub-operation
551
556
  and enables to maintain the complete call stack and allows to pass on context
552
557
  information such as the current user.
553
558
 
554
- ### A note on validations
559
+ ### A Note on Validations
555
560
 
556
561
  As always when calling operations, you can decide whether an execution should
557
562
  raise an exception on validation errors or else just return `false` by using the
@@ -574,15 +579,14 @@ catches any validation errors and re-throws them as
574
579
  {RailsOps::Exceptions::SubOpValidationFailed} which is not caught by the
575
580
  surrounding op.
576
581
 
577
- Contexts
578
- --------
582
+ ## Contexts
579
583
 
580
584
  Most operations make use of generic parameters like the current user or an
581
585
  authorization ability. Sure this could all be passed using the `params` hash,
582
586
  but as this would have to be done for every single operation call, it would be
583
587
  quite cumbersome.
584
588
 
585
- For this reason Rails Ops provides a feature called *Contexts*. Contexts are
589
+ For this reason, Rails Ops provides a feature called *Contexts*. Contexts are
586
590
  simple instances of {RailsOps::Context} that may or may not be passed to
587
591
  operations. Contexts can include the following data:
588
592
 
@@ -625,7 +629,7 @@ operations. Contexts can include the following data:
625
629
  called by a hook (true) or by a regular method call (false). We will introduce
626
630
  hooks below.
627
631
 
628
- ### Instantiating contexts
632
+ ### Instantiating Contexts
629
633
 
630
634
  Contexts behave like a traditional model object and can be instantiated in
631
635
  multiple ways:
@@ -638,7 +642,7 @@ context = Context.new
638
642
  context.user = current_user
639
643
  ```
640
644
 
641
- ### Feeding contexts to operations
645
+ ### Feeding Contexts to Operations
642
646
 
643
647
  Contexts are assigned to operations via the operation's constructor:
644
648
 
@@ -664,8 +668,7 @@ the parent operation.
664
668
  This is called *context spawning* and is performed using the
665
669
  {RailsOps::Context.spawn} method.
666
670
 
667
- Hooks
668
- -----
671
+ ## Hooks
669
672
 
670
673
  In some cases, certain actions must be hooked in after execution of an
671
674
  operation. While this can certainly be done with sub-operations, it is not
@@ -684,7 +687,7 @@ specify which operations should be triggered after which operations. These
684
687
  operations are then automatically triggered after the original operation's
685
688
  `perform` (in the `run` method).
686
689
 
687
- ### Defining hooks
690
+ ### Defining Hooks
688
691
 
689
692
  Hooks are defined in a file named `config/hookup.rb` in your local application.
690
693
  In development mode, this file is automatically reloaded on each request so
@@ -736,7 +739,7 @@ end
736
739
  In most cases though, situations like these should rather be handled by
737
740
  explicitly calling a sub-operation.
738
741
 
739
- ### Hook parameters
742
+ ### Hook Parameters
740
743
 
741
744
  For each hook that is called, at set of parameters is passed to the respective
742
745
  operations. When calling events manually (see section *Events*), you can
@@ -756,7 +759,7 @@ hooks into the source operation and prepares the params specifically for the
756
759
  target operation, which is then called using a sub-operation or the hooking
757
760
  system.
758
761
 
759
- ### Check if called via hook
762
+ ### Check if Called via Hook
760
763
 
761
764
  You can determine whether your operation has been (directly) called via a hook
762
765
  using the `called_via_hook` context method:
@@ -774,11 +777,10 @@ sub-operation is set to `false` again.
774
777
  ### Authorization
775
778
 
776
779
  Operations called via hooks perform normal authorization per default. You can
777
- turn this off by switching off the gobal option
780
+ turn this off by switching off the global option
778
781
  `config.trigger_hookups_without_authorization`.
779
782
 
780
- Authorization
781
- -------------
783
+ ## Authorization
782
784
 
783
785
  Rails Ops offers backend-agnostic authorization using so-called
784
786
  *authorization backends*.
@@ -787,7 +789,7 @@ Authorization basically happens by calling the method `authorize!` (or
787
789
  `authorize_only!`, more on that later) within an operation. What exactly this
788
790
  method does depends on the *authorization backend* specified.
789
791
 
790
- ### Authorization backends
792
+ ### Authorization Backends
791
793
 
792
794
  Authorization backends are simple classes that supply the method `authorize!`.
793
795
  This method, besides the operation instance, can take any number of arguments
@@ -807,12 +809,12 @@ end
807
809
 
808
810
  RailsOps ships with the following backend:
809
811
 
810
- - `RailsOps::AutorizationBackend::Cancancan`
812
+ - `RailsOps::AuthorizationBackend::CanCanCan`
811
813
 
812
- Offers integration of the `cancancan` Gem (which is a fork of the `cancan`
813
- Gem).
814
+ Offers integration of the `cancancan` gem (which is a fork of the `cancan`
815
+ gem).
814
816
 
815
- ### Performing authorization
817
+ ### Performing Authorization
816
818
 
817
819
  Authorization is generally performed by calling `authorize!` in an operation.
818
820
  The arguments, along with the operation instance, are passed on to the
@@ -853,7 +855,7 @@ end
853
855
 
854
856
  See section *Policy chains* for more information.
855
857
 
856
- ### Ensure that authorization has been performed
858
+ ### Ensure That Authorization Has Been Performed
857
859
 
858
860
  As it is a very common programming mistake to mistakenly omit calling
859
861
  authorization, Rails Ops offers a solution for making sure that authorization
@@ -887,7 +889,7 @@ end
887
889
  This method otherwise does exactly the same as `authorize!` (in fact, it's the
888
890
  underlying method used by it).
889
891
 
890
- ### Param authorization
892
+ ### Param Authorization
891
893
 
892
894
  Using the static operation method `authorize_param`, you can perform additional
893
895
  authorization checks when specific params are passed to the operation. This
@@ -940,7 +942,7 @@ class Operations::User::Create < RailsOps::Operation::Model::Create
940
942
  authorize_param %i(user group_id), :assign_group_id
941
943
  ```
942
944
 
943
- ### Disabling authorization
945
+ ### Disabling Authorization
944
946
 
945
947
  Sometimes you don't want a specific operation to perform authorization, or you
946
948
  don't want to perform any authorization at all.
@@ -1045,8 +1047,7 @@ Rails Ops offers multiple ways of disabling authorization:
1045
1047
  ```
1046
1048
 
1047
1049
 
1048
- Model Operations
1049
- ----------------
1050
+ ## Model Operations
1050
1051
 
1051
1052
  One of the key features of RailsOps is model operations. RailsOps provides
1052
1053
  multiple operation base classes which allow convenient manipulation of active
@@ -1059,7 +1060,7 @@ inherit from {RailsOps::Operation::Model} (which in turn inherits from
1059
1060
  The key principle behind these model classes is to associate *one model class*
1060
1061
  and *one model instance* with a particular operation.
1061
1062
 
1062
- ### Setting a model class
1063
+ ### Setting a Model Class
1063
1064
 
1064
1065
  Using the static method `model`, you can assign a model class that is used in
1065
1066
  the scope of this operation.
@@ -1097,7 +1098,7 @@ class SomeOperation < RailsOps::Operation::Model
1097
1098
  end
1098
1099
  ```
1099
1100
 
1100
- ### Obtaining a model instance
1101
+ ### Obtaining a Model Instance
1101
1102
 
1102
1103
  Model instances can be obtained using the *instance* method `model`, which is
1103
1104
  not to be confused with the *class* method of the same name. Other than the
@@ -1126,7 +1127,7 @@ If no cached instance is found, one is built using the instance method
1126
1127
  but only implemented in its subclasses. You can implement and override this
1127
1128
  method to your liking though.
1128
1129
 
1129
- ### Loading models
1130
+ ### Loading Models
1130
1131
 
1131
1132
  Using the base operation class {RailsOps::Operation::Model::Load}, a model can
1132
1133
  be loaded. This is done by implementing the `build_model` mentioned above. In
@@ -1150,7 +1151,7 @@ order to load a model (in fact, it cannot be run unless you override the
1150
1151
  based on a model instance without actually performing any particular action such
1151
1152
  as updating a model.
1152
1153
 
1153
- #### Specifying ID field
1154
+ #### Specifying ID Field
1154
1155
 
1155
1156
  Per default, the model instance is looked up using the field `id` and the ID
1156
1157
  obtained from the method params using `params[:id]`. However, you can customize
@@ -1205,9 +1206,9 @@ class Operations::User::Update < RailsOps::Operation::Model::Load
1205
1206
  end
1206
1207
  ```
1207
1208
 
1208
- One caveat is, that shared locking is only supported for MySQl (MariaDB),
1209
- Postgresql and Oracle DB databases, any other database will always use an
1210
- exlusive lock.
1209
+ One caveat is that shared locking is only supported for MySQL (MariaDB),
1210
+ PostgreSQL and Oracle DB databases, any other database will always use an
1211
+ exclusive lock.
1211
1212
 
1212
1213
  You can also dynamically enable or disable locking by creating an instance
1213
1214
  method `lock_model_at_build?`:
@@ -1225,7 +1226,7 @@ class Operations::User::Update < RailsOps::Operation::Model::Load
1225
1226
  end
1226
1227
  ```
1227
1228
 
1228
- ### Creating models
1229
+ ### Creating Models
1229
1230
 
1230
1231
  For creating models, you can use the base class
1231
1232
  {RailsOps::Operation::Model::Create}.
@@ -1255,7 +1256,7 @@ end
1255
1256
  As this base class is very minimalistic, it is recommended to fully read and
1256
1257
  comprehend its source code.
1257
1258
 
1258
- #### Overriding the perform method
1259
+ #### Overriding the Perform Method
1259
1260
 
1260
1261
  While in many cases there is no need for overriding the `perform` method, this
1261
1262
  can be useful i.e. when assigning or altering properties manually:
@@ -1269,7 +1270,7 @@ def perform
1269
1270
  end
1270
1271
  ```
1271
1272
 
1272
- ### Updating models
1273
+ ### Updating Models
1273
1274
 
1274
1275
  For updating models, you can use the base class
1275
1276
  {RailsOps::Operation::Model::Update} which is an extension of the `Load` base
@@ -1304,7 +1305,7 @@ comprehend its source code.
1304
1305
  As with `Create` operations, the `perform` method can be overwritten at your
1305
1306
  liking.
1306
1307
 
1307
- ### Destroying models
1308
+ ### Destroying Models
1308
1309
 
1309
1310
  For destroying models, you can use the base class
1310
1311
  {RailsOps::Operation::Model::Destroy} which is an extension of the `Load` base
@@ -1326,9 +1327,9 @@ end
1326
1327
  As this base class is very minimalistic, it is recommended to fully read and
1327
1328
  comprehend its source code.
1328
1329
 
1329
- ### Including associated records
1330
+ ### Including Associated Records
1330
1331
 
1331
- Normaly, when inheriting from `RailsOps::Operation::Model::Load` (as well as from the
1332
+ Normally, when inheriting from `RailsOps::Operation::Model::Load` (as well as from the
1332
1333
  `Update` and the `Destroy` operations respectively), RailsOps only loads the instance
1333
1334
  of the model specified by the `id` parameter. In some cases, you'd want to eagerly load
1334
1335
  associations of the model, e.g. when you need to access associated records.
@@ -1354,7 +1355,7 @@ class Operations::User::Load < RailsOps::Operation::Model::Load
1354
1355
  end
1355
1356
  ```
1356
1357
 
1357
- ### Parameter extraction for create and update
1358
+ ### Parameter Extraction for Create and Update
1358
1359
 
1359
1360
  As mentioned before, the `Create` and `Update` base classes provide an
1360
1361
  implementation of `build_model` that assigns parameters to a model.
@@ -1363,12 +1364,12 @@ The attributes are determined by the operation instance method
1363
1364
  `extract_attributes_from_params` - the name being self-explaining. See its
1364
1365
  source code for implementation details.
1365
1366
 
1366
- ### Model authorization
1367
+ ### Model Authorization
1367
1368
 
1368
1369
  While you can use the standard `authorize!` method (see chapter *Authorization*)
1369
1370
  for authorizing models, RailsOps provides a more convenient integration.
1370
1371
 
1371
- #### Basic authorization
1372
+ #### Basic Authorization
1372
1373
 
1373
1374
  Model authorization can be performed via the operation instance methods
1374
1375
  `authorize_model!` and `authorize_model_with_authorize_only!` (see chapter
@@ -1384,7 +1385,7 @@ methods instead of the basic authorization methods for authorizing models.
1384
1385
  If no model is given, the model authorization methods automatically obtain the
1385
1386
  model from the instance method `model`.
1386
1387
 
1387
- #### Automatic authorization
1388
+ #### Automatic Authorization
1388
1389
 
1389
1390
  All model operation classes provide the operation instance method
1390
1391
  `model_authorization` which is automatically run at model instantiation (this is
@@ -1426,7 +1427,7 @@ end
1426
1427
  Note that using the different model base classes, this is already set to a
1427
1428
  sensible default. See the respective class' source code for details.
1428
1429
 
1429
- #### Lazy model update authorization
1430
+ #### Lazy Model Update Authorization
1430
1431
 
1431
1432
  *Please note that using lazy model update authorization is deprecated any may
1432
1433
  be removed in a future release. See the changelog for instructions on how to
@@ -1449,7 +1450,7 @@ class Operations::User::Update < RailsOps::Operation::Model::Update
1449
1450
  end
1450
1451
  ```
1451
1452
 
1452
- ### Model nesting
1453
+ ### Model Nesting
1453
1454
 
1454
1455
  Using active record, multiple nested models can be saved at once by using
1455
1456
  `accepts_nested_attributes_for`. While this is generally supported by RailsOps,
@@ -1477,7 +1478,6 @@ class Operations::Group::Create < RailsOps::Operation::Model::Create
1477
1478
  end
1478
1479
 
1479
1480
  model ::Group
1480
- nest_model_op :group, Operations::Group::Create
1481
1481
  end
1482
1482
  ```
1483
1483
 
@@ -1495,7 +1495,7 @@ class User
1495
1495
  end
1496
1496
  ```
1497
1497
 
1498
- #### Param key
1498
+ #### Param Key
1499
1499
 
1500
1500
  When nesting a model operation, the sub operation is called automatically by
1501
1501
  RailsOps. For this purpose, it needs to know which `param_key` to use for
@@ -1510,7 +1510,7 @@ using the option `param_key`, e.g.:
1510
1510
  nest_model_op :group, Operations::Group::Create, param_key: :my_custom_key
1511
1511
  ```
1512
1512
 
1513
- #### Custom parameters
1513
+ #### Custom Parameters
1514
1514
 
1515
1515
  In the above examples, all `group_attributes` are automatically passed to the
1516
1516
  sub operation. To customize this further, provide a block to the `nest_model_op`
@@ -1526,10 +1526,10 @@ This block receives the params hash as it would be passed to the sub operation
1526
1526
  and allows to modify it. The block's return value is then passed to the
1527
1527
  sub-operation. Do not change the params inplace but instead return a new hash.
1528
1528
 
1529
- ### Single-table inheritance
1529
+ ### Single-Table Inheritance
1530
1530
 
1531
1531
  Model operations also support STI models (Single Table Inheritance). However,
1532
- there is the caviat that if you do extend your model in the operation (e.g.
1532
+ there is the caveat that if you do extend your model in the operation (e.g.
1533
1533
  `model Animal do { ... }`), RailsOps automatically creates an anonymous subclass
1534
1534
  of the given class (e.g. `Animal`). Operations will always load / create models
1535
1535
  that are instances of this anonymous class.
@@ -1548,22 +1548,373 @@ class LoadAnimal < RailsOps::Operation::Model::Load
1548
1548
  end
1549
1549
 
1550
1550
  bird = Bird.create
1551
- op_bird = LoadAnimal.new(id: bird.id)
1551
+ op = LoadAnimal.new(id: bird.id)
1552
+
1553
+ bird.class # => Bird (extending Animal)
1554
+ op.model.class # => Anonymous class extending Animal, not Bird
1555
+ ```
1556
+
1557
+ ## Record Extension and Virtual Records
1558
+
1559
+ RailsOps provides powerful features for extending ActiveRecord models and
1560
+ creating virtual records without affecting your actual model classes. This is
1561
+ achieved through the use of ActiveType and anonymous class generation.
1562
+
1563
+ ### Virtual Models
1564
+
1565
+ Virtual models are non-persisted models that behave like ActiveRecord models
1566
+ but exist only in memory. They're useful for:
1567
+
1568
+ - Form objects that don't map directly to database tables
1569
+ - Temporary data structures for complex operations
1570
+ - Aggregating data from multiple sources
1571
+
1572
+ RailsOps provides `RailsOps::VirtualModel` which extends `ActiveType::Object`:
1573
+
1574
+ ```ruby
1575
+ class Operations::Contact::Create < RailsOps::Operation::Model::Create
1576
+ model do
1577
+ # Virtual attributes
1578
+ attribute :full_name, :string
1579
+ attribute :email, :string
1580
+ attribute :message, :text
1581
+ attribute :newsletter_opt_in, :boolean, default: false
1582
+
1583
+ # Validations work just like regular models
1584
+ validates :full_name, :email, :message, presence: true
1585
+ validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
1586
+ end
1587
+
1588
+ def perform
1589
+ # Process the virtual model data
1590
+ ContactMailer.contact_form(
1591
+ name: model.full_name,
1592
+ email: model.email,
1593
+ message: model.message
1594
+ ).deliver_later
1595
+
1596
+ # Optionally subscribe to newsletter
1597
+ if model.newsletter_opt_in
1598
+ NewsletterService.subscribe(model.email)
1599
+ end
1600
+ end
1601
+ end
1602
+ ```
1603
+
1604
+ ### Model Extension
1605
+
1606
+ When you specify a model with a block in an operation, RailsOps creates an
1607
+ anonymous subclass that extends your model without modifying the original:
1608
+
1609
+ ```ruby
1610
+ class Operations::User::Import < RailsOps::Operation::Model::Create
1611
+ model User do
1612
+ # These changes only apply within this operation
1613
+ attribute :import_source, :string
1614
+ attribute :skip_notifications, :boolean, default: false
1615
+
1616
+ validates :import_source, presence: true
1617
+
1618
+ # Override methods
1619
+ def name=(value)
1620
+ super(value.strip.titleize)
1621
+ end
1622
+
1623
+ # Add callbacks specific to this operation
1624
+ before_save :normalize_phone_number
1625
+
1626
+ private
1627
+
1628
+ def normalize_phone_number
1629
+ self.phone = PhoneNumberService.normalize(phone) if phone.present?
1630
+ end
1631
+ end
1632
+
1633
+ def perform
1634
+ model.imported_at = Time.current
1635
+ super
1636
+
1637
+ unless model.skip_notifications
1638
+ UserMailer.welcome(model).deliver_later
1639
+ end
1640
+ end
1641
+ end
1642
+ ```
1643
+
1644
+ ### Virtual Attributes
1645
+
1646
+ Virtual attributes allow you to add non-persisted attributes to your models
1647
+ that behave like regular attributes:
1648
+
1649
+ ```ruby
1650
+ class Operations::Order::Checkout < RailsOps::Operation::Model::Update
1651
+ model Order do
1652
+ # Virtual attributes for checkout process
1653
+ attribute :card_number, :string
1654
+ attribute :card_cvv, :string
1655
+ attribute :card_exp_month, :integer
1656
+ attribute :card_exp_year, :integer
1657
+ attribute :save_card, :boolean, default: false
1658
+
1659
+ # Validations for virtual attributes
1660
+ validates :card_number, presence: true, length: { is: 16 }
1661
+ validates :card_cvv, presence: true, length: { in: 3..4 }
1662
+ validates :card_exp_month, inclusion: { in: 1..12 }
1663
+ validates :card_exp_year, numericality: {
1664
+ greater_than_or_equal_to: Date.current.year
1665
+ }
1666
+
1667
+ # Virtual attribute for computed values
1668
+ attribute :total_with_tax, :decimal
1669
+
1670
+ before_validation :calculate_total_with_tax
1671
+
1672
+ private
1673
+
1674
+ def calculate_total_with_tax
1675
+ self.total_with_tax = total * (1 + tax_rate)
1676
+ end
1677
+ end
1552
1678
 
1553
- bird.class # => Class "Bird", extending "Animal"
1554
- op_bird.class # => Anonymous class, extending "Animal", not "Bird"
1679
+ def perform
1680
+ # Process payment with virtual attributes
1681
+ payment_result = PaymentGateway.charge(
1682
+ amount: model.total_with_tax,
1683
+ card_number: model.card_number,
1684
+ cvv: model.card_cvv,
1685
+ exp_month: model.card_exp_month,
1686
+ exp_year: model.card_exp_year
1687
+ )
1688
+
1689
+ if payment_result.success?
1690
+ model.payment_id = payment_result.transaction_id
1691
+ model.paid_at = Time.current
1692
+ super # Save the order
1693
+
1694
+ # Optionally save card for future use
1695
+ if model.save_card
1696
+ CreatePaymentMethod.run!(
1697
+ user: model.user,
1698
+ token: payment_result.card_token
1699
+ )
1700
+ end
1701
+ else
1702
+ fail PaymentError, payment_result.error_message
1703
+ end
1704
+ end
1705
+ end
1555
1706
  ```
1556
1707
 
1557
- Record extension and virtual records
1558
- ------------------------------------
1708
+ ### Combining Real and Virtual Models
1709
+
1710
+ You can create operations that work with both persisted and virtual data:
1711
+
1712
+ ```ruby
1713
+ class Operations::Report::Generate < RailsOps::Operation::Model
1714
+ model do
1715
+ attribute :start_date, :date
1716
+ attribute :end_date, :date
1717
+ attribute :include_archived, :boolean, default: false
1718
+ attribute :format, :string, default: 'pdf'
1719
+
1720
+ validates :start_date, :end_date, presence: true
1721
+ validate :end_date_after_start_date
1722
+
1723
+ private
1724
+
1725
+ def end_date_after_start_date
1726
+ return unless start_date && end_date
1727
+ errors.add(:end_date, 'must be after start date') if end_date < start_date
1728
+ end
1729
+ end
1730
+
1731
+ def perform
1732
+ scope = Order.where(created_at: model.start_date..model.end_date)
1733
+ scope = scope.includes(:archived) if model.include_archived
1734
+
1735
+ report_data = ReportBuilder.new(scope).generate
1736
+
1737
+ case model.format
1738
+ when 'pdf'
1739
+ ReportPdfGenerator.new(report_data).to_pdf
1740
+ when 'csv'
1741
+ ReportCsvGenerator.new(report_data).to_csv
1742
+ else
1743
+ report_data
1744
+ end
1745
+ end
1746
+ end
1747
+ ```
1748
+
1749
+ ## Transactions
1750
+
1751
+ When working with database operations, it's crucial to ensure data consistency
1752
+ using transactions, especially when multiple models are involved.
1753
+
1754
+ ### Transaction Behavior in Model Operations
1559
1755
 
1756
+ It's important to understand that RailsOps model operations do NOT automatically
1757
+ start any transactions.
1560
1758
 
1561
- Transactions
1562
- ------------
1759
+ To ensure all operations succeed or fail together, you must explicitly wrap them
1760
+ in a transaction:
1761
+
1762
+ ```ruby
1763
+ class Operations::Order::Process < RailsOps::Operation::Model::Update
1764
+ def perform
1765
+ ActiveRecord::Base.transaction do
1766
+ model.status = 'processing'
1767
+ model.processed_at = Time.current
1768
+ super # Saves the order
1769
+
1770
+ # Now if this fails, everything is rolled back
1771
+ OrderItem.where(order: model).update_all(status: 'processing')
1772
+ InventoryService.reserve_items(model.items)
1773
+ end
1774
+ end
1775
+ end
1776
+ ```
1777
+
1778
+ Typically though, transactions are opened on a higher level and outside of
1779
+ operations, e.g. in controller methods.
1780
+
1781
+ ### Rollback on Exception
1782
+
1783
+ When using `run` (without bang), validation errors are caught and may not cause
1784
+ transaction rollback. The `with_rollback_on_exception` helper ensures that
1785
+ exceptions within its block are re-raised as `RollbackRequired`, which will
1786
+ cause a rollback even when using `run`:
1787
+
1788
+ ```ruby
1789
+ class Operations::User::ComplexUpdate < RailsOps::Operation::Model::Update
1790
+ def perform
1791
+ ActiveRecord::Base.transaction do
1792
+ super # Saves the user
1793
+
1794
+ # Without with_rollback_on_exception, validation errors here won't
1795
+ # roll back the transaction when the operation is called with run
1796
+ with_rollback_on_exception do
1797
+ model.profile.bio = params[:bio]
1798
+ model.profile.save! # If this fails, transaction is rolled back
1799
+
1800
+ model.settings.notifications = params[:notifications]
1801
+ model.settings.save! # If this fails, transaction is rolled back
1802
+ end
1803
+ end
1804
+ end
1805
+ end
1806
+
1807
+ class UsersController < ApplicationController
1808
+ def update
1809
+ ActiveRecord::Base.transaction do
1810
+ if run Operations::User::ComplexUpdate
1811
+ render json: { status: :success }
1812
+ else
1813
+ render json: { status: :validation_error }
1814
+ end
1815
+ end
1816
+ end
1817
+ end
1818
+ ```
1819
+
1820
+ **Important**: `with_rollback_on_exception` only works within an existing
1821
+ transaction. It doesn't create a transaction - it just ensures exceptions
1822
+ cause rollback:
1823
+
1824
+ ```ruby
1825
+ class Operations::Order::Process < RailsOps::Operation::Model::Update
1826
+ def perform
1827
+ # PROBLEMATIC: Each save creates its own transaction
1828
+ super # Order is saved and committed in its own transaction
1829
+
1830
+ with_rollback_on_exception do
1831
+ model.line_items.each { |item| item.update!(status: 'processed') }
1832
+ end
1833
+ end
1834
+ end
1835
+
1836
+ # CORRECT: Wrap in a transaction
1837
+ class Operations::Order::Process < RailsOps::Operation::Model::Update
1838
+ def perform
1839
+ ActiveRecord::Base.transaction do
1840
+ super # Order is saved
1841
+
1842
+ with_rollback_on_exception do
1843
+ model.line_items.each { |item| item.update!(status: 'processed') }
1844
+ end
1845
+ end
1846
+ end
1847
+ end
1848
+ ```
1849
+
1850
+ ### After Commit Callbacks
1851
+
1852
+ When no explicit transaction is used, each `save!` opens and commits its own
1853
+ transaction. You can use Rails' after_commit callbacks in your model
1854
+ extensions for actions that should only run after successful database commits:
1855
+
1856
+ ```ruby
1857
+ # Using after_commit callbacks in model extension
1858
+ class Operations::User::Create < RailsOps::Operation::Model::Create
1859
+ model User do
1860
+ after_commit :send_notifications, on: :create
1861
+
1862
+ private
1863
+
1864
+ def send_notifications
1865
+ UserMailer.welcome(self).deliver_later
1866
+ CrmSyncJob.perform_later(self)
1867
+ end
1868
+ end
1869
+ end
1870
+
1871
+ # Or handle it manually after the operation
1872
+ class Operations::Order::Complete < RailsOps::Operation::Model::Update
1873
+ def perform
1874
+ ActiveRecord::Base.transaction do
1875
+ model.status = 'completed'
1876
+ model.completed_at = Time.current
1877
+ super
1878
+ end
1879
+
1880
+ # This runs after the transaction commits successfully
1881
+ # If there was an exception, we never get here
1882
+ OrderMailer.completed(model).deliver_later
1883
+ end
1884
+ end
1885
+ ```
1886
+
1887
+ **Note**: Be careful with after_commit callbacks when using transactions.
1888
+ They fire after each transaction commits, not after all nested transactions
1889
+ complete.
1890
+
1891
+ ### Important Notes on Transactions
1892
+
1893
+ 1. **Validation Errors**: When using `run` (without bang), validation errors
1894
+ are caught and won't roll back the transaction. Use `run!` for
1895
+ sub-operations to ensure transaction rollback on validation errors.
1896
+
1897
+ 2. **External Services**: Be careful when calling external services within
1898
+ transactions. Long-running external calls can cause database locks:
1899
+
1900
+ ```ruby
1901
+ def perform
1902
+ ActiveRecord::Base.transaction do
1903
+ model.save!
1904
+
1905
+ # DON'T: This could lock the database for a long time
1906
+ # ExternalApi.slow_request(model)
1907
+ end
1908
+
1909
+ # DO: Call external services after the transaction
1910
+ ExternalApi.slow_request(model)
1911
+ end
1912
+ ```
1563
1913
 
1914
+ 3. **Nested Transactions**: Rails uses savepoints for nested transactions,
1915
+ which are fully supported by RailsOps operations.
1564
1916
 
1565
- Controller Integration
1566
- ----------------------
1917
+ ## Controller Integration
1567
1918
 
1568
1919
  While RailsOps certainly does not have to be used from a controller, it
1569
1920
  provides a mixin which extends controller classes with functionality that lets
@@ -1581,7 +1932,7 @@ class ApplicationController
1581
1932
  end
1582
1933
  ```
1583
1934
 
1584
- ### Basic usage
1935
+ ### Basic Usage
1585
1936
 
1586
1937
  The basic concept behind controller integration is to instantiate and
1587
1938
  potentially run a single operation per request. Most of this guide refers to
@@ -1599,7 +1950,7 @@ class SomeController < ApplicationController
1599
1950
  end
1600
1951
  ```
1601
1952
 
1602
- ### Separating instantiation and execution
1953
+ ### Separating Instantiation and Execution
1603
1954
 
1604
1955
  In the previous example, we instantiated and ran an operation in a single
1605
1956
  statement. While this might be feasible for some "fire-and-forget" controller
@@ -1647,12 +1998,12 @@ def update_username
1647
1998
  end
1648
1999
  ```
1649
2000
 
1650
- ### Checking for operations
2001
+ ### Checking for Operations
1651
2002
 
1652
2003
  Using the method `op?`, you can check whether an operation has already been
1653
2004
  instantiated (using `op`).
1654
2005
 
1655
- ### Model shortcut
2006
+ ### Model Shortcut
1656
2007
 
1657
2008
  RailsOps conveniently provides you with a `model` instance method, which is a
1658
2009
  shortcut for `op.model`. This is particularly useful since this is available as
@@ -1661,7 +2012,7 @@ a view helper method as well, see next section.
1661
2012
  You can check whether a model is available by using the `model?` method, which
1662
2013
  is available in both controllers and views.
1663
2014
 
1664
- ### View helper methods
2015
+ ### View Helper Methods
1665
2016
 
1666
2017
  The following controller methods are automatically provided as helper methods
1667
2018
  which can be used in views:
@@ -1705,7 +2056,7 @@ You can also combine these two approaches:
1705
2056
  op SomeOperation, some_param: op_params.slice(:some_param, :some_other_param)
1706
2057
  ```
1707
2058
 
1708
- ### Authorization ensuring
2059
+ ### Authorization Ensuring
1709
2060
 
1710
2061
  For security reasons, RailsOps automatically checks after each action whether
1711
2062
  authorization has been performed. This is to avoid serving an action's response
@@ -1733,7 +2084,7 @@ automatically created. The following fields are set automatically:
1733
2084
  - `session` (uses the `session` controller method)
1734
2085
  - `url_options` (uses the `url_options` controller method)
1735
2086
 
1736
- ### Multiple operations per request
2087
+ ### Multiple Operations per Request
1737
2088
 
1738
2089
  RailsOps does not currently support calling multiple operations in a single
1739
2090
  controller action out-of-the-box. You need to instantiate and run it manually.
@@ -1741,11 +2092,9 @@ controller action out-of-the-box. You need to instantiate and run it manually.
1741
2092
  Another approach is to create a parent operation which calls multiple
1742
2093
  sub-operations, see section *Calling sub-operations* for more information.
1743
2094
 
1744
- Operation Inheritance
1745
- ---------------------
2095
+ ## Operation Inheritance
1746
2096
 
1747
- Generators
1748
- ----------
2097
+ ## Generators
1749
2098
 
1750
2099
  RailsOps features a generator to easily create a structure for common CRUD-style
1751
2100
  constructs. The generator creates the CRUD operations, some empty view files, a
@@ -1833,8 +2182,7 @@ Of course, at this point, the operations will need some adaptions, especially th
1833
2182
  [parameter schemas](#validating-params), and the controllers need the logic for the
1834
2183
  success and failure cases, as this depends on your application.
1835
2184
 
1836
- Lazy Load Hooks
1837
- ---------------
2185
+ ## Lazy Load Hooks
1838
2186
 
1839
2187
  RailsOps provides the following [Rails Lazy Load
1840
2188
  Hooks](https://api.rubyonrails.org/v7.1.3.4/classes/ActiveSupport/LazyLoadHooks.html):
@@ -1851,10 +2199,9 @@ Example usage:
1851
2199
  ActiveSupport.on_load(:rails_ops_op_model_create) { include MyMixin }
1852
2200
  ```
1853
2201
 
1854
- Caveats
1855
- -------
2202
+ ## Caveats
1856
2203
 
1857
- ### Eager loading in development mode
2204
+ ### Eager Loading in Development Mode
1858
2205
 
1859
2206
  Eager loading operation classes containing models with nested models or
1860
2207
  operations can be very slow in performance. In production mode, the same process
@@ -1865,11 +2212,11 @@ setting in production mode though.
1865
2212
 
1866
2213
  ## Contributors
1867
2214
 
1868
- This Gem is heavily inspired by the [trailblazer](http://trailblazer.to/) Gem
2215
+ This gem is heavily inspired by the [trailblazer](http://trailblazer.to/) gem
1869
2216
  which provides a wonderful, high-level architecture for Rails – beyond just
1870
2217
  operations. Be sure to check this out when trying to decide on an alternative
1871
2218
  Rails architecture.
1872
2219
 
1873
2220
  ## Copyright
1874
2221
 
1875
- Copyright © 2017 - 2025 Sitrox. See `LICENSE` for further details.
2222
+ Copyright © 2017 - 2026 Sitrox. See `LICENSE` for further details.