axn 0.1.0.pre.alpha.2.5.3.1 → 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 +25 -1
- data/README.md +2 -11
- data/docs/reference/action-result.md +2 -0
- data/docs/reference/class.md +12 -4
- data/docs/reference/configuration.md +53 -20
- data/docs/reference/instance.md +2 -2
- data/docs/strategies/index.md +1 -1
- data/docs/usage/setup.md +1 -1
- data/docs/usage/writing.md +9 -9
- data/lib/action/attachable/steps.rb +16 -1
- data/lib/action/attachable.rb +3 -3
- data/lib/action/{core/configuration.rb → configuration.rb} +3 -4
- data/lib/action/context.rb +38 -0
- data/lib/action/core/automatic_logging.rb +93 -0
- 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 +167 -211
- data/lib/action/core/contract_for_subfields.rb +84 -82
- data/lib/action/core/contract_validation.rb +62 -0
- 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 +42 -40
- data/lib/action/core/hooks.rb +123 -0
- data/lib/action/core/logging.rb +22 -20
- data/lib/action/core/timing.rb +40 -0
- data/lib/action/core/tracing.rb +17 -0
- data/lib/action/core/use_strategy.rb +19 -17
- data/lib/action/core/validation/fields.rb +2 -0
- data/lib/action/core.rb +100 -0
- data/lib/action/enqueueable/via_sidekiq.rb +2 -2
- data/lib/action/enqueueable.rb +1 -1
- data/lib/action/{core/exceptions.rb → exceptions.rb} +1 -19
- data/lib/action/result.rb +95 -0
- data/lib/axn/factory.rb +27 -9
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +10 -47
- metadata +19 -21
- data/lib/action/core/context_facade.rb +0 -209
- data/lib/action/core/handle_exceptions.rb +0 -163
- data/lib/action/core/top_level_around_hook.rb +0 -108
@@ -1,163 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "event_handlers"
|
4
|
-
|
5
|
-
module Action
|
6
|
-
module HandleExceptions
|
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
|
-
class_attribute :_error_handlers, default: []
|
12
|
-
class_attribute :_exception_handlers, default: []
|
13
|
-
class_attribute :_failure_handlers, default: []
|
14
|
-
|
15
|
-
include InstanceMethods
|
16
|
-
extend ClassMethods
|
17
|
-
|
18
|
-
def run
|
19
|
-
run!
|
20
|
-
rescue StandardError => e
|
21
|
-
# on_error handlers run for both unhandled exceptions and fail!
|
22
|
-
self.class._error_handlers.each do |handler|
|
23
|
-
handler.execute_if_matches(exception: e, action: self)
|
24
|
-
end
|
25
|
-
|
26
|
-
# on_failure handlers run ONLY for fail!
|
27
|
-
if e.is_a?(Action::Failure)
|
28
|
-
@context.instance_variable_set("@error_from_user", e.message) if e.message.present?
|
29
|
-
|
30
|
-
self.class._failure_handlers.each do |handler|
|
31
|
-
handler.execute_if_matches(exception: e, action: self)
|
32
|
-
end
|
33
|
-
else
|
34
|
-
# on_exception handlers run for ONLY for unhandled exceptions. AND NOTE: may be skipped if the exception is rescued via `rescues`.
|
35
|
-
trigger_on_exception(e)
|
36
|
-
|
37
|
-
@context.exception = e
|
38
|
-
end
|
39
|
-
|
40
|
-
@context.instance_variable_set("@failure", true)
|
41
|
-
end
|
42
|
-
|
43
|
-
def trigger_on_exception(exception)
|
44
|
-
interceptor = self.class._error_interceptor_for(exception:, action: self)
|
45
|
-
return if interceptor&.should_report_error == false
|
46
|
-
|
47
|
-
# Call any handlers registered on *this specific action* class
|
48
|
-
self.class._exception_handlers.each do |handler|
|
49
|
-
handler.execute_if_matches(exception:, action: self)
|
50
|
-
end
|
51
|
-
|
52
|
-
# Call any global handlers
|
53
|
-
Action.config.on_exception(exception,
|
54
|
-
action: self,
|
55
|
-
context: respond_to?(:context_for_logging) ? context_for_logging : @context.to_h)
|
56
|
-
rescue StandardError => e
|
57
|
-
# No action needed -- downstream #on_exception implementation should ideally log any internal failures, but
|
58
|
-
# we don't want exception *handling* failures to cascade and overwrite the original exception.
|
59
|
-
Axn::Util.piping_error("executing on_exception hooks", action: self, exception: e)
|
60
|
-
end
|
61
|
-
|
62
|
-
class << base
|
63
|
-
def call!(context = {})
|
64
|
-
result = call(context)
|
65
|
-
return result if result.ok?
|
66
|
-
|
67
|
-
raise result.exception || Action::Failure.new(result.error)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
module ClassMethods
|
74
|
-
def messages(success: nil, error: nil)
|
75
|
-
self._success_msg = success if success.present?
|
76
|
-
self._error_msg = error if error.present?
|
77
|
-
|
78
|
-
true
|
79
|
-
end
|
80
|
-
|
81
|
-
def error_from(matcher = nil, message = nil, **match_and_messages)
|
82
|
-
_register_error_interceptor(matcher, message, should_report_error: true, **match_and_messages)
|
83
|
-
end
|
84
|
-
|
85
|
-
def rescues(matcher = nil, message = nil, **match_and_messages)
|
86
|
-
_register_error_interceptor(matcher, message, should_report_error: false, **match_and_messages)
|
87
|
-
end
|
88
|
-
|
89
|
-
# ONLY raised exceptions (i.e. NOT fail!). Skipped if exception is rescued via .rescues.
|
90
|
-
def on_exception(matcher = -> { true }, &handler)
|
91
|
-
raise ArgumentError, "on_exception must be called with a block" unless block_given?
|
92
|
-
|
93
|
-
self._exception_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
|
94
|
-
end
|
95
|
-
|
96
|
-
# ONLY raised on fail! (i.e. NOT unhandled exceptions).
|
97
|
-
def on_failure(matcher = -> { true }, &handler)
|
98
|
-
raise ArgumentError, "on_failure must be called with a block" unless block_given?
|
99
|
-
|
100
|
-
self._failure_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
|
101
|
-
end
|
102
|
-
|
103
|
-
# Handles both fail! and unhandled exceptions... but is NOT affected by .rescues
|
104
|
-
def on_error(matcher = -> { true }, &handler)
|
105
|
-
raise ArgumentError, "on_error must be called with a block" unless block_given?
|
106
|
-
|
107
|
-
self._error_handlers += [Action::EventHandlers::ConditionalHandler.new(matcher:, handler:)]
|
108
|
-
end
|
109
|
-
|
110
|
-
# Syntactic sugar for "after { try" (after, but if it fails do NOT fail the action)
|
111
|
-
def on_success(&block)
|
112
|
-
raise ArgumentError, "on_success must be called with a block" unless block_given?
|
113
|
-
|
114
|
-
after do
|
115
|
-
try { instance_exec(&block) }
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def default_error = new.internal_context.default_error
|
120
|
-
|
121
|
-
# Private helpers
|
122
|
-
|
123
|
-
def _error_interceptor_for(exception:, action:)
|
124
|
-
Array(_custom_error_interceptors).detect do |int|
|
125
|
-
int.matches?(exception:, action:)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
def _register_error_interceptor(matcher, message, should_report_error:, **match_and_messages)
|
130
|
-
method_name = should_report_error ? "error_from" : "rescues"
|
131
|
-
raise ArgumentError, "#{method_name} must be called with a key/value pair, or else keyword args" if [matcher, message].compact.size == 1
|
132
|
-
|
133
|
-
interceptors = { matcher => message }.compact.merge(match_and_messages).map do |(matcher, message)| # rubocop:disable Lint/ShadowingOuterLocalVariable
|
134
|
-
Action::EventHandlers::CustomErrorInterceptor.new(matcher:, message:, should_report_error:)
|
135
|
-
end
|
136
|
-
|
137
|
-
self._custom_error_interceptors += interceptors
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
module InstanceMethods
|
142
|
-
private
|
143
|
-
|
144
|
-
def fail!(message = nil)
|
145
|
-
@context.instance_variable_set("@failure", true)
|
146
|
-
@context.error_from_user = message if message.present?
|
147
|
-
|
148
|
-
raise Action::Failure, message
|
149
|
-
end
|
150
|
-
|
151
|
-
def try
|
152
|
-
yield
|
153
|
-
rescue Action::Failure => e
|
154
|
-
# NOTE: re-raising so we can still fail! from inside the block
|
155
|
-
raise e
|
156
|
-
rescue StandardError => e
|
157
|
-
trigger_on_exception(e)
|
158
|
-
end
|
159
|
-
|
160
|
-
delegate :default_error, to: :internal_context
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
@@ -1,108 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Action
|
4
|
-
module TopLevelAroundHook
|
5
|
-
def self.included(base)
|
6
|
-
base.class_eval do
|
7
|
-
around :__top_level_around_hook
|
8
|
-
|
9
|
-
extend AutologgingClassMethods
|
10
|
-
include AutologgingInstanceMethods
|
11
|
-
include InstanceMethods
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
module AutologgingClassMethods
|
16
|
-
def default_autolog_level = Action.config.default_autolog_level
|
17
|
-
end
|
18
|
-
|
19
|
-
module AutologgingInstanceMethods
|
20
|
-
private
|
21
|
-
|
22
|
-
def _log_before
|
23
|
-
public_send(
|
24
|
-
self.class.default_autolog_level,
|
25
|
-
[
|
26
|
-
"About to execute",
|
27
|
-
_log_context(:inbound),
|
28
|
-
].compact.join(" with: "),
|
29
|
-
before: Action.config.env.production? ? nil : "\n------\n",
|
30
|
-
)
|
31
|
-
end
|
32
|
-
|
33
|
-
def _log_after(outcome:, timing_start:)
|
34
|
-
elapsed_mils = ((Time.now - timing_start) * 1000).round(3)
|
35
|
-
|
36
|
-
public_send(
|
37
|
-
self.class.default_autolog_level,
|
38
|
-
[
|
39
|
-
"Execution completed (with outcome: #{outcome}) in #{elapsed_mils} milliseconds",
|
40
|
-
_log_context(:outbound),
|
41
|
-
].compact.join(". Set: "),
|
42
|
-
after: Action.config.env.production? ? nil : "\n------\n",
|
43
|
-
)
|
44
|
-
end
|
45
|
-
|
46
|
-
def _log_context(direction)
|
47
|
-
data = context_for_logging(direction)
|
48
|
-
return unless data.present?
|
49
|
-
|
50
|
-
max_length = 150
|
51
|
-
suffix = "…<truncated>…"
|
52
|
-
|
53
|
-
_log_object(data).tap do |str|
|
54
|
-
return str[0, max_length - suffix.length] + suffix if str.length > max_length
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def _log_object(data)
|
59
|
-
case data
|
60
|
-
when Hash
|
61
|
-
# NOTE: slightly more manual in order to avoid quotes around ActiveRecord objects' <Class#id> formatting
|
62
|
-
"{#{data.map { |k, v| "#{k}: #{_log_object(v)}" }.join(", ")}}"
|
63
|
-
when Array
|
64
|
-
data.map { |v| _log_object(v) }
|
65
|
-
else
|
66
|
-
return data.to_unsafe_h if defined?(ActionController::Parameters) && data.is_a?(ActionController::Parameters)
|
67
|
-
return "<#{data.class.name}##{data.to_param.presence || "unpersisted"}>" if defined?(ActiveRecord::Base) && data.is_a?(ActiveRecord::Base)
|
68
|
-
|
69
|
-
data.inspect
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
module InstanceMethods
|
75
|
-
def __top_level_around_hook(hooked)
|
76
|
-
timing_start = Time.now
|
77
|
-
_log_before
|
78
|
-
|
79
|
-
_configurable_around_wrapper do
|
80
|
-
(@outcome, @exception) = _call_and_return_outcome(hooked)
|
81
|
-
end
|
82
|
-
|
83
|
-
_log_after(timing_start:, outcome: @outcome)
|
84
|
-
|
85
|
-
raise @exception if @exception
|
86
|
-
end
|
87
|
-
|
88
|
-
private
|
89
|
-
|
90
|
-
def _configurable_around_wrapper(&)
|
91
|
-
return yield unless Action.config.top_level_around_hook
|
92
|
-
|
93
|
-
Action.config.top_level_around_hook.call(self.class.name || "AnonymousClass", &)
|
94
|
-
end
|
95
|
-
|
96
|
-
def _call_and_return_outcome(hooked)
|
97
|
-
hooked.call
|
98
|
-
|
99
|
-
"success"
|
100
|
-
rescue StandardError => e
|
101
|
-
[
|
102
|
-
e.is_a?(Action::Failure) ? "failure" : "exception",
|
103
|
-
e,
|
104
|
-
]
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|