axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.4

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/commands/pr.md +36 -0
  3. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  4. data/.cursor/rules/general-coding-standards.mdc +27 -0
  5. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  6. data/CHANGELOG.md +57 -0
  7. data/Rakefile +114 -4
  8. data/docs/.vitepress/config.mjs +19 -10
  9. data/docs/advanced/conventions.md +3 -3
  10. data/docs/advanced/mountable.md +476 -0
  11. data/docs/advanced/profiling.md +351 -0
  12. data/docs/advanced/rough.md +27 -8
  13. data/docs/index.md +5 -3
  14. data/docs/intro/about.md +1 -1
  15. data/docs/intro/overview.md +6 -6
  16. data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
  17. data/docs/recipes/memoization.md +103 -18
  18. data/docs/recipes/rubocop-integration.md +38 -284
  19. data/docs/recipes/testing.md +14 -14
  20. data/docs/recipes/validating-user-input.md +1 -1
  21. data/docs/reference/async.md +429 -0
  22. data/docs/reference/axn-result.md +107 -0
  23. data/docs/reference/class.md +225 -64
  24. data/docs/reference/configuration.md +366 -34
  25. data/docs/reference/form-object.md +252 -0
  26. data/docs/reference/instance.md +14 -29
  27. data/docs/strategies/client.md +212 -0
  28. data/docs/strategies/form.md +235 -0
  29. data/docs/strategies/index.md +21 -21
  30. data/docs/strategies/transaction.md +1 -1
  31. data/docs/usage/setup.md +16 -2
  32. data/docs/usage/steps.md +7 -7
  33. data/docs/usage/using.md +23 -12
  34. data/docs/usage/writing.md +191 -12
  35. data/lib/axn/async/adapters/active_job.rb +74 -0
  36. data/lib/axn/async/adapters/disabled.rb +41 -0
  37. data/lib/axn/async/adapters/sidekiq.rb +67 -0
  38. data/lib/axn/async/adapters.rb +26 -0
  39. data/lib/axn/async/batch_enqueue/config.rb +38 -0
  40. data/lib/axn/async/batch_enqueue.rb +99 -0
  41. data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
  42. data/lib/axn/async.rb +178 -0
  43. data/lib/axn/configuration.rb +113 -0
  44. data/lib/{action → axn}/context.rb +22 -4
  45. data/lib/axn/core/automatic_logging.rb +89 -0
  46. data/lib/axn/core/context/facade.rb +69 -0
  47. data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
  48. data/lib/{action → axn}/core/context/internal.rb +5 -5
  49. data/lib/{action → axn}/core/contract.rb +111 -73
  50. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  51. data/lib/{action → axn}/core/contract_validation.rb +27 -12
  52. data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
  53. data/lib/axn/core/default_call.rb +63 -0
  54. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  55. data/lib/axn/core/field_resolvers/model.rb +63 -0
  56. data/lib/axn/core/field_resolvers.rb +24 -0
  57. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  58. data/lib/{action → axn}/core/flow/exception_execution.rb +9 -13
  59. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  60. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  61. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +23 -11
  62. data/lib/axn/core/flow/handlers/invoker.rb +47 -0
  63. data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
  64. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  65. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  66. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  67. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  68. data/lib/axn/core/flow/handlers.rb +20 -0
  69. data/lib/{action → axn}/core/flow/messages.rb +8 -8
  70. data/lib/{action → axn}/core/flow.rb +4 -4
  71. data/lib/{action → axn}/core/hooks.rb +17 -5
  72. data/lib/axn/core/logging.rb +48 -0
  73. data/lib/axn/core/memoization.rb +53 -0
  74. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  75. data/lib/{action → axn}/core/timing.rb +1 -1
  76. data/lib/axn/core/tracing.rb +90 -0
  77. data/lib/axn/core/use_strategy.rb +29 -0
  78. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  79. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  80. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  81. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  82. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  83. data/lib/{action → axn}/core.rb +55 -55
  84. data/lib/{action → axn}/exceptions.rb +12 -2
  85. data/lib/axn/extras/strategies/client.rb +150 -0
  86. data/lib/axn/extras/strategies/vernier.rb +121 -0
  87. data/lib/axn/extras.rb +4 -0
  88. data/lib/axn/factory.rb +122 -34
  89. data/lib/axn/form_object.rb +90 -0
  90. data/lib/axn/internal/logging.rb +30 -0
  91. data/lib/axn/internal/registry.rb +87 -0
  92. data/lib/axn/mountable/descriptor.rb +76 -0
  93. data/lib/axn/mountable/helpers/class_builder.rb +193 -0
  94. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  95. data/lib/axn/mountable/helpers/namespace_manager.rb +38 -0
  96. data/lib/axn/mountable/helpers/validator.rb +112 -0
  97. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  98. data/lib/axn/mountable/mounting_strategies/_base.rb +87 -0
  99. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  100. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  101. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  102. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  103. data/lib/axn/mountable.rb +119 -0
  104. data/lib/axn/rails/engine.rb +51 -0
  105. data/lib/axn/rails/generators/axn_generator.rb +86 -0
  106. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  107. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  108. data/lib/{action → axn}/result.rb +32 -13
  109. data/lib/axn/strategies/form.rb +98 -0
  110. data/lib/axn/strategies/transaction.rb +26 -0
  111. data/lib/axn/strategies.rb +20 -0
  112. data/lib/axn/testing/spec_helpers.rb +6 -8
  113. data/lib/axn/util/callable.rb +120 -0
  114. data/lib/axn/util/contract_error_handling.rb +32 -0
  115. data/lib/axn/util/execution_context.rb +34 -0
  116. data/lib/axn/util/global_id_serialization.rb +52 -0
  117. data/lib/axn/util/logging.rb +87 -0
  118. data/lib/axn/util/memoization.rb +20 -0
  119. data/lib/axn/version.rb +1 -1
  120. data/lib/axn.rb +26 -16
  121. data/lib/rubocop/cop/axn/README.md +23 -23
  122. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  123. metadata +106 -64
  124. data/.rspec +0 -3
  125. data/.rubocop.yml +0 -76
  126. data/.tool-versions +0 -1
  127. data/docs/reference/action-result.md +0 -37
  128. data/lib/action/attachable/base.rb +0 -43
  129. data/lib/action/attachable/steps.rb +0 -63
  130. data/lib/action/attachable/subactions.rb +0 -70
  131. data/lib/action/attachable.rb +0 -17
  132. data/lib/action/configuration.rb +0 -55
  133. data/lib/action/core/automatic_logging.rb +0 -93
  134. data/lib/action/core/context/facade.rb +0 -48
  135. data/lib/action/core/flow/handlers/invoker.rb +0 -73
  136. data/lib/action/core/flow/handlers.rb +0 -20
  137. data/lib/action/core/logging.rb +0 -37
  138. data/lib/action/core/tracing.rb +0 -17
  139. data/lib/action/core/use_strategy.rb +0 -30
  140. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  141. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  142. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  143. data/lib/action/enqueueable.rb +0 -13
  144. data/lib/action/strategies/transaction.rb +0 -19
  145. data/lib/action/strategies.rb +0 -48
  146. data/lib/axn/util.rb +0 -24
  147. data/package.json +0 -10
  148. data/yarn.lock +0 -1166
