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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -1
- data/README.md +2 -11
- data/docs/reference/configuration.md +15 -4
- data/docs/reference/instance.md +2 -2
- data/docs/strategies/index.md +1 -1
- data/docs/usage/setup.md +1 -1
- data/docs/usage/writing.md +8 -8
- data/lib/action/attachable.rb +3 -3
- data/lib/action/{core/configuration.rb → configuration.rb} +1 -1
- data/lib/action/context.rb +28 -0
- data/lib/action/core/automatic_logging.rb +77 -0
- data/lib/action/core/context_facade.rb +1 -1
- data/lib/action/core/contract.rb +153 -214
- data/lib/action/core/contract_for_subfields.rb +84 -82
- data/lib/action/core/contract_validation.rb +51 -0
- data/lib/action/core/handle_exceptions.rb +102 -122
- data/lib/action/core/hoist_errors.rb +42 -40
- data/lib/action/core/hooks.rb +123 -0
- data/lib/action/core/logging.rb +22 -20
- data/lib/action/core/timing.rb +22 -0
- data/lib/action/core/use_strategy.rb +19 -17
- data/lib/action/core.rb +153 -0
- data/lib/action/enqueueable.rb +1 -1
- data/lib/action/{core/exceptions.rb → exceptions.rb} +1 -19
- data/lib/axn/factory.rb +0 -12
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +10 -47
- metadata +10 -19
- data/lib/action/core/top_level_around_hook.rb +0 -108
data/lib/action/core.rb
ADDED
@@ -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
|
data/lib/action/enqueueable.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Action
|
4
|
-
# Raised internally when fail! is called
|
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
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
|
-
|
11
|
-
|
12
|
-
|
5
|
+
module Axn; end
|
6
|
+
require "axn/version"
|
7
|
+
require "axn/util"
|
8
|
+
require "axn/factory"
|
13
9
|
|
14
|
-
|
15
|
-
|
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
|
-
|
13
|
+
require "action/core"
|
25
14
|
|
26
|
-
|
27
|
-
|
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
|
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.
|
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
|
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/
|
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/
|
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
|