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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/CHANGELOG.md +25 -1
  4. data/README.md +2 -11
  5. data/docs/reference/action-result.md +2 -0
  6. data/docs/reference/class.md +12 -4
  7. data/docs/reference/configuration.md +53 -20
  8. data/docs/reference/instance.md +2 -2
  9. data/docs/strategies/index.md +1 -1
  10. data/docs/usage/setup.md +1 -1
  11. data/docs/usage/writing.md +9 -9
  12. data/lib/action/attachable/steps.rb +16 -1
  13. data/lib/action/attachable.rb +3 -3
  14. data/lib/action/{core/configuration.rb → configuration.rb} +3 -4
  15. data/lib/action/context.rb +38 -0
  16. data/lib/action/core/automatic_logging.rb +93 -0
  17. data/lib/action/core/context/facade.rb +69 -0
  18. data/lib/action/core/context/facade_inspector.rb +63 -0
  19. data/lib/action/core/context/internal.rb +32 -0
  20. data/lib/action/core/contract.rb +167 -211
  21. data/lib/action/core/contract_for_subfields.rb +84 -82
  22. data/lib/action/core/contract_validation.rb +62 -0
  23. data/lib/action/core/flow/callbacks.rb +54 -0
  24. data/lib/action/core/flow/exception_execution.rb +79 -0
  25. data/lib/action/core/flow/messages.rb +61 -0
  26. data/lib/action/core/flow.rb +19 -0
  27. data/lib/action/core/hoist_errors.rb +42 -40
  28. data/lib/action/core/hooks.rb +123 -0
  29. data/lib/action/core/logging.rb +22 -20
  30. data/lib/action/core/timing.rb +40 -0
  31. data/lib/action/core/tracing.rb +17 -0
  32. data/lib/action/core/use_strategy.rb +19 -17
  33. data/lib/action/core/validation/fields.rb +2 -0
  34. data/lib/action/core.rb +100 -0
  35. data/lib/action/enqueueable/via_sidekiq.rb +2 -2
  36. data/lib/action/enqueueable.rb +1 -1
  37. data/lib/action/{core/exceptions.rb → exceptions.rb} +1 -19
  38. data/lib/action/result.rb +95 -0
  39. data/lib/axn/factory.rb +27 -9
  40. data/lib/axn/version.rb +1 -1
  41. data/lib/axn.rb +10 -47
  42. metadata +19 -21
  43. data/lib/action/core/context_facade.rb +0 -209
  44. data/lib/action/core/handle_exceptions.rb +0 -163
  45. 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