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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/CHANGELOG.md +23 -2
  4. data/docs/reference/action-result.md +2 -0
  5. data/docs/reference/class.md +140 -19
  6. data/docs/reference/configuration.md +42 -20
  7. data/docs/usage/writing.md +1 -1
  8. data/lib/action/attachable/steps.rb +16 -1
  9. data/lib/action/configuration.rb +2 -3
  10. data/lib/action/context.rb +28 -18
  11. data/lib/action/core/automatic_logging.rb +24 -8
  12. data/lib/action/core/context/facade.rb +39 -0
  13. data/lib/action/core/context/facade_inspector.rb +63 -0
  14. data/lib/action/core/context/internal.rb +38 -0
  15. data/lib/action/core/contract.rb +25 -8
  16. data/lib/action/core/contract_for_subfields.rb +1 -1
  17. data/lib/action/core/contract_validation.rb +15 -4
  18. data/lib/action/core/flow/callbacks.rb +54 -0
  19. data/lib/action/core/flow/exception_execution.rb +65 -0
  20. data/lib/action/core/flow/handlers/base_handler.rb +32 -0
  21. data/lib/action/core/flow/handlers/callback_handler.rb +21 -0
  22. data/lib/action/core/flow/handlers/invoker.rb +73 -0
  23. data/lib/action/core/flow/handlers/matcher.rb +85 -0
  24. data/lib/action/core/flow/handlers/message_handler.rb +27 -0
  25. data/lib/action/core/flow/handlers/registry.rb +40 -0
  26. data/lib/action/core/flow/handlers.rb +17 -0
  27. data/lib/action/core/flow/messages.rb +75 -0
  28. data/lib/action/core/flow.rb +19 -0
  29. data/lib/action/core/hoist_errors.rb +2 -2
  30. data/lib/action/core/hooks.rb +15 -15
  31. data/lib/action/core/logging.rb +2 -2
  32. data/lib/action/core/timing.rb +18 -0
  33. data/lib/action/core/tracing.rb +17 -0
  34. data/lib/action/core/validation/fields.rb +2 -0
  35. data/lib/action/core.rb +25 -78
  36. data/lib/action/enqueueable/via_sidekiq.rb +2 -2
  37. data/lib/action/result.rb +114 -0
  38. data/lib/axn/factory.rb +45 -7
  39. data/lib/axn/version.rb +1 -1
  40. metadata +18 -5
  41. data/lib/action/core/context_facade.rb +0 -209
  42. data/lib/action/core/event_handlers.rb +0 -62
  43. data/lib/action/core/handle_exceptions.rb +0 -143
@@ -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
@@ -46,8 +46,8 @@ module Action
46
46
 
47
47
  # Separate method to allow overriding in subclasses
48
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?
49
+ @__context.exception = result.exception if result.exception.present?
50
+ @__context.error_prefix = prefix if prefix.present?
51
51
 
52
52
  error = result.exception.is_a?(Action::Failure) ? result.exception.message : result.error
53
53
  fail! error
@@ -67,44 +67,44 @@ module Action
67
67
 
68
68
  private
69
69
 
70
- def with_hooks
71
- run_around_hooks do
72
- run_before_hooks
70
+ def _with_hooks
71
+ _run_around_hooks do
72
+ _run_before_hooks
73
73
  yield
74
- run_after_hooks
74
+ _run_after_hooks
75
75
  end
76
76
  end
77
77
 
78
78
  # Around hooks are reversed before injection to ensure parent hooks wrap
79
79
  # child hooks (parent outside, child inside).
80
- def run_around_hooks(&block)
80
+ def _run_around_hooks(&block)
81
81
  self.class.around_hooks.reverse.inject(block) do |chain, hook|
82
- proc { run_hook(hook, chain) }
82
+ proc { _run_hook(hook, chain) }
83
83
  end.call
84
84
  end
85
85
 
86
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)
87
+ def _run_before_hooks
88
+ _run_hooks(self.class.before_hooks)
89
89
  end
90
90
 
91
91
  # After hooks are reversed to ensure child hooks run before parent hooks
92
92
  # (specific cleanup first, then general).
93
- def run_after_hooks
94
- run_hooks(self.class.after_hooks.reverse)
93
+ def _run_after_hooks
94
+ _run_hooks(self.class.after_hooks.reverse)
95
95
  end
