axn 0.1.0.pre.alpha.2.8 → 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 +47 -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 +43 -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 +6 -6
- 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
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "axn/mountable/inherit_profiles"
|
4
|
+
|
5
|
+
module Axn
|
6
|
+
module Mountable
|
7
|
+
module Helpers
|
8
|
+
# Handles building and configuring Axn action classes for mounting
|
9
|
+
class ClassBuilder
|
10
|
+
def initialize(descriptor)
|
11
|
+
@descriptor = descriptor
|
12
|
+
end
|
13
|
+
|
14
|
+
def mount(target, name)
|
15
|
+
namespace = Helpers::NamespaceManager.get_or_create_namespace(target)
|
16
|
+
return unless should_register_constant?(namespace)
|
17
|
+
|
18
|
+
build_and_configure_action_class(target, name, namespace)
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate_constant_name(name)
|
22
|
+
name.to_s.parameterize(separator: "_").classify
|
23
|
+
end
|
24
|
+
|
25
|
+
def build_and_configure_action_class(target, name, namespace)
|
26
|
+
mounted_axn = build_action_class(target)
|
27
|
+
configure_class_name_and_constant(mounted_axn, name, namespace)
|
28
|
+
configure_axn_mounted_to(mounted_axn, target)
|
29
|
+
mounted_axn
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def build_action_class(target)
|
35
|
+
existing_axn_klass = @descriptor.instance_variable_get(:@existing_axn_klass)
|
36
|
+
return existing_axn_klass if existing_axn_klass
|
37
|
+
|
38
|
+
kwargs = @descriptor.instance_variable_get(:@kwargs)
|
39
|
+
block = @descriptor.instance_variable_get(:@block)
|
40
|
+
|
41
|
+
# Remove axn_klass from kwargs as it's not a valid parameter for Factory.build
|
42
|
+
factory_kwargs = kwargs.except(:axn_klass)
|
43
|
+
|
44
|
+
unless factory_kwargs.key?(:superclass)
|
45
|
+
# Get inherit configuration
|
46
|
+
inherit_config = @descriptor.options[:inherit]
|
47
|
+
|
48
|
+
# Determine superclass based on inherit configuration
|
49
|
+
factory_kwargs[:superclass] = create_superclass_for_inherit_mode(target, inherit_config)
|
50
|
+
end
|
51
|
+
|
52
|
+
Axn::Factory.build(**factory_kwargs, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def configure_class_name_and_constant(axn_klass, name, axn_namespace)
|
56
|
+
configure_class_name(axn_klass, name, axn_namespace) if name.present?
|
57
|
+
register_constant(axn_klass, name, axn_namespace) if should_register_constant?(axn_namespace)
|
58
|
+
end
|
59
|
+
|
60
|
+
def configure_axn_mounted_to(axn_klass, target)
|
61
|
+
axn_klass.define_singleton_method(:__axn_mounted_to__) { target }
|
62
|
+
axn_klass.define_method(:__axn_mounted_to__) { target }
|
63
|
+
end
|
64
|
+
|
65
|
+
def should_register_constant?(axn_namespace)
|
66
|
+
axn_namespace&.name&.end_with?("::Axns")
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_superclass_for_inherit_mode(target, inherit_config)
|
70
|
+
# Handle module targets - convert to a base class first, then apply inherit mode
|
71
|
+
target = create_module_base_class(target) if target.is_a?(Module) && !target.is_a?(Class)
|
72
|
+
|
73
|
+
# Resolve inherit configuration to a hash
|
74
|
+
resolved_config = InheritProfiles.resolve(inherit_config)
|
75
|
+
|
76
|
+
# If nothing should be inherited, return Object
|
77
|
+
return Object if resolved_config.values.none?
|
78
|
+
|
79
|
+
# If everything should be inherited, return target as-is
|
80
|
+
return target if resolved_config.values.all?
|
81
|
+
|
82
|
+
# Otherwise, create a class with selective inheritance
|
83
|
+
create_class_with_selective_inheritance(target, resolved_config)
|
84
|
+
end
|
85
|
+
|
86
|
+
def create_module_base_class(target_module)
|
87
|
+
# Create a base class that includes the target module
|
88
|
+
# This allows the action class to inherit from a class while still having access to module methods
|
89
|
+
Class.new do
|
90
|
+
include target_module
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_class_with_selective_inheritance(target, inherit_config)
|
95
|
+
# Create a class that inherits from target but selectively clears features
|
96
|
+
Class.new(target) do
|
97
|
+
# Only clear Axn-specific attributes if target includes Axn
|
98
|
+
if respond_to?(:internal_field_configs=)
|
99
|
+
# Clear fields if not inherited
|
100
|
+
unless inherit_config[:fields]
|
101
|
+
self.internal_field_configs = []
|
102
|
+
self.external_field_configs = []
|
103
|
+
end
|
104
|
+
|
105
|
+
# Clear hooks if not inherited
|
106
|
+
unless inherit_config[:hooks]
|
107
|
+
self.around_hooks = []
|
108
|
+
self.before_hooks = []
|
109
|
+
self.after_hooks = []
|
110
|
+
end
|
111
|
+
|
112
|
+
# Clear callbacks if not inherited
|
113
|
+
self._callbacks_registry = Axn::Core::Flow::Handlers::Registry.empty unless inherit_config[:callbacks]
|
114
|
+
|
115
|
+
# Clear messages if not inherited
|
116
|
+
self._messages_registry = Axn::Core::Flow::Handlers::Registry.empty unless inherit_config[:messages]
|
117
|
+
|
118
|
+
# Clear async config if not inherited (nil = use Axn.config defaults)
|
119
|
+
unless inherit_config[:async]
|
120
|
+
self._async_adapter = nil
|
121
|
+
self._async_config = nil
|
122
|
+
self._async_config_block = nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# NOTE: Strategies are always inherited as they're mixed in via `include` and become
|
127
|
+
# part of the ancestry chain. This cannot be controlled via the inherit configuration.
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def configure_class_name(axn_klass, name, axn_namespace)
|
132
|
+
class_name = name.to_s.classify
|
133
|
+
|
134
|
+
axn_klass.define_singleton_method(:name) do
|
135
|
+
# Evaluate namespace name dynamically when the method is called
|
136
|
+
current_namespace_name = axn_namespace&.name
|
137
|
+
|
138
|
+
if current_namespace_name&.end_with?("::Axns")
|
139
|
+
# We're already in a namespace, just add the method name
|
140
|
+
"#{current_namespace_name}::#{class_name}"
|
141
|
+
elsif current_namespace_name
|
142
|
+
# Create the Axns namespace
|
143
|
+
"#{current_namespace_name}::Axns::#{class_name}"
|
144
|
+
else
|
145
|
+
# Fallback for anonymous classes
|
146
|
+
"AnonymousAxn::#{class_name}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def register_constant(axn_klass, name, axn_namespace)
|
152
|
+
constant_name = generate_constant_name(name)
|
153
|
+
|
154
|
+
# Only set the constant if it doesn't exist
|
155
|
+
return if axn_namespace.const_defined?(constant_name, false)
|
156
|
+
|
157
|
+
axn_namespace.const_set(constant_name, axn_klass)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "axn/mountable/descriptor"
|
4
|
+
|
5
|
+
module Axn
|
6
|
+
module Mountable
|
7
|
+
module Helpers
|
8
|
+
# Helper class for mounting actions via strategies
|
9
|
+
class Mounter
|
10
|
+
# Mount an action using the specified strategy
|
11
|
+
#
|
12
|
+
# @param target [Class] The target class to mount the action to
|
13
|
+
# @param as [Symbol] The strategy to use (:axn, :method, :step, :enqueue_all)
|
14
|
+
# @param name [Symbol] The name of the action
|
15
|
+
# @param axn_klass [Class, nil] Optional existing action class
|
16
|
+
# @param kwargs [Hash] Additional strategy-specific options
|
17
|
+
# @param block [Proc] The action block
|
18
|
+
def self.mount_via_strategy(
|
19
|
+
target:,
|
20
|
+
as: :axn,
|
21
|
+
name: nil,
|
22
|
+
axn_klass: nil,
|
23
|
+
**kwargs,
|
24
|
+
&block
|
25
|
+
)
|
26
|
+
descriptor = Descriptor.new(name:, axn_klass:, as:, block:, kwargs:)
|
27
|
+
target._mounted_axn_descriptors += [descriptor]
|
28
|
+
descriptor.mount(target:)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Mountable
|
5
|
+
module Helpers
|
6
|
+
# Handles namespace management for mounting
|
7
|
+
module NamespaceManager
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def get_or_create_namespace(target)
|
11
|
+
# Check if :Axns is defined directly on this class (not inherited)
|
12
|
+
if target.const_defined?(:Axns, false)
|
13
|
+
axn_class = target.const_get(:Axns)
|
14
|
+
return axn_class if axn_class.is_a?(Class)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Create a namespace class that inherits from parent's Axns if available
|
18
|
+
client_class = target
|
19
|
+
parent_axns = find_parent_axns_namespace(client_class)
|
20
|
+
namespace_class = create_namespace_class(client_class, parent_axns)
|
21
|
+
|
22
|
+
# Only set the constant if it doesn't exist
|
23
|
+
return if target.const_defined?(:Axns, false)
|
24
|
+
|
25
|
+
target.const_set(:Axns, namespace_class)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def find_parent_axns_namespace(client_class)
|
31
|
+
return nil unless client_class.is_a?(Class)
|
32
|
+
return nil unless client_class.superclass.respond_to?(:_mounted_axn_descriptors)
|
33
|
+
return nil unless client_class.superclass.const_defined?(:Axns, false)
|
34
|
+
|
35
|
+
client_class.superclass.const_get(:Axns)
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_namespace_class(client_class, parent_axns)
|
39
|
+
base_class = parent_axns || Class.new
|
40
|
+
|
41
|
+
Class.new(base_class) do
|
42
|
+
define_singleton_method(:__axn_mounted_to__) { client_class }
|
43
|
+
|
44
|
+
define_singleton_method(:name) do
|
45
|
+
client_name = client_class.name.presence || "AnonymousClient_#{client_class.object_id}"
|
46
|
+
"#{client_name}::Axns"
|
47
|
+
end
|
48
|
+
end.tap do |ns|
|
49
|
+
update_inherited_action_classes(ns, client_class) if parent_axns
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def update_inherited_action_classes(namespace, client_class)
|
54
|
+
namespace.constants.each do |const_name|
|
55
|
+
const_value = namespace.const_get(const_name)
|
56
|
+
next unless const_value.is_a?(Class) && const_value.respond_to?(:__axn_mounted_to__)
|
57
|
+
|
58
|
+
# Update __axn_mounted_to__ method on the existing class
|
59
|
+
const_value.define_singleton_method(:__axn_mounted_to__) { client_class }
|
60
|
+
const_value.define_method(:__axn_mounted_to__) { client_class }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Mountable
|
5
|
+
module Helpers
|
6
|
+
# Handles validation logic for Descriptor
|
7
|
+
class Validator
|
8
|
+
def initialize(descriptor)
|
9
|
+
@descriptor = descriptor
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate!
|
13
|
+
validate_name!
|
14
|
+
validate_axn_class_or_block!
|
15
|
+
validate_method_name!(@descriptor.name.to_s)
|
16
|
+
validate_superclass_and_inherit_conflict!
|
17
|
+
|
18
|
+
if @descriptor.existing_axn_klass
|
19
|
+
validate_existing_axn_class!
|
20
|
+
elsif @descriptor.block.present?
|
21
|
+
validate_callable!(@descriptor.block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def validate_name!
|
28
|
+
name = @descriptor.name
|
29
|
+
invalid!("name must be a string or symbol") unless name.is_a?(String) || name.is_a?(Symbol)
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_axn_class_or_block!
|
33
|
+
existing_axn_klass = @descriptor.existing_axn_klass
|
34
|
+
block = @descriptor.block
|
35
|
+
|
36
|
+
invalid!("must provide either an axn class or a block") if existing_axn_klass.nil? && block.nil?
|
37
|
+
|
38
|
+
return unless existing_axn_klass.present? && block.present?
|
39
|
+
|
40
|
+
invalid!("cannot provide both an axn class and a block")
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_method_name!(method_name)
|
44
|
+
# Check for empty names
|
45
|
+
invalid!("method name cannot be empty") if method_name.nil? || method_name == ""
|
46
|
+
|
47
|
+
# Check for whitespace-only names
|
48
|
+
invalid!("method name '#{method_name}' must be convertible to a valid constant name") if method_name.strip.empty?
|
49
|
+
|
50
|
+
# Check for names that don't start with a letter (only reject numbers)
|
51
|
+
invalid!("method name '#{method_name}' must be convertible to a valid constant name") if method_name.match?(/\A[0-9]/)
|
52
|
+
|
53
|
+
# Check for method suffixes that would conflict with generated methods
|
54
|
+
return unless method_name.match?(/[!?=]/)
|
55
|
+
|
56
|
+
invalid!("method name '#{method_name}' cannot contain method suffixes")
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate_callable!(callable)
|
60
|
+
return if callable.respond_to?(:call)
|
61
|
+
|
62
|
+
invalid!("block must be callable (respond to :call)")
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_superclass_and_inherit_conflict!
|
66
|
+
# Check if user explicitly provided superclass in kwargs
|
67
|
+
return unless @descriptor.kwargs.key?(:superclass)
|
68
|
+
|
69
|
+
# Get the inherit option value
|
70
|
+
inherit_option = @descriptor.options[:inherit]
|
71
|
+
|
72
|
+
# Get the default inherit value for this strategy
|
73
|
+
default_inherit = mount_strategy.default_inherit_mode
|
74
|
+
|
75
|
+
# If inherit was explicitly provided and differs from default, raise error
|
76
|
+
return if inherit_option == default_inherit
|
77
|
+
|
78
|
+
invalid!("cannot specify both 'superclass:' and 'inherit:' options - use one or the other")
|
79
|
+
end
|
80
|
+
|
81
|
+
def validate_existing_axn_class!
|
82
|
+
existing_axn_klass = @descriptor.existing_axn_klass
|
83
|
+
|
84
|
+
invalid!("axn class must be a Class") unless existing_axn_klass.is_a?(Class)
|
85
|
+
|
86
|
+
invalid!("axn class must include Axn module") unless existing_axn_klass.included_modules.include?(::Axn) || existing_axn_klass < ::Axn
|
87
|
+
|
88
|
+
# Check raw_kwargs (before preprocessing) to see if user provided any factory kwargs
|
89
|
+
# Exclude axn_klass and all strategy-specific kwargs
|
90
|
+
user_provided_kwargs = @descriptor.raw_kwargs.except(:axn_klass, *mount_strategy.strategy_specific_kwargs)
|
91
|
+
|
92
|
+
return unless user_provided_kwargs.present? && mount_strategy != MountingStrategies::Step
|
93
|
+
|
94
|
+
invalid!("was given an existing axn class and also keyword arguments - only one is allowed")
|
95
|
+
end
|
96
|
+
|
97
|
+
def mount_strategy
|
98
|
+
@descriptor.mount_strategy
|
99
|
+
end
|
100
|
+
|
101
|
+
def mounting_type_name
|
102
|
+
mount_strategy = @descriptor.mount_strategy
|
103
|
+
mount_strategy.name.split("::").last.underscore.to_s.humanize
|
104
|
+
end
|
105
|
+
|
106
|
+
def invalid!(msg)
|
107
|
+
raise MountingError, "#{mounting_type_name} #{msg}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Mountable
|
5
|
+
module InheritProfiles
|
6
|
+
# Predefined inheritance profiles for mounting strategies
|
7
|
+
PROFILES = {
|
8
|
+
# Inherits parent's lifecycle (hooks, callbacks, messages, async) but not fields
|
9
|
+
# Use this when the mounted action should participate in the parent's execution lifecycle
|
10
|
+
# but have its own independent contract
|
11
|
+
# NOTE: Strategies cannot be controlled - they're mixed in via `include` and become part of the ancestry
|
12
|
+
lifecycle: {
|
13
|
+
fields: false,
|
14
|
+
messages: true,
|
15
|
+
hooks: true,
|
16
|
+
callbacks: true,
|
17
|
+
async: true,
|
18
|
+
}.freeze,
|
19
|
+
|
20
|
+
# Only inherits async config - for utility methods like enqueue_all
|
21
|
+
# Use this when you need async capability but nothing else from the parent
|
22
|
+
async_only: {
|
23
|
+
fields: false,
|
24
|
+
messages: false,
|
25
|
+
hooks: false,
|
26
|
+
callbacks: false,
|
27
|
+
async: true,
|
28
|
+
}.freeze,
|
29
|
+
|
30
|
+
# Inherits nothing - completely standalone
|
31
|
+
# Use this when the mounted action should be completely independent from the parent
|
32
|
+
none: {
|
33
|
+
fields: false,
|
34
|
+
messages: false,
|
35
|
+
hooks: false,
|
36
|
+
callbacks: false,
|
37
|
+
async: false,
|
38
|
+
}.freeze,
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
# Resolve an inherit configuration to a full hash
|
42
|
+
# @param inherit [Symbol, Hash] The inherit configuration
|
43
|
+
# @return [Hash] A hash with all inheritance options set to true/false
|
44
|
+
def self.resolve(inherit)
|
45
|
+
case inherit
|
46
|
+
when Symbol
|
47
|
+
PROFILES.fetch(inherit) do
|
48
|
+
raise ArgumentError, "Unknown inherit profile: #{inherit.inspect}. Valid profiles: #{PROFILES.keys.join(", ")}"
|
49
|
+
end
|
50
|
+
when Hash
|
51
|
+
# Validate hash keys
|
52
|
+
invalid_keys = inherit.keys - PROFILES[:none].keys
|
53
|
+
raise ArgumentError, "Invalid inherit keys: #{invalid_keys.join(", ")}. Valid keys: #{PROFILES[:none].keys.join(", ")}" if invalid_keys.any?
|
54
|
+
|
55
|
+
# Merge with none profile to ensure all keys are present
|
56
|
+
PROFILES[:none].merge(inherit)
|
57
|
+
else
|
58
|
+
raise ArgumentError, "inherit must be a Symbol or Hash. Got: #{inherit.class}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Check if a specific feature should be inherited
|
63
|
+
# @param inherit [Symbol, Hash] The inherit configuration
|
64
|
+
# @param feature [Symbol] The feature to check (e.g., :hooks, :async)
|
65
|
+
# @return [Boolean]
|
66
|
+
def self.inherit?(inherit, feature)
|
67
|
+
resolved = resolve(inherit)
|
68
|
+
resolved.fetch(feature)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "axn/exceptions"
|
4
|
+
require "axn/mountable/descriptor"
|
5
|
+
|
6
|
+
module Axn
|
7
|
+
module Mountable
|
8
|
+
class MountingStrategies
|
9
|
+
# Base module for all attachment strategies
|
10
|
+
module Base
|
11
|
+
# Hooks for strategy modules to configure themselves
|
12
|
+
def preprocess_kwargs(**kwargs) = kwargs
|
13
|
+
def strategy_specific_kwargs = [:inherit]
|
14
|
+
|
15
|
+
def default_inherit_mode = raise ArgumentError, "Strategy modules must implement default_inherit_mode"
|
16
|
+
|
17
|
+
# The actual per-strategy mounting logic
|
18
|
+
def mount(descriptor:, target:)
|
19
|
+
mount_to_namespace(descriptor:, target:)
|
20
|
+
mount_to_target(descriptor:, target:)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Mount methods directly to the target class
|
24
|
+
def mount_to_target(descriptor:, target:) = raise NotImplementedError, "Strategy modules must implement mount_to_target"
|
25
|
+
|
26
|
+
def key = name.split("::").last.underscore.to_sym
|
27
|
+
|
28
|
+
# Helper method to define a method on target with collision checking
|
29
|
+
def mount_method(target:, method_name:, &)
|
30
|
+
# Check if method collision should raise an error
|
31
|
+
# We allow overriding if this is a child class with a parent that has axn methods (inheritance scenario)
|
32
|
+
# Otherwise, we raise an error for same-class method collisions
|
33
|
+
if _should_raise_method_collision_error?(target, method_name)
|
34
|
+
raise MountingError, "#{name.split("::").last} unable to attach -- method '#{method_name}' is already taken"
|
35
|
+
end
|
36
|
+
|
37
|
+
target.define_singleton_method(method_name, &)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Mount methods to the namespace and register the action class
|
43
|
+
def mount_to_namespace(descriptor:, target:)
|
44
|
+
action_class_builder = Helpers::ClassBuilder.new(descriptor)
|
45
|
+
namespace = Helpers::NamespaceManager.get_or_create_namespace(target)
|
46
|
+
name = descriptor.name
|
47
|
+
descriptor_ref = descriptor
|
48
|
+
|
49
|
+
# Mount methods that delegate to the cached action
|
50
|
+
namespace.define_singleton_method(name) do |**kwargs|
|
51
|
+
axn = descriptor_ref.mounted_axn_for(target:)
|
52
|
+
axn.call(**kwargs)
|
53
|
+
end
|
54
|
+
|
55
|
+
namespace.define_singleton_method("#{name}!") do |**kwargs|
|
56
|
+
axn = descriptor_ref.mounted_axn_for(target:)
|
57
|
+
axn.call!(**kwargs)
|
58
|
+
end
|
59
|
+
|
60
|
+
namespace.define_singleton_method("#{name}_async") do |**kwargs|
|
61
|
+
axn = descriptor_ref.mounted_axn_for(target:)
|
62
|
+
axn.call_async(**kwargs)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Register the action class as a constant in the namespace
|
66
|
+
action_class_builder.mount(target, name.to_s)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check if we should raise an error for method collision
|
70
|
+
# Returns true if method exists AND target is not overriding a parent's method (same-class collision)
|
71
|
+
def _should_raise_method_collision_error?(target, method_name)
|
72
|
+
return false unless target.respond_to?(method_name)
|
73
|
+
|
74
|
+
# Check if this is an inheritance override by seeing if the parent has the same method
|
75
|
+
is_inheritance_override = target.superclass&.respond_to?(method_name)
|
76
|
+
|
77
|
+
# Only raise error if it's a same-class collision (not inheritance override)
|
78
|
+
!is_inheritance_override
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Mountable
|
5
|
+
class MountingStrategies
|
6
|
+
module Axn
|
7
|
+
include Base
|
8
|
+
extend self # rubocop:disable Style/ModuleFunction -- module_function breaks inheritance
|
9
|
+
|
10
|
+
def default_inherit_mode = :lifecycle
|
11
|
+
|
12
|
+
module DSL
|
13
|
+
def mount_axn(name, axn_klass = nil, inherit: MountingStrategies::Axn.default_inherit_mode, **, &)
|
14
|
+
# mount_axn defaults to :lifecycle - participates in parent's execution lifecycle
|
15
|
+
Helpers::Mounter.mount_via_strategy(
|
16
|
+
target: self,
|
17
|
+
as: :axn,
|
18
|
+
name:,
|
19
|
+
axn_klass:,
|
20
|
+
inherit:,
|
21
|
+
**,
|
22
|
+
&
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def mount_to_target(descriptor:, target:)
|
28
|
+
name = descriptor.name
|
29
|
+
|
30
|
+
mount_method(target:, method_name: name) do |**kwargs|
|
31
|
+
axn = descriptor.mounted_axn_for(target: self)
|
32
|
+
axn.call(**kwargs)
|
33
|
+
end
|
34
|
+
|
35
|
+
mount_method(target:, method_name: "#{name}!") do |**kwargs|
|
36
|
+
axn = descriptor.mounted_axn_for(target: self)
|
37
|
+
axn.call!(**kwargs)
|
38
|
+
end
|
39
|
+
|
40
|
+
mount_method(target:, method_name: "#{name}_async") do |**kwargs|
|
41
|
+
axn = descriptor.mounted_axn_for(target: self)
|
42
|
+
axn.call_async(**kwargs)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Mountable
|
5
|
+
class MountingStrategies
|
6
|
+
module EnqueueAll
|
7
|
+
include Base
|
8
|
+
extend self # rubocop:disable Style/ModuleFunction -- module_function breaks inheritance
|
9
|
+
|
10
|
+
def default_inherit_mode = :async_only
|
11
|
+
|
12
|
+
module DSL
|
13
|
+
def enqueue_all_via(axn_klass = nil, inherit: MountingStrategies::EnqueueAll.default_inherit_mode, **, &)
|
14
|
+
# enqueue_all_via defaults to :async_only - only needs async config for batch enqueuing
|
15
|
+
Helpers::Mounter.mount_via_strategy(
|
16
|
+
target: self,
|
17
|
+
as: :enqueue_all,
|
18
|
+
name: "enqueue_all",
|
19
|
+
axn_klass:,
|
20
|
+
inherit:,
|
21
|
+
**,
|
22
|
+
&
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def mount_to_target(descriptor:, target:)
|
28
|
+
name = descriptor.name
|
29
|
+
|
30
|
+
mount_method(target:, method_name: name) do |**kwargs|
|
31
|
+
axn = descriptor.mounted_axn_for(target: self)
|
32
|
+
axn.call!(**kwargs)
|
33
|
+
true # Raise or return true
|
34
|
+
end
|
35
|
+
|
36
|
+
mount_method(target:, method_name: "#{name}_async") do |**kwargs|
|
37
|
+
axn = descriptor.mounted_axn_for(target: self)
|
38
|
+
axn.call_async(**kwargs)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def mount_to_namespace(descriptor:, target:)
|
43
|
+
super
|
44
|
+
|
45
|
+
# Add enqueue shortcut to enqueue the *attached-to* axn without
|
46
|
+
# the user having to reference __axn_mounted_to__ in their own code
|
47
|
+
mounted_axn = descriptor.mounted_axn_for(target:)
|
48
|
+
mounted_axn.define_method(:enqueue) do |**kwargs|
|
49
|
+
__axn_mounted_to__.call_async(**kwargs)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|