axn 0.1.0.pre.alpha.2.6 → 0.1.0.pre.alpha.2.7
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 +23 -2
- data/docs/reference/action-result.md +2 -0
- data/docs/reference/class.md +140 -19
- 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 +39 -0
- data/lib/action/core/context/facade_inspector.rb +63 -0
- data/lib/action/core/context/internal.rb +38 -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 +65 -0
- data/lib/action/core/flow/handlers/base_handler.rb +32 -0
- data/lib/action/core/flow/handlers/callback_handler.rb +21 -0
- data/lib/action/core/flow/handlers/invoker.rb +73 -0
- data/lib/action/core/flow/handlers/matcher.rb +85 -0
- data/lib/action/core/flow/handlers/message_handler.rb +27 -0
- data/lib/action/core/flow/handlers/registry.rb +40 -0
- data/lib/action/core/flow/handlers.rb +17 -0
- data/lib/action/core/flow/messages.rb +75 -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 +114 -0
- data/lib/axn/factory.rb +45 -7
- data/lib/axn/version.rb +1 -1
- metadata +18 -5
- data/lib/action/core/context_facade.rb +0 -209
- data/lib/action/core/event_handlers.rb +0 -62
- data/lib/action/core/handle_exceptions.rb +0 -143
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/context/facade"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
# Inbound / Internal ContextFacade
|
7
|
+
class InternalContext < ContextFacade
|
8
|
+
# Available for use from within message callables
|
9
|
+
def default_error
|
10
|
+
msg = action.class._static_message_for(:error, action:, exception: @context.exception || Action::Failure.new)
|
11
|
+
[@context.error_prefix, msg.presence || "Something went wrong"].compact.join(" ").squeeze(" ")
|
12
|
+
end
|
13
|
+
|
14
|
+
def default_success
|
15
|
+
msg = action.class._static_message_for(:success, action:, exception: nil)
|
16
|
+
msg.presence || "Action completed successfully"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def context_data_source = @context.provided_data
|
22
|
+
|
23
|
+
def method_missing(method_name, ...) # rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)
|
24
|
+
if @context.__combined_data.key?(method_name.to_sym)
|
25
|
+
msg = <<~MSG
|
26
|
+
Method ##{method_name} is not available on Action::InternalContext!
|
27
|
+
|
28
|
+
#{action_name} may be missing a line like:
|
29
|
+
expects :#{method_name}
|
30
|
+
MSG
|
31
|
+
|
32
|
+
raise Action::ContractViolation::MethodNotAllowed, msg
|
33
|
+
end
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/action/core/contract.rb
CHANGED
@@ -4,7 +4,8 @@ require "active_support/core_ext/enumerable"
|
|
4
4
|
require "active_support/core_ext/module/delegation"
|
5
5
|
|
6
6
|
require "action/core/validation/fields"
|
7
|
-
require "action/
|
7
|
+
require "action/result"
|
8
|
+
require "action/core/context/internal"
|
8
9
|
|
9
10
|
module Action
|
10
11
|
module Core
|
@@ -70,13 +71,13 @@ module Action
|
|
70
71
|
private
|
71
72
|
|
72
73
|
RESERVED_FIELD_NAMES_FOR_EXPECTATIONS = %w[
|
73
|
-
fail!
|
74
|
+
fail! ok?
|
74
75
|
inspect default_error
|
75
76
|
each_pair
|
76
77
|
].freeze
|
77
78
|
|
78
79
|
RESERVED_FIELD_NAMES_FOR_EXPOSURES = %w[
|
79
|
-
fail!
|
80
|
+
fail! ok?
|
80
81
|
inspect each_pair default_error
|
81
82
|
ok error success message
|
82
83
|
].freeze
|
@@ -114,13 +115,15 @@ module Action
|
|
114
115
|
define_method(field) { internal_context.public_send(field) }
|
115
116
|
end
|
116
117
|
|
117
|
-
def _define_model_reader(field, klass)
|
118
|
+
def _define_model_reader(field, klass, &id_extractor)
|
118
119
|
name = field.to_s.delete_suffix("_id")
|
119
120
|
raise ArgumentError, "Model validation expects to be given a field ending in _id (given: #{field})" unless field.to_s.end_with?("_id")
|
120
121
|
raise ArgumentError, "Failed to define model reader - #{name} is already defined" if method_defined?(name)
|
121
122
|
|
123
|
+
id_extractor ||= -> { public_send(field) }
|
124
|
+
|
122
125
|
define_memoized_reader_method(name) do
|
123
|
-
Validators::ModelValidator.instance_for(field:, klass:, id:
|
126
|
+
Validators::ModelValidator.instance_for(field:, klass:, id: instance_exec(&id_extractor))
|
124
127
|
end
|
125
128
|
end
|
126
129
|
|
@@ -166,23 +169,37 @@ module Action
|
|
166
169
|
kwargs.each do |key, value|
|
167
170
|
raise Action::ContractViolation::UnknownExposure, key unless result.respond_to?(key)
|
168
171
|
|
169
|
-
@
|
172
|
+
@__context.exposed_data[key] = value
|
170
173
|
end
|
171
174
|
end
|
172
175
|
|
173
176
|
def context_for_logging(direction = nil)
|
174
|
-
inspection_filter.filter(@
|
177
|
+
inspection_filter.filter(@__context.__combined_data.slice(*_declared_fields(direction)))
|
175
178
|
end
|
176
179
|
|
177
180
|
private
|
178
181
|
|
182
|
+
def _with_contract
|
183
|
+
_apply_inbound_preprocessing!
|
184
|
+
_apply_defaults!(:inbound)
|
185
|
+
_validate_contract!(:inbound)
|
186
|
+
|
187
|
+
yield
|
188
|
+
|
189
|
+
_apply_defaults!(:outbound)
|
190
|
+
_validate_contract!(:outbound)
|
191
|
+
|
192
|
+
# TODO: improve location of this triggering
|
193
|
+
_trigger_on_success if respond_to?(:_trigger_on_success)
|
194
|
+
end
|
195
|
+
|
179
196
|
def _build_context_facade(direction)
|
180
197
|
raise ArgumentError, "Invalid direction: #{direction}" unless %i[inbound outbound].include?(direction)
|
181
198
|
|
182
199
|
klass = direction == :inbound ? Action::InternalContext : Action::Result
|
183
200
|
implicitly_allowed_fields = direction == :inbound ? _declared_fields(:outbound) : []
|
184
201
|
|
185
|
-
klass.new(action: self, context: @
|
202
|
+
klass.new(action: self, context: @__context, declared_fields: _declared_fields(direction), implicitly_allowed_fields:)
|
186
203
|
end
|
187
204
|
|
188
205
|
def inspection_filter
|
@@ -86,7 +86,7 @@ module Action
|
|
86
86
|
Action::Validation::Subfields.extract(field, public_send(on))
|
87
87
|
end
|
88
88
|
|
89
|
-
_define_model_reader(field, validations[:model]) if validations.key?(:model)
|
89
|
+
_define_model_reader(field, validations[:model]) { Action::Validation::Subfields.extract(field, public_send(on)) } if validations.key?(:model)
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
@@ -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/flow/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 :_callbacks_registry, default: Action::Core::Flow::Handlers::Registry.empty
|
12
|
+
|
13
|
+
extend ClassMethods
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
# Internal dispatcher
|
19
|
+
def _dispatch_callbacks(event_type, action:, exception: nil)
|
20
|
+
_callbacks_registry.for(event_type).each do |handler|
|
21
|
+
handler.apply(action:, exception:)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# ONLY raised exceptions (i.e. NOT fail!).
|
26
|
+
def on_exception(**, &block) = _add_callback(:exception, **, block:)
|
27
|
+
|
28
|
+
# ONLY raised on fail! (i.e. NOT unhandled exceptions).
|
29
|
+
def on_failure(**, &block) = _add_callback(:failure, **, block:)
|
30
|
+
|
31
|
+
# Handles both fail! and unhandled exceptions
|
32
|
+
def on_error(**, &block) = _add_callback(:error, **, block:)
|
33
|
+
|
34
|
+
# Executes when the action completes successfully (after all after hooks complete successfully)
|
35
|
+
# Runs in child-first order (child handlers before parent handlers)
|
36
|
+
def on_success(**, &block) = _add_callback(:success, **, block:)
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def _add_callback(event_type, block:, **kwargs)
|
41
|
+
raise ArgumentError, "on_#{event_type} cannot be called with both :if and :unless" if kwargs.key?(:if) && kwargs.key?(:unless)
|
42
|
+
|
43
|
+
condition = kwargs.key?(:if) ? kwargs[:if] : kwargs[:unless]
|
44
|
+
raise ArgumentError, "on_#{event_type} must be called with a block" unless block
|
45
|
+
|
46
|
+
matcher = condition.nil? ? nil : Action::Core::Flow::Handlers::Matcher.new(condition, invert: kwargs.key?(:unless))
|
47
|
+
entry = Action::Core::Flow::Handlers::CallbackHandler.new(matcher:, handler: block)
|
48
|
+
self._callbacks_registry = _callbacks_registry.register(event_type:, entry:)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,65 @@
|
|
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
|
+
# Call any handlers registered on *this specific action* class
|
13
|
+
self.class._dispatch_callbacks(:exception, action: self, exception:)
|
14
|
+
|
15
|
+
# Call any global handlers
|
16
|
+
Action.config.on_exception(exception, action: self, context: context_for_logging)
|
17
|
+
rescue StandardError => e
|
18
|
+
# No action needed -- downstream #on_exception implementation should ideally log any internal failures, but
|
19
|
+
# we don't want exception *handling* failures to cascade and overwrite the original exception.
|
20
|
+
Axn::Util.piping_error("executing on_exception hooks", action: self, exception: e)
|
21
|
+
end
|
22
|
+
|
23
|
+
def _trigger_on_success
|
24
|
+
# Call success handlers in child-first order (like after hooks)
|
25
|
+
self.class._dispatch_callbacks(:success, action: self, exception: nil)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
private
|
32
|
+
|
33
|
+
def _with_exception_handling
|
34
|
+
yield
|
35
|
+
rescue StandardError => e
|
36
|
+
# on_error handlers run for both unhandled exceptions and fail!
|
37
|
+
self.class._dispatch_callbacks(:error, action: self, exception: e)
|
38
|
+
|
39
|
+
# on_failure handlers run ONLY for fail!
|
40
|
+
if e.is_a?(Action::Failure)
|
41
|
+
self.class._dispatch_callbacks(:failure, action: self, exception: e)
|
42
|
+
else
|
43
|
+
# on_exception handlers run for ONLY for unhandled exceptions.
|
44
|
+
_trigger_on_exception(e)
|
45
|
+
|
46
|
+
@__context.exception = e
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set failure state using accessor method
|
50
|
+
@__context.send(:failure=, true)
|
51
|
+
end
|
52
|
+
|
53
|
+
def try
|
54
|
+
yield
|
55
|
+
rescue Action::Failure => e
|
56
|
+
# NOTE: re-raising so we can still fail! from inside the block
|
57
|
+
raise e
|
58
|
+
rescue StandardError => e
|
59
|
+
_trigger_on_exception(e)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/handlers/matcher"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
module Core
|
7
|
+
module Flow
|
8
|
+
# "Handlers" doesn't feel like *quite* the right name for this, but basically things in this namespace
|
9
|
+
# relate to conditionally-invoked code blocks (e.g. callbacks, messages, etc.)
|
10
|
+
module Handlers
|
11
|
+
class BaseHandler
|
12
|
+
def initialize(matcher: nil, handler: nil)
|
13
|
+
@matcher = matcher
|
14
|
+
@handler = handler
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :handler
|
18
|
+
|
19
|
+
def static? = @matcher.nil?
|
20
|
+
|
21
|
+
def matches?(action:, exception:)
|
22
|
+
return true if static?
|
23
|
+
|
24
|
+
@matcher.call(exception:, action:)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Subclasses should implement `apply(action:, exception:)`
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/handlers/base_handler"
|
4
|
+
require "action/core/flow/handlers/invoker"
|
5
|
+
|
6
|
+
module Action
|
7
|
+
module Core
|
8
|
+
module Flow
|
9
|
+
module Handlers
|
10
|
+
class CallbackHandler < BaseHandler
|
11
|
+
def apply(action:, exception:)
|
12
|
+
return false unless matches?(action:, exception:)
|
13
|
+
|
14
|
+
Invoker.call(action:, handler:, exception:, operation: "executing handler")
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module Flow
|
6
|
+
module Handlers
|
7
|
+
# Shared block evaluation with consistent arity handling and error piping
|
8
|
+
module Invoker
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def call(action:, handler:, exception: nil, operation: "executing handler")
|
12
|
+
return call_symbol_handler(action:, symbol: handler, exception:) if symbol?(handler)
|
13
|
+
return call_callable_handler(action:, callable: handler, exception:) if callable?(handler)
|
14
|
+
|
15
|
+
literal_value(handler)
|
16
|
+
rescue StandardError => e
|
17
|
+
Axn::Util.piping_error(operation, action:, exception: e)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Shared introspection helpers
|
21
|
+
def accepts_exception_keyword?(callable_or_method)
|
22
|
+
return false unless callable_or_method.respond_to?(:parameters)
|
23
|
+
|
24
|
+
params = callable_or_method.parameters
|
25
|
+
params.any? { |type, name| %i[keyreq key].include?(type) && name == :exception } ||
|
26
|
+
params.any? { |type, _| type == :keyrest }
|
27
|
+
end
|
28
|
+
|
29
|
+
def accepts_positional_exception?(callable_or_method)
|
30
|
+
return false unless callable_or_method.respond_to?(:arity)
|
31
|
+
|
32
|
+
arity = callable_or_method.arity
|
33
|
+
arity == 1 || arity.negative?
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def symbol?(value) = value.is_a?(Symbol)
|
39
|
+
|
40
|
+
def callable?(value) = value.respond_to?(:arity)
|
41
|
+
|
42
|
+
def call_symbol_handler(action:, symbol:, exception: nil)
|
43
|
+
unless action.respond_to?(symbol)
|
44
|
+
action.warn("Ignoring apparently-invalid symbol #{symbol.inspect} -- action does not respond to method")
|
45
|
+
return nil
|
46
|
+
end
|
47
|
+
|
48
|
+
method = action.method(symbol)
|
49
|
+
if exception && accepts_exception_keyword?(method)
|
50
|
+
action.public_send(symbol, exception:)
|
51
|
+
elsif exception && accepts_positional_exception?(method)
|
52
|
+
action.public_send(symbol, exception)
|
53
|
+
else
|
54
|
+
action.public_send(symbol)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def call_callable_handler(action:, callable:, exception: nil)
|
59
|
+
if exception && accepts_exception_keyword?(callable)
|
60
|
+
action.instance_exec(exception:, &callable)
|
61
|
+
elsif exception && accepts_positional_exception?(callable)
|
62
|
+
action.instance_exec(exception, &callable)
|
63
|
+
else
|
64
|
+
action.instance_exec(&callable)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def literal_value(value) = value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/handlers/invoker"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
module Core
|
7
|
+
module Flow
|
8
|
+
module Handlers
|
9
|
+
class Matcher
|
10
|
+
def initialize(rule, invert: false)
|
11
|
+
@rule = rule
|
12
|
+
@invert = invert
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(exception:, action:)
|
16
|
+
@invert ? !matches?(exception:, action:) : matches?(exception:, action:)
|
17
|
+
rescue StandardError => e
|
18
|
+
Axn::Util.piping_error("determining if handler applies to exception", action:, exception: e)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def matches?(exception:, action:)
|
24
|
+
return apply_callable(action:, exception:) if callable?
|
25
|
+
return apply_symbol(action:, exception:) if symbol?
|
26
|
+
return apply_string(exception:) if string?
|
27
|
+
return apply_exception_class(exception:) if exception_class?
|
28
|
+
|
29
|
+
handle_invalid(action:)
|
30
|
+
end
|
31
|
+
|
32
|
+
def callable? = @rule.respond_to?(:call)
|
33
|
+
def symbol? = @rule.is_a?(Symbol)
|
34
|
+
def string? = @rule.is_a?(String)
|
35
|
+
def exception_class? = @rule.is_a?(Class) && @rule <= Exception
|
36
|
+
|
37
|
+
def apply_callable(action:, exception:)
|
38
|
+
if exception && Invoker.accepts_exception_keyword?(@rule)
|
39
|
+
!!action.instance_exec(exception:, &@rule)
|
40
|
+
elsif exception && Invoker.accepts_positional_exception?(@rule)
|
41
|
+
!!action.instance_exec(exception, &@rule)
|
42
|
+
else
|
43
|
+
!!action.instance_exec(&@rule)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def apply_symbol(action:, exception:)
|
48
|
+
if action.respond_to?(@rule)
|
49
|
+
method = action.method(@rule)
|
50
|
+
if exception && Invoker.accepts_exception_keyword?(method)
|
51
|
+
!!action.public_send(@rule, exception:)
|
52
|
+
elsif exception && Invoker.accepts_positional_exception?(method)
|
53
|
+
!!action.public_send(@rule, exception)
|
54
|
+
else
|
55
|
+
!!action.public_send(@rule)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
begin
|
59
|
+
klass = Object.const_get(@rule.to_s)
|
60
|
+
klass && exception.is_a?(klass)
|
61
|
+
rescue NameError
|
62
|
+
action.warn("Ignoring apparently-invalid matcher #{@rule.inspect} -- neither action method nor constant found")
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def apply_string(exception:)
|
69
|
+
klass = Object.const_get(@rule.to_s)
|
70
|
+
klass && exception.is_a?(klass)
|
71
|
+
end
|
72
|
+
|
73
|
+
def apply_exception_class(exception:)
|
74
|
+
exception.is_a?(@rule)
|
75
|
+
end
|
76
|
+
|
77
|
+
def handle_invalid(action:)
|
78
|
+
action.warn("Ignoring apparently-invalid matcher #{@rule.inspect} -- could not find way to apply it")
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/handlers/base_handler"
|
4
|
+
require "action/core/flow/handlers/invoker"
|
5
|
+
|
6
|
+
module Action
|
7
|
+
module Core
|
8
|
+
module Flow
|
9
|
+
module Handlers
|
10
|
+
class MessageHandler < BaseHandler
|
11
|
+
# Returns a string (truthy) when it applies and yields a non-blank message; otherwise nil
|
12
|
+
def apply(action:, exception:)
|
13
|
+
return nil unless matches?(action:, exception:)
|
14
|
+
|
15
|
+
value =
|
16
|
+
if handler.is_a?(Symbol) || handler.respond_to?(:call)
|
17
|
+
Invoker.call(action:, handler:, exception:, operation: "determining message callable")
|
18
|
+
else
|
19
|
+
handler
|
20
|
+
end
|
21
|
+
value.respond_to?(:presence) ? value.presence : value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module Flow
|
6
|
+
module Handlers
|
7
|
+
# Small, immutable, copy-on-write registry keyed by event_type.
|
8
|
+
# Stores arrays of entries (handlers/interceptors) in insertion order.
|
9
|
+
class Registry
|
10
|
+
def self.empty = new({})
|
11
|
+
|
12
|
+
def initialize(index)
|
13
|
+
# Freeze arrays and the index for immutability
|
14
|
+
@index = index.transform_values { |arr| Array(arr).freeze }.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
# Always register most-recent-first (last-defined wins). Simpler mental model.
|
18
|
+
def register(event_type:, entry:)
|
19
|
+
key = event_type.to_sym
|
20
|
+
existing = Array(@index[key])
|
21
|
+
updated = [entry] + existing
|
22
|
+
self.class.new(@index.merge(key => updated.freeze))
|
23
|
+
end
|
24
|
+
|
25
|
+
def for(event_type)
|
26
|
+
Array(@index[event_type.to_sym])
|
27
|
+
end
|
28
|
+
|
29
|
+
def empty?
|
30
|
+
@index.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
attr_reader :index
|
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 Flow
|
6
|
+
module Handlers
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
require "action/core/flow/handlers/invoker"
|
13
|
+
require "action/core/flow/handlers/registry"
|
14
|
+
require "action/core/flow/handlers/matcher"
|
15
|
+
require "action/core/flow/handlers/base_handler"
|
16
|
+
require "action/core/flow/handlers/callback_handler"
|
17
|
+
require "action/core/flow/handlers/message_handler"
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/handlers"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
module Core
|
7
|
+
module Flow
|
8
|
+
module Messages
|
9
|
+
def self.included(base)
|
10
|
+
base.class_eval do
|
11
|
+
class_attribute :_messages_registry, default: Action::Core::Flow::Handlers::Registry.empty
|
12
|
+
|
13
|
+
extend ClassMethods
|
14
|
+
include InstanceMethods
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
# Internal: resolve a message for the given event (conditional first, then static)
|
20
|
+
def _message_for(event_type, action:, exception: nil)
|
21
|
+
_conditional_message_for(event_type, action:, exception:) ||
|
22
|
+
_static_message_for(event_type, action:, exception:)
|
23
|
+
end
|
24
|
+
|
25
|
+
def _conditional_message_for(event_type, action:, exception: nil)
|
26
|
+
_messages_registry.for(event_type).each do |handler|
|
27
|
+
next if handler.respond_to?(:static?) && handler.static?
|
28
|
+
|
29
|
+
msg = handler.apply(action:, exception:)
|
30
|
+
return msg if msg.present?
|
31
|
+
end
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def _static_message_for(event_type, action:, exception: nil)
|
36
|
+
_messages_registry.for(event_type).each do |handler|
|
37
|
+
next unless handler.respond_to?(:static?) && handler.static?
|
38
|
+
|
39
|
+
msg = handler.apply(action:, exception:)
|
40
|
+
return msg if msg.present?
|
41
|
+
end
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def success(message = nil, **, &) = _add_message(:success, message:, **, &)
|
46
|
+
def error(message = nil, **, &) = _add_message(:error, message:, **, &)
|
47
|
+
|
48
|
+
def default_error = new.internal_context.default_error
|
49
|
+
def default_success = new.internal_context.default_success
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def _add_message(kind, message:, **kwargs, &block)
|
54
|
+
raise ArgumentError, "#{kind} cannot be called with both :if and :unless" if kwargs.key?(:if) && kwargs.key?(:unless)
|
55
|
+
|
56
|
+
condition = kwargs.key?(:if) ? kwargs[:if] : kwargs[:unless]
|
57
|
+
raise ArgumentError, "Provide either a message or a block, not both" if message && block_given?
|
58
|
+
raise ArgumentError, "Provide a message or a block" unless message || block_given?
|
59
|
+
|
60
|
+
handler = block_given? ? block : message
|
61
|
+
|
62
|
+
matcher = condition.nil? ? nil : Action::Core::Flow::Handlers::Matcher.new(condition, invert: kwargs.key?(:unless))
|
63
|
+
entry = Action::Core::Flow::Handlers::MessageHandler.new(matcher:, handler:)
|
64
|
+
self._messages_registry = _messages_registry.register(event_type: kind, entry:)
|
65
|
+
true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module InstanceMethods
|
70
|
+
delegate :default_error, :default_success, to: :internal_context
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|