data/lib/axn/factory.rb CHANGED
@@ -2,13 +2,20 @@
2
2
 
3
3
  module Axn
4
4
  class Factory
5
+ NOT_PROVIDED = :__not_provided__
6
+
5
7
  class << self
6
8
  # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/ParameterLists
7
9
  def build(
10
+ callable = nil,
8
11
  # Builder-specific options
9
- name: nil,
10
12
  superclass: nil,
11
- expose_return_as: :nil,
13
+ expose_return_as: nil,
14
+
15
+ # Module inclusion options
16
+ include: [],
17
+ extend: [],
18
+ prepend: [],
12
19
 
13
20
  # Expose standard class-level options
14
21
  exposes: [],
@@ -30,19 +37,35 @@ module Axn
30
37
  # Strategies
31
38
  use: [],
32
39
 
40
+ # Async configuration
41
+ async: nil,
42
+
43
+ # Logging configuration
44
+ log_calls: NOT_PROVIDED,
45
+ log_errors: NOT_PROVIDED,
46
+
47
+ # Internal flag to prevent recursion during action class creation
48
+ # Tracks which target class is having an action class created for it
49
+ _creating_action_class_for: nil,
50
+
33
51
  &block
34
52
  )
35
- args = block.parameters.each_with_object(_hash_with_default_array) { |(type, field), hash| hash[type] << field }
53
+ raise ArgumentError, "[Axn::Factory] Cannot receive both a callable and a block" if callable.present? && block_given?
54
+
55
+ executable = callable || block
56
+ raise ArgumentError, "[Axn::Factory] Must provide either a callable or a block" unless executable
57
+
58
+ args = executable.parameters.each_with_object(_hash_with_default_array) { |(type, field), hash| hash[type] << field }
36
59
 