96
96
 
97
- # Internal: Run a collection of hooks. The "run_hooks" method is the common
97
+ # Internal: Run a collection of hooks. The "_run_hooks" method is the common
98
98
  # interface by which collections of either before or after hooks are run.
99
99
  #
100
100
  # hooks - An Array of Symbol and Procs.
101
101
  #
102
102
  # Returns nothing.
103
- def run_hooks(hooks)
104
- hooks.each { |hook| run_hook(hook) }
103
+ def _run_hooks(hooks)
104
+ hooks.each { |hook| _run_hook(hook) }
105
105
  end
106
106
 
107
- # Internal: Run an individual hook. The "run_hook" method is the common
107
+ # Internal: Run an individual hook. The "_run_hook" method is the common
108
108
  # interface by which an individual hook is run. If the given hook is a
109
109
  # symbol, the method is invoked whether public or private. If the hook is a
110
110
  # proc, the proc is evaluated in the context of the current instance.
@@ -115,7 +115,7 @@ module Action
115
115
  # Symbol method name.
116
116
  #
117
117
  # Returns nothing.
118
- def run_hook(hook, *)
118
+ def _run_hook(hook, *)
119
119
  hook.is_a?(Symbol) ? send(hook, *) : instance_exec(*, &hook)
120
120
  end
121
121
  end
@@ -15,9 +15,9 @@ module Action
15
15
  end
16
16
 
17
17
  module ClassMethods
18
- def default_log_level = Action.config.default_log_level
18
+ def log_level = Action.config.log_level
19
19
 
20
- def log(message, level: default_log_level, before: nil, after: nil)
20
+ def log(message, level: log_level, before: nil, after: nil)
21
21
  msg = [_log_prefix, message].compact_blank.join(" ")
22
22
  msg = [before, msg, after].compact_blank.join if before || after
23
23
 
@@ -3,6 +3,12 @@
3
3
  module Action
4
4
  module Core
5
5
  module Timing
6
+ def self.included(base)
7
+ base.class_eval do
8
+ include InstanceMethods
9
+ end
10
+ end
11
+
6
12
  # Get the current monotonic time
7
13
  def self.now
8
14
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -17,6 +23,18 @@ module Action
17
23
  def self.elapsed_seconds(start_time)
18
24
  (now - start_time).round(6)
19
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
20
38
  end
21
39
  end
22
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
@@ -15,6 +15,8 @@ module Action
15
15
  end
16
16
 
17
17
  def read_attribute_for_validation(attr)
18
+ # The context here is actually a facade (InternalContext or Result)
19
+ # which already handles reading from the correct data source
18
20
  @context.public_send(attr)
19
21
  end
20
22
 
data/lib/action/core.rb CHANGED
@@ -6,9 +6,10 @@ require "action/strategies"
6
6
  require "action/core/hooks"
7
7
  require "action/core/logging"
8
8
  require "action/core/hoist_errors"
9
- require "action/core/handle_exceptions"
9
+ require "action/core/flow"
10
10
  require "action/core/automatic_logging"
11
11
  require "action/core/use_strategy"
12
+ require "action/core/tracing"
12
13
 
13
14
  # CONSIDER: make class names match file paths?
14
15
  require "action/core/validation/validators/model_validator"
@@ -28,8 +29,10 @@ module Action
28
29
  include Core::Hooks
29
30
  include Core::Logging
30
31
  include Core::AutomaticLogging
32
+ include Core::Tracing
33
+ include Core::Timing
31
34
 
32
- include Core::HandleExceptions
35
+ include Core::Flow
33
36
 
34
37
  include Core::ContractValidation
35
38
  include Core::Contract
@@ -41,84 +44,32 @@ module Action
41
44
  end
42
45
 
43
46
  module ClassMethods
44
- def call(context = {})
45
- new(context).tap(&:run).result
47
+ def call(**)
48
+ new(**).tap(&:_run).result
46
49
  end
47
50
 
48
- def call!(context = {})
49
- result = call(context)
51
+ def call!(**)
52
+ result = call(**)
50
53
  return result if result.ok?
51
54
 
52
55
  raise result.exception || Action::Failure.new(result.error)
53
56
  end
54
57
  end
55
58
 
