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.
- checksums.yaml +4 -4
- data/.cursor/commands/pr.md +36 -0
- 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 +57 -0
- data/Rakefile +114 -4
- data/docs/.vitepress/config.mjs +19 -10
- data/docs/advanced/conventions.md +3 -3
- data/docs/advanced/mountable.md +476 -0
- data/docs/advanced/profiling.md +351 -0
- data/docs/advanced/rough.md +27 -8
- data/docs/index.md +5 -3
- data/docs/intro/about.md +1 -1
- data/docs/intro/overview.md +6 -6
- data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
- data/docs/recipes/memoization.md +103 -18
- 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 +429 -0
- data/docs/reference/axn-result.md +107 -0
- data/docs/reference/class.md +225 -64
- data/docs/reference/configuration.md +366 -34
- data/docs/reference/form-object.md +252 -0
- data/docs/reference/instance.md +14 -29
- data/docs/strategies/client.md +212 -0
- data/docs/strategies/form.md +235 -0
- data/docs/strategies/index.md +21 -21
- data/docs/strategies/transaction.md +1 -1
- data/docs/usage/setup.md +16 -2
- data/docs/usage/steps.md +7 -7
- data/docs/usage/using.md +23 -12
- data/docs/usage/writing.md +191 -12
- data/lib/axn/async/adapters/active_job.rb +74 -0
- data/lib/axn/async/adapters/disabled.rb +41 -0
- data/lib/axn/async/adapters/sidekiq.rb +67 -0
- data/lib/axn/async/adapters.rb +26 -0
- data/lib/axn/async/batch_enqueue/config.rb +38 -0
- data/lib/axn/async/batch_enqueue.rb +99 -0
- data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
- data/lib/axn/async.rb +178 -0
- data/lib/axn/configuration.rb +113 -0
- data/lib/{action → axn}/context.rb +22 -4
- data/lib/axn/core/automatic_logging.rb +89 -0
- data/lib/axn/core/context/facade.rb +69 -0
- data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
- data/lib/{action → axn}/core/context/internal.rb +5 -5
- data/lib/{action → axn}/core/contract.rb +111 -73
- data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
- data/lib/{action → axn}/core/contract_validation.rb +27 -12
- data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
- data/lib/axn/core/default_call.rb +63 -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 +9 -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 +23 -11
- data/lib/axn/core/flow/handlers/invoker.rb +47 -0
- data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
- 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 +8 -8
- data/lib/{action → axn}/core/flow.rb +4 -4
- data/lib/{action → axn}/core/hooks.rb +17 -5
- data/lib/axn/core/logging.rb +48 -0
- data/lib/axn/core/memoization.rb +53 -0
- data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
- data/lib/{action → axn}/core/timing.rb +1 -1
- data/lib/axn/core/tracing.rb +90 -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/{action → axn}/core.rb +55 -55
- data/lib/{action → axn}/exceptions.rb +12 -2
- data/lib/axn/extras/strategies/client.rb +150 -0
- data/lib/axn/extras/strategies/vernier.rb +121 -0
- data/lib/axn/extras.rb +4 -0
- data/lib/axn/factory.rb +122 -34
- data/lib/axn/form_object.rb +90 -0
- data/lib/axn/internal/logging.rb +30 -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 +193 -0
- data/lib/axn/mountable/helpers/mounter.rb +33 -0
- data/lib/axn/mountable/helpers/namespace_manager.rb +38 -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 +87 -0
- data/lib/axn/mountable/mounting_strategies/axn.rb +48 -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 +119 -0
- data/lib/axn/rails/engine.rb +51 -0
- data/lib/axn/rails/generators/axn_generator.rb +86 -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 +32 -13
- data/lib/axn/strategies/form.rb +98 -0
- data/lib/axn/strategies/transaction.rb +26 -0
- data/lib/axn/strategies.rb +20 -0
- data/lib/axn/testing/spec_helpers.rb +6 -8
- data/lib/axn/util/callable.rb +120 -0
- data/lib/axn/util/contract_error_handling.rb +32 -0
- data/lib/axn/util/execution_context.rb +34 -0
- data/lib/axn/util/global_id_serialization.rb +52 -0
- data/lib/axn/util/logging.rb +87 -0
- data/lib/axn/util/memoization.rb +20 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +26 -16
- data/lib/rubocop/cop/axn/README.md +23 -23
- data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
- metadata +106 -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/configuration.rb +0 -55
- data/lib/action/core/automatic_logging.rb +0 -93
- data/lib/action/core/context/facade.rb +0 -48
- data/lib/action/core/flow/handlers/invoker.rb +0 -73
- data/lib/action/core/flow/handlers.rb +0 -20
- data/lib/action/core/logging.rb +0 -37
- 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/enqueueable/via_sidekiq.rb +0 -76
- data/lib/action/enqueueable.rb +0 -13
- data/lib/action/strategies/transaction.rb +0 -19
- data/lib/action/strategies.rb +0 -48
- data/lib/axn/util.rb +0 -24
- data/package.json +0 -10
- 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:
|
|
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
|
-
|
|
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
|
|
62
|
+
"[Axn::Factory] Cannot convert callable to action: callable expects positional arguments"
|
|
40
63
|
end
|
|
41
|
-
raise ArgumentError, "[Axn::Factory] Cannot convert
|
|
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
|
|
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
|
-
|
|
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,
|
|
85
|
-
_apply_handlers(axn, :error, error,
|
|
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,
|
|
94
|
-
_apply_handlers(axn, :on_failure, on_failure,
|
|
95
|
-
_apply_handlers(axn, :on_error, on_error,
|
|
96
|
-
_apply_handlers(axn, :on_exception, on_exception,
|
|
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,
|
|
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
|
|
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
|
|
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
|