37
60
  if args[:opt].present? || args[:req].present? || args[:rest].present?
38
61
  raise ArgumentError,
39
- "[Axn::Factory] Cannot convert block to action: block expects positional arguments"
62
+ "[Axn::Factory] Cannot convert callable to action: callable expects positional arguments"
40
63
  end
41
- raise ArgumentError, "[Axn::Factory] Cannot convert block to action: block expects a splat of keyword arguments" if args[:keyrest].present?
64
+ raise ArgumentError, "[Axn::Factory] Cannot convert callable to action: callable expects a splat of keyword arguments" if args[:keyrest].present?
42
65
 
43
66
  if args[:key].present?
44
67
  raise ArgumentError,
45
- "[Axn::Factory] Cannot convert block to action: block expects keyword arguments with defaults (ruby does not allow introspecting)"
68
+ "[Axn::Factory] Cannot convert callable to action: callable expects keyword arguments with defaults (ruby does not allow introspecting)"
46
69
  end
47
70
 
48
71
  expects = _hydrate_hash(expects)
@@ -53,25 +76,7 @@ module Axn
53
76
  end
54
77
 
55
78
  # NOTE: inheriting from wrapping class, so we can set default values (e.g. for HTTP headers)
56
- Class.new(superclass || Object) do
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|
79
+ _build_axn_class(superclass:, args:, executable:, expose_return_as:, include:, extend:, prepend:, _creating_action_class_for:).tap do |axn|
75
80
  expects.each do |field, opts|
76
81
  axn.expects(field, **opts)
77
82
  end
@@ -80,9 +85,13 @@ module Axn
80
85
  axn.exposes(field, **opts)
81
86
  end
82
87
 
88
+ # Apply logging configuration (always apply if provided to override defaults)
89
+ axn.log_calls(log_calls) unless log_calls == NOT_PROVIDED
90
+ axn.log_errors(log_errors) unless log_errors == NOT_PROVIDED
91
+
83
92
  # Apply success and error handlers
84
- _apply_handlers(axn, :success, success, Action::Core::Flow::Handlers::Descriptors::MessageDescriptor)
85
- _apply_handlers(axn, :error, error, Action::Core::Flow::Handlers::Descriptors::MessageDescriptor)
93
+ _apply_handlers(axn, :success, success, Axn::Core::Flow::Handlers::Descriptors::MessageDescriptor)
94
+ _apply_handlers(axn, :error, error, Axn::Core::Flow::Handlers::Descriptors::MessageDescriptor)
86
95
 
87
96
  # Hooks
88
97
  axn.before(before) if before.present?
@@ -90,10 +99,10 @@ module Axn
90
99
  axn.around(around) if around.present?
91
100
 
92
101
  # Callbacks