56
- def initialize(context = {})
57
- @context = Action::Context.build(context)
59
+ def initialize(**)
60
+ @__context = Action::Context.new(**)
58
61
  end
59
62
 
60
- def with_tracing(&)
61
- return yield unless Action.config.wrap_with_trace
62
-
63
- Action.config.wrap_with_trace.call(self.class.name || "AnonymousClass", &)
64
- rescue StandardError => e
65
- Axn::Util.piping_error("running trace hook", action: self, exception: e)
66
- end
67
-
68
- def with_logging
69
- timing_start = Core::Timing.now
70
- _log_before
71
- yield
72
- ensure
73
- _log_after(timing_start:, outcome: _determine_outcome)
74
- end
75
-
76
- def with_contract
77
- _apply_inbound_preprocessing!
78
- _apply_defaults!(:inbound)
79
- _validate_contract!(:inbound)
80
-
81
- yield
82
-
83
- _apply_defaults!(:outbound)
84
- _validate_contract!(:outbound)
85
-
86
- # TODO: improve location of this triggering
87
- trigger_on_success if respond_to?(:trigger_on_success)
88
- end
89
-
90
- def with_exception_swallowing
91
- yield
92
- rescue StandardError => e
93
- # on_error handlers run for both unhandled exceptions and fail!
94
- self.class._error_handlers.each do |handler|
95
- handler.execute_if_matches(exception: e, action: self)
96
- end
97
-
98
- # on_failure handlers run ONLY for fail!
99
- if e.is_a?(Action::Failure)
100
- @context.instance_variable_set("@error_from_user", e.message) if e.message.present?
101
-
102
- self.class._failure_handlers.each do |handler|
103
- handler.execute_if_matches(exception: e, action: self)
104
- end
105
- else
106
- # on_exception handlers run for ONLY for unhandled exceptions. AND NOTE: may be skipped if the exception is rescued via `rescues`.
107
- trigger_on_exception(e)
108
-
109
- @context.exception = e
110
- end
111
-
112
- @context.instance_variable_set("@failure", true)
113
- end
114
-
115
- def run
116
- with_tracing do
117
- with_logging do
118
- with_exception_swallowing do # Exceptions stop here; outer wrappers access result status (and must not introduce another exception layer)
119
- with_contract do # Library internals -- any failures (e.g. contract violations) *should* fail the Action::Result
120
- with_hooks do # User hooks -- any failures here *should* fail the Action::Result
121
- call
63
+ # Main entry point for action execution
64
+ 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
72
+ end
122
73
  end
123
74
  end
124
75
  end
@@ -128,8 +79,11 @@ module Action
128
79
  _emit_metrics
129
80
  end
130
81
 
82
+ # User-defined action logic - override this method in your action classes
131
83
  def call; end
132
84
 
85
+ delegate :fail!, to: :@__context
86
+
133
87
  private
134
88
 
135
89
  def _emit_metrics
@@ -137,17 +91,10 @@ module Action
137
91
 
138
92
  Action.config.emit_metrics.call(
139
93
  self.class.name || "AnonymousClass",
140
- _determine_outcome,
94
+ result,
141
95
  )
142
96
  rescue StandardError => e
143
97
  Axn::Util.piping_error("running metrics hook", action: self, exception: e)
144
98
  end
145
-
146
- def _determine_outcome
147
- return "exception" if @context.exception
148
- return "failure" if @context.failure?
149
-
150
- "success"
151
- end
152
99
  end
153
100
  end
@@ -18,9 +18,9 @@ module Action
18
18
  bang = args.size > 1 ? args.last : false
19
19
 
20
20
  if bang
21
- self.class.call!(context)
21
+ self.class.call!(**context)
22
22
  else
23
- self.class.call(context)
23
+ self.class.call(**context)
24
24
  end
25
25
  end
