axn 0.1.0.pre.alpha.2.5.3.1 → 0.1.0.pre.alpha.2.6

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.
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action/context"
4
+
5
+ require "action/strategies"
6
+ require "action/core/hooks"
7
+ require "action/core/logging"
8
+ require "action/core/hoist_errors"
9
+ require "action/core/handle_exceptions"
10
+ require "action/core/automatic_logging"
11
+ require "action/core/use_strategy"
12
+
13
+ # CONSIDER: make class names match file paths?
14
+ require "action/core/validation/validators/model_validator"
15
+ require "action/core/validation/validators/type_validator"
16
+ require "action/core/validation/validators/validate_validator"
17
+
18
+ require "action/core/contract_validation"
19
+ require "action/core/contract"
20
+ require "action/core/contract_for_subfields"
21
+ require "action/core/timing"
22
+
23
+ module Action
24
+ module Core
25
+ def self.included(base)
26
+ base.class_eval do
27
+ extend ClassMethods
28
+ include Core::Hooks
29
+ include Core::Logging
30
+ include Core::AutomaticLogging
31
+
32
+ include Core::HandleExceptions
33
+
34
+ include Core::ContractValidation
35
+ include Core::Contract
36
+ include Core::ContractForSubfields
37
+
38
+ include Core::HoistErrors
39
+ include Core::UseStrategy
40
+ end
41
+ end
42
+
43
+ module ClassMethods
44
+ def call(context = {})
45
+ new(context).tap(&:run).result
46
+ end
47
+
48
+ def call!(context = {})
49
+ result = call(context)
50
+ return result if result.ok?
51
+
52
+ raise result.exception || Action::Failure.new(result.error)
53
+ end
54
+ end
55
+
56
+ def initialize(context = {})
57
+ @context = Action::Context.build(context)
58
+ end
59
+
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
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ ensure
128
+ _emit_metrics
129
+ end
130
+
131
+ def call; end
132
+
133
+ private
134
+
135
+ def _emit_metrics
136
+ return unless Action.config.emit_metrics
137
+
138
+ Action.config.emit_metrics.call(
139
+ self.class.name || "AnonymousClass",
140
+ _determine_outcome,
141
+ )
142
+ rescue StandardError => e
143
+ Axn::Util.piping_error("running metrics hook", action: self, exception: e)
144
+ end
145
+
146
+ def _determine_outcome
147
+ return "exception" if @context.exception
148
+ return "failure" if @context.failure?
149
+
150
+ "success"
151
+ end
152
+ end
153
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "enqueueable/via_sidekiq"
3
+ require "action/enqueueable/via_sidekiq"
4
4
 
5
5
  module Action
6
6
  module Enqueueable
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Action
4
- # Raised internally when fail! is called -- triggers failure + rollback handling
4
+ # Raised internally when fail! is called
5
5
  class Failure < StandardError
6
6
  DEFAULT_MESSAGE = "Execution was halted"
7
7
 
@@ -17,24 +17,6 @@ module Action
17
17
  def inspect = "#<#{self.class.name} '#{message}'>"
18
18
  end
19
19
 
20
- class StepsRequiredForInheritanceSupportError < StandardError
21
- def message
22
- <<~MSG
23
- ** Inheritance support requires the following steps: **
24
-
25
- Add this to your Gemfile:
26
- gem "interactor", github: "kaspermeyer/interactor", branch: "fix-hook-inheritance"
27
-
28
- Explanation:
29
- Unfortunately the upstream interactor gem does not support inheritance of hooks, which is required for this feature.
30
- This branch is a temporary fork that adds support for inheritance of hooks, but published gems cannot specify a branch dependency.
31
- In the future we may inline the upstream Interactor gem entirely and remove this necessity, but while we're in alpha we're continuing
32
- to use the upstream gem for stability (and there has been recent activity on the project, so they *may* be adding additional functionality
33
- soon).
34
- MSG
35
- end
36
- end
37
-
38
20
  class ContractViolation < StandardError
39
21
  class ReservedAttributeError < ContractViolation
40
22
  def initialize(name)
data/lib/axn/factory.rb CHANGED
@@ -22,8 +22,6 @@ module Axn
22
22
  after: nil,
23
23
  around: nil,
24
24
 
25
- # Allow dynamically assigning rollback method
26
- rollback: nil,
27
25
  &block
28
26
  )
29
27
  args = block.parameters.each_with_object(_hash_with_default_array) { |(type, field), hash| hash[type] << field }
@@ -84,16 +82,6 @@ module Axn
84
82
  axn.after(after) if after.present?
