rails_ops 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rubocop.yml +84 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +1216 -0
  8. data/RUBY_VERSION +1 -0
  9. data/Rakefile +39 -0
  10. data/VERSION +1 -0
  11. data/lib/rails_ops.rb +96 -0
  12. data/lib/rails_ops/authorization_backend/abstract.rb +7 -0
  13. data/lib/rails_ops/authorization_backend/can_can_can.rb +14 -0
  14. data/lib/rails_ops/configuration.rb +4 -0
  15. data/lib/rails_ops/context.rb +35 -0
  16. data/lib/rails_ops/controller_mixin.rb +105 -0
  17. data/lib/rails_ops/exceptions.rb +19 -0
  18. data/lib/rails_ops/hooked_job.rb +25 -0
  19. data/lib/rails_ops/hookup.rb +80 -0
  20. data/lib/rails_ops/hookup/dsl.rb +29 -0
  21. data/lib/rails_ops/hookup/dsl_validator.rb +45 -0
  22. data/lib/rails_ops/hookup/hook.rb +11 -0
  23. data/lib/rails_ops/log_subscriber.rb +24 -0
  24. data/lib/rails_ops/mixins.rb +2 -0
  25. data/lib/rails_ops/mixins/authorization.rb +83 -0
  26. data/lib/rails_ops/mixins/log_settings.rb +20 -0
  27. data/lib/rails_ops/mixins/model.rb +4 -0
  28. data/lib/rails_ops/mixins/model/authorization.rb +64 -0
  29. data/lib/rails_ops/mixins/model/nesting.rb +180 -0
  30. data/lib/rails_ops/mixins/policies.rb +42 -0
  31. data/lib/rails_ops/mixins/require_context.rb +33 -0
  32. data/lib/rails_ops/mixins/routes.rb +35 -0
  33. data/lib/rails_ops/mixins/schema_validation.rb +25 -0
  34. data/lib/rails_ops/mixins/sub_ops.rb +35 -0
  35. data/lib/rails_ops/model_casting.rb +17 -0
  36. data/lib/rails_ops/model_mixins.rb +12 -0
  37. data/lib/rails_ops/model_mixins/ar_extension.rb +20 -0
  38. data/lib/rails_ops/model_mixins/parent_op.rb +10 -0
  39. data/lib/rails_ops/model_mixins/protected_attributes.rb +78 -0
  40. data/lib/rails_ops/model_mixins/virtual_attributes.rb +24 -0
  41. data/lib/rails_ops/model_mixins/virtual_attributes/virtual_column_wrapper.rb +9 -0
  42. data/lib/rails_ops/model_mixins/virtual_has_one.rb +32 -0
  43. data/lib/rails_ops/operation.rb +215 -0
  44. data/lib/rails_ops/operation/model.rb +168 -0
  45. data/lib/rails_ops/operation/model/create.rb +35 -0
  46. data/lib/rails_ops/operation/model/destroy.rb +26 -0
  47. data/lib/rails_ops/operation/model/load.rb +72 -0
  48. data/lib/rails_ops/operation/model/update.rb +31 -0
  49. data/lib/rails_ops/patches/active_type_patch.rb +52 -0
  50. data/lib/rails_ops/profiler.rb +47 -0
  51. data/lib/rails_ops/profiler/node.rb +64 -0
  52. data/lib/rails_ops/railtie.rb +19 -0
  53. data/lib/rails_ops/scoped_env.rb +20 -0
  54. data/lib/rails_ops/virtual_model.rb +19 -0
  55. data/rails_ops.gemspec +58 -0
  56. data/test/test_helper.rb +3 -0
  57. metadata +252 -0