26
26
 
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action/core/context/facade"
4
+ require "action/core/context/facade_inspector"
5
+
6
+ module Action
7
+ # Outbound / External ContextFacade
8
+ class Result < ContextFacade
9
+ # For ease of mocking return results in tests
10
+ class << self
11
+ def ok(msg = nil, **exposures)
12
+ exposes = exposures.keys.to_h { |key| [key, { allow_blank: true }] }
13
+
14
+ Axn::Factory.build(exposes:, success: msg) do
15
+ exposures.each do |key, value|
16
+ expose(key, value)
17
+ end
18
+ end.call
19
+ end
20
+
21
+ def error(msg = nil, **exposures, &block)
22
+ exposes = exposures.keys.to_h { |key| [key, { allow_blank: true }] }
23
+
24
+ Axn::Factory.build(exposes:, error: msg) do
25
+ exposures.each do |key, value|
26
+ expose(key, value)
27
+ end
28
+ if block_given?
29
+ begin
30
+ block.call
31
+ rescue StandardError => e
32
+ # Set the exception directly without triggering on_exception handlers
33
+ @__context.exception = e
34
+ end
35
+ end
36
+ fail!
37
+ end.call
38
+ end
39
+ end
40
+
41
+ # Poke some holes for necessary internal control methods
42
+ delegate :each_pair, to: :context
43
+
44
+ # External interface
45
+ delegate :ok?, :exception, to: :context
46
+
47
+ def error
48
+ return if ok?
49
+
50
+ [@context.error_prefix, determine_error_message].compact.join(" ").squeeze(" ")
51
+ end
52
+
53
+ def success
54
+ return unless ok?
55
+
56
+ determine_success_message
57
+ end
58
+
59
+ def ok = success
60
+
61
+ def message = error || success
62
+
63
+ # Outcome constants for action execution results
64
+ OUTCOMES = [
65
+ OUTCOME_SUCCESS = :success,
66
+ OUTCOME_FAILURE = :failure,
67
+ OUTCOME_EXCEPTION = :exception,
68
+ ].freeze
69
+
70
+ def outcome
71
+ return OUTCOME_EXCEPTION if exception
72
+ return OUTCOME_FAILURE if @context.failed?
73
+
74
+ OUTCOME_SUCCESS
75
+ end
76
+
77
+ # Elapsed time in milliseconds
78
+ def elapsed_time
79
+ @context.elapsed_time
80
+ end
81
+
82
+ private
83
+
84
+ def context_data_source = @context.exposed_data
85
+
86
+ def determine_error_message
87
+ return @context.error_from_user if @context.error_from_user.present?
88
+
89
+ exception = @context.exception || Action::Failure.new
90
+ msg = action.class._message_for(:error, action:, exception:)
91
+ msg.presence || "Something went wrong"
92
+ end
93
+
94
+ def determine_success_message
95
+ msg = action.class._message_for(:success, action:, exception: nil)
96
+ msg.presence || "Action completed successfully"
97
+ end
98
+
99
+ def method_missing(method_name, ...) # rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)
100
+ if @context.__combined_data.key?(method_name.to_sym)
101
+ msg = <<~MSG
102
+ Method ##{method_name} is not available on Action::Result!
103
+
104
+ #{action_name} may be missing a line like:
105
+ exposes :#{method_name}
106
+ MSG
107
+
108
+ raise Action::ContractViolation::MethodNotAllowed, msg
109
+ end
110
+
111
+ super
112
+ end
113
+ end
114
+ end
data/lib/axn/factory.rb CHANGED
@@ -13,15 +13,23 @@ module Axn
13
13
  # Expose standard class-level options
14
14
  exposes: [],
15
15
  expects: [],
16
- messages: {},
17
- error_from: {},
18
- rescues: {},
16
+ success: nil,
17
+ error: nil,
19
18
 
20
19
  # Hooks
21
20
  before: nil,
22
21
  after: nil,
23
22
  around: nil,
24
23
 
24
+ # Callbacks
25
+ on_success: nil,
26
+ on_failure: nil,
27
+ on_error: nil,
28
+ on_exception: nil,
29
+
30
+ # Strategies
31
+ use: [],
32
+
25
33
  &block
26
34
  )
27
35
  args = block.parameters.each_with_object(_hash_with_default_array) { |(type, field), hash| hash[type] << field }
@@ -64,6 +72,17 @@ module Axn
64
72
  expose(expose_return_as => retval) if expose_return_as.present?
65
73
  end
66
74
  end.tap do |axn|
75
+ apply_message = lambda do |kind, value|
76
+ return unless value.present?
77
+
78
+ if value.is_a?(Array) && value.size == 2
79
+ matcher, msg = value
80
+ axn.public_send(kind, msg, if: matcher)
81
+ else
82
+ axn.public_send(kind, value)
83
+ end
84
+ end
85
+
67
86
  expects.each do |field, opts|