85
83
  axn.around(around) if around.present?
86
84
 
87
- # Rollback
88
- if rollback.present?
89
- raise ArgumentError, "[Axn::Factory] Rollback must be a callable" unless rollback.respond_to?(:call) && rollback.respond_to?(:arity)
90
- raise ArgumentError, "[Axn::Factory] Rollback must be a callable with no arguments" unless rollback.arity.zero?
91
-
92
- axn.define_method(:rollback) do
93
- instance_exec(&rollback)
94
- end
95
- end
96
-
97
85
  # Default exposure
98
86
  axn.exposes(expose_return_as, allow_blank: true) if expose_return_as.present?
99
87
  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.5.3.1"
4
+ VERSION = "0.1.0-alpha.2.6"
5
5
  end
data/lib/axn.rb CHANGED
@@ -1,31 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Axn; end
4
- require_relative "axn/version"
5
- require_relative "axn/util"
6
-
7
- require "interactor"
8
3
  require "active_support"
9
4
 
10
- require_relative "action/core/validation/validators/model_validator"
11
- require_relative "action/core/validation/validators/type_validator"
12
- require_relative "action/core/validation/validators/validate_validator"
5
+ module Axn; end
6
+ require "axn/version"
7
+ require "axn/util"
8
+ require "axn/factory"
13
9
 
14
- require_relative "action/core/exceptions"
15
- require_relative "action/core/logging"
16
- require_relative "action/core/configuration"
17
- require_relative "action/core/top_level_around_hook"
18
- require_relative "action/core/contract"
19
- require_relative "action/core/contract_for_subfields"
20
- require_relative "action/core/handle_exceptions"
21
- require_relative "action/core/hoist_errors"
22
- require_relative "action/core/use_strategy"
10
+ require "action/configuration"
11
+ require "action/exceptions"
23
12
 
24
- require_relative "axn/factory"
13
+ require "action/core"
25
14
 
26
- require_relative "action/attachable"
27
- require_relative "action/enqueueable"
28
- require_relative "action/strategies"
15
+ require "action/attachable"
16
+ require "action/enqueueable"
29
17
 
30
18
  def Axn(callable, **) # rubocop:disable Naming/MethodName
31
19
  return callable if callable.is_a?(Class) && callable < Action
@@ -36,22 +24,7 @@ end
36
24
  module Action
37
25
  def self.included(base)
38
26
  base.class_eval do
39
- include Interactor
40
-
41
- # Include first so other modules can assume `log` is available
42
- include Logging
43
-
44
- # NOTE: include before any others that set hooks (like contract validation), so we
45
- # can include those hook executions in any traces set from this hook.
46
- include TopLevelAroundHook
47
-
48
- include HandleExceptions
49
- include Contract
50
- include ContractForSubfields
51
-
52
- include HoistErrors
53
-
54
- include UseStrategy
27
+ include Core
55
28
 
56
29
  # --- Extensions ---
57
30
  include Attachable
@@ -59,16 +32,6 @@ module Action
59
32
 
60
33
  # Allow additional automatic includes to be configured
61
34
  Array(Action.config.additional_includes).each { |mod| include mod }
62
-
63
- # ----
64
-
65
- # ALPHA: Everything below here is to support inheritance
66
-
67
- base.define_singleton_method(:inherited) do |base_klass|
68
- return super(base_klass) if Interactor::Hooks::ClassMethods.private_method_defined?(:ancestor_hooks)
69
-
70
- raise StepsRequiredForInheritanceSupportError
71
- end
72
35
  end
73
36
  end
74
37
  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.5.3.1
4
+ version: 0.1.0.pre.alpha.2.6
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-07-30 00:00:00.000000000 Z
11
+ date: 2025-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - ">"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '7.0'
41
- - !ruby/object:Gem::Dependency
42
- name: interactor
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - '='
46
- - !ruby/object:Gem::Version
47
- version: 3.1.2
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - '='
53
- - !ruby/object:Gem::Version
54
- version: 3.1.2
55
41
  description: Pattern for writing callable service objects with contract validation
56
42
  and error swallowing
57
43
  email:
@@ -90,16 +76,20 @@ files:
90
76
  - lib/action/attachable/base.rb
91
77
  - lib/action/attachable/steps.rb
92
78
  - lib/action/attachable/subactions.rb
93
- - lib/action/core/configuration.rb
79
+ - lib/action/configuration.rb
80
+ - lib/action/context.rb
81
+ - lib/action/core.rb
82
+ - lib/action/core/automatic_logging.rb
94
83
  - lib/action/core/context_facade.rb
