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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  3. data/.cursor/rules/general-coding-standards.mdc +27 -0
  4. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  5. data/CHANGELOG.md +43 -0
  6. data/Rakefile +12 -2
  7. data/docs/.vitepress/config.mjs +8 -3
  8. data/docs/advanced/conventions.md +2 -2
  9. data/docs/advanced/mountable.md +562 -0
  10. data/docs/advanced/profiling.md +355 -0
  11. data/docs/advanced/rough.md +1 -1
  12. data/docs/index.md +5 -3
  13. data/docs/intro/about.md +1 -1
  14. data/docs/intro/overview.md +5 -5
  15. data/docs/recipes/memoization.md +2 -2
  16. data/docs/recipes/rubocop-integration.md +38 -284
  17. data/docs/recipes/testing.md +14 -14
  18. data/docs/recipes/validating-user-input.md +1 -1
  19. data/docs/reference/async.md +160 -0
  20. data/docs/reference/axn-result.md +107 -0
  21. data/docs/reference/class.md +123 -25
  22. data/docs/reference/configuration.md +191 -10
  23. data/docs/reference/instance.md +14 -29
  24. data/docs/strategies/index.md +21 -21
  25. data/docs/strategies/transaction.md +1 -1
  26. data/docs/usage/setup.md +14 -0
  27. data/docs/usage/steps.md +7 -7
  28. data/docs/usage/using.md +23 -12
  29. data/docs/usage/writing.md +92 -11
  30. data/lib/axn/async/adapters/active_job.rb +65 -0
  31. data/lib/axn/async/adapters/disabled.rb +26 -0
  32. data/lib/axn/async/adapters/sidekiq.rb +74 -0
  33. data/lib/axn/async/adapters.rb +26 -0
  34. data/lib/axn/async.rb +61 -0
  35. data/lib/{action → axn}/configuration.rb +21 -3
  36. data/lib/{action → axn}/context.rb +21 -4
  37. data/lib/{action → axn}/core/automatic_logging.rb +6 -6
  38. data/lib/axn/core/context/facade.rb +69 -0
  39. data/lib/{action → axn}/core/context/facade_inspector.rb +31 -4
  40. data/lib/{action → axn}/core/context/internal.rb +5 -5
  41. data/lib/{action → axn}/core/contract.rb +41 -46
  42. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  43. data/lib/{action → axn}/core/contract_validation.rb +16 -6
  44. data/lib/axn/core/contract_validation_for_subfields.rb +158 -0
  45. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  46. data/lib/axn/core/field_resolvers/model.rb +63 -0
  47. data/lib/axn/core/field_resolvers.rb +24 -0
  48. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  49. data/lib/{action → axn}/core/flow/exception_execution.rb +4 -13
  50. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  51. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  52. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +6 -6
  53. data/lib/{action → axn}/core/flow/handlers/invoker.rb +2 -2
  54. data/lib/{action → axn}/core/flow/handlers/matcher.rb +5 -5
  55. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  56. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  57. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  58. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  59. data/lib/axn/core/flow/handlers.rb +20 -0
  60. data/lib/{action → axn}/core/flow/messages.rb +7 -7
  61. data/lib/{action → axn}/core/flow.rb +4 -4
  62. data/lib/{action → axn}/core/hooks.rb +16 -5
  63. data/lib/{action → axn}/core/logging.rb +3 -3
  64. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  65. data/lib/axn/core/profiling.rb +124 -0
  66. data/lib/{action → axn}/core/timing.rb +1 -1
  67. data/lib/axn/core/tracing.rb +17 -0
  68. data/lib/axn/core/use_strategy.rb +29 -0
  69. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  70. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  71. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  72. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  73. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  74. data/lib/axn/core.rb +123 -0
  75. data/lib/{action → axn}/exceptions.rb +12 -2
  76. data/lib/axn/factory.rb +102 -34
  77. data/lib/axn/internal/logging.rb +26 -0
  78. data/lib/axn/internal/registry.rb +87 -0
  79. data/lib/axn/mountable/descriptor.rb +76 -0
  80. data/lib/axn/mountable/helpers/class_builder.rb +162 -0
  81. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  82. data/lib/axn/mountable/helpers/namespace_manager.rb +66 -0
  83. data/lib/axn/mountable/helpers/validator.rb +112 -0
  84. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  85. data/lib/axn/mountable/mounting_strategies/_base.rb +83 -0
  86. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  87. data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +55 -0
  88. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  89. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  90. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  91. data/lib/axn/mountable.rb +85 -0
  92. data/lib/axn/rails/engine.rb +51 -0
  93. data/lib/axn/rails/generators/axn_generator.rb +68 -0
  94. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  95. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  96. data/lib/{action → axn}/result.rb +30 -11
  97. data/lib/{action → axn}/strategies/transaction.rb +1 -1
  98. data/lib/axn/strategies.rb +20 -0
  99. data/lib/axn/testing/spec_helpers.rb +6 -8
  100. data/lib/axn/util/memoization.rb +20 -0
  101. data/lib/axn/version.rb +1 -1
  102. data/lib/axn.rb +17 -16
  103. data/lib/rubocop/cop/axn/README.md +23 -23
  104. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  105. metadata +88 -64
  106. data/.rspec +0 -3
  107. data/.rubocop.yml +0 -76
  108. data/.tool-versions +0 -1
  109. data/docs/reference/action-result.md +0 -37
  110. data/lib/action/attachable/base.rb +0 -43
  111. data/lib/action/attachable/steps.rb +0 -63
  112. data/lib/action/attachable/subactions.rb +0 -70
  113. data/lib/action/attachable.rb +0 -17
  114. data/lib/action/core/context/facade.rb +0 -48
  115. data/lib/action/core/flow/handlers.rb +0 -20
  116. data/lib/action/core/tracing.rb +0 -17
  117. data/lib/action/core/use_strategy.rb +0 -30
  118. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  119. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  120. data/lib/action/core.rb +0 -108
  121. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  122. data/lib/action/enqueueable.rb +0 -13
  123. data/lib/action/strategies.rb +0 -48
  124. data/lib/axn/util.rb +0 -24
  125. data/package.json +0 -10
  126. 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 "action/core/context/facade"