93
- _apply_handlers(axn, :on_success, on_success, Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
94
- _apply_handlers(axn, :on_failure, on_failure, Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
95
- _apply_handlers(axn, :on_error, on_error, Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
96
- _apply_handlers(axn, :on_exception, on_exception, Action::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
102
+ _apply_handlers(axn, :on_success, on_success, Axn::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
103
+ _apply_handlers(axn, :on_failure, on_failure, Axn::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
104
+ _apply_handlers(axn, :on_error, on_error, Axn::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
105
+ _apply_handlers(axn, :on_exception, on_exception, Axn::Core::Flow::Handlers::Descriptors::CallbackDescriptor)
97
106
 
98
107
  # Strategies
99
108
  Array(use).each do |strategy|
@@ -110,8 +119,19 @@ module Axn
110
119
  end
111
120
  end
112
121
 
122
+ # Async configuration
123
+ unless async.nil?
124
+ async_array = Array(async)
125
+ # Skip async configuration if adapter is nil (but not if array is empty)
126
+ if !async_array.empty? && async_array[0].nil?
127
+ # Do nothing - skip async configuration
128
+ else
129
+ _apply_async_config(axn, async_array)
130
+ end
131
+ end
132
+
113
133
  # Default exposure
114
- axn.exposes(expose_return_as, allow_blank: true) if expose_return_as.present?
134
+ axn.exposes(expose_return_as, optional: true) if expose_return_as.present?
115
135
  end
116
136
  end
117
137
  # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/ParameterLists
@@ -138,16 +158,84 @@ module Axn
138
158
  return unless value.present?
139
159
 
140
160
  # Check if the value itself is a hash (this catches the case where someone passes a hash literal)
141
- raise Action::UnsupportedArgument, "Cannot pass hash directly to #{method_name} - use descriptor objects for kwargs" if value.is_a?(Hash)
161
+ raise Axn::UnsupportedArgument, "Cannot pass hash directly to #{method_name} - use descriptor objects for kwargs" if value.is_a?(Hash)
142
162
 
143
163
  # Wrap in Array() to handle both single values and arrays
144
164
  Array(value).each do |handler|
145
- raise Action::UnsupportedArgument, "Cannot pass hash directly to #{method_name} - use descriptor objects for kwargs" if handler.is_a?(Hash)
165
+ raise Axn::UnsupportedArgument, "Cannot pass hash directly to #{method_name} - use descriptor objects for kwargs" if handler.is_a?(Hash)
146
166
 
147
167
  # Both descriptor objects and simple cases (string/proc) can be used directly
148
168
  axn.public_send(method_name, handler)
149
169
  end
150
170
  end
171
+
172
+ def _build_axn_class(superclass:, args:, executable:, expose_return_as:, include: nil, extend: nil, prepend: nil, _creating_action_class_for: nil) # rubocop:disable Lint/UnderscorePrefixedVariableName
173
+ # Mark superclass if we're creating an action class (for recursion prevention)
174
+ # Track which target class is having an action created for it
175
+ superclass.instance_variable_set(:@_axn_creating_action_class_for, _creating_action_class_for) if _creating_action_class_for && superclass
176
+
177
+ Class.new(superclass || Object) do
178
+ include Axn unless self < Axn
179
+
180
+ Array(include).each { |mod| include mod }
181
+ Array(extend).each { |mod| extend mod }
182
+ Array(prepend).each { |mod| prepend mod }
183
+
184
+ # Set a default name for anonymous classes to help with debugging
185
+ define_singleton_method(:name) do
186
+ "AnonymousAxn_#{object_id}"
187
+ end
188
+
189
+ define_method(:call) do
190
+ unwrapped_kwargs = Array(args[:keyreq]).each_with_object({}) do |field, hash|
191
+ hash[field] = public_send(field)
192
+ end
193
+
194
+ retval = instance_exec(**unwrapped_kwargs, &executable)
195
+ expose(expose_return_as => retval) if expose_return_as.present?
196
+ end
197
+ end
198
+ ensure
199
+ superclass.instance_variable_set(:@_axn_creating_action_class_for, nil) if _creating_action_class_for && superclass
200
+ end
201
+
202
+ def _apply_async_config(axn, async)
203
+ raise ArgumentError, "[Axn::Factory] Invalid async configuration" unless _validate_async_config(async)
204
+
205
+ adapter, *config_args = async
206
+
207
+ # Determine hash config and callable config
208
+ config = config_args.find { |arg| arg.is_a?(Hash) }
209
+ block = config_args.find { |arg| arg.respond_to?(:call) }
210
+
211
+ # Call async once with the determined values
212
+ axn.async(adapter, **(config || {}), &block)
213
+ end
214
+
215
+ def _validate_async_config(async_array)
216
+ return false unless async_array.length.between?(1, 3)
217
+
218
+ adapter = async_array[0]
219
+ second_arg = async_array[1]
220
+ third_arg = async_array[2]
221
+
222
+ # First arg must be adapter (symbol/string), false, or nil
223
+ return false unless adapter.is_a?(Symbol) || adapter.is_a?(String) || adapter == false || adapter.nil?
224
+
225
+ case async_array.length
226
+ when 1
227
+ # Pattern A: [:sidekiq], [false], or [nil]
228
+ true
229
+ when 2
230
+ # Pattern B: [:sidekiq, hash_or_callable] or [nil, hash_or_callable]
231
+ second_arg.is_a?(Hash) || second_arg.respond_to?(:call)
232
+ when 3
233
+ # Pattern C: [:sidekiq, hash, callable] or [nil, hash, callable]
234
+ second_arg.is_a?(Hash) && third_arg.respond_to?(:call)
235
+ else
236
+ false
237
+ end
238
+ end
151
239
  end
152
240
  end
153
241
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is a base class for all form objects that are used with Axn actions.
4
+ #
5
+ # It provides a number of conveniences for working with form objects, including:
6
+ # - Automatically attr_accessor any attribute for which we add a validation
7
+ # - Add support for nested forms
8
+ # - Add support for money objects (analogous to 'include MoneyRails::ActiveRecord::Monetizable' for ActiveRecord models)
9
+ module Axn
10
+ class FormObject
11
+ include ActiveModel::Model
12
+
13
+ class << self
14
+ attr_accessor :field_names
15
+
16
+ def inherited(subclass)
17
+ # Inherit field_names from parent class, or initialize as empty array if parent doesn't have any
18
+ subclass.field_names = (field_names || []).dup
19
+
20
+ super
21
+ end
22
+
23
+ # Override attr_accessor to track field names for automatic #to_h support
24
+ def attr_accessor(*attributes)
25
+ # Initialize field_names if not already set
26
+ self.field_names ||= []
27
+
28
+ # Add new attributes to the field_names array
29
+ self.field_names += attributes.map(&:to_sym)
30
+
31
+ super
32
+ end
33
+
34
+ # Automatically attr_accessor any attribute for which we add a validation
35
+ def validates(*attributes)
36
+ our_attributes = attributes.dup
37
+
38
+ # Pulled from upstream: https://github.com/rails/rails/blob/6f0d1ad14b92b9f5906e44740fce8b4f1c7075dc/activemodel/lib/active_model/validations/validates.rb#L106
39
+ our_attributes.extract_options!
40
+ our_attributes.each { |attr| attr_accessor(attr) }
41
+
42
+ super
43
+ end
44
+
45
+ # Add support for nested forms
46
+ def nested_forms(**kwargs)
47
+ kwargs.each do |name, klass|
48
+ validates name, presence: true
49
+
50
+ define_method("#{name}=") do |params|
51
+ return instance_variable_set("@#{name}", nil) if params.nil?
52
+
53
+ child_params = params.dup
54
+
55
+ # Automatically inject the parent into the child form if it has a parent= method
56
+ child_params[:parent_form] = self if klass.instance_methods.include?(:parent_form=)
57
+
58
+ instance_variable_set("@#{name}", klass.new(child_params))
59
+ end
60
+
61
+ validation_method_name = :"validate_#{name}_form"
62
+ validate validation_method_name
63
+ define_method(validation_method_name) do
64
+ return if public_send(name).nil? || public_send(name).valid?
65
+
66
+ public_send(name).errors.each do |error|
67
+ errors.add("#{name}.#{error.attribute}", error.message)
68
+ end
69
+ end
70
+ private validation_method_name
71
+ end
72
+ end
73
+ alias nested_form nested_forms
74
+ end
75
+
76
+ def to_h
77
+ return {} if self.class.field_names.nil?
78
+
79
+ self.class.field_names.each_with_object({}) do |field_name, hash|
80
+ next unless respond_to?(field_name)
81
+
82
+ # Skip parent_form to avoid infinite recursion with circular references
83
+ next if field_name == :parent_form
84
+
85
+ value = public_send(field_name)
86
+ hash[field_name] = value.is_a?(Axn::FormObject) ? value.to_h : value
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,30 @@
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
+ # If raise_piping_errors_in_dev is enabled and we're in development, raise instead of log.
8
+ # Test and production environments always swallow the error to match production behavior.
9
+ raise exception if Axn.config.raise_piping_errors_in_dev && Axn.config.env.development?
10
+
11
+ # Extract just filename/line number from backtrace
12
+ src = exception.backtrace.first.split.first.split("/").last.split(":")[0, 2].join(":")
13
+
14
+ message = if Axn.config.env.production?
15
+ "Ignoring exception raised while #{desc}: #{exception.class.name} - #{exception.message} (from #{src})"
16
+ else
17
+ msg = "!! IGNORING EXCEPTION RAISED WHILE #{desc.upcase} !!\n\n" \
18
+ "\t* Exception: #{exception.class.name}\n" \
19
+ "\t* Message: #{exception.message}\n" \
20
+ "\t* From: #{src}"
21
+ "#{'⌵' * 30}\n\n#{msg}\n\n#{'^' * 30}"
22
+ end
23
+
24
+ (action || Axn.config.logger).send(:warn, message)
25
+
26
+ nil
27
+ end
28
+ end
29
+ end
30
+ 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