68
87
  axn.expects(field, **opts)
69
88
  end
@@ -72,16 +91,35 @@ module Axn
72
91
  axn.exposes(field, **opts)
73
92
  end
74
93
 
75
- axn.messages(**messages) if messages.present? && messages.values.any?(&:present?)
76
-
77
- axn.error_from(**_array_to_hash(error_from)) if error_from.present?
78
- axn.rescues(**_array_to_hash(rescues)) if rescues.present?
94
+ apply_message.call(:success, success)
95
+ apply_message.call(:error, error)
79
96
 
80
97
  # Hooks
81
98
  axn.before(before) if before.present?
82
99
  axn.after(after) if after.present?
83
100
  axn.around(around) if around.present?
84
101
 
102
+ # Callbacks
103
+ axn.on_success(&on_success) if on_success.present?
104
+ axn.on_failure(&on_failure) if on_failure.present?
105
+ axn.on_error(&on_error) if on_error.present?
106
+ axn.on_exception(&on_exception) if on_exception.present?
107
+
108
+ # Strategies
109
+ Array(use).each do |strategy|
110
+ if strategy.is_a?(Array)
111
+ strategy_name, *config_args = strategy
112
+ if config_args.last.is_a?(Hash)
113
+ *other_args, config = config_args
114
+ axn.use(strategy_name, *other_args, **config)
115
+ else
116
+ axn.use(strategy_name, *config_args)
117
+ end
118
+ else
119
+ axn.use(strategy)
120
+ end
121
+ end
122
+
85
123
  # Default exposure
86
124
  axn.exposes(expose_return_as, allow_blank: true) if expose_return_as.present?
87
125
  end
data/lib/axn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Axn
4
- VERSION = "0.1.0-alpha.2.6"
4
+ VERSION = "0.1.0-alpha.2.7"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: axn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.alpha.2.6
4
+ version: 0.1.0.pre.alpha.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kali Donovan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-07 00:00:00.000000000 Z
11
+ date: 2025-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -80,16 +80,28 @@ files:
80
80
  - lib/action/context.rb
81
81
  - lib/action/core.rb
82
82
  - lib/action/core/automatic_logging.rb
83
- - lib/action/core/context_facade.rb
83
+ - lib/action/core/context/facade.rb
84
+ - lib/action/core/context/facade_inspector.rb
85
+ - lib/action/core/context/internal.rb
84
86
  - lib/action/core/contract.rb
85
87
  - lib/action/core/contract_for_subfields.rb
86
88
  - lib/action/core/contract_validation.rb
87
- - lib/action/core/event_handlers.rb
88
- - lib/action/core/handle_exceptions.rb
89
+ - lib/action/core/flow.rb
90
+ - lib/action/core/flow/callbacks.rb
91
+ - lib/action/core/flow/exception_execution.rb
92
+ - lib/action/core/flow/handlers.rb
93
+ - lib/action/core/flow/handlers/base_handler.rb
94
+ - lib/action/core/flow/handlers/callback_handler.rb
95
+ - lib/action/core/flow/handlers/invoker.rb
96
+ - lib/action/core/flow/handlers/matcher.rb
97
+ - lib/action/core/flow/handlers/message_handler.rb
98
+ - lib/action/core/flow/handlers/registry.rb
99
+ - lib/action/core/flow/messages.rb
89
100
  - lib/action/core/hoist_errors.rb
90
101
  - lib/action/core/hooks.rb
91
102
  - lib/action/core/logging.rb
92
103
  - lib/action/core/timing.rb
104
+ - lib/action/core/tracing.rb
93
105
  - lib/action/core/use_strategy.rb
94
106
  - lib/action/core/validation/fields.rb
95
107
  - lib/action/core/validation/subfields.rb
@@ -99,6 +111,7 @@ files:
99
111
  - lib/action/enqueueable.rb
100
112
  - lib/action/enqueueable/via_sidekiq.rb
101
113
  - lib/action/exceptions.rb
114
+ - lib/action/result.rb
102
115
  - lib/action/strategies.rb
103
116
  - lib/action/strategies/transaction.rb
104
117
  - lib/axn.rb