95
84
  - lib/action/core/contract.rb
96
85
  - lib/action/core/contract_for_subfields.rb
86
+ - lib/action/core/contract_validation.rb
97
87
  - lib/action/core/event_handlers.rb
98
- - lib/action/core/exceptions.rb
99
88
  - lib/action/core/handle_exceptions.rb
100
89
  - lib/action/core/hoist_errors.rb
90
+ - lib/action/core/hooks.rb
101
91
  - lib/action/core/logging.rb
102
- - lib/action/core/top_level_around_hook.rb
92
+ - lib/action/core/timing.rb
103
93
  - lib/action/core/use_strategy.rb
104
94
  - lib/action/core/validation/fields.rb
105
95
  - lib/action/core/validation/subfields.rb
@@ -108,6 +98,7 @@ files:
108
98
  - lib/action/core/validation/validators/validate_validator.rb
109
99
  - lib/action/enqueueable.rb
110
100
  - lib/action/enqueueable/via_sidekiq.rb
101
+ - lib/action/exceptions.rb
111
102
  - lib/action/strategies.rb
112
103
  - lib/action/strategies/transaction.rb
113
104
  - lib/axn.rb
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module TopLevelAroundHook
5
- def self.included(base)
6
- base.class_eval do
7
- around :__top_level_around_hook
8
-
9
- extend AutologgingClassMethods
10
- include AutologgingInstanceMethods
11
- include InstanceMethods
12
- end
13
- end
14
-
15
- module AutologgingClassMethods
16
- def default_autolog_level = Action.config.default_autolog_level
17
- end
18
-
19
- module AutologgingInstanceMethods
20
- private
21
-
22
- def _log_before
23
- public_send(
24
- self.class.default_autolog_level,
25
- [
26
- "About to execute",
27
- _log_context(:inbound),
28
- ].compact.join(" with: "),
29
- before: Action.config.env.production? ? nil : "\n------\n",
30
- )
31
- end
32
-
33
- def _log_after(outcome:, timing_start:)
34
- elapsed_mils = ((Time.now - timing_start) * 1000).round(3)
35
-
36
- public_send(
37
- self.class.default_autolog_level,
38
- [
39
- "Execution completed (with outcome: #{outcome}) in #{elapsed_mils} milliseconds",
40
- _log_context(:outbound),
41
- ].compact.join(". Set: "),
42
- after: Action.config.env.production? ? nil : "\n------\n",
43
- )
44
- end
45
-
46
- def _log_context(direction)
47
- data = context_for_logging(direction)
48
- return unless data.present?
49
-
50
- max_length = 150
51
- suffix = "…<truncated>…"
52
-
53
- _log_object(data).tap do |str|
54
- return str[0, max_length - suffix.length] + suffix if str.length > max_length
55
- end
56
- end
57
-
58
- def _log_object(data)
59
- case data
60
- when Hash
61
- # NOTE: slightly more manual in order to avoid quotes around ActiveRecord objects' <Class#id> formatting
62
- "{#{data.map { |k, v| "#{k}: #{_log_object(v)}" }.join(", ")}}"
63
- when Array
64
- data.map { |v| _log_object(v) }
65
- else
66
- return data.to_unsafe_h if defined?(ActionController::Parameters) && data.is_a?(ActionController::Parameters)
67
- return "<#{data.class.name}##{data.to_param.presence || "unpersisted"}>" if defined?(ActiveRecord::Base) && data.is_a?(ActiveRecord::Base)
68
-
69
- data.inspect
70
- end
71
- end
72
- end
73
-
74
- module InstanceMethods
75
- def __top_level_around_hook(hooked)
76
- timing_start = Time.now
77
- _log_before
78
-
79
- _configurable_around_wrapper do
80
- (@outcome, @exception) = _call_and_return_outcome(hooked)
81
- end
82
-
83
- _log_after(timing_start:, outcome: @outcome)
84
-
85
- raise @exception if @exception
86
- end
87
-
88
- private
89
-
90
- def _configurable_around_wrapper(&)
91
- return yield unless Action.config.top_level_around_hook
92
-
93
- Action.config.top_level_around_hook.call(self.class.name || "AnonymousClass", &)
94
- end
95
-
96
- def _call_and_return_outcome(hooked)
97
- hooked.call
98
-
99
- "success"
100
- rescue StandardError => e
101
- [
102
- e.is_a?(Action::Failure) ? "failure" : "exception",
103
- e,
104
- ]
105
- end
106
- end
107
- end
108
- end