axn 0.1.0.pre.alpha.2.6 → 0.1.0.pre.alpha.2.6.1
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 +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +13 -0
- data/docs/reference/action-result.md +2 -0
- data/docs/reference/class.md +12 -4
- data/docs/reference/configuration.md +42 -20
- data/docs/usage/writing.md +1 -1
- data/lib/action/attachable/steps.rb +16 -1
- data/lib/action/configuration.rb +2 -3
- data/lib/action/context.rb +28 -18
- data/lib/action/core/automatic_logging.rb +24 -8
- data/lib/action/core/context/facade.rb +69 -0
- data/lib/action/core/context/facade_inspector.rb +63 -0
- data/lib/action/core/context/internal.rb +32 -0
- data/lib/action/core/contract.rb +25 -8
- data/lib/action/core/contract_for_subfields.rb +1 -1
- data/lib/action/core/contract_validation.rb +15 -4
- data/lib/action/core/flow/callbacks.rb +54 -0
- data/lib/action/core/flow/exception_execution.rb +79 -0
- data/lib/action/core/flow/messages.rb +61 -0
- data/lib/action/core/flow.rb +19 -0
- data/lib/action/core/hoist_errors.rb +2 -2
- data/lib/action/core/hooks.rb +15 -15
- data/lib/action/core/logging.rb +2 -2
- data/lib/action/core/timing.rb +18 -0
- data/lib/action/core/tracing.rb +17 -0
- data/lib/action/core/validation/fields.rb +2 -0
- data/lib/action/core.rb +25 -78
- data/lib/action/enqueueable/via_sidekiq.rb +2 -2
- data/lib/action/result.rb +95 -0
- data/lib/axn/factory.rb +30 -0
- data/lib/axn/version.rb +1 -1
- metadata +11 -4
- data/lib/action/core/context_facade.rb +0 -209
- data/lib/action/core/handle_exceptions.rb +0 -143
@@ -9,9 +9,9 @@ module Action
|
|
9
9
|
internal_field_configs.each do |config|
|
10
10
|
next unless config.preprocess
|
11
11
|
|
12
|
-
initial_value = @
|
12
|
+
initial_value = @__context.provided_data[config.field]
|
13
13
|
new_value = config.preprocess.call(initial_value)
|
14
|
-
@
|
14
|
+
@__context.provided_data[config.field] = new_value
|
15
15
|
rescue StandardError => e
|
16
16
|
raise Action::ContractViolation::PreprocessingError, "Error preprocessing field '#{config.field}': #{e.message}"
|
17
17
|
end
|
@@ -33,17 +33,28 @@ module Action
|
|
33
33
|
def _apply_defaults!(direction)
|
34
34
|
raise ArgumentError, "Invalid direction: #{direction}" unless %i[inbound outbound].include?(direction)
|
35
35
|
|
36
|
+
if direction == :outbound
|
37
|
+
# For outbound defaults, first copy values from provided_data for fields that are both expected and exposed
|
38
|
+
external_field_configs.each do |config|
|
39
|
+
field = config.field
|
40
|
+
next if @__context.exposed_data[field].present? # Already has a value
|
41
|
+
|
42
|
+
@__context.exposed_data[field] = @__context.provided_data[field] if @__context.provided_data[field].present?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
36
46
|
configs = direction == :inbound ? internal_field_configs : external_field_configs
|
37
47
|
defaults_mapping = configs.each_with_object({}) do |config, hash|
|
38
48
|
hash[config.field] = config.default
|
39
49
|
end.compact
|
40
50
|
|
41
51
|
defaults_mapping.each do |field, default_value_getter|
|
42
|
-
|
52
|
+
data_hash = direction == :inbound ? @__context.provided_data : @__context.exposed_data
|
53
|
+
next if data_hash[field].present?
|
43
54
|
|
44
55
|
default_value = default_value_getter.respond_to?(:call) ? instance_exec(&default_value_getter) : default_value_getter
|
45
56
|
|
46
|
-
|
57
|
+
data_hash[field] = default_value
|
47
58
|
end
|
48
59
|
end
|
49
60
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/event_handlers"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
module Core
|
7
|
+
module Flow
|
8
|
+
module Callbacks
|
9
|
+
def self.included(base)
|
10
|
+
base.class_eval do
|
11
|
+
class_attribute :_error_handlers, default: []
|
12
|
+
class_attribute :_exception_handlers, default: []
|
13
|
+
class_attribute :_failure_handlers, default: []
|
14
|
+
class_attribute :_success_handlers, default: []
|
15
|
+
|
16
|
+
extend ClassMethods
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
# ONLY raised exceptions (i.e. NOT fail!). Skipped if exception is rescued via .rescues.
|
22
|
+
def on_exception(matcher = -> { true }, &handler)
|
23
|
+
raise ArgumentError, "on_exception must be called with a block" unless block_given?
|
24
|
+
|
25
|
+
self._exception_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
|
26
|
+
end
|
27
|
+
|
28
|
+
# ONLY raised on fail! (i.e. NOT unhandled exceptions).
|
29
|
+
def on_failure(matcher = -> { true }, &handler)
|
30
|
+
raise ArgumentError, "on_failure must be called with a block" unless block_given?
|
31
|
+
|
32
|
+
self._failure_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Handles both fail! and unhandled exceptions... but is NOT affected by .rescues
|
36
|
+
def on_error(matcher = -> { true }, &handler)
|
37
|
+
raise ArgumentError, "on_error must be called with a block" unless block_given?
|
38
|
+
|
39
|
+
self._error_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Executes when the action completes successfully (after all after hooks complete successfully)
|
43
|
+
# Runs in child-first order (child handlers before parent handlers)
|
44
|
+
def on_success(&handler)
|
45
|
+
raise ArgumentError, "on_success must be called with a block" unless block_given?
|
46
|
+
|
47
|
+
# Prepend like after hooks - child handlers run before parent handlers
|
48
|
+
self._success_handlers = [handler] + _success_handlers
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module Flow
|
6
|
+
module ExceptionExecution
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
include InstanceMethods
|
10
|
+
|
11
|
+
def _trigger_on_exception(exception)
|
12
|
+
interceptor = self.class._error_interceptor_for(exception:, action: self)
|
13
|
+
return if interceptor&.should_report_error == false
|
14
|
+
|
15
|
+
# Call any handlers registered on *this specific action* class
|
16
|
+
self.class._exception_handlers.each do |handler|
|
17
|
+
handler.execute_if_matches(exception:, action: self)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Call any global handlers
|
21
|
+
Action.config.on_exception(exception, action: self, context: context_for_logging)
|
22
|
+
rescue StandardError => e
|
23
|
+
# No action needed -- downstream #on_exception implementation should ideally log any internal failures, but
|
24
|
+
# we don't want exception *handling* failures to cascade and overwrite the original exception.
|
25
|
+
Axn::Util.piping_error("executing on_exception hooks", action: self, exception: e)
|
26
|
+
end
|
27
|
+
|
28
|
+
def _trigger_on_success
|
29
|
+
# Call success handlers in child-first order (like after hooks)
|
30
|
+
self.class._success_handlers.each do |handler|
|
31
|
+
instance_exec(&handler)
|
32
|
+
rescue StandardError => e
|
33
|
+
# Log the error but continue with other handlers
|
34
|
+
Axn::Util.piping_error("executing on_success hook", action: self, exception: e)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module InstanceMethods
|
41
|
+
private
|
42
|
+
|
43
|
+
def _with_exception_handling
|
44
|
+
yield
|
45
|
+
rescue StandardError => e
|
46
|
+
# on_error handlers run for both unhandled exceptions and fail!
|
47
|
+
self.class._error_handlers.each do |handler|
|
48
|
+
handler.execute_if_matches(exception: e, action: self)
|
49
|
+
end
|
50
|
+
|
51
|
+
# on_failure handlers run ONLY for fail!
|
52
|
+
if e.is_a?(Action::Failure)
|
53
|
+
self.class._failure_handlers.each do |handler|
|
54
|
+
handler.execute_if_matches(exception: e, action: self)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
# on_exception handlers run for ONLY for unhandled exceptions. AND NOTE: may be skipped if the exception is rescued via `rescues`.
|
58
|
+
_trigger_on_exception(e)
|
59
|
+
|
60
|
+
@__context.exception = e
|
61
|
+
end
|
62
|
+
|
63
|
+
# Set failure state using accessor method
|
64
|
+
@__context.send(:failure=, true)
|
65
|
+
end
|
66
|
+
|
67
|
+
def try
|
68
|
+
yield
|
69
|
+
rescue Action::Failure => e
|
70
|
+
# NOTE: re-raising so we can still fail! from inside the block
|
71
|
+
raise e
|
72
|
+
rescue StandardError => e
|
73
|
+
_trigger_on_exception(e)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module Flow
|
6
|
+
module Messages
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
class_attribute :_success_msg, :_error_msg
|
10
|
+
class_attribute :_custom_error_interceptors, default: []
|
11
|
+
|
12
|
+
extend ClassMethods
|
13
|
+
include InstanceMethods
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def messages(success: nil, error: nil)
|
19
|
+
self._success_msg = success if success.present?
|
20
|
+
self._error_msg = error if error.present?
|
21
|
+
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def error_from(matcher = nil, message = nil, **match_and_messages)
|
26
|
+
_register_error_interceptor(matcher, message, should_report_error: true, **match_and_messages)
|
27
|
+
end
|
28
|
+
|
29
|
+
def rescues(matcher = nil, message = nil, **match_and_messages)
|
30
|
+
_register_error_interceptor(matcher, message, should_report_error: false, **match_and_messages)
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_error = new.internal_context.default_error
|
34
|
+
|
35
|
+
# Private helpers
|
36
|
+
|
37
|
+
def _error_interceptor_for(exception:, action:)
|
38
|
+
Array(_custom_error_interceptors).detect do |int|
|
39
|
+
int.matches?(exception:, action:)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def _register_error_interceptor(matcher, message, should_report_error:, **match_and_messages)
|
44
|
+
method_name = should_report_error ? "error_from" : "rescues"
|
45
|
+
raise ArgumentError, "#{method_name} must be called with a key/value pair, or else keyword args" if [matcher, message].compact.size == 1
|
46
|
+
|
47
|
+
interceptors = { matcher => message }.compact.merge(match_and_messages).map do |(matcher, message)| # rubocop:disable Lint/ShadowingOuterLocalVariable
|
48
|
+
Action::EventHandlers::CustomErrorInterceptor.new(matcher:, message:, should_report_error:)
|
49
|
+
end
|
50
|
+
|
51
|
+
self._custom_error_interceptors += interceptors
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module InstanceMethods
|
56
|
+
delegate :default_error, to: :internal_context
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/messages"
|
4
|
+
require "action/core/flow/callbacks"
|
5
|
+
require "action/core/flow/exception_execution"
|
6
|
+
|
7
|
+
module Action
|
8
|
+
module Core
|
9
|
+
module Flow
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval do
|
12
|
+
include Messages
|
13
|
+
include Callbacks
|
14
|
+
include ExceptionExecution
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -46,8 +46,8 @@ module Action
|
|
46
46
|
|
47
47
|
# Separate method to allow overriding in subclasses
|
48
48
|
def _handle_hoisted_errors(result, prefix: nil)
|
49
|
-
@
|
50
|
-
@
|
49
|
+
@__context.exception = result.exception if result.exception.present?
|
50
|
+
@__context.error_prefix = prefix if prefix.present?
|
51
51
|
|
52
52
|
error = result.exception.is_a?(Action::Failure) ? result.exception.message : result.error
|
53
53
|
fail! error
|
data/lib/action/core/hooks.rb
CHANGED
@@ -67,44 +67,44 @@ module Action
|
|
67
67
|
|
68
68
|
private
|
69
69
|
|
70
|
-
def
|
71
|
-
|
72
|
-
|
70
|
+
def _with_hooks
|
71
|
+
_run_around_hooks do
|
72
|
+
_run_before_hooks
|
73
73
|
yield
|
74
|
-
|
74
|
+
_run_after_hooks
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
78
|
# Around hooks are reversed before injection to ensure parent hooks wrap
|
79
79
|
# child hooks (parent outside, child inside).
|
80
|
-
def
|
80
|
+
def _run_around_hooks(&block)
|
81
81
|
self.class.around_hooks.reverse.inject(block) do |chain, hook|
|
82
|
-
proc {
|
82
|
+
proc { _run_hook(hook, chain) }
|
83
83
|
end.call
|
84
84
|
end
|
85
85
|
|
86
86
|
# Before hooks run in the order they were added (parent first, then child).
|
87
|
-
def
|
88
|
-
|
87
|
+
def _run_before_hooks
|
88
|
+
_run_hooks(self.class.before_hooks)
|
89
89
|
end
|
90
90
|
|
91
91
|
# After hooks are reversed to ensure child hooks run before parent hooks
|
92
92
|
# (specific cleanup first, then general).
|
93
|
-
def
|
94
|
-
|
93
|
+
def _run_after_hooks
|
94
|
+
_run_hooks(self.class.after_hooks.reverse)
|
95
95
|
end
|
96
96
|
|
97
|
-
# Internal: Run a collection of hooks. The "
|
97
|
+
# Internal: Run a collection of hooks. The "_run_hooks" method is the common
|
98
98
|
# interface by which collections of either before or after hooks are run.
|
99
99
|
#
|
100
100
|
# hooks - An Array of Symbol and Procs.
|
101
101
|
#
|
102
102
|
# Returns nothing.
|
103
|
-
def
|
104
|
-
hooks.each { |hook|
|
103
|
+
def _run_hooks(hooks)
|
104
|
+
hooks.each { |hook| _run_hook(hook) }
|
105
105
|
end
|
106
106
|
|
107
|
-
# Internal: Run an individual hook. The "
|
107
|
+
# Internal: Run an individual hook. The "_run_hook" method is the common
|
108
108
|
# interface by which an individual hook is run. If the given hook is a
|
109
109
|
# symbol, the method is invoked whether public or private. If the hook is a
|
110
110
|
# proc, the proc is evaluated in the context of the current instance.
|
@@ -115,7 +115,7 @@ module Action
|
|
115
115
|
# Symbol method name.
|
116
116
|
#
|
117
117
|
# Returns nothing.
|
118
|
-
def
|
118
|
+
def _run_hook(hook, *)
|
119
119
|
hook.is_a?(Symbol) ? send(hook, *) : instance_exec(*, &hook)
|
120
120
|
end
|
121
121
|
end
|
data/lib/action/core/logging.rb
CHANGED
@@ -15,9 +15,9 @@ module Action
|
|
15
15
|
end
|
16
16
|
|
17
17
|
module ClassMethods
|
18
|
-
def
|
18
|
+
def log_level = Action.config.log_level
|
19
19
|
|
20
|
-
def log(message, level:
|
20
|
+
def log(message, level: log_level, before: nil, after: nil)
|
21
21
|
msg = [_log_prefix, message].compact_blank.join(" ")
|
22
22
|
msg = [before, msg, after].compact_blank.join if before || after
|
23
23
|
|
data/lib/action/core/timing.rb
CHANGED
@@ -3,6 +3,12 @@
|
|
3
3
|
module Action
|
4
4
|
module Core
|
5
5
|
module Timing
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
include InstanceMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
6
12
|
# Get the current monotonic time
|
7
13
|
def self.now
|
8
14
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
@@ -17,6 +23,18 @@ module Action
|
|
17
23
|
def self.elapsed_seconds(start_time)
|
18
24
|
(now - start_time).round(6)
|
19
25
|
end
|
26
|
+
|
27
|
+
module InstanceMethods
|
28
|
+
private
|
29
|
+
|
30
|
+
def _with_timing
|
31
|
+
timing_start = Core::Timing.now
|
32
|
+
yield
|
33
|
+
ensure
|
34
|
+
elapsed_mils = Core::Timing.elapsed_ms(timing_start)
|
35
|
+
@__context.elapsed_time = elapsed_mils
|
36
|
+
end
|
37
|
+
end
|
20
38
|
end
|
21
39
|
end
|
22
40
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module Tracing
|
6
|
+
private
|
7
|
+
|
8
|
+
def _with_tracing(&)
|
9
|
+
return yield unless Action.config.wrap_with_trace
|
10
|
+
|
11
|
+
Action.config.wrap_with_trace.call(self.class.name || "AnonymousClass", &)
|
12
|
+
rescue StandardError => e
|
13
|
+
Axn::Util.piping_error("running trace hook", action: self, exception: e)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/action/core.rb
CHANGED
@@ -6,9 +6,10 @@ require "action/strategies"
|
|
6
6
|
require "action/core/hooks"
|
7
7
|
require "action/core/logging"
|
8
8
|
require "action/core/hoist_errors"
|
9
|
-
require "action/core/
|
9
|
+
require "action/core/flow"
|
10
10
|
require "action/core/automatic_logging"
|
11
11
|
require "action/core/use_strategy"
|
12
|
+
require "action/core/tracing"
|
12
13
|
|
13
14
|
# CONSIDER: make class names match file paths?
|
14
15
|
require "action/core/validation/validators/model_validator"
|
@@ -28,8 +29,10 @@ module Action
|
|
28
29
|
include Core::Hooks
|
29
30
|
include Core::Logging
|
30
31
|
include Core::AutomaticLogging
|
32
|
+
include Core::Tracing
|
33
|
+
include Core::Timing
|
31
34
|
|
32
|
-
include Core::
|
35
|
+
include Core::Flow
|
33
36
|
|
34
37
|
include Core::ContractValidation
|
35
38
|
include Core::Contract
|
@@ -41,84 +44,32 @@ module Action
|
|
41
44
|
end
|
42
45
|
|
43
46
|
module ClassMethods
|
44
|
-
def call(
|
45
|
-
new(
|
47
|
+
def call(**)
|
48
|
+
new(**).tap(&:_run).result
|
46
49
|
end
|
47
50
|
|
48
|
-
def call!(
|
49
|
-
result = call(
|
51
|
+
def call!(**)
|
52
|
+
result = call(**)
|
50
53
|
return result if result.ok?
|
51
54
|
|
52
55
|
raise result.exception || Action::Failure.new(result.error)
|
53
56
|
end
|
54
57
|
end
|
55
58
|
|
56
|
-
def initialize(
|
57
|
-
@
|
59
|
+
def initialize(**)
|
60
|
+
@__context = Action::Context.new(**)
|
58
61
|
end
|
59
62
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
_log_before
|
71
|
-
yield
|
72
|
-
ensure
|
73
|
-
_log_after(timing_start:, outcome: _determine_outcome)
|
74
|
-
end
|
75
|
-
|
76
|
-
def with_contract
|
77
|
-
_apply_inbound_preprocessing!
|
78
|
-
_apply_defaults!(:inbound)
|
79
|
-
_validate_contract!(:inbound)
|
80
|
-
|
81
|
-
yield
|
82
|
-
|
83
|
-
_apply_defaults!(:outbound)
|
84
|
-
_validate_contract!(:outbound)
|
85
|
-
|
86
|
-
# TODO: improve location of this triggering
|
87
|
-
trigger_on_success if respond_to?(:trigger_on_success)
|
88
|
-
end
|
89
|
-
|
90
|
-
def with_exception_swallowing
|
91
|
-
yield
|
92
|
-
rescue StandardError => e
|
93
|
-
# on_error handlers run for both unhandled exceptions and fail!
|
94
|
-
self.class._error_handlers.each do |handler|
|
95
|
-
handler.execute_if_matches(exception: e, action: self)
|
96
|
-
end
|
97
|
-
|
98
|
-
# on_failure handlers run ONLY for fail!
|
99
|
-
if e.is_a?(Action::Failure)
|
100
|
-
@context.instance_variable_set("@error_from_user", e.message) if e.message.present?
|
101
|
-
|
102
|
-
self.class._failure_handlers.each do |handler|
|
103
|
-
handler.execute_if_matches(exception: e, action: self)
|
104
|
-
end
|
105
|
-
else
|
106
|
-
# on_exception handlers run for ONLY for unhandled exceptions. AND NOTE: may be skipped if the exception is rescued via `rescues`.
|
107
|
-
trigger_on_exception(e)
|
108
|
-
|
109
|
-
@context.exception = e
|
110
|
-
end
|
111
|
-
|
112
|
-
@context.instance_variable_set("@failure", true)
|
113
|
-
end
|
114
|
-
|
115
|
-
def run
|
116
|
-
with_tracing do
|
117
|
-
with_logging do
|
118
|
-
with_exception_swallowing do # Exceptions stop here; outer wrappers access result status (and must not introduce another exception layer)
|
119
|
-
with_contract do # Library internals -- any failures (e.g. contract violations) *should* fail the Action::Result
|
120
|
-
with_hooks do # User hooks -- any failures here *should* fail the Action::Result
|
121
|
-
call
|
63
|
+
# Main entry point for action execution
|
64
|
+
def _run
|
65
|
+
_with_tracing do
|
66
|
+
_with_logging do
|
67
|
+
_with_timing do
|
68
|
+
_with_exception_handling do # Exceptions stop here; outer wrappers access result status (and must not introduce another exception layer)
|
69
|
+
_with_contract do # Library internals -- any failures (e.g. contract violations) *should* fail the Action::Result
|
70
|
+
_with_hooks do # User hooks -- any failures here *should* fail the Action::Result
|
71
|
+
call
|
72
|
+
end
|
122
73
|
end
|
123
74
|
end
|
124
75
|
end
|
@@ -128,8 +79,11 @@ module Action
|
|
128
79
|
_emit_metrics
|
129
80
|
end
|
130
81
|
|
82
|
+
# User-defined action logic - override this method in your action classes
|
131
83
|
def call; end
|
132
84
|
|
85
|
+
delegate :fail!, to: :@__context
|
86
|
+
|
133
87
|
private
|
134
88
|
|
135
89
|
def _emit_metrics
|
@@ -137,17 +91,10 @@ module Action
|
|
137
91
|
|
138
92
|
Action.config.emit_metrics.call(
|
139
93
|
self.class.name || "AnonymousClass",
|
140
|
-
|
94
|
+
result,
|
141
95
|
)
|
142
96
|
rescue StandardError => e
|
143
97
|
Axn::Util.piping_error("running metrics hook", action: self, exception: e)
|
144
98
|
end
|
145
|
-
|
146
|
-
def _determine_outcome
|
147
|
-
return "exception" if @context.exception
|
148
|
-
return "failure" if @context.failure?
|
149
|
-
|
150
|
-
"success"
|
151
|
-
end
|
152
99
|
end
|
153
100
|
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/context/facade"
|
4
|
+
require "action/core/context/facade_inspector"
|
5
|
+
|
6
|
+
module Action
|
7
|
+
# Outbound / External ContextFacade
|
8
|
+
class Result < ContextFacade
|
9
|
+
# For ease of mocking return results in tests
|
10
|
+
class << self
|
11
|
+
def ok(msg = nil, **exposures)
|
12
|
+
exposes = exposures.keys.to_h { |key| [key, { allow_blank: true }] }
|
13
|
+
|
14
|
+
Axn::Factory.build(exposes:, messages: { success: msg }) do
|
15
|
+
exposures.each do |key, value|
|
16
|
+
expose(key, value)
|
17
|
+
end
|
18
|
+
end.call
|
19
|
+
end
|
20
|
+
|
21
|
+
def error(msg = nil, **exposures, &block)
|
22
|
+
exposes = exposures.keys.to_h { |key| [key, { allow_blank: true }] }
|
23
|
+
rescues = [-> { true }, msg]
|
24
|
+
|
25
|
+
Axn::Factory.build(exposes:, rescues:) do
|
26
|
+
exposures.each do |key, value|
|
27
|
+
expose(key, value)
|
28
|
+
end
|
29
|
+
block.call if block_given?
|
30
|
+
fail!
|
31
|
+
end.call
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Poke some holes for necessary internal control methods
|
36
|
+
delegate :each_pair, to: :context
|
37
|
+
|
38
|
+
# External interface
|
39
|
+
delegate :ok?, :exception, to: :context
|
40
|
+
|
41
|
+
def error
|
42
|
+
return if ok?
|
43
|
+
|
44
|
+
[@context.error_prefix, determine_error_message].compact.join(" ").squeeze(" ")
|
45
|
+
end
|
46
|
+
|
47
|
+
def success
|
48
|
+
return unless ok?
|
49
|
+
|
50
|
+
stringified(action._success_msg).presence || "Action completed successfully"
|
51
|
+
end
|
52
|
+
|
53
|
+
def ok = success
|
54
|
+
|
55
|
+
def message = error || success
|
56
|
+
|
57
|
+
# Outcome constants for action execution results
|
58
|
+
OUTCOMES = [
|
59
|
+
OUTCOME_SUCCESS = :success,
|
60
|
+
OUTCOME_FAILURE = :failure,
|
61
|
+
OUTCOME_EXCEPTION = :exception,
|
62
|
+
].freeze
|
63
|
+
|
64
|
+
def outcome
|
65
|
+
return OUTCOME_EXCEPTION if exception
|
66
|
+
return OUTCOME_FAILURE if @context.failed?
|
67
|
+
|
68
|
+
OUTCOME_SUCCESS
|
69
|
+
end
|
70
|
+
|
71
|
+
# Elapsed time in milliseconds
|
72
|
+
def elapsed_time
|
73
|
+
@context.elapsed_time
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def context_data_source = @context.exposed_data
|
79
|
+
|
80
|
+
def method_missing(method_name, ...) # rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)
|
81
|
+
if @context.__combined_data.key?(method_name.to_sym)
|
82
|
+
msg = <<~MSG
|
83
|
+
Method ##{method_name} is not available on Action::Result!
|
84
|
+
|
85
|
+
#{action_name} may be missing a line like:
|
86
|
+
exposes :#{method_name}
|
87
|
+
MSG
|
88
|
+
|
89
|
+
raise Action::ContractViolation::MethodNotAllowed, msg
|
90
|
+
end
|
91
|
+
|
92
|
+
super
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|