axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.3
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/.cursor/rules/axn-framework-patterns.mdc +43 -0
- data/.cursor/rules/general-coding-standards.mdc +27 -0
- data/.cursor/rules/spec/testing-patterns.mdc +40 -0
- data/CHANGELOG.md +43 -0
- data/Rakefile +12 -2
- data/docs/.vitepress/config.mjs +8 -3
- data/docs/advanced/conventions.md +2 -2
- data/docs/advanced/mountable.md +562 -0
- data/docs/advanced/profiling.md +355 -0
- data/docs/advanced/rough.md +1 -1
- data/docs/index.md +5 -3
- data/docs/intro/about.md +1 -1
- data/docs/intro/overview.md +5 -5
- data/docs/recipes/memoization.md +2 -2
- data/docs/recipes/rubocop-integration.md +38 -284
- data/docs/recipes/testing.md +14 -14
- data/docs/recipes/validating-user-input.md +1 -1
- data/docs/reference/async.md +160 -0
- data/docs/reference/axn-result.md +107 -0
- data/docs/reference/class.md +123 -25
- data/docs/reference/configuration.md +191 -10
- data/docs/reference/instance.md +14 -29
- data/docs/strategies/index.md +21 -21
- data/docs/strategies/transaction.md +1 -1
- data/docs/usage/setup.md +14 -0
- data/docs/usage/steps.md +7 -7
- data/docs/usage/using.md +23 -12
- data/docs/usage/writing.md +92 -11
- data/lib/axn/async/adapters/active_job.rb +65 -0
- data/lib/axn/async/adapters/disabled.rb +26 -0
- data/lib/axn/async/adapters/sidekiq.rb +74 -0
- data/lib/axn/async/adapters.rb +26 -0
- data/lib/axn/async.rb +61 -0
- data/lib/{action → axn}/configuration.rb +21 -3
- data/lib/{action → axn}/context.rb +21 -4
- data/lib/{action → axn}/core/automatic_logging.rb +6 -6
- data/lib/axn/core/context/facade.rb +69 -0
- data/lib/{action → axn}/core/context/facade_inspector.rb +31 -4
- data/lib/{action → axn}/core/context/internal.rb +5 -5
- data/lib/{action → axn}/core/contract.rb +41 -46
- data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
- data/lib/{action → axn}/core/contract_validation.rb +16 -6
- data/lib/axn/core/contract_validation_for_subfields.rb +158 -0
- data/lib/axn/core/field_resolvers/extract.rb +32 -0
- data/lib/axn/core/field_resolvers/model.rb +63 -0
- data/lib/axn/core/field_resolvers.rb +24 -0
- data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
- data/lib/{action → axn}/core/flow/exception_execution.rb +4 -13
- data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +6 -6
- data/lib/{action → axn}/core/flow/handlers/invoker.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/matcher.rb +5 -5
- data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
- data/lib/axn/core/flow/handlers.rb +20 -0
- data/lib/{action → axn}/core/flow/messages.rb +7 -7
- data/lib/{action → axn}/core/flow.rb +4 -4
- data/lib/{action → axn}/core/hooks.rb +16 -5
- data/lib/{action → axn}/core/logging.rb +3 -3
- data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
- data/lib/axn/core/profiling.rb +124 -0
- data/lib/{action → axn}/core/timing.rb +1 -1
- data/lib/axn/core/tracing.rb +17 -0
- data/lib/axn/core/use_strategy.rb +29 -0
- data/lib/{action → axn}/core/validation/fields.rb +26 -2
- data/lib/{action → axn}/core/validation/subfields.rb +14 -12
- data/lib/axn/core/validation/validators/model_validator.rb +36 -0
- data/lib/axn/core/validation/validators/type_validator.rb +80 -0
- data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
- data/lib/axn/core.rb +123 -0
- data/lib/{action → axn}/exceptions.rb +12 -2
- data/lib/axn/factory.rb +102 -34
- data/lib/axn/internal/logging.rb +26 -0
- data/lib/axn/internal/registry.rb +87 -0
- data/lib/axn/mountable/descriptor.rb +76 -0
- data/lib/axn/mountable/helpers/class_builder.rb +162 -0
- data/lib/axn/mountable/helpers/mounter.rb +33 -0
- data/lib/axn/mountable/helpers/namespace_manager.rb +66 -0
- data/lib/axn/mountable/helpers/validator.rb +112 -0
- data/lib/axn/mountable/inherit_profiles.rb +72 -0
- data/lib/axn/mountable/mounting_strategies/_base.rb +83 -0
- data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
- data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +55 -0
- data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
- data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
- data/lib/axn/mountable/mounting_strategies.rb +32 -0
- data/lib/axn/mountable.rb +85 -0
- data/lib/axn/rails/engine.rb +51 -0
- data/lib/axn/rails/generators/axn_generator.rb +68 -0
- data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
- data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
- data/lib/{action → axn}/result.rb +30 -11
- data/lib/{action → axn}/strategies/transaction.rb +1 -1
- data/lib/axn/strategies.rb +20 -0
- data/lib/axn/testing/spec_helpers.rb +6 -8
- data/lib/axn/util/memoization.rb +20 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +17 -16
- data/lib/rubocop/cop/axn/README.md +23 -23
- data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
- metadata +88 -64
- data/.rspec +0 -3
- data/.rubocop.yml +0 -76
- data/.tool-versions +0 -1
- data/docs/reference/action-result.md +0 -37
- data/lib/action/attachable/base.rb +0 -43
- data/lib/action/attachable/steps.rb +0 -63
- data/lib/action/attachable/subactions.rb +0 -70
- data/lib/action/attachable.rb +0 -17
- data/lib/action/core/context/facade.rb +0 -48
- data/lib/action/core/flow/handlers.rb +0 -20
- data/lib/action/core/tracing.rb +0 -17
- data/lib/action/core/use_strategy.rb +0 -30
- data/lib/action/core/validation/validators/model_validator.rb +0 -34
- data/lib/action/core/validation/validators/type_validator.rb +0 -30
- data/lib/action/core.rb +0 -108
- data/lib/action/enqueueable/via_sidekiq.rb +0 -76
- data/lib/action/enqueueable.rb +0 -13
- data/lib/action/strategies.rb +0 -48
- data/lib/axn/util.rb +0 -24
- data/package.json +0 -10
- data/yarn.lock +0 -1166
@@ -2,14 +2,24 @@
|
|
2
2
|
|
3
3
|
require "active_model"
|
4
4
|
|
5
|
-
module
|
5
|
+
module Axn
|
6
6
|
module Validators
|
7
7
|
class ValidateValidator < ActiveModel::EachValidator
|
8
|
+
def self.apply_syntactic_sugar(value, _fields)
|
9
|
+
return value if value.is_a?(Hash)
|
10
|
+
|
11
|
+
{ with: value }
|
12
|
+
end
|
13
|
+
|
14
|
+
def check_validity!
|
15
|
+
raise ArgumentError, "must supply :with" if options[:with].nil?
|
16
|
+
end
|
17
|
+
|
8
18
|
def validate_each(record, attribute, value)
|
9
19
|
msg = begin
|
10
20
|
options[:with].call(value)
|
11
21
|
rescue StandardError => e
|
12
|
-
Axn::
|
22
|
+
Axn::Internal::Logging.piping_error("applying custom validation on field '#{attribute}'", exception: e)
|
13
23
|
|
14
24
|
"failed validation: #{e.message}"
|
15
25
|
end
|
data/lib/axn/core.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "axn/internal/logging"
|
4
|
+
|
5
|
+
require "axn/context"
|
6
|
+
|
7
|
+
require "axn/strategies"
|
8
|
+
require "axn/core/hooks"
|
9
|
+
require "axn/core/logging"
|
10
|
+
require "axn/core/flow"
|
11
|
+
require "axn/core/automatic_logging"
|
12
|
+
require "axn/core/use_strategy"
|
13
|
+
require "axn/core/timing"
|
14
|
+
require "axn/core/tracing"
|
15
|
+
require "axn/core/profiling"
|
16
|
+
require "axn/core/nesting_tracking"
|
17
|
+
|
18
|
+
# CONSIDER: make class names match file paths?
|
19
|
+
require "axn/core/validation/validators/model_validator"
|
20
|
+
require "axn/core/validation/validators/type_validator"
|
21
|
+
require "axn/core/validation/validators/validate_validator"
|
22
|
+
|
23
|
+
require "axn/core/field_resolvers"
|
24
|
+
require "axn/core/contract_validation"
|
25
|
+
require "axn/core/contract_validation_for_subfields"
|
26
|
+
require "axn/core/contract"
|
27
|
+
require "axn/core/contract_for_subfields"
|
28
|
+
|
29
|
+
module Axn
|
30
|
+
module Core
|
31
|
+
def self.included(base)
|
32
|
+
base.class_eval do
|
33
|
+
extend ClassMethods
|
34
|
+
include Core::Hooks
|
35
|
+
include Core::Logging
|
36
|
+
include Core::AutomaticLogging
|
37
|
+
include Core::Tracing
|
38
|
+
include Core::Timing
|
39
|
+
include Core::Profiling
|
40
|
+
|
41
|
+
include Core::Flow
|
42
|
+
|
43
|
+
include Core::ContractValidation
|
44
|
+
include Core::ContractValidationForSubfields
|
45
|
+
include Core::Contract
|
46
|
+
include Core::ContractForSubfields
|
47
|
+
include Core::NestingTracking
|
48
|
+
|
49
|
+
include Core::UseStrategy
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module ClassMethods
|
54
|
+
def call(**)
|
55
|
+
new(**).tap(&:_run).result
|
56
|
+
end
|
57
|
+
|
58
|
+
def call!(**)
|
59
|
+
result = call(**)
|
60
|
+
return result if result.ok?
|
61
|
+
|
62
|
+
# When we're nested, we want to raise a failure that includes the source action to support
|
63
|
+
# the error message generation's `from` filter
|
64
|
+
raise Axn::Failure.new(result.error, source: result.__action__), cause: result.exception if _nested_in_another_axn?
|
65
|
+
|
66
|
+
raise result.exception
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(**)
|
71
|
+
@__context = Axn::Context.new(**)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Main entry point for action execution
|
75
|
+
def _run
|
76
|
+
_tracking_nesting(self) do
|
77
|
+
_with_profiling do
|
78
|
+
_with_tracing do
|
79
|
+
_with_logging do
|
80
|
+
_with_timing do
|
81
|
+
_with_exception_handling do # Exceptions stop here; outer wrappers access result status (and must not introduce another exception layer)
|
82
|
+
_with_contract do # Library internals -- any failures (e.g. contract violations) *should* fail the Action::Result
|
83
|
+
_with_hooks do # User hooks -- any failures here *should* fail the Action::Result
|
84
|
+
call
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
ensure
|
94
|
+
_emit_metrics
|
95
|
+
end
|
96
|
+
|
97
|
+
# User-defined action logic - override this method in your action classes
|
98
|
+
def call; end
|
99
|
+
|
100
|
+
def fail!(message = nil, **exposures)
|
101
|
+
expose(**exposures) if exposures.any?
|
102
|
+
raise Axn::Failure, message
|
103
|
+
end
|
104
|
+
|
105
|
+
def done!(message = nil, **exposures)
|
106
|
+
expose(**exposures) if exposures.any?
|
107
|
+
raise Axn::Internal::EarlyCompletion, message
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def _emit_metrics
|
113
|
+
return unless Axn.config.emit_metrics
|
114
|
+
|
115
|
+
Axn.config.emit_metrics.call(
|
116
|
+
self.class.name || "AnonymousClass",
|
117
|
+
result,
|
118
|
+
)
|
119
|
+
rescue StandardError => e
|
120
|
+
Axn::Internal::Logging.piping_error("running metrics hook", action: self, exception: e)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -1,7 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
|
3
|
+
module Axn
|
4
|
+
module Internal
|
5
|
+
# Internal only -- rescued before Axn::Result is returned
|
6
|
+
class EarlyCompletion < StandardError; end
|
7
|
+
end
|
8
|
+
|
9
|
+
# Raised when fail! is called
|
5
10
|
class Failure < StandardError
|
6
11
|
DEFAULT_MESSAGE = "Execution was halted"
|
7
12
|
|
@@ -22,6 +27,10 @@ module Action
|
|
22
27
|
def inspect = "#<#{self.class.name} '#{message}'>"
|
23
28
|
end
|
24
29
|
|
30
|
+
module Mountable
|
31
|
+
class MountingError < ArgumentError; end
|
32
|
+
end
|
33
|
+
|
25
34
|
class ContractViolation < StandardError
|
26
35
|
class ReservedAttributeError < ContractViolation
|
27
36
|
def initialize(name)
|
@@ -34,6 +43,7 @@ module Action
|
|
34
43
|
|
35
44
|
class MethodNotAllowed < ContractViolation; end
|
36
45
|
class PreprocessingError < ContractViolation; end
|
46
|
+
class DefaultAssignmentError < ContractViolation; end
|
37
47
|
|
38
48
|
class UnknownExposure < ContractViolation
|
39
49
|
def initialize(key)
|
data/lib/axn/factory.rb
CHANGED
@@ -5,10 +5,15 @@ module Axn
|
|
5
5
|
class << self
|
6
6
|
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/ParameterLists
|
7
7
|
def build(
|
8
|
+
callable = nil,
|
8
9
|
# Builder-specific options
|
9
|
-
name: nil,
|
10
10
|
superclass: nil,
|
11
|
-
expose_return_as:
|
11
|
+
expose_return_as: nil,
|
12
|
+
|
13
|
+
# Module inclusion options
|
14
|
+
include: [],
|
15
|
+
extend: [],
|
16
|
+
prepend: [],
|
12
17
|
|
13
18
|
# Expose standard class-level options
|
14
19
|
exposes: [],
|
@@ -30,19 +35,27 @@ module Axn
|
|
30
35
|
# Strategies
|
31
36
|
use: [],
|
32
37
|
|
38
|
+
# Async configuration
|
39
|
+
async: nil,
|
40
|
+
|
33
41
|
&block
|
34
42
|
)
|
35
|
-
|
43
|
+
raise ArgumentError, "[Axn::Factory] Cannot receive both a callable and a block" if callable.present? && block_given?
|
44
|
+
|
45
|
+
executable = callable || block
|
46
|
+
raise ArgumentError, "[Axn::Factory] Must provide either a callable or a block" unless executable
|
47
|
+
|
48
|
+
args = executable.parameters.each_with_object(_hash_with_default_array) { |(type, field), hash| hash[type] << field }
|
36
49
|
|
37
50
|
if args[:opt].present? || args[:req].present? || args[:rest].present?
|
38
51
|
raise ArgumentError,
|
39
|
-
"[Axn::Factory] Cannot convert
|
52
|
+
"[Axn::Factory] Cannot convert callable to action: callable expects positional arguments"
|
40
53
|
end
|
41
|
-
raise ArgumentError, "[Axn::Factory] Cannot convert
|
54
|
+
raise ArgumentError, "[Axn::Factory] Cannot convert callable to action: callable expects a splat of keyword arguments" if args[:keyrest].present?
|
42
55
|
|
43
56
|
if args[:key].present?
|
44
57
|
raise ArgumentError,
|
45
|
-
"[Axn::Factory] Cannot convert
|
58
|
+
"[Axn::Factory] Cannot convert callable to action: callable expects keyword arguments with defaults (ruby does not allow introspecting)"
|
46
59
|
end
|
47
60
|
|
48
61
|
expects = _hydrate_hash(expects)
|
@@ -53,25 +66,7 @@ module Axn
|
|
53
66
|
end
|
54
67
|
|
55
68
|
# NOTE: inheriting from wrapping class, so we can set default values (e.g. for HTTP headers)
|
56
|
-
|
57
|
-
include Action unless self < Action
|
58
|
-
|
59
|
-
define_singleton_method(:name) do
|
60
|
-
[
|
61
|
-
superclass&.name.presence || "AnonymousAction",
|
62
|
-
name,
|
63
|
-
].compact.join("#")
|
64
|
-
end
|
65
|
-
|
66
|
-
define_method(:call) do
|
67
|
-
unwrapped_kwargs = Array(args[:keyreq]).each_with_object({}) do |field, hash|
|
68
|
-
hash[field] = public_send(field)
|
69
|
-
end
|
70
|
-
|
71
|
-
retval = instance_exec(**unwrapped_kwargs, &block)
|
72
|
-
expose(expose_return_as => retval) if expose_return_as.present?
|
73
|
-
end
|
74
|
-
end.tap do |axn|
|
69
|
+
_build_axn_class(superclass:, args:, executable:, expose_return_as:, include:, extend:, prepend:).tap do |axn|
|
75
70
|
expects.each do |field, opts|
|
76
71
|
axn.expects(field, **opts)
|
77
72
|
end
|
@@ -81,8 +76,8 @@ module Axn
|
|
81
76
|
end
|
82
77
|
|
83
78
|
# Apply success and error handlers
|
84
|
-
_apply_handlers(axn, :success, success,
|
85
|
-
_apply_handlers(axn, :error, error,
|
79
|
+
_apply_handlers(axn, :success, success, Axn::Core::Flow::Handlers::Descriptors::MessageDescriptor)
|
80
|
+
_apply_handlers(axn, :error, error, Axn::Core::Flow::Handlers::Descriptors::MessageDescriptor)
|
86
81
|
|
87
82
|
# Hooks
|
88
83
|
axn.before(before) if before.present?
|
@@ -90,10 +85,10 @@ module Axn
|
|
90
85
|
axn.around(around) if around.present?
|
91
86
|
|
92
87
|
# Callbacks
|
93
|
-
_apply_handlers(axn, :on_success, on_success,
|
94
|
-
_apply_handlers(axn, :on_failure, on_failure,
|
95
|
-
_apply_handlers(axn, :on_error, on_error,
|
96
|
-
_apply_handlers(axn, :on_exception, on_exception,
|
88
|
+
_apply_handlers(axn, :on_success, on_success, Axn::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
|
89
|
+
_apply_handlers(axn, :on_failure, on_failure, Axn::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
|
90
|
+
_apply_handlers(axn, :on_error, on_error, Axn::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
|
91
|
+
_apply_handlers(axn, :on_exception, on_exception, Axn::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
|
97
92
|
|
98
93
|
# Strategies
|
99
94
|
Array(use).each do |strategy|
|
@@ -110,8 +105,19 @@ module Axn
|
|
110
105
|
end
|
111
106
|
end
|
112
107
|
|
108
|
+
# Async configuration
|
109
|
+
unless async.nil?
|
110
|
+
async_array = Array(async)
|
111
|
+
# Skip async configuration if adapter is nil (but not if array is empty)
|
112
|
+
if !async_array.empty? && async_array[0].nil?
|
113
|
+
# Do nothing - skip async configuration
|
114
|
+
else
|
115
|
+
_apply_async_config(axn, async_array)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
113
119
|
# Default exposure
|
114
|
-
axn.exposes(expose_return_as,
|
120
|
+
axn.exposes(expose_return_as, optional: true) if expose_return_as.present?
|
115
121
|
end
|
116
122
|
end
|
117
123
|
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/ParameterLists
|
@@ -138,16 +144,78 @@ module Axn
|
|
138
144
|
return unless value.present?
|
139
145
|
|
140
146
|
# Check if the value itself is a hash (this catches the case where someone passes a hash literal)
|
141
|
-
raise
|
147
|
+
raise Axn::UnsupportedArgument, "Cannot pass hash directly to #{method_name} - use descriptor objects for kwargs" if value.is_a?(Hash)
|
142
148
|
|
143
149
|
# Wrap in Array() to handle both single values and arrays
|
144
150
|
Array(value).each do |handler|
|
145
|
-
raise
|
151
|
+
raise Axn::UnsupportedArgument, "Cannot pass hash directly to #{method_name} - use descriptor objects for kwargs" if handler.is_a?(Hash)
|
146
152
|
|
147
153
|
# Both descriptor objects and simple cases (string/proc) can be used directly
|
148
154
|
axn.public_send(method_name, handler)
|
149
155
|
end
|
150
156
|
end
|
157
|
+
|
158
|
+
def _build_axn_class(superclass:, args:, executable:, expose_return_as:, include: nil, extend: nil, prepend: nil)
|
159
|
+
Class.new(superclass || Object) do
|
160
|
+
include Axn unless self < Axn
|
161
|
+
|
162
|
+
Array(include).each { |mod| include mod }
|
163
|
+
Array(extend).each { |mod| extend mod }
|
164
|
+
Array(prepend).each { |mod| prepend mod }
|
165
|
+
|
166
|
+
# Set a default name for anonymous classes to help with debugging
|
167
|
+
define_singleton_method(:name) do
|
168
|
+
"AnonymousAxn_#{object_id}"
|
169
|
+
end
|
170
|
+
|
171
|
+
define_method(:call) do
|
172
|
+
unwrapped_kwargs = Array(args[:keyreq]).each_with_object({}) do |field, hash|
|
173
|
+
hash[field] = public_send(field)
|
174
|
+
end
|
175
|
+
|
176
|
+
retval = instance_exec(**unwrapped_kwargs, &executable)
|
177
|
+
expose(expose_return_as => retval) if expose_return_as.present?
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def _apply_async_config(axn, async)
|
183
|
+
raise ArgumentError, "[Axn::Factory] Invalid async configuration" unless _validate_async_config(async)
|
184
|
+
|
185
|
+
adapter, *config_args = async
|
186
|
+
|
187
|
+
# Determine hash config and callable config
|
188
|
+
config = config_args.find { |arg| arg.is_a?(Hash) }
|
189
|
+
block = config_args.find { |arg| arg.respond_to?(:call) }
|
190
|
+
|
191
|
+
# Call async once with the determined values
|
192
|
+
axn.async(adapter, **(config || {}), &block)
|
193
|
+
end
|
194
|
+
|
195
|
+
def _validate_async_config(async_array)
|
196
|
+
return false unless async_array.length.between?(1, 3)
|
197
|
+
|
198
|
+
adapter = async_array[0]
|
199
|
+
second_arg = async_array[1]
|
200
|
+
third_arg = async_array[2]
|
201
|
+
|
202
|
+
# First arg must be adapter (symbol/string), false, or nil
|
203
|
+
return false unless adapter.is_a?(Symbol) || adapter.is_a?(String) || adapter == false || adapter.nil?
|
204
|
+
|
205
|
+
case async_array.length
|
206
|
+
when 1
|
207
|
+
# Pattern A: [:sidekiq], [false], or [nil]
|
208
|
+
true
|
209
|
+
when 2
|
210
|
+
# Pattern B: [:sidekiq, hash_or_callable] or [nil, hash_or_callable]
|
211
|
+
second_arg.is_a?(Hash) || second_arg.respond_to?(:call)
|
212
|
+
when 3
|
213
|
+
# Pattern C: [:sidekiq, hash, callable] or [nil, hash, callable]
|
214
|
+
second_arg.is_a?(Hash) && third_arg.respond_to?(:call)
|
215
|
+
else
|
216
|
+
false
|
217
|
+
end
|
218
|
+
end
|
151
219
|
end
|
152
220
|
end
|
153
221
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Internal
|
5
|
+
module Logging
|
6
|
+
def self.piping_error(desc, exception:, action: nil)
|
7
|
+
# Extract just filename/line number from backtrace
|
8
|
+
src = exception.backtrace.first.split.first.split("/").last.split(":")[0, 2].join(":")
|
9
|
+
|
10
|
+
message = if Axn.config.env.production?
|
11
|
+
"Ignoring exception raised while #{desc}: #{exception.class.name} - #{exception.message} (from #{src})"
|
12
|
+
else
|
13
|
+
msg = "!! IGNORING EXCEPTION RAISED WHILE #{desc.upcase} !!\n\n" \
|
14
|
+
"\t* Exception: #{exception.class.name}\n" \
|
15
|
+
"\t* Message: #{exception.message}\n" \
|
16
|
+
"\t* From: #{src}"
|
17
|
+
"#{"⌵" * 30}\n\n#{msg}\n\n#{"^" * 30}"
|
18
|
+
end
|
19
|
+
|
20
|
+
(action || Axn.config.logger).send(:warn, message)
|
21
|
+
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/string/inflections"
|
4
|
+
|
5
|
+
module Axn
|
6
|
+
module Internal
|
7
|
+
class Registry
|
8
|
+
class NotFound < StandardError; end
|
9
|
+
class DuplicateError < StandardError; end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def built_in
|
13
|
+
@built_in ||= begin
|
14
|
+
# Get the directory name from the class name (e.g., "Strategies" -> "strategies")
|
15
|
+
dir_name = name.split("::").last.underscore
|
16
|
+
|
17
|
+
# Load all files from the directory
|
18
|
+
files = ::Dir[File.join(registry_directory, dir_name, "*.rb")]
|
19
|
+
files.each { |file| require file }
|
20
|
+
|
21
|
+
# Get all modules defined within this class
|
22
|
+
constants = self.constants.map { |const| const_get(const) }
|
23
|
+
items = select_constants_to_load(constants)
|
24
|
+
|
25
|
+
# Convert module names to keys
|
26
|
+
items.to_h do |item|
|
27
|
+
name = item.name.split("::").last
|
28
|
+
key = name.underscore.to_sym
|
29
|
+
[key, item]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def register(name, item)
|
35
|
+
items = all # ensure built_in is initialized
|
36
|
+
key = name.to_sym
|
37
|
+
raise duplicate_error_class, "#{item_type} #{name} already registered" if items.key?(key)
|
38
|
+
|
39
|
+
items[key] = item
|
40
|
+
items
|
41
|
+
end
|
42
|
+
|
43
|
+
def all
|
44
|
+
@items ||= built_in.dup
|
45
|
+
end
|
46
|
+
|
47
|
+
def clear!
|
48
|
+
@items = built_in.dup
|
49
|
+
end
|
50
|
+
|
51
|
+
def find(name)
|
52
|
+
raise not_found_error_class, "#{item_type} name cannot be nil" if name.nil?
|
53
|
+
raise not_found_error_class, "#{item_type} name cannot be empty" if name.to_s.strip.empty?
|
54
|
+
|
55
|
+
all[name.to_sym] or raise not_found_error_class, "#{item_type} '#{name}' not found"
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def item_type
|
61
|
+
# Subclasses can override this for better error messages
|
62
|
+
"Item"
|
63
|
+
end
|
64
|
+
|
65
|
+
def not_found_error_class
|
66
|
+
# Subclasses can override this to return their specific error class
|
67
|
+
NotFound
|
68
|
+
end
|
69
|
+
|
70
|
+
def duplicate_error_class
|
71
|
+
# Subclasses can override this to return their specific error class
|
72
|
+
DuplicateError
|
73
|
+
end
|
74
|
+
|
75
|
+
def registry_directory
|
76
|
+
# Subclasses must override this to return their directory
|
77
|
+
raise NotImplementedError, "Subclasses must implement registry_directory method"
|
78
|
+
end
|
79
|
+
|
80
|
+
def select_constants_to_load(constants)
|
81
|
+
# Subclasses can override this to select which constants to load
|
82
|
+
constants.select { |const| const.is_a?(Module) }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Mountable
|
5
|
+
# Descriptor holds the information needed to mount an action
|
6
|
+
class Descriptor
|
7
|
+
attr_reader :name, :options, :mounted_axn, :mount_strategy, :existing_axn_klass, :block, :raw_kwargs, :kwargs
|
8
|
+
|
9
|
+
def initialize(name:, as:, axn_klass: nil, block: nil, kwargs: {})
|
10
|
+
@mount_strategy = MountingStrategies.find(as)
|
11
|
+
@existing_axn_klass = axn_klass
|
12
|
+
|
13
|
+
@name = name
|
14
|
+
@block = block
|
15
|
+
@raw_kwargs = kwargs
|
16
|
+
|
17
|
+
@kwargs = mount_strategy.preprocess_kwargs(**kwargs.except(*mount_strategy.strategy_specific_kwargs), axn_klass:)
|
18
|
+
@options = kwargs.slice(*mount_strategy.strategy_specific_kwargs)
|
19
|
+
|
20
|
+
@validator = Helpers::Validator.new(self)
|
21
|
+
|
22
|
+
@validator.validate!
|
23
|
+
freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
def mount(target:)
|
27
|
+
validate_before_mount!(target:)
|
28
|
+
mount_strategy.mount(descriptor: self, target:)
|
29
|
+
end
|
30
|
+
|
31
|
+
def mounted_axn_for(target:)
|
32
|
+
# Check if the target already has this action class cached
|
33
|
+
cache_key = "#{@name}_#{object_id}_#{target.object_id}"
|
34
|
+
|
35
|
+
# Use a class variable to store the cache on the target
|
36
|
+
cache_var = :@_axn_cache
|
37
|
+
target.instance_variable_set(cache_var, {}) unless target.instance_variable_defined?(cache_var)
|
38
|
+
cache = target.instance_variable_get(cache_var)
|
39
|
+
|
40
|
+
return cache[cache_key] if cache.key?(cache_key)
|
41
|
+
|
42
|
+
# Check if constant is already registered
|
43
|
+
action_class_builder = Helpers::ClassBuilder.new(self)
|
44
|
+
namespace = Helpers::NamespaceManager.get_or_create_namespace(target)
|
45
|
+
constant_name = action_class_builder.generate_constant_name(@name.to_s)
|
46
|
+
if namespace.const_defined?(constant_name, false)
|
47
|
+
mounted_axn = namespace.const_get(constant_name)
|
48
|
+
cache[cache_key] = mounted_axn
|
49
|
+
return mounted_axn
|
50
|
+
end
|
51
|
+
|
52
|
+
# Build and configure action class
|
53
|
+
mounted_axn = action_class_builder.build_and_configure_action_class(target, @name.to_s, namespace)
|
54
|
+
|
55
|
+
# Cache on the target
|
56
|
+
cache[cache_key] = mounted_axn
|
57
|
+
mounted_axn
|
58
|
+
end
|
59
|
+
|
60
|
+
def mounted? = @mounted_axn.present?
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def method_name = @name.to_s.underscore
|
65
|
+
|
66
|
+
def validate_before_mount!(target:)
|
67
|
+
# Method name collision validation is now handled in mount_axn
|
68
|
+
# This method is kept for potential future validation needs
|
69
|
+
end
|
70
|
+
|
71
|
+
def mounting_type_name
|
72
|
+
mount_strategy.name.split("::").last.underscore.to_s.humanize
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|