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
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module ContractValidation
|
6
|
+
private
|
7
|
+
|
8
|
+
def _apply_inbound_preprocessing!
|
9
|
+
internal_field_configs.each do |config|
|
10
|
+
next unless config.preprocess
|
11
|
+
|
12
|
+
initial_value = @__context.provided_data[config.field]
|
13
|
+
new_value = config.preprocess.call(initial_value)
|
14
|
+
@__context.provided_data[config.field] = new_value
|
15
|
+
rescue StandardError => e
|
16
|
+
raise Action::ContractViolation::PreprocessingError, "Error preprocessing field '#{config.field}': #{e.message}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def _validate_contract!(direction)
|
21
|
+
raise ArgumentError, "Invalid direction: #{direction}" unless %i[inbound outbound].include?(direction)
|
22
|
+
|
23
|
+
configs = direction == :inbound ? internal_field_configs : external_field_configs
|
24
|
+
validations = configs.each_with_object({}) do |config, hash|
|
25
|
+
hash[config.field] = config.validations
|
26
|
+
end
|
27
|
+
context = direction == :inbound ? internal_context : result
|
28
|
+
exception_klass = direction == :inbound ? Action::InboundValidationError : Action::OutboundValidationError
|
29
|
+
|
30
|
+
Validation::Fields.validate!(validations:, context:, exception_klass:)
|
31
|
+
end
|
32
|
+
|
33
|
+
def _apply_defaults!(direction)
|
34
|
+
raise ArgumentError, "Invalid direction: #{direction}" unless %i[inbound outbound].include?(direction)
|
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
|
+
|
46
|
+
configs = direction == :inbound ? internal_field_configs : external_field_configs
|
47
|
+
defaults_mapping = configs.each_with_object({}) do |config, hash|
|
48
|
+
hash[config.field] = config.default
|
49
|
+
end.compact
|
50
|
+
|
51
|
+
defaults_mapping.each do |field, default_value_getter|
|
52
|
+
data_hash = direction == :inbound ? @__context.provided_data : @__context.exposed_data
|
53
|
+
next if data_hash[field].present?
|
54
|
+
|
55
|
+
default_value = default_value_getter.respond_to?(:call) ? instance_exec(&default_value_getter) : default_value_getter
|
56
|
+
|
57
|
+
data_hash[field] = default_value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
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
|
@@ -1,55 +1,57 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Action
|
4
|
-
module
|
5
|
-
|
6
|
-
base
|
7
|
-
|
4
|
+
module Core
|
5
|
+
module HoistErrors
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
include InstanceMethods
|
9
|
+
end
|
8
10
|
end
|
9
|
-
end
|
10
11
|
|
11
|
-
|
12
|
-
|
12
|
+
module InstanceMethods
|
13
|
+
private
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
end
|
17
|
-
|
18
|
-
# This method is used to ensure that the result of a block is successful before proceeding.
|
19
|
-
#
|
20
|
-
# Assumes success unless the block raises an exception or returns a failed result.
|
21
|
-
# (i.e. if you wrap logic that is NOT an action call, it'll be successful unless it raises an exception)
|
22
|
-
def hoist_errors(prefix: nil)
|
23
|
-
raise ArgumentError, "#hoist_errors must be given a block to execute" unless block_given?
|
24
|
-
|
25
|
-
result = begin
|
26
|
-
yield
|
27
|
-
rescue StandardError => e
|
28
|
-
log "hoist_errors block transforming a #{e.class.name} exception: #{e.message}"
|
29
|
-
MinimalFailedResult.new(error: nil, exception: e)
|
15
|
+
MinimalFailedResult = Data.define(:error, :exception) do
|
16
|
+
def ok? = false
|
30
17
|
end
|
31
18
|
|
32
|
-
# This
|
33
|
-
#
|
34
|
-
# CAUTION: if there are multiple calls per block, only the last one will be checked!
|
19
|
+
# This method is used to ensure that the result of a block is successful before proceeding.
|
35
20
|
#
|
36
|
-
unless result.
|
37
|
-
|
38
|
-
|
21
|
+
# Assumes success unless the block raises an exception or returns a failed result.
|
22
|
+
# (i.e. if you wrap logic that is NOT an action call, it'll be successful unless it raises an exception)
|
23
|
+
def hoist_errors(prefix: nil)
|
24
|
+
raise ArgumentError, "#hoist_errors must be given a block to execute" unless block_given?
|
25
|
+
|
26
|
+
result = begin
|
27
|
+
yield
|
28
|
+
rescue StandardError => e
|
29
|
+
log "hoist_errors block transforming a #{e.class.name} exception: #{e.message}"
|
30
|
+
MinimalFailedResult.new(error: nil, exception: e)
|
31
|
+
end
|
32
|
+
|
33
|
+
# This ensures the last line of hoist_errors was an Action call
|
34
|
+
#
|
35
|
+
# CAUTION: if there are multiple calls per block, only the last one will be checked!
|
36
|
+
#
|
37
|
+
unless result.respond_to?(:ok?)
|
38
|
+
raise ArgumentError,
|
39
|
+
"#hoist_errors is expected to wrap an Action call, but it returned a #{result.class.name} instead"
|
40
|
+
end
|
41
|
+
|
42
|
+
return result if result.ok?
|
43
|
+
|
44
|
+
_handle_hoisted_errors(result, prefix:)
|
39
45
|
end
|
40
46
|
|
41
|
-
|
47
|
+
# Separate method to allow overriding in subclasses
|
48
|
+
def _handle_hoisted_errors(result, prefix: nil)
|
49
|
+
@__context.exception = result.exception if result.exception.present?
|
50
|
+
@__context.error_prefix = prefix if prefix.present?
|
42
51
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
# Separate method to allow overriding in subclasses
|
47
|
-
def _handle_hoisted_errors(result, prefix: nil)
|
48
|
-
@context.exception = result.exception if result.exception.present?
|
49
|
-
@context.error_prefix = prefix if prefix.present?
|
50
|
-
|
51
|
-
error = result.exception.is_a?(Action::Failure) ? result.exception.message : result.error
|
52
|
-
fail! error
|
52
|
+
error = result.exception.is_a?(Action::Failure) ? result.exception.message : result.error
|
53
|
+
fail! error
|
54
|
+
end
|
53
55
|
end
|
54
56
|
end
|
55
57
|
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module Hooks
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
class_attribute :around_hooks, default: []
|
9
|
+
class_attribute :before_hooks, default: []
|
10
|
+
class_attribute :after_hooks, default: []
|
11
|
+
|
12
|
+
extend ClassMethods
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
# Public: Declare hooks to run around action execution. The around
|
18
|
+
# method may be called multiple times; subsequent calls append declared
|
19
|
+
# hooks to existing around hooks.
|
20
|
+
#
|
21
|
+
# Around hooks wrap the entire action execution, including before and
|
22
|
+
# after hooks. Parent hooks wrap child hooks (parent outside, child inside).
|
23
|
+
#
|
24
|
+
# hooks - Zero or more Symbol method names representing instance methods
|
25
|
+
# to be called around action execution. Each instance method
|
26
|
+
# invocation receives an argument representing the next link in
|
27
|
+
# the around hook chain.
|
28
|
+
# block - An optional block to be executed as a hook. If given, the block
|
29
|
+
# is executed after methods corresponding to any given Symbols.
|
30
|
+
def around(*hooks, &block)
|
31
|
+
hooks << block if block
|
32
|
+
hooks.each { |hook| self.around_hooks += [hook] }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Public: Declare hooks to run before action execution. The before
|
36
|
+
# method may be called multiple times; subsequent calls append declared
|
37
|
+
# hooks to existing before hooks.
|
38
|
+
#
|
39
|
+
# Before hooks run in parent-first order (general setup first, then specific).
|
40
|
+
# Parent hooks run before child hooks.
|
41
|
+
#
|
42
|
+
# hooks - Zero or more Symbol method names representing instance methods
|
43
|
+
# to be called before action execution.
|
44
|
+
# block - An optional block to be executed as a hook. If given, the block
|
45
|
+
# is executed after methods corresponding to any given Symbols.
|
46
|
+
def before(*hooks, &block)
|
47
|
+
hooks << block if block
|
48
|
+
hooks.each { |hook| self.before_hooks += [hook] }
|
49
|
+
end
|
50
|
+
|
51
|
+
# Public: Declare hooks to run after action execution. The after
|
52
|
+
# method may be called multiple times; subsequent calls prepend declared
|
53
|
+
# hooks to existing after hooks.
|
54
|
+
#
|
55
|
+
# After hooks run in child-first order (specific cleanup first, then general).
|
56
|
+
# Child hooks run before parent hooks.
|
57
|
+
#
|
58
|
+
# hooks - Zero or more Symbol method names representing instance methods
|
59
|
+
# to be called after action execution.
|
60
|
+
# block - An optional block to be executed as a hook. If given, the block
|
61
|
+
# is executed before methods corresponding to any given Symbols.
|
62
|
+
def after(*hooks, &block)
|
63
|
+
hooks << block if block
|
64
|
+
hooks.each { |hook| self.after_hooks = [hook] + after_hooks }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def _with_hooks
|
71
|
+
_run_around_hooks do
|
72
|
+
_run_before_hooks
|
73
|
+
yield
|
74
|
+
_run_after_hooks
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Around hooks are reversed before injection to ensure parent hooks wrap
|
79
|
+
# child hooks (parent outside, child inside).
|
80
|
+
def _run_around_hooks(&block)
|
81
|
+
self.class.around_hooks.reverse.inject(block) do |chain, hook|
|
82
|
+
proc { _run_hook(hook, chain) }
|
83
|
+
end.call
|
84
|
+
end
|
85
|
+
|
86
|
+
# Before hooks run in the order they were added (parent first, then child).
|
87
|
+
def _run_before_hooks
|
88
|
+
_run_hooks(self.class.before_hooks)
|
89
|
+
end
|
90
|
+
|
91
|
+
# After hooks are reversed to ensure child hooks run before parent hooks
|
92
|
+
# (specific cleanup first, then general).
|
93
|
+
def _run_after_hooks
|
94
|
+
_run_hooks(self.class.after_hooks.reverse)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Internal: Run a collection of hooks. The "_run_hooks" method is the common
|
98
|
+
# interface by which collections of either before or after hooks are run.
|
99
|
+
#
|
100
|
+
# hooks - An Array of Symbol and Procs.
|
101
|
+
#
|
102
|
+
# Returns nothing.
|
103
|
+
def _run_hooks(hooks)
|
104
|
+
hooks.each { |hook| _run_hook(hook) }
|
105
|
+
end
|
106
|
+
|
107
|
+
# Internal: Run an individual hook. The "_run_hook" method is the common
|
108
|
+
# interface by which an individual hook is run. If the given hook is a
|
109
|
+
# symbol, the method is invoked whether public or private. If the hook is a
|
110
|
+
# proc, the proc is evaluated in the context of the current instance.
|
111
|
+
#
|
112
|
+
# hook - A Symbol or Proc hook.
|
113
|
+
# args - Zero or more arguments to be passed as block arguments into the
|
114
|
+
# given block or as arguments into the method described by the given
|
115
|
+
# Symbol method name.
|
116
|
+
#
|
117
|
+
# Returns nothing.
|
118
|
+
def _run_hook(hook, *)
|
119
|
+
hook.is_a?(Symbol) ? send(hook, *) : instance_exec(*, &hook)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/action/core/logging.rb
CHANGED
@@ -3,33 +3,35 @@
|
|
3
3
|
require "active_support/core_ext/module/delegation"
|
4
4
|
|
5
5
|
module Action
|
6
|
-
module
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
base
|
11
|
-
|
12
|
-
|
6
|
+
module Core
|
7
|
+
module Logging
|
8
|
+
LEVELS = %i[debug info warn error fatal].freeze
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval do
|
12
|
+
extend ClassMethods
|
13
|
+
delegate :log, *LEVELS, to: :class
|
14
|
+
end
|
13
15
|
end
|
14
|
-
end
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
module ClassMethods
|
18
|
+
def log_level = Action.config.log_level
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
def log(message, level: log_level, before: nil, after: nil)
|
21
|
+
msg = [_log_prefix, message].compact_blank.join(" ")
|
22
|
+
msg = [before, msg, after].compact_blank.join if before || after
|
22
23
|
|
23
|
-
|
24
|
-
|
24
|
+
Action.config.logger.send(level, msg)
|
25
|
+
end
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
27
|
+
LEVELS.each do |level|
|
28
|
+
define_method(level) do |message, before: nil, after: nil|
|
29
|
+
log(message, level:, before:, after:)
|
30
|
+
end
|
29
31
|
end
|
30
|
-
end
|
31
32
|
|
32
|
-
|
33
|
+
def _log_prefix = "[#{name.presence || "Anonymous Class"}]"
|
34
|
+
end
|
33
35
|
end
|
34
36
|
end
|
35
37
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module Timing
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
include InstanceMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get the current monotonic time
|
13
|
+
def self.now
|
14
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Calculate elapsed time in milliseconds
|
18
|
+
def self.elapsed_ms(start_time)
|
19
|
+
((now - start_time) * 1000).round(3)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Calculate elapsed time in seconds
|
23
|
+
def self.elapsed_seconds(start_time)
|
24
|
+
(now - start_time).round(6)
|
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
|
38
|
+
end
|
39
|
+
end
|
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
|
@@ -1,27 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Action
|
4
|
-
module
|
5
|
-
|
4
|
+
module Core
|
5
|
+
module UseStrategy
|
6
|
+
extend ActiveSupport::Concern
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
class_methods do
|
9
|
+
def use(strategy_name, **config, &block)
|
10
|
+
strategy = Action::Strategies.all[strategy_name.to_sym]
|
11
|
+
raise StrategyNotFound, "Strategy #{strategy_name} not found" if strategy.blank?
|
12
|
+
raise ArgumentError, "Strategy #{strategy_name} does not support config" if config.any? && !strategy.respond_to?(:setup)
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
# Allow dynamic setup of strategy (i.e. dynamically define module before returning)
|
15
|
+
if strategy.respond_to?(:setup)
|
16
|
+
configured = strategy.setup(**config, &block)
|
17
|
+
raise ArgumentError, "Strategy #{strategy_name} setup method must return a module" unless configured.is_a?(Module)
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
strategy = configured
|
20
|
+
else
|
21
|
+
raise ArgumentError, "Strategy #{strategy_name} does not support config (define #setup method)" if config.any?
|
22
|
+
raise ArgumentError, "Strategy #{strategy_name} does not support blocks (define #setup method)" if block_given?
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
+
include strategy
|
26
|
+
end
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|