axn 0.1.0.pre.alpha.2.7.1 → 0.1.0.pre.alpha.2.8
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 +11 -5
- data/CHANGELOG.md +10 -0
- data/Rakefile +12 -0
- data/docs/intro/about.md +2 -2
- data/docs/intro/overview.md +18 -0
- data/docs/recipes/rubocop-integration.md +352 -0
- data/docs/reference/action-result.md +1 -1
- data/docs/reference/class.md +110 -2
- data/docs/reference/configuration.md +5 -3
- data/docs/reference/instance.md +0 -52
- data/docs/usage/setup.md +4 -0
- data/docs/usage/steps.md +335 -0
- data/docs/usage/writing.md +67 -0
- data/lib/action/attachable/steps.rb +18 -17
- data/lib/action/attachable/subactions.rb +1 -1
- data/lib/action/context.rb +10 -14
- data/lib/action/core/context/facade.rb +11 -2
- data/lib/action/core/context/facade_inspector.rb +3 -2
- data/lib/action/core/context/internal.rb +3 -11
- data/lib/action/core/contract_validation.rb +1 -1
- data/lib/action/core/flow/callbacks.rb +22 -8
- data/lib/action/core/flow/exception_execution.rb +2 -5
- data/lib/action/core/flow/handlers/{base_handler.rb → base_descriptor.rb} +7 -4
- data/lib/action/core/flow/handlers/descriptors/callback_descriptor.rb +17 -0
- data/lib/action/core/flow/handlers/descriptors/message_descriptor.rb +53 -0
- data/lib/action/core/flow/handlers/matcher.rb +41 -2
- data/lib/action/core/flow/handlers/resolvers/base_resolver.rb +28 -0
- data/lib/action/core/flow/handlers/resolvers/callback_resolver.rb +29 -0
- data/lib/action/core/flow/handlers/resolvers/message_resolver.rb +59 -0
- data/lib/action/core/flow/handlers.rb +7 -4
- data/lib/action/core/flow/messages.rb +15 -41
- data/lib/action/core/nesting_tracking.rb +31 -0
- data/lib/action/core/timing.rb +1 -1
- data/lib/action/core.rb +20 -12
- data/lib/action/exceptions.rb +20 -2
- data/lib/action/result.rb +30 -32
- data/lib/axn/factory.rb +22 -23
- data/lib/axn/rubocop.rb +10 -0
- data/lib/axn/version.rb +1 -1
- data/lib/rubocop/cop/axn/README.md +237 -0
- data/lib/rubocop/cop/axn/unchecked_result.rb +327 -0
- metadata +14 -6
- data/lib/action/core/flow/handlers/callback_handler.rb +0 -21
- data/lib/action/core/flow/handlers/message_handler.rb +0 -27
- data/lib/action/core/hoist_errors.rb +0 -58
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "action/core/flow/handlers"
|
4
|
+
require "action/core/flow/handlers/resolvers/callback_resolver"
|
4
5
|
|
5
6
|
module Action
|
6
7
|
module Core
|
@@ -17,9 +18,13 @@ module Action
|
|
17
18
|
module ClassMethods
|
18
19
|
# Internal dispatcher
|
19
20
|
def _dispatch_callbacks(event_type, action:, exception: nil)
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
resolver = Action::Core::Flow::Handlers::Resolvers::CallbackResolver.new(
|
22
|
+
_callbacks_registry,
|
23
|
+
event_type,
|
24
|
+
action:,
|
25
|
+
exception:,
|
26
|
+
)
|
27
|
+
resolver.execute_callbacks
|
23
28
|
end
|
24
29
|
|
25
30
|
# ONLY raised exceptions (i.e. NOT fail!).
|
@@ -39,14 +44,23 @@ module Action
|
|
39
44
|
|
40
45
|
def _add_callback(event_type, handler: nil, block: nil, **kwargs)
|
41
46
|
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]
|
47
|
+
raise ArgumentError, "on_#{event_type} cannot be called with both a block and a handler" if block && handler
|
44
48
|
raise ArgumentError, "on_#{event_type} must be called with a block or symbol" unless block || handler
|
45
49
|
|
46
|
-
|
47
|
-
|
48
|
-
|
50
|
+
# If handler is already a descriptor, use it directly
|
51
|
+
entry = if handler.is_a?(Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
|
52
|
+
raise ArgumentError, "Cannot pass additional configuration with prebuilt descriptor" if kwargs.any? || block
|
53
|
+
|
54
|
+
handler
|
55
|
+
else
|
56
|
+
Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor.build(
|
57
|
+
handler: handler || block,
|
58
|
+
**kwargs,
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
49
62
|
self._callbacks_registry = _callbacks_registry.register(event_type:, entry:)
|
63
|
+
true
|
50
64
|
end
|
51
65
|
end
|
52
66
|
end
|
@@ -33,6 +33,8 @@ module Action
|
|
33
33
|
def _with_exception_handling
|
34
34
|
yield
|
35
35
|
rescue StandardError => e
|
36
|
+
@__context.__record_exception(e)
|
37
|
+
|
36
38
|
# on_error handlers run for both unhandled exceptions and fail!
|
37
39
|
self.class._dispatch_callbacks(:error, action: self, exception: e)
|
38
40
|
|
@@ -42,12 +44,7 @@ module Action
|
|
42
44
|
else
|
43
45
|
# on_exception handlers run for ONLY for unhandled exceptions.
|
44
46
|
_trigger_on_exception(e)
|
45
|
-
|
46
|
-
@__context.exception = e
|
47
47
|
end
|
48
|
-
|
49
|
-
# Set failure state using accessor method
|
50
|
-
@__context.send(:failure=, true)
|
51
48
|
end
|
52
49
|
|
53
50
|
def try
|
@@ -8,15 +8,15 @@ module Action
|
|
8
8
|
# "Handlers" doesn't feel like *quite* the right name for this, but basically things in this namespace
|
9
9
|
# relate to conditionally-invoked code blocks (e.g. callbacks, messages, etc.)
|
10
10
|
module Handlers
|
11
|
-
class
|
11
|
+
class BaseDescriptor
|
12
12
|
def initialize(matcher: nil, handler: nil)
|
13
13
|
@matcher = matcher
|
14
14
|
@handler = handler
|
15
15
|
end
|
16
16
|
|
17
|
-
attr_reader :handler
|
17
|
+
attr_reader :handler, :matcher
|
18
18
|
|
19
|
-
def static? = @matcher.nil?
|
19
|
+
def static? = @matcher.nil? || @matcher.static?
|
20
20
|
|
21
21
|
def matches?(action:, exception:)
|
22
22
|
return true if static?
|
@@ -24,7 +24,10 @@ module Action
|
|
24
24
|
@matcher.call(exception:, action:)
|
25
25
|
end
|
26
26
|
|
27
|
-
|
27
|
+
def self.build(handler: nil, if: nil, unless: nil, **)
|
28
|
+
matcher = Matcher.build(if:, unless:)
|
29
|
+
new(matcher:, handler:)
|
30
|
+
end
|
28
31
|
end
|
29
32
|
end
|
30
33
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/handlers/base_descriptor"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
module Core
|
7
|
+
module Flow
|
8
|
+
module Handlers
|
9
|
+
module Descriptors
|
10
|
+
# Data structure for callback configuration - no behavior, just data
|
11
|
+
class CallbackDescriptor < BaseDescriptor
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action/core/flow/handlers/base_descriptor"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
module Core
|
7
|
+
module Flow
|
8
|
+
module Handlers
|
9
|
+
module Descriptors
|
10
|
+
# Data structure for message configuration - no behavior, just data
|
11
|
+
class MessageDescriptor < BaseDescriptor
|
12
|
+
attr_reader :prefix
|
13
|
+
|
14
|
+
def initialize(matcher:, handler:, prefix: nil)
|
15
|
+
super(matcher:, handler:)
|
16
|
+
@prefix = prefix
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.build(handler: nil, if: nil, unless: nil, prefix: nil, from: nil, **)
|
20
|
+
new(
|
21
|
+
handler:,
|
22
|
+
prefix:,
|
23
|
+
matcher: _build_matcher(if:, unless:, from:),
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self._build_matcher(if:, unless:, from:)
|
28
|
+
rules = [
|
29
|
+
binding.local_variable_get(:if),
|
30
|
+
binding.local_variable_get(:unless),
|
31
|
+
_build_rule_for_from_condition(from),
|
32
|
+
].compact
|
33
|
+
|
34
|
+
Action::Core::Flow::Handlers::Matcher.new(rules, invert: !!binding.local_variable_get(:unless))
|
35
|
+
end
|
36
|
+
|
37
|
+
def self._build_rule_for_from_condition(from_class)
|
38
|
+
return nil unless from_class
|
39
|
+
|
40
|
+
if from_class.is_a?(String)
|
41
|
+
lambda { |exception:, **|
|
42
|
+
exception.is_a?(Action::Failure) && exception.source&.class&.name == from_class
|
43
|
+
}
|
44
|
+
else
|
45
|
+
->(exception:, **) { exception.is_a?(Action::Failure) && exception.source.is_a?(from_class) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -6,14 +6,15 @@ module Action
|
|
6
6
|
module Core
|
7
7
|
module Flow
|
8
8
|
module Handlers
|
9
|
-
class
|
9
|
+
class SingleRuleMatcher
|
10
10
|
def initialize(rule, invert: false)
|
11
11
|
@rule = rule
|
12
12
|
@invert = invert
|
13
13
|
end
|
14
14
|
|
15
15
|
def call(exception:, action:)
|
16
|
-
|
16
|
+
result = matches?(exception:, action:)
|
17
|
+
@invert ? !result : result
|
17
18
|
rescue StandardError => e
|
18
19
|
Axn::Util.piping_error("determining if handler applies to exception", action:, exception: e)
|
19
20
|
end
|
@@ -79,6 +80,44 @@ module Action
|
|
79
80
|
false
|
80
81
|
end
|
81
82
|
end
|
83
|
+
|
84
|
+
class Matcher
|
85
|
+
# NOTE: invert means it's an unless rather than an if. This will apply to ALL rules (sufficient for current use case,
|
86
|
+
# but flagging if we ever extend this for complex matching)
|
87
|
+
def initialize(rules, invert: false)
|
88
|
+
@rules = Array(rules).compact
|
89
|
+
@invert = invert
|
90
|
+
end
|
91
|
+
|
92
|
+
def call(exception:, action:)
|
93
|
+
matches?(exception:, action:)
|
94
|
+
rescue StandardError => e
|
95
|
+
Axn::Util.piping_error("determining if handler applies to exception", action:, exception: e)
|
96
|
+
end
|
97
|
+
|
98
|
+
def static? = @rules.empty?
|
99
|
+
def invert? = !!@invert
|
100
|
+
|
101
|
+
# Class method to build matcher from kwargs
|
102
|
+
def self.build(if: nil, unless: nil)
|
103
|
+
if_condition = binding.local_variable_get(:if)
|
104
|
+
unless_condition = binding.local_variable_get(:unless)
|
105
|
+
|
106
|
+
raise Action::UnsupportedArgument, "providing both :if and :unless" if if_condition && unless_condition
|
107
|
+
|
108
|
+
new(Array(if_condition || unless_condition).compact, invert: !!unless_condition)
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def matches?(exception:, action:)
|
114
|
+
return true if @rules.empty?
|
115
|
+
|
116
|
+
@rules.all? do |rule|
|
117
|
+
SingleRuleMatcher.new(rule, invert: @invert).call(exception:, action:)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
82
121
|
end
|
83
122
|
end
|
84
123
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module Flow
|
6
|
+
module Handlers
|
7
|
+
module Resolvers
|
8
|
+
class BaseResolver
|
9
|
+
def initialize(registry, event_type, action:, exception:)
|
10
|
+
@registry = registry
|
11
|
+
@event_type = event_type
|
12
|
+
@action = action
|
13
|
+
@exception = exception
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
attr_reader :registry, :event_type, :action, :exception
|
19
|
+
|
20
|
+
def candidate_entries = registry.for(event_type)
|
21
|
+
def matching_entries = candidate_entries.select { |descriptor| descriptor.matches?(action:, exception:) }
|
22
|
+
def static_entries = candidate_entries.select(&:static?)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
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
|
+
module Resolvers
|
10
|
+
# Internal: resolves and executes callbacks
|
11
|
+
class CallbackResolver < BaseResolver
|
12
|
+
def execute_callbacks
|
13
|
+
matching_entries.each do |descriptor|
|
14
|
+
execute_callback(descriptor)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Executes a specific callback descriptor
|
21
|
+
def execute_callback(descriptor)
|
22
|
+
Invoker.call(operation: "executing callback", action:, handler: descriptor.handler, exception:)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,59 @@
|
|
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
|
+
module Resolvers
|
10
|
+
# Internal: resolves messages with different strategies
|
11
|
+
class MessageResolver < BaseResolver
|
12
|
+
DEFAULT_ERROR = "Something went wrong"
|
13
|
+
DEFAULT_SUCCESS = "Action completed successfully"
|
14
|
+
|
15
|
+
def resolve_message
|
16
|
+
descriptor = matching_entries.detect { |d| message_from(d) }
|
17
|
+
message_from(descriptor) || fallback_message
|
18
|
+
end
|
19
|
+
|
20
|
+
def resolve_default_message
|
21
|
+
message_from(default_descriptor) || fallback_message
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def default_descriptor
|
27
|
+
# NOTE: descriptor.handler check avoids returning a prefix-only descriptor (which
|
28
|
+
# needs to look up a default handler via this method to return a message)
|
29
|
+
static_entries.detect { |descriptor| descriptor.handler && message_from(descriptor) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def message_from(descriptor)
|
33
|
+
message = resolved_message_body(descriptor)
|
34
|
+
return nil unless message.present?
|
35
|
+
|
36
|
+
descriptor.prefix ? "#{descriptor.prefix}#{message}" : message
|
37
|
+
end
|
38
|
+
|
39
|
+
def resolved_message_body(descriptor)
|
40
|
+
return nil unless descriptor
|
41
|
+
|
42
|
+
if descriptor.handler
|
43
|
+
invoke_handler(descriptor.handler)
|
44
|
+
elsif exception
|
45
|
+
exception.message
|
46
|
+
elsif descriptor.prefix
|
47
|
+
# For prefix-only success messages, find a default message from other descriptors
|
48
|
+
invoke_handler(default_descriptor&.handler)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def invoke_handler(handler) = handler ? Invoker.call(operation: "determining message callable", action:, handler:, exception:).presence : nil
|
53
|
+
def fallback_message = event_type == :success ? DEFAULT_SUCCESS : DEFAULT_ERROR
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -9,9 +9,12 @@ module Action
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
+
require "action/core/flow/handlers/base_descriptor"
|
13
|
+
require "action/core/flow/handlers/matcher"
|
14
|
+
require "action/core/flow/handlers/resolvers/base_resolver"
|
15
|
+
require "action/core/flow/handlers/descriptors/message_descriptor"
|
16
|
+
require "action/core/flow/handlers/descriptors/callback_descriptor"
|
12
17
|
require "action/core/flow/handlers/invoker"
|
18
|
+
require "action/core/flow/handlers/resolvers/callback_resolver"
|
13
19
|
require "action/core/flow/handlers/registry"
|
14
|
-
require "action/core/flow/handlers/
|
15
|
-
require "action/core/flow/handlers/base_handler"
|
16
|
-
require "action/core/flow/handlers/callback_handler"
|
17
|
-
require "action/core/flow/handlers/message_handler"
|
20
|
+
require "action/core/flow/handlers/resolvers/message_resolver"
|
@@ -11,64 +11,38 @@ module Action
|
|
11
11
|
class_attribute :_messages_registry, default: Action::Core::Flow::Handlers::Registry.empty
|
12
12
|
|
13
13
|
extend ClassMethods
|
14
|
-
include InstanceMethods
|
15
14
|
end
|
16
15
|
end
|
17
16
|
|
18
17
|
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
18
|
def success(message = nil, **, &) = _add_message(:success, message:, **, &)
|
46
19
|
def error(message = nil, **, &) = _add_message(:error, message:, **, &)
|
47
20
|
|
48
|
-
def default_error = new.internal_context.default_error
|
49
|
-
def default_success = new.internal_context.default_success
|
50
|
-
|
51
21
|
private
|
52
22
|
|
53
23
|
def _add_message(kind, message:, **kwargs, &block)
|
54
|
-
raise
|
55
|
-
|
56
|
-
condition = kwargs.key?(:if) ? kwargs[:if] : kwargs[:unless]
|
24
|
+
raise Action::UnsupportedArgument, "calling #{kind} with both :if and :unless" if kwargs.key?(:if) && kwargs.key?(:unless)
|
25
|
+
raise Action::UnsupportedArgument, "Combining from: with if: or unless:" if kwargs.key?(:from) && (kwargs.key?(:if) || kwargs.key?(:unless))
|
57
26
|
raise ArgumentError, "Provide either a message or a block, not both" if message && block_given?
|
58
|
-
raise ArgumentError, "Provide a message or
|
27
|
+
raise ArgumentError, "Provide a message, block, or prefix" unless message || block_given? || kwargs[:prefix]
|
28
|
+
raise ArgumentError, "from: only applies to error messages" if kwargs.key?(:from) && kind != :error
|
59
29
|
|
60
|
-
|
30
|
+
# If message is already a descriptor, use it directly
|
31
|
+
entry = if message.is_a?(Action::Core::Flow::Handlers::Descriptors::MessageDescriptor)
|
32
|
+
raise ArgumentError, "Cannot pass additional configuration with prebuilt descriptor" if kwargs.any? || block_given?
|
33
|
+
|
34
|
+
message
|
35
|
+
else
|
36
|
+
Action::Core::Flow::Handlers::Descriptors::MessageDescriptor.build(
|
37
|
+
handler: block_given? ? block : message,
|
38
|
+
**kwargs,
|
39
|
+
)
|
40
|
+
end
|
61
41
|
|
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
42
|
self._messages_registry = _messages_registry.register(event_type: kind, entry:)
|
65
43
|
true
|
66
44
|
end
|
67
45
|
end
|
68
|
-
|
69
|
-
module InstanceMethods
|
70
|
-
delegate :default_error, :default_success, to: :internal_context
|
71
|
-
end
|
72
46
|
end
|
73
47
|
end
|
74
48
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
module Core
|
5
|
+
module NestingTracking
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
extend ClassMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def _nested_in_another_axn?
|
14
|
+
NestingTracking._current_axn_stack.any?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def _tracking_nesting(axn)
|
19
|
+
NestingTracking._current_axn_stack.push(axn)
|
20
|
+
yield
|
21
|
+
ensure
|
22
|
+
NestingTracking._current_axn_stack.pop
|
23
|
+
end
|
24
|
+
|
25
|
+
# Shared method for both class and instance access
|
26
|
+
def self._current_axn_stack
|
27
|
+
ActiveSupport::IsolatedExecutionState[:_axn_stack] ||= []
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/action/core/timing.rb
CHANGED
data/lib/action/core.rb
CHANGED
@@ -5,11 +5,12 @@ require "action/context"
|
|
5
5
|
require "action/strategies"
|
6
6
|
require "action/core/hooks"
|
7
7
|
require "action/core/logging"
|
8
|
-
require "action/core/hoist_errors"
|
9
8
|
require "action/core/flow"
|
10
9
|
require "action/core/automatic_logging"
|
11
10
|
require "action/core/use_strategy"
|
11
|
+
require "action/core/timing"
|
12
12
|
require "action/core/tracing"
|
13
|
+
require "action/core/nesting_tracking"
|
13
14
|
|
14
15
|
# CONSIDER: make class names match file paths?
|
15
16
|
require "action/core/validation/validators/model_validator"
|
@@ -19,7 +20,6 @@ require "action/core/validation/validators/validate_validator"
|
|
19
20
|
require "action/core/contract_validation"
|
20
21
|
require "action/core/contract"
|
21
22
|
require "action/core/contract_for_subfields"
|
22
|
-
require "action/core/timing"
|
23
23
|
|
24
24
|
module Action
|
25
25
|
module Core
|
@@ -37,8 +37,8 @@ module Action
|
|
37
37
|
include Core::ContractValidation
|
38
38
|
include Core::Contract
|
39
39
|
include Core::ContractForSubfields
|
40
|
+
include Core::NestingTracking
|
40
41
|
|
41
|
-
include Core::HoistErrors
|
42
42
|
include Core::UseStrategy
|
43
43
|
end
|
44
44
|
end
|
@@ -52,7 +52,11 @@ module Action
|
|
52
52
|
result = call(**)
|
53
53
|
return result if result.ok?
|
54
54
|
|
55
|
-
raise
|
55
|
+
# When we're nested, we want to raise a failure that includes the source action to support
|
56
|
+
# the error message generation's `from` filter
|
57
|
+
raise Action::Failure.new(result.error, source: result.__action__), cause: result.exception if _nested_in_another_axn?
|
58
|
+
|
59
|
+
raise result.exception
|
56
60
|
end
|
57
61
|
end
|
58
62
|
|
@@ -62,13 +66,15 @@ module Action
|
|
62
66
|
|
63
67
|
# Main entry point for action execution
|
64
68
|
def _run
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
69
|
+
_tracking_nesting(self) do
|
70
|
+
_with_tracing do
|
71
|
+
_with_logging do
|
72
|
+
_with_timing do
|
73
|
+
_with_exception_handling do # Exceptions stop here; outer wrappers access result status (and must not introduce another exception layer)
|
74
|
+
_with_contract do # Library internals -- any failures (e.g. contract violations) *should* fail the Action::Result
|
75
|
+
_with_hooks do # User hooks -- any failures here *should* fail the Action::Result
|
76
|
+
call
|
77
|
+
end
|
72
78
|
end
|
73
79
|
end
|
74
80
|
end
|
@@ -82,7 +88,9 @@ module Action
|
|
82
88
|
# User-defined action logic - override this method in your action classes
|
83
89
|
def call; end
|
84
90
|
|
85
|
-
|
91
|
+
def fail!(message = nil)
|
92
|
+
raise Action::Failure, message
|
93
|
+
end
|
86
94
|
|
87
95
|
private
|
88
96
|
|
data/lib/action/exceptions.rb
CHANGED
@@ -5,15 +5,20 @@ module Action
|
|
5
5
|
class Failure < StandardError
|
6
6
|
DEFAULT_MESSAGE = "Execution was halted"
|
7
7
|
|
8
|
-
|
8
|
+
attr_reader :source
|
9
|
+
|
10
|
+
def initialize(message = nil, source: nil)
|
11
|
+
@source = source
|
9
12
|
@message = message
|
10
|
-
super(
|
13
|
+
super(message)
|
11
14
|
end
|
12
15
|
|
13
16
|
def message
|
14
17
|
@message.presence || DEFAULT_MESSAGE
|
15
18
|
end
|
16
19
|
|
20
|
+
def default_message? = message == DEFAULT_MESSAGE
|
21
|
+
|
17
22
|
def inspect = "#<#{self.class.name} '#{message}'>"
|
18
23
|
end
|
19
24
|
|
@@ -56,4 +61,17 @@ module Action
|
|
56
61
|
|
57
62
|
class InboundValidationError < ValidationError; end
|
58
63
|
class OutboundValidationError < ValidationError; end
|
64
|
+
|
65
|
+
class UnsupportedArgument < ArgumentError
|
66
|
+
def initialize(feature)
|
67
|
+
@feature = feature
|
68
|
+
super()
|
69
|
+
end
|
70
|
+
|
71
|
+
def message
|
72
|
+
"#{@feature} is not currently supported.\n\n" \
|
73
|
+
"Implementation is technically possible but very complex. " \
|
74
|
+
"Please submit a Github Issue if you have a real-world need for this functionality."
|
75
|
+
end
|
76
|
+
end
|
59
77
|
end
|