@@ -0,0 +1,42 @@
1
+ # Mixin for the {RailsOps::Operation} class that provides *policies*. Policies
2
+ # are simple blocks of code that run at specific places in your operation and
3
+ # can be used to check conditions such as params or permissions. Policies are
4
+ # inherited to subclasses of operations.
5
+ module RailsOps::Mixins::Policies
6
+ extend ActiveSupport::Concern
7
+
8
+ POLICY_CHAIN_KEYS = [:on_init, :before_perform, :after_perform].freeze
9
+
10
+ included do
11
+ class_attribute :_policy_chains
12
+ self._policy_chains = Hash[POLICY_CHAIN_KEYS.map { |key| [key, [].freeze] }]
13
+ end
14
+
15
+ module ClassMethods
16
+ # Register a new policy block that will be executed in the given `chain`.
17
+ # The policy block will be executed in the operation's instance context.
18
+ def policy(chain = :before_perform, &block)
19
+ unless POLICY_CHAIN_KEYS.include?(chain)
20
+ fail "Unknown policy chain #{chain.inspect}, available are #{POLICY_CHAIN_KEYS.inspect}."
21
+ end
22
+
23
+ self._policy_chains = _policy_chains.dup
24
+ _policy_chains[chain] += [block]
25
+ end
26
+
27
+ # Returns all registered validation blocks for this operation class.
28
+ def policies_for(chain)
29
+ unless POLICY_CHAIN_KEYS.include?(chain)
30
+ fail "Unknown policy chain #{chain.inspect}, available are #{POLICY_CHAIN_KEYS.inspect}."
31
+ end
32
+
33
+ return _policy_chains[chain]
34
+ end
35
+ end
36
+
37
+ def run_policies(chain)
38
+ self.class.policies_for(chain).each do |block|
39
+ instance_eval(&block)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # Mixin for the {RailsOps::Operation} class that provides *require_context*.
2
+ module RailsOps::Mixins::RequireContext
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # This DSL method allows you to make sure that a context is provided to your
7
+ # operation. If used, it will fail at operation instantiation if no context
8
+ # is provided. By providing one or more `attributes`, you can optionally
9
+ # specify which context attributes need to be present (not nil).
10
+ #
11
+ # This can be useful to fail early if you're going to be accessing e.g.
12
+ # `context.user` and `context.session`. In this case, you can specify:
13
+ #
14
+ # ```ruby
15
+ # class MyOp < RailsOps::Operation
16
+ # require_context :user, :session
17
+ # end
18
+ # ```
19
+ #
20
+ # @param args [Array<Symbol>] Specify which context attributes need to be
21
+ # present
22
+ def require_context(*attributes)
23
+ policy :on_init do
24
+ attributes.each do |attribute|
25
+ if context.attributes[attribute.to_s].nil?
26
+ fail RailsOps::Exceptions::MissingContextAttribute,
27
+ "This operation requires the context attribute #{attribute.inspect} to be present."
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ module RailsOps::Mixins::Routes
2
+ extend ActiveSupport::Concern
3
+
4
+ # This class can't be defined at load time of this file as `Rails.application`
5
+ # would not exist at this point in time. Instead, we're creating and caching
6
+ # this class on the first call. This is not thread-safe, but the worst case is
7
+ # that this is performed more than once.
8
+ def self.container_class
9
+ @container ||= Class.new do
10
+ include Rails.application.routes.url_helpers
11
+
12
+ attr_reader :url_options
13
+
14
+ def initialize(url_options)
15
+ @url_options = url_options
16
+ end
17
+ end
18
+ end
19
+
20
+ # Returns an object that responds to all URL helper methods using the
21
+ # `url_options` provided with the operation's context. If no URL options are
22
+ # given, this method will raise an exception.
23
+ def routes
24
+ unless @routes
25
+ if context.url_options.nil?
26
+ fail RailsOps::Exceptions::RoutingNotAvailable,
27
+ 'Can not access routes helpers, no url_options given in context.'
28
+ end
29
+
30
+ @routes = RailsOps::Mixins::Routes.container_class.new(context.url_options)
31
+ end
32
+
33
+ return @routes
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # Mixin for the {RailsOps::Operation} class that provides a simple way of
2
+ # validation the params hash against a specific schema. It internally uses
3
+ # policies for running the validations (see {RailsOps::Mixins::Policies}).
4
+ module RailsOps::Mixins::SchemaValidation
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ # Creates a policy to validate the params hash against the given schema. See
9
+ # {Schemacop::Validator} for more information on how schemas are built.
10
+ #
11
+ # Using `policy_chain`, you can control when the validation is performed.
12
+ # Per default, validation is done before performing the operation.
13
+ #
14
+ # @param *args [Array] Parameters to pass at schema initialization
15
+ # @param policy_chain [Symbol] The policy chain to perform the schema validation in
16
+ # @yield Block to pass at schema initialization
17
+ def schema(*args, policy_chain: :before_perform, &block)
18
+ full_schema = Schemacop::Schema.new(*args, &block)
19
+
20
+ policy policy_chain do
21
+ full_schema.validate!(params)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ # Mixin for the {RailsOps::Operation} class that provides a simple way of
2
+ # running arbitrary operations within operations, automatically passing a
3
+ # modified version of the current operation's context to them.
4
+ module RailsOps::Mixins::SubOps
5
+ extend ActiveSupport::Concern
6
+
7
+ # Instantiates and returns a new operation of the given class and
8
+ # automatically passes a modified version of the current operation's context
9
+ # to it. For one-line runs of operations please use {run_sub!} or {run_sub}
10
+ # which internally use this method.
11
+ def sub_op(op, params = {})
12
+ new_context = context.spawn(self)
13
+ return op.new(new_context, params)
14
+ end
15
+
16
+ # Operation-equivalent of controller method 'run!': Instantiates and runs the
17
+ # given operation class. See {sub_op} for more details on how instantiation
18
+ # and context modification is done.
19
+ def run_sub!(klass, params = {})
20
+ op = sub_op(klass, params)
21
+
22
+ begin
23
+ return op.run!
24
+ rescue op.validation_errors => e
25
+ fail RailsOps::Exceptions::SubOpValidationFailed, e
26
+ end
27
+ end
28
+
29
+ # Operation-equivalent of controller method 'run': Instantiates and runs the
30
+ # given operation class. See {sub_op} for more details on how instantiation
31
+ # and context modification is done.
32
+ def run_sub(op, params = {})
33
+ sub_op(op, params).run
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ module RailsOps::ModelCasting
2
+ def self.cast(model)
3
+ if model.class.respond_to?(:extended_record_base_class)
4
+ return ActiveType.cast(model, model.class.extended_record_base_class)
5
+ else
6
+ return model
7
+ end
8
+ end
9
+
10
+ def self.original_class_for(model_class)
11
+ if model_class.respond_to?(:extended_record_base_class)
12
+ return model_class.extended_record_base_class
13
+ else
14
+ return model_class
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # A collection of mixins that are useful when using models in operations.
2
+ module RailsOps::ModelMixins
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include ArExtension # Provides correct behaviour of model_name when extending AR objects.
7
+ include ParentOp # Provides parent_op accessor.
8
+ include ProtectedAttributes # Provides attr_accessible and attr_protected.
9
+ include VirtualAttributes # Provides virtual attributes functionality.
10
+ include VirtualHasOne # Provides virtual_has_one.
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ # When extending an ActiveRecord::Base model class, you can mix this module
2
+ # into the extending class to make it behave like it was the original one
3
+ # in terms of it's model_name identity.
4
+ #
5
+ # Note that you have to mix this in directly into the first class inheriting
6
+ # from your real model class.
7
+ module RailsOps::ModelMixins::ArExtension
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class_attribute :extended_record_base_class
12
+ self.extended_record_base_class = superclass
13
+ end
14
+
15
+ module ClassMethods
16
+ def model_name
17
+ (extended_record_base_class || superclass).model_name
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ # This is automatically mixed into every model passed to an operation
2
+ # and provides the :parent_op accessor that is set to the enclosing
3
+ # operation's instance.
4
+ module RailsOps::ModelMixins::ParentOp
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attr_accessor :parent_op
9
+ end
10
+ end
@@ -0,0 +1,78 @@
1
+ # This mixin can be used with any Activemodel Model to allow mass assignment
2
+ # security. This is a simpler implementation of Rails' former mass assignment
3
+ # security functionality.
4
+ #
5
+ # Attribute protection is disabled per default. It can be enabled by either
6
+ # calling `attr_protection true` or one of `attr_accessible` and
7
+ # `attr_protected`.
8
+ module RailsOps::ModelMixins::ProtectedAttributes
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ class_attribute :accessible_attributes
13
+ self.accessible_attributes = nil
14
+ end
15
+
16
+ # TODO: Document.
17
+ def assign_attributes_with_protection(attributes)
18
+ self.class._verify_protected_attributes!(attributes.dup)
19
+ assign_attributes(attributes)
20
+ end
21
+
22
+ module ClassMethods
23
+ # Returns whether attribute protection is enabled for this model.
24
+ def attr_protection?
25
+ !accessible_attributes.nil?
26
+ end
27
+
28
+ # Performs attribute protection verification (if enabled, see
29
+ # {attr_protection?}). If forbidden attributes are found, an
30
+ # {ActiveModel::ForbiddenAttributesError} exception is thrown.
31
+ def _verify_protected_attributes!(attributes) # :nodoc:
32
+ return unless attr_protection?
33
+
34
+ received_keys = attributes.keys.map(&:to_sym).to_set
35
+
36
+ disallowed_attributes = received_keys - accessible_attributes
37
+
38
+ if disallowed_attributes.any?
39
+ fail ActiveModel::ForbiddenAttributesError,
40
+ "The following attributes are forbidden to be assigned to #{model_name}: " \
41
+ "#{disallowed_attributes.to_a}, allowed are: #{accessible_attributes.to_a}."
42
+ end
43
+ end
44
+
45
+ # Specifically turn on or off attribute protection for this model (and
46
+ # models inheriting from it without overriding this setting).
47
+ #
48
+ # Note that attribute protection is turned on automatically when calling
49
+ # {attr_accessible} or {attr_protected}.
50
+ #
51
+ # If you're using this method to turn protection off, the list of accessible
52
+ # attributes is wiped. So if you re-enable it, no attributes will be
53
+ # accessible.
54
+ def attr_protection(enable)
55
+ if !enable
56
+ self.accessible_attributes = nil
57
+ elsif accessible_attributes.nil?
58
+ self.accessible_attributes = Set.new
59
+ end
60
+ end
61
+
62
+ # Specifies the given attribute(s) as accessible. This automatically turns
63
+ # on attribute protection for this model. This configuration is inherited to
64
+ # model inheriting from it.
65
+ def attr_accessible(*attributes)
66
+ attr_protection true
67
+ self.accessible_attributes += attributes.map(&:to_sym)
68
+ end
69
+
70
+ # Specifies the given attribute(s) as protected. This automatically turns on
71
+ # attribute protection for this model. This configuration is inherited
72
+ # to models inheriting from it.
73
+ def attr_protected(*attributes)
74
+ attr_protection true
75
+ self.accessible_attributes -= attributes.map(&:to_sym)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,24 @@
1
+ module RailsOps::ModelMixins::VirtualAttributes
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ include ActiveType::VirtualAttributes
6
+ end
7
+
8
+ # rubocop: disable Style/PredicateName
9
+ # TODO: Document this. Why is this necessary and not part of ActiveType?
10
+ def has_attribute?(name)
11
+ return true if self.class._has_virtual_column?(name)
12
+ return super
13
+ end
14
+ # rubocop: enable Style/PredicateName
15
+
16
+ # TODO: Document this. Why is this necessary and not part of ActiveType?
17
+ def column_for_attribute(name)
18
+ if self.class._has_virtual_column?(name)
19
+ return VirtualColumnWrapper.new(singleton_class._virtual_column(name))
20
+ else
21
+ super
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ class RailsOps::ModelMixins::VirtualAttributes::VirtualColumnWrapper
2
+ def initialize(virtual_column)
3
+ @virtual_column = virtual_column
4
+ end
5
+
6
+ def type
7
+ @virtual_column.instance_variable_get(:@type_caster).instance_variable_get(:@type)
8
+ end
9
+ end
@@ -0,0 +1,32 @@
1
+ module RailsOps
2
+ module ModelMixins
3
+ module VirtualHasOne
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ # TODO: Passing type Fixnum currently requires a monkey-patch of ActiveType.
8
+ # This would need to be changed when releasing this functionality as a Gem.
9
+ # See config/initializers/patch_active_type.rb and
10
+ # https://github.com/remofritzsche/active_type/commit/fb8c2cb4cccaaec
11
+ #
12
+ # TODO: Document.
13
+ def virtual_has_one(name, base_class, required: false, accessible: true, default: nil, type: Integer)
14
+ fk = "#{name}_id"
15
+ attribute fk, type, default: default
16
+
17
+ if required
18
+ validates fk, presence: true
19
+ end
20
+
21
+ if accessible
22
+ attr_accessible name, fk
23
+ end
24
+
25
+ belongs_to name, anonymous_class: base_class, foreign_key: fk, class_name: base_class.name
26
+
27
+ return reflect_on_association(name)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,215 @@
1
+ class RailsOps::Operation
2
+ include RailsOps::Mixins::Policies
3
+ include RailsOps::Mixins::SubOps
4
+ include RailsOps::Mixins::SchemaValidation
5
+ include RailsOps::Mixins::Authorization
6
+ include RailsOps::Mixins::RequireContext
7
+ include RailsOps::Mixins::LogSettings
8
+
9
+ WHITELISTED_BASE_CLASSES_FOR_PARAM_INSPECTION = [
10
+ ActiveRecord::Base,
11
+ String,
12
+ Integer,
13
+ Symbol
14
+ ].freeze
15
+
16
+ attr_reader :params
17
+ attr_reader :context
18
+
19
+ def self.run!(*args)
20
+ new(*args).run!
21
+ end
22
+
23
+ def self.run(*args)
24
+ new(*args).run
25
+ end
26
+
27
+ # Constructs a new operation instance with the given (optional) context and
28
+ # the given (optional) params. This is the only way of assigning context and
29
+ # params to an operation.
30
+ #
31
+ # If no context is provided, an empty context will be created.
32
+ #
33
+ # Note that, if provided, `params` must be a `Hash`. Other types such as
34
+ # `ActiveSupport::HashWithIndifferentAccess` or `ActionController::Parameters`
35
+ # are not supported.
36
+ #
37
+ # @param context [RailsOps::Context] Optional context
38
+ # @param params [Hash] Optional parameters hash
39
+ def initialize(context_or_params = {}, params = {})
40
+ # Handle parameter signature
41
+ if context_or_params.is_a?(RailsOps::Context)
42
+ context = context_or_params
43
+ elsif context_or_params.is_a?(Hash) || context_or_params.is_a?(ActionController::Parameters)
44
+ context = nil
45
+ params = context_or_params
46
+ end
47
+
48
+ @performed = false
49
+ @context = context || RailsOps::Context.new
50
+
51
+ # Convert ActionController::Parameters to a regular hash as we want to
52
+ # bypass Rails' strong parameters for operation use.
53
+ if params.is_a?(ActionController::Parameters)
54
+ params = params.permit!.to_h
55
+ end
56
+
57
+ # Remove web-specific param entries (such as `authenticity_token`)
58
+ @params = params.to_h.with_indifferent_access.except(
59
+ *ActionController::ParamsWrapper::EXCLUDE_PARAMETERS
60
+ )
61
+
62
+ run_policies :on_init
63
+ end
64
+
65
+ # Returns an array of exception classes that are considered as validation
66
+ # errors.
67
+ def validation_errors
68
+ [RailsOps::Exceptions::ValidationFailed, ActiveRecord::RecordInvalid]
69
+ end
70
+
71
+ # Returns a copy of the operation's params, wrapped in an OpenStruct object.
72
+ def osparams
73
+ @osparams ||= OpenStruct.new(params)
74
+ end
75
+
76
+ # Return a hash of parameters with all sensitive data replaced.
77
+ def filtered_params
78
+ f = ActionDispatch::Http::ParameterFilter.new(Rails.application.config.filter_parameters)
79
+ return f.filter(params)
80
+ end
81
+
82
+ # Runs the operation using {run!} but rescues certain exceptions. Returns
83
+ # `true` on success, otherwise `false`.
84
+ def run
85
+ run!
86
+ return true
87
+ rescue validation_errors
88
+ return false
89
+ end
90
+
91
+ # Runs the operation. This internally calls the {perform} method and can only
92
+ # be called once per operation instance. This is a bang method that raises at
93
+ # any validation exception.
94
+ def run!
95
+ ActiveSupport::Notifications.instrument('run.operation', operation: self) do
96
+ ::RailsOps::Profiler.profile(object_id, inspect) do
97
+ fail 'An operation can only be performed once.' if performed?
98
+ @performed = true
99
+ run_policies :before_perform
100
+ perform
101
+ end
102
+ end
103
+
104
+ trigger :after_run, after_run_trigger_params
105
+
106
+ return self
107
+ end
108
+
109
+ # Returns the contents of the operation as a nicely formatted string.
110
+ def inspect
111
+ inspection = self.class.name
112
+ if params
113
+ inspection << " (#{inspect_params(filtered_params)})"
114
+ end
115
+ return inspection
116
+ end
117
+
118
+ # Determines if the operation has been performed yet.
119
+ def performed?
120
+ @performed
121
+ end
122
+
123
+ # Fails with an exception if the operation has not been performed yet.
124
+ def check_performed!
125
+ fail 'Operation has not yet been perfomed.' unless performed?
126
+ end
127
+
128
+ protected
129
+
130
+ # This method actually performs the operation's logic and is called by {run}
131
+ # or {run!}. Never call this method directly. Overwrite this method for
132
+ # supplying operation logic.
133
+ def perform
134
+ fail NotImplementedError
135
+ end
136
+
137
+ # Determines a basic set of parameters that will be passed to the `after_run`
138
+ # event. This is empty per default and is meant to overridden by superclasses
139
+ # where necessary.
140
+ def after_run_trigger_params
141
+ {}
142
+ end
143
+
144
+ # Triggers an event of the given name using the given params using the
145
+ # {RailsOps::Hookup} functionality. Any potential operation called by this
146
+ # trigger will receive an operation context based on the context of the
147
+ # current operation, but with an updated `op_chain` and with the `params`
148
+ # supplied.
149
+ #
150
+ # @param [string] event The event name to trigger
151
+ # @param [hash] params The params to provide to any ops called by this trigger
152
+ def trigger(event, params = nil)
153
+ RailsOps.hookup.trigger(self, event, params)
154
+ end
155
+
156
+ # Yields the given block and rethrows any possible exception as a
157
+ # {RailsOps::Exceptions::RollbackRequired} exception.
158
+ #
159
+ # For illustration of potential use cases, consider the following example:
160
+ #
161
+ # class User::Create < RailsOps::Operation::Model::Create
162
+ # def perform
163
+ # super # Saves the user
164
+ #
165
+ # model.some_field = 'some value'
166
+ # model.save! # Throws validation error
167
+ # end
168
+ # end
169
+ #
170
+ # User::Create.run(user: { some: :values })
171
+ #
172
+ # Since this operation is run without the bang method, validation errors are
173
+ # caught and won't result in the transaction beeing rolled back. However, the
174
+ # `super` call already saved the user while the exception happens only at
175
+ # the manual call to `model.save!`. Thus the user will still be in the DB,
176
+ # despite the fact that the second update didn't run.
177
+ #
178
+ # The correct example would therefore be:
179
+ #
180
+ # class User::Create < RailsOps::Operation::Model::Create
181
+ # def perform
182
+ # super # Saves the user
183
+ #
184
+ # with_rollback_on_exception do
185
+ # model.some_field = 'some value'
186
+ # model.save! # Throws validation error
187
+ # end
188
+ # end
189
+ # end
190
+ #
191
+ # This method is one possible solution for issue #28535. There might be a more
192
+ # elegant and transparent approach as explained in the issue.
193
+ def with_rollback_on_exception(&_block)
194
+ yield
195
+ rescue => e
196
+ fail RailsOps::Exceptions::RollbackRequired, e
197
+ end
198
+
199
+ # Returns the contents of the params as a nicely formatted string.
200
+ def inspect_params(params)
201
+ params.each do |key, value|
202
+ if value.is_a?(Hash)
203
+ inspect_params(value)
204
+ elsif WHITELISTED_BASE_CLASSES_FOR_PARAM_INSPECTION.any? { |klass| value.is_a?(klass) }
205
+ formatted_value = value
206
+ else
207
+ formatted_value = "#<#{value.class}>"
208
+ end
209
+
210
+ params[key] = formatted_value
211
+ end
212
+
213
+ return params.inspect
214
+ end
215
+ end