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