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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -5
  3. data/CHANGELOG.md +10 -0
  4. data/Rakefile +12 -0
  5. data/docs/intro/about.md +2 -2
  6. data/docs/intro/overview.md +18 -0
  7. data/docs/recipes/rubocop-integration.md +352 -0
  8. data/docs/reference/action-result.md +1 -1
  9. data/docs/reference/class.md +110 -2
  10. data/docs/reference/configuration.md +5 -3
  11. data/docs/reference/instance.md +0 -52
  12. data/docs/usage/setup.md +4 -0
  13. data/docs/usage/steps.md +335 -0
  14. data/docs/usage/writing.md +67 -0
  15. data/lib/action/attachable/steps.rb +18 -17
  16. data/lib/action/attachable/subactions.rb +1 -1
  17. data/lib/action/context.rb +10 -14
  18. data/lib/action/core/context/facade.rb +11 -2
  19. data/lib/action/core/context/facade_inspector.rb +3 -2
  20. data/lib/action/core/context/internal.rb +3 -11
  21. data/lib/action/core/contract_validation.rb +1 -1
  22. data/lib/action/core/flow/callbacks.rb +22 -8
  23. data/lib/action/core/flow/exception_execution.rb +2 -5
  24. data/lib/action/core/flow/handlers/{base_handler.rb → base_descriptor.rb} +7 -4
  25. data/lib/action/core/flow/handlers/descriptors/callback_descriptor.rb +17 -0
  26. data/lib/action/core/flow/handlers/descriptors/message_descriptor.rb +53 -0
  27. data/lib/action/core/flow/handlers/matcher.rb +41 -2
  28. data/lib/action/core/flow/handlers/resolvers/base_resolver.rb +28 -0
  29. data/lib/action/core/flow/handlers/resolvers/callback_resolver.rb +29 -0
  30. data/lib/action/core/flow/handlers/resolvers/message_resolver.rb +59 -0
  31. data/lib/action/core/flow/handlers.rb +7 -4
  32. data/lib/action/core/flow/messages.rb +15 -41
  33. data/lib/action/core/nesting_tracking.rb +31 -0
  34. data/lib/action/core/timing.rb +1 -1
  35. data/lib/action/core.rb +20 -12
  36. data/lib/action/exceptions.rb +20 -2
  37. data/lib/action/result.rb +30 -32
  38. data/lib/axn/factory.rb +22 -23
  39. data/lib/axn/rubocop.rb +10 -0
  40. data/lib/axn/version.rb +1 -1
  41. data/lib/rubocop/cop/axn/README.md +237 -0
  42. data/lib/rubocop/cop/axn/unchecked_result.rb +327 -0
  43. metadata +14 -6
  44. data/lib/action/core/flow/handlers/callback_handler.rb +0 -21
  45. data/lib/action/core/flow/handlers/message_handler.rb +0 -27
  46. 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
- _callbacks_registry.for(event_type).each do |handler|
21
- handler.apply(action:, exception:)
22
- end
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
- callback_handler = block || handler
47
- matcher = condition.nil? ? nil : Action::Core::Flow::Handlers::Matcher.new(condition, invert: kwargs.key?(:unless))
48
- entry = Action::Core::Flow::Handlers::CallbackHandler.new(matcher:, handler: callback_handler)
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 BaseHandler
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
- # Subclasses should implement `apply(action:, exception:)`
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 Matcher
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
- @invert ? !matches?(exception:, action:) : matches?(exception:, action:)
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/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"
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 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]
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 a block" unless message || block_given?
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
- handler = block_given? ? block : message
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
@@ -32,7 +32,7 @@ module Action
32
32
  yield
33
33
  ensure
34
34
  elapsed_mils = Core::Timing.elapsed_ms(timing_start)
35
- @__context.elapsed_time = elapsed_mils
35
+ @__context.send(:elapsed_time=, elapsed_mils)
36
36
  end
37
37
  end
38
38
  end
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 result.exception || Action::Failure.new(result.error)
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
- _with_tracing do
66
- _with_logging do
67
- _with_timing do
68
- _with_exception_handling do # Exceptions stop here; outer wrappers access result status (and must not introduce another exception layer)
69
- _with_contract do # Library internals -- any failures (e.g. contract violations) *should* fail the Action::Result
70
- _with_hooks do # User hooks -- any failures here *should* fail the Action::Result
71
- call
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
- delegate :fail!, to: :@__context
91
+ def fail!(message = nil)
92
+ raise Action::Failure, message
93
+ end
86
94
 
87
95
  private
88
96
 
@@ -5,15 +5,20 @@ module Action
5
5
  class Failure < StandardError
6
6
  DEFAULT_MESSAGE = "Execution was halted"
7
7
 
8
- def initialize(message = nil, **)
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