rails_ops 1.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +84 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +1216 -0
- data/RUBY_VERSION +1 -0
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/lib/rails_ops.rb +96 -0
- data/lib/rails_ops/authorization_backend/abstract.rb +7 -0
- data/lib/rails_ops/authorization_backend/can_can_can.rb +14 -0
- data/lib/rails_ops/configuration.rb +4 -0
- data/lib/rails_ops/context.rb +35 -0
- data/lib/rails_ops/controller_mixin.rb +105 -0
- data/lib/rails_ops/exceptions.rb +19 -0
- data/lib/rails_ops/hooked_job.rb +25 -0
- data/lib/rails_ops/hookup.rb +80 -0
- data/lib/rails_ops/hookup/dsl.rb +29 -0
- data/lib/rails_ops/hookup/dsl_validator.rb +45 -0
- data/lib/rails_ops/hookup/hook.rb +11 -0
- data/lib/rails_ops/log_subscriber.rb +24 -0
- data/lib/rails_ops/mixins.rb +2 -0
- data/lib/rails_ops/mixins/authorization.rb +83 -0
- data/lib/rails_ops/mixins/log_settings.rb +20 -0
- data/lib/rails_ops/mixins/model.rb +4 -0
- data/lib/rails_ops/mixins/model/authorization.rb +64 -0
- data/lib/rails_ops/mixins/model/nesting.rb +180 -0
- data/lib/rails_ops/mixins/policies.rb +42 -0
- data/lib/rails_ops/mixins/require_context.rb +33 -0
- data/lib/rails_ops/mixins/routes.rb +35 -0
- data/lib/rails_ops/mixins/schema_validation.rb +25 -0
- data/lib/rails_ops/mixins/sub_ops.rb +35 -0
- data/lib/rails_ops/model_casting.rb +17 -0
- data/lib/rails_ops/model_mixins.rb +12 -0
- data/lib/rails_ops/model_mixins/ar_extension.rb +20 -0
- data/lib/rails_ops/model_mixins/parent_op.rb +10 -0
- data/lib/rails_ops/model_mixins/protected_attributes.rb +78 -0
- data/lib/rails_ops/model_mixins/virtual_attributes.rb +24 -0
- data/lib/rails_ops/model_mixins/virtual_attributes/virtual_column_wrapper.rb +9 -0
- data/lib/rails_ops/model_mixins/virtual_has_one.rb +32 -0
- data/lib/rails_ops/operation.rb +215 -0
- data/lib/rails_ops/operation/model.rb +168 -0
- data/lib/rails_ops/operation/model/create.rb +35 -0
- data/lib/rails_ops/operation/model/destroy.rb +26 -0
- data/lib/rails_ops/operation/model/load.rb +72 -0
- data/lib/rails_ops/operation/model/update.rb +31 -0
- data/lib/rails_ops/patches/active_type_patch.rb +52 -0
- data/lib/rails_ops/profiler.rb +47 -0
- data/lib/rails_ops/profiler/node.rb +64 -0
- data/lib/rails_ops/railtie.rb +19 -0
- data/lib/rails_ops/scoped_env.rb +20 -0
- data/lib/rails_ops/virtual_model.rb +19 -0
- data/rails_ops.gemspec +58 -0
- data/test/test_helper.rb +3 -0
- metadata +252 -0
@@ -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,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
|