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,29 @@
|
|
1
|
+
class RailsOps::Hookup::DSL
|
2
|
+
attr_reader :hooks
|
3
|
+
|
4
|
+
def initialize(&block)
|
5
|
+
@current_target = nil
|
6
|
+
@validator = nil
|
7
|
+
@hooks = ActiveSupport::OrderedHash.new
|
8
|
+
|
9
|
+
RailsOps::ScopedEnv.new(self, [:run]).instance_exec(&block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(target, &block)
|
13
|
+
@current_target = target
|
14
|
+
RailsOps::ScopedEnv.new(self, [:on]).instance_exec(&block)
|
15
|
+
@current_target = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def on(source, event = :after_run)
|
19
|
+
@hooks[source] ||= ActiveSupport::OrderedHash.new
|
20
|
+
@hooks[source][event] ||= []
|
21
|
+
@hooks[source][event] << RailsOps::Hookup::Hook.new(source, event, @current_target)
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate!
|
25
|
+
@validator = RailsOps::Hookup::DSLValidator.new @hooks
|
26
|
+
@validator.validate!
|
27
|
+
@validator
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class RailsOps::Hookup::DSLValidator
|
2
|
+
attr_reader :error, :trace
|
3
|
+
|
4
|
+
def initialize(hooks)
|
5
|
+
@hooks = hooks
|
6
|
+
@trace = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate!
|
10
|
+
# Check infinity loop
|
11
|
+
if target_hooks.any? { |name, targets| recursion?(targets, name) }
|
12
|
+
fail StandardError::SystemStackError.new, "Infinite loop detected in hooks configuration: #{inspect_trace}."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def target_hooks
|
17
|
+
@target_hooks ||= @hooks.map do |name, hash|
|
18
|
+
[name, drilldown(hash)]
|
19
|
+
end.to_h
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def inspect_trace
|
25
|
+
@trace.map(&:to_s).join(' ~> ')
|
26
|
+
end
|
27
|
+
|
28
|
+
def recursion?(targets, name)
|
29
|
+
if targets.include? name
|
30
|
+
@trace << name.to_s
|
31
|
+
return true
|
32
|
+
end
|
33
|
+
|
34
|
+
return targets.any? do |target|
|
35
|
+
if @hooks[target]
|
36
|
+
@trace |= [name, target]
|
37
|
+
recursion? drilldown(@hooks[target]), name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def drilldown(hash)
|
43
|
+
hash.values.flatten.map(&:target_operation)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class RailsOps::Hookup::Hook
|
2
|
+
attr_reader :on_operation
|
3
|
+
attr_reader :on_event
|
4
|
+
attr_reader :target_operation
|
5
|
+
|
6
|
+
def initialize(on_operation, on_event, target_operation)
|
7
|
+
@on_operation = on_operation
|
8
|
+
@on_event = on_event
|
9
|
+
@target_operation = target_operation
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module RailsOps
|
2
|
+
# This class subscribes to Rails Ops events via the ActiveSupport
|
3
|
+
# instrumentation API.
|
4
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
5
|
+
# This gets called whenever an operation has been performed and logs the
|
6
|
+
# operation via Rails' `debug` logging method.
|
7
|
+
def run(event)
|
8
|
+
op = event.payload[:operation]
|
9
|
+
|
10
|
+
return if op.class.logging_skipped?
|
11
|
+
|
12
|
+
message = 'OP'
|
13
|
+
|
14
|
+
profile = ::RailsOps::Profiler.node(op.object_id)
|
15
|
+
message += " (#{profile.t_self_ms.round(1)}ms / #{profile.t_kids_ms.round(1)}ms)"
|
16
|
+
profile.free
|
17
|
+
|
18
|
+
message = color(message, YELLOW, true)
|
19
|
+
message += color(" #{op.class.name}", YELLOW, false)
|
20
|
+
|
21
|
+
debug " #{message}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# Mixin for the {RailsOps::Operation} class that provides basic authorization
|
2
|
+
# methods based on the cancan(can) ability that can be provided using an
|
3
|
+
# operation's context.
|
4
|
+
module RailsOps::Mixins::Authorization
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :_authorization_disabled
|
9
|
+
class_attribute :_authorization_disabled_for_sub_ops
|
10
|
+
|
11
|
+
policy :after_perform do
|
12
|
+
ensure_authorize_called!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
# Disables authorization (and authorization check) for this
|
18
|
+
# operation. If `include_sub_ops` is `true` (default `false`),
|
19
|
+
# this setting also applies to sub operations called by this
|
20
|
+
# operation (either via run_sub(!) or via a hook).
|
21
|
+
def without_authorization(include_sub_ops: false)
|
22
|
+
fail 'Option include_sub_ops is not yet supported.' if include_sub_ops
|
23
|
+
|
24
|
+
self._authorization_disabled = true
|
25
|
+
self._authorization_disabled_for_sub_ops = include_sub_ops
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Checks whether authorization is currently enabled and possible
|
30
|
+
# (an ability is present).
|
31
|
+
def authorization_enabled?
|
32
|
+
# Do not perform authorization if it is disabled globally
|
33
|
+
return false unless RailsOps.authorization_enabled?
|
34
|
+
|
35
|
+
# Do not perform authorization if it is disabled for this operation
|
36
|
+
return false if self.class._authorization_disabled
|
37
|
+
|
38
|
+
# Do not perform authorization if no ability is present
|
39
|
+
return false unless context.ability
|
40
|
+
|
41
|
+
# Perform authorization
|
42
|
+
return true
|
43
|
+
end
|
44
|
+
|
45
|
+
def authorization_enabled_for_sub_ops?
|
46
|
+
!@_authorization_disabled_for_sub_ops
|
47
|
+
end
|
48
|
+
|
49
|
+
def authorize_called?
|
50
|
+
@_authorize_called ||= false
|
51
|
+
end
|
52
|
+
|
53
|
+
# Operations within the given block will have disabled authorization.
|
54
|
+
# This only applies to the current thread.
|
55
|
+
def without_authorization(&block)
|
56
|
+
RailsOps.without_authorization(&block)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Checks authorization against the configured authentication backend. Fails if
|
60
|
+
# authentication is not successfull or could not be performed. Does not
|
61
|
+
# perform anything if authorization is disabled.
|
62
|
+
def authorize!(action, *args)
|
63
|
+
authorize_only!(action, *args)
|
64
|
+
@_authorize_called = true
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks authorization against the configured authentication backend. Fails if
|
68
|
+
# authentication is not successfull or could not be performed. Does not
|
69
|
+
# perform anything if authorization is disabled. Calling authorize_only! does
|
70
|
+
# not count as authorized concerning ensure_authorize_called!.
|
71
|
+
def authorize_only!(*args)
|
72
|
+
return unless authorization_enabled?
|
73
|
+
RailsOps.authorization_backend.authorize!(self, *args)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Checks if an authentication check has been performed if authorization is
|
77
|
+
# enabled.
|
78
|
+
def ensure_authorize_called!
|
79
|
+
return unless authorization_enabled?
|
80
|
+
return if authorize_called?
|
81
|
+
fail RailsOps::Exceptions::NoAuthorizationPerformed, "Operation #{self.class.name} has been performed without authorization."
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RailsOps::Mixins::LogSettings
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
class_attribute :_skip_logging
|
6
|
+
self._skip_logging = false
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Allows to skip logging for this operation via the
|
11
|
+
# {RailsOps::LogSubscriber}.
|
12
|
+
def skip_logging(skip = true)
|
13
|
+
self._skip_logging = skip
|
14
|
+
end
|
15
|
+
|
16
|
+
def logging_skipped?
|
17
|
+
_skip_logging
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Mixin for the {RailsOps::Operation::Model} class that provides the method
|
2
|
+
# {authorize_model!} which authorizes the operation's model class or instance
|
3
|
+
# against the authorization methods of {RailsOps::Mixins::Authorization}.
|
4
|
+
module RailsOps::Mixins::Model::Authorization
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :_model_authorization_action
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
# Gets or sets the action verb used for authorizing models.
|
13
|
+
def model_authorization_action(*action)
|
14
|
+
if action.size == 1
|
15
|
+
self._model_authorization_action = action.first
|
16
|
+
elsif action.size > 1
|
17
|
+
fail ArgumentError, 'Too many arguments'
|
18
|
+
end
|
19
|
+
|
20
|
+
return _model_authorization_action
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def model_authorization_action
|
25
|
+
self.class.model_authorization_action
|
26
|
+
end
|
27
|
+
|
28
|
+
# Performs authorization on the given model using the {authorize?} method.
|
29
|
+
# Models are casted using {cast_model_for_authorization} so that they can be
|
30
|
+
# used for authorization. If no `model_class_or_instance` is given, the
|
31
|
+
# {model} instance method will be used.
|
32
|
+
def authorize_model!(action, model_class_or_instance = model, *extra_args)
|
33
|
+
authorize! action, cast_model_for_authorization(model_class_or_instance), *extra_args
|
34
|
+
end
|
35
|
+
|
36
|
+
# Performs an authorization check like {authorize_model!}, but does not mark
|
37
|
+
# this operation instance as checked. This means that there must be at least
|
38
|
+
# one other authorization call within execution of this operation for the
|
39
|
+
# operation not to fail.
|
40
|
+
def authorize_model_with_authorize_only!(action, model_class_or_instance = model, *extra_args)
|
41
|
+
authorize_only! action, cast_model_for_authorization(model_class_or_instance), *extra_args
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Cast {ActiveType::Record} classes or instances to regular AR models in order
|
47
|
+
# for cancan(can) to work properly. Classes and instances that are no active
|
48
|
+
# type records will be returned as-is.
|
49
|
+
#
|
50
|
+
# TODO: Use ModelCasting module instead?
|
51
|
+
def cast_model_for_authorization(model_class_or_instance)
|
52
|
+
if model_class_or_instance.is_a?(Class)
|
53
|
+
if model_class_or_instance.respond_to?(:extended_record_base_class)
|
54
|
+
return model_class_or_instance.extended_record_base_class
|
55
|
+
else
|
56
|
+
return model_class_or_instance
|
57
|
+
end
|
58
|
+
elsif model_class_or_instance.class.respond_to?(:extended_record_base_class)
|
59
|
+
return ActiveType.cast(model_class_or_instance, model_class_or_instance.class.extended_record_base_class)
|
60
|
+
else
|
61
|
+
model_class_or_instance
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module RailsOps::Mixins::Model::Nesting
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
class_attribute :_nested_model_ops
|
6
|
+
self._nested_model_ops = {}.freeze
|
7
|
+
|
8
|
+
attr_reader :nested_model_ops
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def nest_model_op(attribute, klass, lookup_via_id_on_update: true, ¶ms_block)
|
13
|
+
# ---------------------------------------------------------------
|
14
|
+
# Make sure we're working with an extension / copy
|
15
|
+
# of the given model class
|
16
|
+
# ---------------------------------------------------------------
|
17
|
+
unless always_extend_model_class?
|
18
|
+
fail 'This operation class must be configured to always extend the model class as `nest_model_op` modifies it.'
|
19
|
+
end
|
20
|
+
|
21
|
+
# ---------------------------------------------------------------
|
22
|
+
# Validate association (currently, we only support belongs_to)
|
23
|
+
# ---------------------------------------------------------------
|
24
|
+
reflection = model.reflect_on_association(attribute)
|
25
|
+
|
26
|
+
if reflection.nil?
|
27
|
+
fail "Association #{attribute} could not be found for #{model.model_name}."
|
28
|
+
elsif !reflection.belongs_to?
|
29
|
+
fail 'Method nest_model_op only supports :belongs_to associations, '\
|
30
|
+
"but association #{attribute} of model #{model.model_name} is a "\
|
31
|
+
"#{reflection.macro} association."
|
32
|
+
elsif reflection.options[:autosave] != false
|
33
|
+
fail "Association #{attribute} of #{model.model_name} has :autosave turned on. "\
|
34
|
+
'This is not supported by nest_model_op.'
|
35
|
+
elsif !reflection.options[:validate]
|
36
|
+
fail "Association #{attribute} of #{model.model_name} has :validate turned off. "\
|
37
|
+
'This is not supported by nest_model_op.'
|
38
|
+
end
|
39
|
+
|
40
|
+
# ---------------------------------------------------------------
|
41
|
+
# Define attributes setter on model.
|
42
|
+
#
|
43
|
+
# Model nesting is not compatible with accepts_nested_attributes_for
|
44
|
+
# as it automatically enables :autosave which is not supported.
|
45
|
+
# As we can't use accepts_nested_attributes_for, no nested attributes
|
46
|
+
# setter is generated. This also means that `fields_for` in form
|
47
|
+
# generation don't detect the attribute as nested and mistakenly omit the
|
48
|
+
# _attributes suffix. Therefore, we define this method but fail if it
|
49
|
+
# ever gets called.
|
50
|
+
# ---------------------------------------------------------------
|
51
|
+
model.send(:define_method, "#{attribute}_attributes=") do |_value|
|
52
|
+
fail 'This operation model does not allow receiving nested attributes' \
|
53
|
+
"for #{attribute}, as this is saved using a nested model operation."
|
54
|
+
end
|
55
|
+
|
56
|
+
# ---------------------------------------------------------------
|
57
|
+
# Validate inverse association reflection if given
|
58
|
+
# ---------------------------------------------------------------
|
59
|
+
if (inverse_reflection = reflection.inverse_of)
|
60
|
+
if inverse_reflection.options[:autosave] != false
|
61
|
+
fail "Association #{inverse_reflection.name} of #{inverse_reflection.active_record} has :autosave turned on. "\
|
62
|
+
'This is not supported by nest_model_op.'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# ---------------------------------------------------------------
|
67
|
+
# Store configuration
|
68
|
+
# ---------------------------------------------------------------
|
69
|
+
self._nested_model_ops = _nested_model_ops.merge(
|
70
|
+
attribute => {
|
71
|
+
klass: klass,
|
72
|
+
params_proc: params_block,
|
73
|
+
lookup_via_id_on_update: lookup_via_id_on_update
|
74
|
+
}
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def nested_model_param_keys
|
79
|
+
_nested_model_ops.keys.collect do |attribute|
|
80
|
+
"#{attribute}_attributes"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def nested_model_ops_performed?
|
86
|
+
@nested_model_ops_performed || false
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
def nested_model_op(attribute)
|
92
|
+
fail 'Nested model operations have not been built yet.' unless @nested_model_ops
|
93
|
+
return @nested_model_ops[attribute]
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_nested_model_ops(action)
|
97
|
+
# Validate action
|
98
|
+
fail 'Unsupported action.' unless [:create, :update].include?(action)
|
99
|
+
|
100
|
+
# Make sure that this method can only be run once per operation
|
101
|
+
fail 'Nested model operations can only be built once.' if @nested_model_ops
|
102
|
+
@nested_model_ops = {}
|
103
|
+
|
104
|
+
self.class._nested_model_ops.each do |attribute, config|
|
105
|
+
op_params = extract_attributes_from_params["#{attribute}_attributes"] || {}
|
106
|
+
|
107
|
+
# Remove id field as this is commonly supplied by Rails' `fields_for` if
|
108
|
+
# the nested model is persisted. We don't usually need this.
|
109
|
+
op_params = op_params.except(:id)
|
110
|
+
|
111
|
+
# Apply custom params processing callback if given
|
112
|
+
if config[:params_proc]
|
113
|
+
op_params = instance_exec(op_params, &config[:params_proc])
|
114
|
+
end
|
115
|
+
|
116
|
+
# Wrap parameters for nested model operation
|
117
|
+
if action == :create
|
118
|
+
wrapped_params = {
|
119
|
+
attribute => op_params
|
120
|
+
}
|
121
|
+
elsif action == :update
|
122
|
+
if config[:lookup_via_id_on_update]
|
123
|
+
foreign_key = model.class.reflect_on_association(attribute).foreign_key
|
124
|
+
id = model.send(foreign_key)
|
125
|
+
else
|
126
|
+
id = model.send(attribute).id
|
127
|
+
end
|
128
|
+
|
129
|
+
wrapped_params = {
|
130
|
+
:id => id,
|
131
|
+
attribute => op_params
|
132
|
+
}
|
133
|
+
else
|
134
|
+
fail "Unsupported action #{action}."
|
135
|
+
end
|
136
|
+
|
137
|
+
# Instantiate nested operation
|
138
|
+
@nested_model_ops[attribute] = sub_op(config[:klass], wrapped_params)
|
139
|
+
|
140
|
+
# Inject model of nested operation to our own model
|
141
|
+
nested_model = @nested_model_ops[attribute].model
|
142
|
+
model.send("#{attribute}=", nested_model)
|
143
|
+
|
144
|
+
# Inject our own model to model of nested operation (if the inverse
|
145
|
+
# reflection can be resolved)
|
146
|
+
if (inverse_reflection = model.class.reflect_on_association(attribute).inverse_of)
|
147
|
+
nested_model.send("#{inverse_reflection.name}=", model)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Tries to save nested models using their respective operations.
|
153
|
+
def perform_nested_model_ops!
|
154
|
+
fail 'Nested model operations can only be performed once.' if nested_model_ops_performed?
|
155
|
+
|
156
|
+
# Validate the whole model hierarchy. Since we're calling 'model' here, this
|
157
|
+
# line also makes sure a model is built.
|
158
|
+
model.validate!
|
159
|
+
|
160
|
+
# Make sure nested model operations are build
|
161
|
+
fail 'Nested model operations are not built yet. Make sure the model is built.' unless @nested_model_ops
|
162
|
+
|
163
|
+
@nested_model_ops.each do |attribute, op|
|
164
|
+
# Run the nested model operation and fail hard if a validation error
|
165
|
+
# arises. This should generally not happen as the whole hierarchy has been
|
166
|
+
# validated before. It is vital that the transaction gets rolled back if
|
167
|
+
# an exception happens here.
|
168
|
+
begin
|
169
|
+
op.run!
|
170
|
+
rescue op.validation_errors => e
|
171
|
+
fail RailsOps::Exceptions::SubOpValidationFailed, e
|
172
|
+
end
|
173
|
+
|
174
|
+
# Assign model again so that the ID gets updated
|
175
|
+
model.send("#{attribute}=", op.model)
|
176
|
+
end
|
177
|
+
|
178
|
+
@nested_model_ops_performed = true
|
179
|
+
end
|
180
|
+
end
|