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
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Mountable
|
5
|
+
class MountingStrategies
|
6
|
+
module Method
|
7
|
+
include Base
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def default_inherit_mode = :lifecycle
|
11
|
+
|
12
|
+
module DSL
|
13
|
+
def mount_axn_method(name, axn_klass = nil, inherit: MountingStrategies::Method.default_inherit_mode, **, &)
|
14
|
+
# mount_axn_method defaults to :lifecycle - participates in parent's execution lifecycle
|
15
|
+
Helpers::Mounter.mount_via_strategy(
|
16
|
+
target: self,
|
17
|
+
as: :method,
|
18
|
+
name:,
|
19
|
+
axn_klass:,
|
20
|
+
inherit:,
|
21
|
+
**,
|
22
|
+
&
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def strategy_specific_kwargs = super + [:expose_return_as]
|
28
|
+
|
29
|
+
def preprocess_kwargs(**kwargs)
|
30
|
+
# Call parent preprocessing first
|
31
|
+
processed_kwargs = super
|
32
|
+
|
33
|
+
# Methods require a return value
|
34
|
+
processed_kwargs[:expose_return_as] = processed_kwargs[:expose_return_as].presence || :value
|
35
|
+
|
36
|
+
# Methods aren't capable of returning multiple values
|
37
|
+
if processed_kwargs[:exposes].present?
|
38
|
+
raise MountingError,
|
39
|
+
"Methods aren't capable of exposing multiple values (will automatically expose return value instead)"
|
40
|
+
end
|
41
|
+
|
42
|
+
# Check for existing axn class with multiple exposed fields
|
43
|
+
if processed_kwargs[:axn_klass].present?
|
44
|
+
axn_klass = processed_kwargs[:axn_klass]
|
45
|
+
exposed_fields = axn_klass.external_field_configs.map(&:field)
|
46
|
+
|
47
|
+
if exposed_fields.size > 1
|
48
|
+
raise MountingError,
|
49
|
+
"Cannot determine expose_return_as for existing axn class with multiple exposed fields: #{exposed_fields.join(", ")}. " \
|
50
|
+
"Use a fresh block with mount_axn_method or ensure the axn class has exactly one exposed field."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
processed_kwargs
|
55
|
+
end
|
56
|
+
|
57
|
+
def mount_to_target(descriptor:, target:)
|
58
|
+
name = descriptor.name
|
59
|
+
|
60
|
+
mount_method(target:, method_name: "#{name}!") do |**kwargs|
|
61
|
+
# Lazy load the action class for the current target
|
62
|
+
axn_klass = descriptor.mounted_axn_for(target: self)
|
63
|
+
|
64
|
+
# Determine expose_return_as by introspecting the axn class
|
65
|
+
exposed_fields = axn_klass.external_field_configs.map(&:field)
|
66
|
+
expose_return_as = exposed_fields.size == 1 ? exposed_fields.first : nil
|
67
|
+
|
68
|
+
result = axn_klass.call!(**kwargs)
|
69
|
+
return result if expose_return_as.nil?
|
70
|
+
|
71
|
+
result.public_send(expose_return_as)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def _determine_exposure_to_return(axn_klass)
|
78
|
+
# Introspect the axn class to determine expose_return_as
|
79
|
+
exposed_fields = axn_klass.external_field_configs.map(&:field)
|
80
|
+
|
81
|
+
case exposed_fields.size
|
82
|
+
when 0
|
83
|
+
nil # No exposed fields, return nil to avoid public_send
|
84
|
+
when 1
|
85
|
+
exposed_fields.first # Single field, assume it's expose_return_as
|
86
|
+
else
|
87
|
+
raise MountingError,
|
88
|
+
"Cannot determine expose_return_as for existing axn class with multiple exposed fields: #{exposed_fields.join(", ")}. " \
|
89
|
+
"Use a fresh block with mount_axn_method or ensure the axn class has exactly one exposed field."
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Mountable
|
5
|
+
class MountingStrategies
|
6
|
+
module Step
|
7
|
+
include Base
|
8
|
+
extend self # rubocop:disable Style/ModuleFunction -- module_function breaks inheritance
|
9
|
+
|
10
|
+
def default_inherit_mode = :none
|
11
|
+
|
12
|
+
module DSL
|
13
|
+
def steps(*steps)
|
14
|
+
Array(steps).compact.each do |step_class|
|
15
|
+
next unless step_class.is_a?(Class)
|
16
|
+
raise ArgumentError, "Step #{step_class} must include Axn module" if !step_class.included_modules.include?(::Axn) && !step_class < ::Axn
|
17
|
+
|
18
|
+
num_steps = _mounted_axn_descriptors.count { |descriptor| descriptor.mount_strategy.key == :step }
|
19
|
+
step("Step #{num_steps + 1}", step_class)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def step(name, axn_klass = nil, error_prefix: nil, inherit: MountingStrategies::Step.default_inherit_mode, **, &)
|
24
|
+
# Steps default to :none - they are isolated units of work
|
25
|
+
Helpers::Mounter.mount_via_strategy(
|
26
|
+
target: self,
|
27
|
+
as: :step,
|
28
|
+
name:,
|
29
|
+
axn_klass:,
|
30
|
+
error_prefix:,
|
31
|
+
inherit:,
|
32
|
+
**,
|
33
|
+
&
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def strategy_specific_kwargs = super + [:error_prefix]
|
39
|
+
|
40
|
+
def mount_to_target(descriptor:, target:)
|
41
|
+
error_prefix = descriptor.options[:error_prefix] || "#{descriptor.name}: "
|
42
|
+
axn_klass = descriptor.mounted_axn_for(target:)
|
43
|
+
|
44
|
+
target.error from: axn_klass do |e|
|
45
|
+
"#{error_prefix}#{e.message}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Only define #call method once
|
49
|
+
return if target.instance_variable_defined?(:@_axn_call_method_defined_for_steps)
|
50
|
+
|
51
|
+
target.define_method(:call) do
|
52
|
+
step_descriptors = self.class._mounted_axn_descriptors.select { |d| d.mount_strategy.key == :step }
|
53
|
+
|
54
|
+
step_descriptors.each do |step_descriptor|
|
55
|
+
axn = step_descriptor.mounted_axn_for(target:)
|
56
|
+
step_result = axn.call!(**@__context.__combined_data)
|
57
|
+
|
58
|
+
# Extract exposed fields from step result and update exposed_data
|
59
|
+
step_result.declared_fields.each do |field|
|
60
|
+
@__context.exposed_data[field] = step_result.public_send(field)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
target.instance_variable_set(:@_axn_call_method_defined_for_steps, true)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "axn/internal/registry"
|
4
|
+
|
5
|
+
module Axn
|
6
|
+
module Mountable
|
7
|
+
class MountingTypeNotFound < Axn::Internal::Registry::NotFound; end
|
8
|
+
class DuplicateMountingTypeError < Axn::Internal::Registry::DuplicateError; end
|
9
|
+
|
10
|
+
class MountingStrategies < Axn::Internal::Registry
|
11
|
+
class << self
|
12
|
+
def registry_directory = __dir__
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def item_type = "Mounting Type"
|
17
|
+
def not_found_error_class = MountingTypeNotFound
|
18
|
+
def duplicate_error_class = DuplicateMountingTypeError
|
19
|
+
|
20
|
+
def select_constants_to_load(constants)
|
21
|
+
# Select modules that are not the Base module
|
22
|
+
constants.select do |const|
|
23
|
+
const.is_a?(Module) && const != Base
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Trigger registry loading to ensure mounting strategies are available
|
30
|
+
MountingStrategies.all
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "axn/mountable/inherit_profiles"
|
4
|
+
require "axn/mountable/mounting_strategies"
|
5
|
+
require "axn/mountable/descriptor"
|
6
|
+
require "axn/mountable/helpers/validator"
|
7
|
+
require "axn/mountable/helpers/class_builder"
|
8
|
+
require "axn/mountable/helpers/namespace_manager"
|
9
|
+
require "axn/mountable/helpers/mounter"
|
10
|
+
|
11
|
+
module Axn
|
12
|
+
# Mountable provides functionality for mounting actions to classes
|
13
|
+
#
|
14
|
+
# ## Inheritance Behavior
|
15
|
+
#
|
16
|
+
# Mounted actions inherit features from their target class in different ways depending on the
|
17
|
+
# mounting strategy. Each strategy has sensible defaults, but you can customize inheritance
|
18
|
+
# behavior using the `inherit` parameter.
|
19
|
+
#
|
20
|
+
# ### Default Inheritance Modes
|
21
|
+
#
|
22
|
+
# - `mount_axn` and `mount_axn_method`: `:lifecycle` - Inherits hooks, callbacks, messages, and async config (but not fields)
|
23
|
+
# - `step`: `:none` - Completely independent to avoid conflicts
|
24
|
+
# - `enqueue_all_via`: `:async_only` - Only inherits async configuration
|
25
|
+
#
|
26
|
+
# ### Inheritance Profiles
|
27
|
+
#
|
28
|
+
# - `:lifecycle` - Inherits everything except fields (hooks, callbacks, messages, async config)
|
29
|
+
# - `:async_only` - Only inherits async configuration
|
30
|
+
# - `:none` - Completely standalone with no inheritance
|
31
|
+
#
|
32
|
+
# You can also use a hash for granular control:
|
33
|
+
# `inherit: { fields: false, hooks: true, callbacks: false, messages: true, async: true }`
|
34
|
+
#
|
35
|
+
# Available hash keys: `:fields`, `:hooks`, `:callbacks`, `:messages`, `:async`
|
36
|
+
#
|
37
|
+
# @example Default inheritance behavior
|
38
|
+
# class MyClass
|
39
|
+
# include Axn
|
40
|
+
#
|
41
|
+
# before :log_start
|
42
|
+
# on_success :track_success
|
43
|
+
# async :sidekiq
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# # mount_axn uses :lifecycle (inherits hooks, callbacks, messages, async)
|
47
|
+
# MyClass.mount_axn :my_action do
|
48
|
+
# # Will run log_start before and track_success after
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# # step uses :none (completely independent)
|
52
|
+
# MyClass.step :my_step do
|
53
|
+
# # Will NOT run log_start or track_success
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# # enqueue_all_via uses :async_only (only inherits async config)
|
57
|
+
# MyClass.enqueue_all_via do
|
58
|
+
# # Can call enqueue (uses inherited async config)
|
59
|
+
# # Does NOT inherit hooks, callbacks, or messages
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @example Custom inheritance control
|
63
|
+
# # Override step default to inherit lifecycle
|
64
|
+
# MyClass.step :my_step, inherit: :lifecycle do
|
65
|
+
# # Will now run hooks and callbacks
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# # Use granular control
|
69
|
+
# MyClass.mount_axn :my_action, inherit: { hooks: true, callbacks: false } do
|
70
|
+
# # Will run hooks but not callbacks
|
71
|
+
# end
|
72
|
+
module Mountable
|
73
|
+
extend ActiveSupport::Concern
|
74
|
+
|
75
|
+
def self.included(base)
|
76
|
+
base.class_eval do
|
77
|
+
class_attribute :_mounted_axn_descriptors, default: []
|
78
|
+
end
|
79
|
+
|
80
|
+
MountingStrategies.all.each do |(_name, klass)|
|
81
|
+
base.extend klass::DSL if klass.const_defined?(:DSL)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Only define the Engine if Rails is available
|
4
|
+
if defined?(Rails) && Rails.const_defined?(:Engine)
|
5
|
+
module Axn
|
6
|
+
module RailsIntegration
|
7
|
+
class Engine < Rails::Engine
|
8
|
+
# Set a custom engine name that's more concise than the module path
|
9
|
+
engine_name "axn_rails"
|
10
|
+
|
11
|
+
# This engine is automatically loaded when AXN is used in a Rails context
|
12
|
+
# It ensures proper initialization and integration with Rails
|
13
|
+
|
14
|
+
# The engine is intentionally minimal - AXN is designed to work
|
15
|
+
# as a standalone library that can be used in any Ruby context
|
16
|
+
|
17
|
+
# However, when used alongside Rails, we ensure that the app/actions
|
18
|
+
# directory is automatically added to the autoloader so that Rails can
|
19
|
+
# automatically load the actions.
|
20
|
+
initializer "axn.add_app_actions_to_autoload", after: :load_config_initializers do |app|
|
21
|
+
actions_path = app.root.join("app/actions")
|
22
|
+
|
23
|
+
# Only add if the directory exists
|
24
|
+
next unless File.directory?(actions_path)
|
25
|
+
|
26
|
+
# Use modern Rails autoloader API (Rails 7.2+)
|
27
|
+
# Namespace is configurable via Axn.config.rails.app_actions_autoload_namespace
|
28
|
+
autoload_namespace = Axn.config.rails.app_actions_autoload_namespace
|
29
|
+
|
30
|
+
if autoload_namespace
|
31
|
+
# Create the namespace module if it doesn't exist
|
32
|
+
namespace = Object.const_get(autoload_namespace) if Object.const_defined?(autoload_namespace)
|
33
|
+
unless namespace
|
34
|
+
namespace = Module.new
|
35
|
+
Object.const_set(autoload_namespace, namespace)
|
36
|
+
end
|
37
|
+
Rails.autoloaders.main.push_dir(actions_path, namespace:)
|
38
|
+
else
|
39
|
+
# No namespace - load directly
|
40
|
+
Rails.autoloaders.main.push_dir(actions_path)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Register the generator
|
45
|
+
generators do
|
46
|
+
require_relative "generators/axn_generator"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module RailsIntegration
|
5
|
+
module Generators
|
6
|
+
class AxnGenerator < Rails::Generators::NamedBase
|
7
|
+
namespace "axn"
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
argument :expectations, type: :array, default: [], banner: "expectation1 expectation2 ..."
|
11
|
+
|
12
|
+
def create_action_file
|
13
|
+
template "action.rb.erb", "app/actions/#{file_path}.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_spec_file
|
17
|
+
return unless spec_generation_enabled?
|
18
|
+
|
19
|
+
template "action_spec.rb.erb", "spec/actions/#{file_path}_spec.rb"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def class_name
|
25
|
+
@class_name ||= name.camelize
|
26
|
+
end
|
27
|
+
|
28
|
+
def file_path
|
29
|
+
@file_path ||= name.underscore
|
30
|
+
end
|
31
|
+
|
32
|
+
def expectations_with_types
|
33
|
+
expectations.map { |exp| { name: exp, type: "String" } }
|
34
|
+
end
|
35
|
+
|
36
|
+
def spec_generation_enabled?
|
37
|
+
return false unless rspec_available?
|
38
|
+
return false if spec_generation_skipped?
|
39
|
+
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def rspec_available?
|
44
|
+
defined?(RSpec)
|
45
|
+
end
|
46
|
+
|
47
|
+
def spec_generation_skipped?
|
48
|
+
return false unless defined?(Rails) && Rails.application&.config&.generators
|
49
|
+
|
50
|
+
generators_config = Rails.application.config.generators
|
51
|
+
|
52
|
+
# Check individual boolean flags (modern style)
|
53
|
+
return true if generators_config.respond_to?(:test_framework) &&
|
54
|
+
generators_config.test_framework == false
|
55
|
+
|
56
|
+
# Check for specific spec-related flags
|
57
|
+
spec_flags = %w[specs axn_specs]
|
58
|
+
spec_flags.each do |flag|
|
59
|
+
return true if generators_config.respond_to?(flag) &&
|
60
|
+
generators_config.public_send(flag) == false
|
61
|
+
end
|
62
|
+
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class <%= class_name %>
|
4
|
+
include Axn
|
5
|
+
<% if expectations.any? -%>
|
6
|
+
|
7
|
+
<% expectations.each do |expectation| -%>
|
8
|
+
expects :<%= expectation %>
|
9
|
+
<% end -%>
|
10
|
+
|
11
|
+
<% else -%>
|
12
|
+
|
13
|
+
<% end -%>
|
14
|
+
def call
|
15
|
+
# TODO: Implement action logic
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe <%= class_name %> do
|
4
|
+
<% if expectations.any? -%>
|
5
|
+
<% expectations.each do |expectation| -%>
|
6
|
+
let(:<%= expectation %>) { "<%= expectation %>" }
|
7
|
+
<% end -%>
|
8
|
+
|
9
|
+
<% end -%>
|
10
|
+
describe ".call" do
|
11
|
+
subject(:result) { described_class.call(<%= expectations.any? ? expectations.map { |exp| "#{exp}:" }.join(", ") : "" %>) }
|
12
|
+
|
13
|
+
it "executes successfully" do
|
14
|
+
expect(result).to be_ok
|
15
|
+
end
|
16
|
+
<% if expectations.any? -%>
|
17
|
+
|
18
|
+
it "TODO: replace with a meaningful failure case" do
|
19
|
+
result = described_class.call
|
20
|
+
expect(result).not_to be_ok
|
21
|
+
expect(result.error).to eq("Something went wrong")
|
22
|
+
end
|
23
|
+
<% end -%>
|
24
|
+
end
|
25
|
+
end
|
@@ -1,15 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "
|
3
|
+
require "axn/core/context/facade"
|
4
|
+
require "axn/core/context/facade_inspector"
|
5
5
|
|
6
|
-
module
|
6
|
+
module Axn
|
7
7
|
# Outbound / External ContextFacade
|
8
8
|
class Result < ContextFacade
|
9
9
|
# For ease of mocking return results in tests
|
10
10
|
class << self
|
11
11
|
def ok(msg = nil, **exposures)
|
12
|
-
exposes = exposures.keys.to_h { |key| [key, {
|
12
|
+
exposes = exposures.keys.to_h { |key| [key, { optional: true }] }
|
13
13
|
|
14
14
|
Axn::Factory.build(exposes:, success: msg) do
|
15
15
|
exposures.each do |key, value|
|
@@ -19,7 +19,7 @@ module Action
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def error(msg = nil, **exposures, &block)
|
22
|
-
exposes = exposures.keys.to_h { |key| [key, {
|
22
|
+
exposes = exposures.keys.to_h { |key| [key, { optional: true }] }
|
23
23
|
|
24
24
|
Axn::Factory.build(exposes:, error: msg) do
|
25
25
|
exposures.each do |key, value|
|
@@ -40,7 +40,7 @@ module Action
|
|
40
40
|
end
|
41
41
|
|
42
42
|
# External interface
|
43
|
-
delegate :ok?, :exception, :elapsed_time, to: :context
|
43
|
+
delegate :ok?, :exception, :elapsed_time, :finalized?, to: :context
|
44
44
|
|
45
45
|
def error
|
46
46
|
return if ok?
|
@@ -64,7 +64,7 @@ module Action
|
|
64
64
|
].freeze
|
65
65
|
|
66
66
|
def outcome
|
67
|
-
label = if exception.is_a?(
|
67
|
+
label = if exception.is_a?(Axn::Failure)
|
68
68
|
OUTCOME_FAILURE
|
69
69
|
elsif exception
|
70
70
|
OUTCOME_EXCEPTION
|
@@ -79,15 +79,34 @@ module Action
|
|
79
79
|
# TODO: exposed for errors :from support, but should be private if possible
|
80
80
|
def __action__ = @action
|
81
81
|
|
82
|
+
# Enable pattern matching support for Ruby 3+
|
83
|
+
def deconstruct_keys(keys)
|
84
|
+
attrs = {
|
85
|
+
ok: ok?,
|
86
|
+
success:,
|
87
|
+
error:,
|
88
|
+
message:,
|
89
|
+
outcome: outcome.to_sym,
|
90
|
+
finalized: finalized?,
|
91
|
+
}
|
92
|
+
|
93
|
+
# Add all exposed data
|
94
|
+
attrs.merge!(@context.exposed_data)
|
95
|
+
|
96
|
+
# Return filtered attributes if keys specified
|
97
|
+
keys ? attrs.slice(*keys) : attrs
|
98
|
+
end
|
99
|
+
|
82
100
|
private
|
83
101
|
|
84
102
|
def _context_data_source = @context.exposed_data
|
85
103
|
|
86
|
-
|
87
|
-
|
104
|
+
def _user_provided_success_message
|
105
|
+
@context.__early_completion_message.presence
|
106
|
+
end
|
88
107
|
|
89
108
|
def _user_provided_error_message
|
90
|
-
return unless exception.is_a?(
|
109
|
+
return unless exception.is_a?(Axn::Failure)
|
91
110
|
return if exception.default_message?
|
92
111
|
return if exception.cause # We raised this ourselves from nesting
|
93
112
|
|
@@ -103,7 +122,7 @@ module Action
|
|
103
122
|
exposes :#{method_name}
|
104
123
|
MSG
|
105
124
|
|
106
|
-
raise
|
125
|
+
raise Axn::ContractViolation::MethodNotAllowed, msg
|
107
126
|
end
|
108
127
|
|
109
128
|
super
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "axn/internal/registry"
|
4
|
+
|
5
|
+
module Axn
|
6
|
+
class StrategyNotFound < Axn::Internal::Registry::NotFound; end
|
7
|
+
class DuplicateStrategyError < Axn::Internal::Registry::DuplicateError; end
|
8
|
+
|
9
|
+
class Strategies < Axn::Internal::Registry
|
10
|
+
class << self
|
11
|
+
def registry_directory = __dir__
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def item_type = "Strategy"
|
16
|
+
def not_found_error_class = StrategyNotFound
|
17
|
+
def duplicate_error_class = DuplicateStrategyError
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -3,19 +3,17 @@
|
|
3
3
|
module Axn
|
4
4
|
module Testing
|
5
5
|
module SpecHelpers
|
6
|
-
def
|
7
|
-
action = Class.new.send(:include,
|
6
|
+
def build_axn(&block)
|
7
|
+
action = Class.new.send(:include, Axn)
|
8
8
|
action.class_eval(&block) if block
|
9
9
|
action
|
10
10
|
end
|
11
|
-
|
12
|
-
def build_axn(**, &)
|
13
|
-
Axn::Factory.build(**, &)
|
14
|
-
end
|
15
11
|
end
|
16
12
|
end
|
17
13
|
end
|
18
14
|
|
19
|
-
RSpec
|
20
|
-
|
15
|
+
if defined?(RSpec)
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.include Axn::Testing::SpecHelpers
|
18
|
+
end
|
21
19
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Axn
|
4
|
+
module Util
|
5
|
+
module Memoization
|
6
|
+
UNSET = Object.new.freeze
|
7
|
+
|
8
|
+
def self.define_memoized_reader_method(target, field, &block)
|
9
|
+
target.define_method(field) do
|
10
|
+
ivar = :"@_memoized_reader_#{field}"
|
11
|
+
cached_val = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : UNSET
|
12
|
+
return cached_val unless cached_val == UNSET
|
13
|
+
|
14
|
+
value = instance_exec(&block)
|
15
|
+
instance_variable_set(ivar, value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/axn/version.rb
CHANGED
data/lib/axn.rb
CHANGED
@@ -1,37 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "active_support"
|
4
|
+
require "active_support/concern"
|
4
5
|
|
5
|
-
|
6
|
+
# Standalone
|
6
7
|
require "axn/version"
|
7
|
-
require "axn/util"
|
8
8
|
require "axn/factory"
|
9
|
+
require "axn/configuration"
|
10
|
+
require "axn/exceptions"
|
9
11
|
|
10
|
-
|
11
|
-
require "
|
12
|
+
# The core implementation
|
13
|
+
require "axn/core"
|
12
14
|
|
13
|
-
|
15
|
+
# Utilities
|
16
|
+
require "axn/util/memoization"
|
14
17
|
|
15
|
-
|
16
|
-
require "
|
18
|
+
# Extensions
|
19
|
+
require "axn/mountable"
|
20
|
+
require "axn/async"
|
17
21
|
|
18
|
-
|
19
|
-
|
22
|
+
# Rails integration (if in Rails context)
|
23
|
+
require "axn/rails/engine" if defined?(Rails) && Rails.const_defined?(:Engine)
|
20
24
|
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
module Action
|
25
|
+
module Axn
|
25
26
|
def self.included(base)
|
26
27
|
base.class_eval do
|
27
28
|
include Core
|
28
29
|
|
29
30
|
# --- Extensions ---
|
30
|
-
include
|
31
|
-
include
|
31
|
+
include Mountable
|
32
|
+
include Async
|
32
33
|
|
33
34
|
# Allow additional automatic includes to be configured
|
34
|
-
Array(
|
35
|
+
Array(Axn.config.additional_includes).each { |mod| include mod }
|
35
36
|
end
|
36
37
|
end
|
37
38
|
end
|