rails_ops 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|