4
- require "action/core/context/facade_inspector"
3
+ require "axn/core/context/facade"
4
+ require "axn/core/context/facade_inspector"
5
5
 
6
- module Action
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, { allow_blank: true }] }
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, { allow_blank: true }] }
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?(Action::Failure)
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
- # TODO: hook for adding early-return success at some point
87
- def _user_provided_success_message = nil
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?(Action::Failure)
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 Action::ContractViolation::MethodNotAllowed, msg
125
+ raise Axn::ContractViolation::MethodNotAllowed, msg
107
126
  end
108
127
 
109
128
  super
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Action
3
+ module Axn
4
4
  class Strategies
5
5
  module Transaction
6
6
  extend ActiveSupport::Concern
@@ -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 build_action(&block)
7
- action = Class.new.send(:include, Action)
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.configure do |config|
20
- config.include Axn::Testing::SpecHelpers
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Axn
4
- VERSION = "0.1.0-alpha.2.8.1"
4
+ VERSION = "0.1.0-alpha.3"
5
5
  end
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
- module Axn; end
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
- require "action/configuration"
11
- require "action/exceptions"
12
+ # The core implementation
13
+ require "axn/core"
12
14
 
13
- require "action/core"
15
+ # Utilities
16
+ require "axn/util/memoization"
14
17
 
15
- require "action/attachable"
16
- require "action/enqueueable"
18
+ # Extensions
19
+ require "axn/mountable"
20
+ require "axn/async"
17
21
 
18
- def Axn(callable, **) # rubocop:disable Naming/MethodName
19
- return callable if callable.is_a?(Class) && callable < Action
22
+ # Rails integration (if in Rails context)
23
+ require "axn/rails/engine" if defined?(Rails) && Rails.const_defined?(:Engine)
20
24
 
21
- Axn::Factory.build(**, &callable)
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 Attachable
31
- include Enqueueable
31
+ include Mountable
32
+ include Async
32
33
 
33
34
  # Allow additional automatic includes to be configured
34
- Array(Action.config.additional_includes).each { |mod| include mod }
35
+ Array(Axn.config.additional_includes).each { |mod| include mod }
35
36
  end
36
37
  end
37
38
  end