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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "axn/core/flow/handlers/base_descriptor"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module Axn
|
|
6
6
|
module Core
|
|
7
7
|
module Flow
|
|
8
8
|
module Handlers
|
|
@@ -12,8 +12,8 @@ module Action
|
|
|
12
12
|
attr_reader :prefix
|
|
13
13
|
|
|
14
14
|
def initialize(matcher:, handler:, prefix: nil)
|
|
15
|
-
super(matcher:, handler:)
|
|
16
15
|
@prefix = prefix
|
|
16
|
+
super(matcher:, handler:)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def self.build(handler: nil, if: nil, unless: nil, prefix: nil, from: nil, **)
|
|
@@ -31,19 +31,31 @@ module Action
|
|
|
31
31
|
_build_rule_for_from_condition(from),
|
|
32
32
|
].compact
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Axn::Core::Flow::Handlers::Matcher.new(rules, invert: !!binding.local_variable_get(:unless))
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def self._build_rule_for_from_condition(from_class)
|
|
38
38
|
return nil unless from_class
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
# Special case: `from: true` means "from any child action"
|
|
41
|
+
return ->(exception:, **) { exception.is_a?(Axn::Failure) && exception.source } if from_class == true
|
|
42
|
+
|
|
43
|
+
from_classes = Array(from_class)
|
|
44
|
+
lambda { |exception:, **|
|
|
45
|
+
return false unless exception.is_a?(Axn::Failure) && exception.source
|
|
46
|
+
|
|
47
|
+
source = exception.source
|
|
48
|
+
from_classes.any? do |cls|
|
|
49
|
+
if cls.is_a?(String)
|
|
50
|
+
# rubocop:disable Style/ClassEqualityComparison
|
|
51
|
+
# We're comparing class name strings, not classes themselves
|
|
52
|
+
source.class.name == cls
|
|
53
|
+
# rubocop:enable Style/ClassEqualityComparison
|
|
54
|
+
else
|
|
55
|
+
source.is_a?(cls)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
}
|
|
47
59
|
end
|
|
48
60
|
end
|
|
49
61
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Axn
|
|
4
|
+
module Core
|
|
5
|
+
module Flow
|
|
6
|
+
module Handlers
|
|
7
|
+
# Shared block evaluation with consistent arity handling and error piping
|
|
8
|
+
module Invoker
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
def call(action:, handler:, exception: nil, operation: "executing handler")
|
|
12
|
+
return call_symbol_handler(action:, symbol: handler, exception:) if symbol?(handler)
|
|
13
|
+
return call_callable_handler(action:, callable: handler, exception:) if callable?(handler)
|
|
14
|
+
|
|
15
|
+
literal_value(handler)
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
Axn::Internal::Logging.piping_error(operation, action:, exception: e)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def symbol?(value) = value.is_a?(Symbol)
|
|
23
|
+
|
|
24
|
+
def callable?(value) = value.respond_to?(:arity)
|
|
25
|
+
|
|
26
|
+
def call_symbol_handler(action:, symbol:, exception: nil)
|
|
27
|
+
unless action.respond_to?(symbol, true)
|
|
28
|
+
action.warn("Ignoring apparently-invalid symbol #{symbol.inspect} -- action does not respond to method")
|
|
29
|
+
return nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
method = action.method(symbol)
|
|
33
|
+
filtered_args, filtered_kwargs = Axn::Util::Callable.only_requested_params_for_exception(method, exception)
|
|
34
|
+
action.send(symbol, *filtered_args, **filtered_kwargs)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def call_callable_handler(action:, callable:, exception: nil)
|
|
38
|
+
filtered_args, filtered_kwargs = Axn::Util::Callable.only_requested_params_for_exception(callable, exception)
|
|
39
|
+
action.instance_exec(*filtered_args, **filtered_kwargs, &callable)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def literal_value(value) = value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "axn/core/flow/handlers/invoker"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module Axn
|
|
6
6
|
module Core
|
|
7
7
|
module Flow
|
|
8
8
|
module Handlers
|
|
@@ -16,7 +16,7 @@ module Action
|
|
|
16
16
|
result = matches?(exception:, action:)
|
|
17
17
|
@invert ? !result : result
|
|
18
18
|
rescue StandardError => e
|
|
19
|
-
Axn::
|
|
19
|
+
Axn::Internal::Logging.piping_error("determining if handler applies to exception", action:, exception: e)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
private
|
|
@@ -36,25 +36,15 @@ module Action
|
|
|
36
36
|
def exception_class? = @rule.is_a?(Class) && @rule <= Exception
|
|
37
37
|
|
|
38
38
|
def apply_callable(action:, exception:)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
elsif exception && Invoker.accepts_positional_exception?(@rule)
|
|
42
|
-
!!action.instance_exec(exception, &@rule)
|
|
43
|
-
else
|
|
44
|
-
!!action.instance_exec(&@rule)
|
|
45
|
-
end
|
|
39
|
+
filtered_args, filtered_kwargs = Axn::Util::Callable.only_requested_params_for_exception(@rule, exception)
|
|
40
|
+
!!action.instance_exec(*filtered_args, **filtered_kwargs, &@rule)
|
|
46
41
|
end
|
|
47
42
|
|
|
48
43
|
def apply_symbol(action:, exception:)
|
|
49
44
|
if action.respond_to?(@rule)
|
|
50
45
|
method = action.method(@rule)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
elsif exception && Invoker.accepts_positional_exception?(method)
|
|
54
|
-
!!action.public_send(@rule, exception)
|
|
55
|
-
else
|
|
56
|
-
!!action.public_send(@rule)
|
|
57
|
-
end
|
|
46
|
+
filtered_args, filtered_kwargs = Axn::Util::Callable.only_requested_params_for_exception(method, exception)
|
|
47
|
+
!!action.public_send(@rule, *filtered_args, **filtered_kwargs)
|
|
58
48
|
else
|
|
59
49
|
begin
|
|
60
50
|
klass = Object.const_get(@rule.to_s)
|
|
@@ -92,7 +82,7 @@ module Action
|
|
|
92
82
|
def call(exception:, action:)
|
|
93
83
|
matches?(exception:, action:)
|
|
94
84
|
rescue StandardError => e
|
|
95
|
-
Axn::
|
|
85
|
+
Axn::Internal::Logging.piping_error("determining if handler applies to exception", action:, exception: e)
|
|
96
86
|
end
|
|
97
87
|
|
|
98
88
|
def static? = @rules.empty?
|
|
@@ -103,7 +93,7 @@ module Action
|
|
|
103
93
|
if_condition = binding.local_variable_get(:if)
|
|
104
94
|
unless_condition = binding.local_variable_get(:unless)
|
|
105
95
|
|
|
106
|
-
raise
|
|
96
|
+
raise Axn::UnsupportedArgument, "providing both :if and :unless" if if_condition && unless_condition
|
|
107
97
|
|
|
108
98
|
new(Array(if_condition || unless_condition).compact, invert: !!unless_condition)
|
|
109
99
|
end
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Axn
|
|
4
4
|
module Core
|
|
5
5
|
module Flow
|
|
6
6
|
module Handlers
|
|
7
7
|
# Small, immutable, copy-on-write registry keyed by event_type.
|
|
8
8
|
# Stores arrays of entries (handlers/interceptors) in insertion order.
|
|
9
|
+
#
|
|
10
|
+
# NOTE: serves different need than user-mutable e.g. Axn::Async::Adapters
|
|
9
11
|
class Registry
|
|
10
12
|
def self.empty = new({})
|
|
11
13
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "axn/core/flow/handlers/invoker"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module Axn
|
|
6
6
|
module Core
|
|
7
7
|
module Flow
|
|
8
8
|
module Handlers
|
|
@@ -33,7 +33,7 @@ module Action
|
|
|
33
33
|
message = resolved_message_body(descriptor)
|
|
34
34
|
return nil unless message.present?
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
"#{resolved_prefix(descriptor)}#{message}"
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def resolved_message_body(descriptor)
|
|
@@ -49,6 +49,15 @@ module Action
|
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def resolved_prefix(descriptor)
|
|
53
|
+
return nil unless descriptor.prefix
|
|
54
|
+
return descriptor.prefix if descriptor.prefix.is_a?(String)
|
|
55
|
+
|
|
56
|
+
Invoker.call(action:, handler: descriptor.prefix, exception:, operation: "determining prefix callable")
|
|
57
|
+
rescue StandardError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
52
61
|
def invoke_handler(handler) = handler ? Invoker.call(operation: "determining message callable", action:, handler:, exception:).presence : nil
|
|
53
62
|
def fallback_message = event_type == :success ? DEFAULT_SUCCESS : DEFAULT_ERROR
|
|
54
63
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Axn
|
|
4
|
+
module Core
|
|
5
|
+
module Flow
|
|
6
|
+
module Handlers
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require "axn/core/flow/handlers/base_descriptor"
|
|
13
|
+
require "axn/core/flow/handlers/matcher"
|
|
14
|
+
require "axn/core/flow/handlers/resolvers/base_resolver"
|
|
15
|
+
require "axn/core/flow/handlers/descriptors/message_descriptor"
|
|
16
|
+
require "axn/core/flow/handlers/descriptors/callback_descriptor"
|
|
17
|
+
require "axn/core/flow/handlers/invoker"
|
|
18
|
+
require "axn/core/flow/handlers/resolvers/callback_resolver"
|
|
19
|
+
require "axn/core/flow/handlers/registry"
|
|
20
|
+
require "axn/core/flow/handlers/resolvers/message_resolver"
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "axn/core/flow/handlers"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module Axn
|
|
6
6
|
module Core
|
|
7
7
|
module Flow
|
|
8
8
|
module Messages
|
|
9
9
|
def self.included(base)
|
|
10
10
|
base.class_eval do
|
|
11
|
-
class_attribute :_messages_registry, default:
|
|
11
|
+
class_attribute :_messages_registry, default: Axn::Core::Flow::Handlers::Registry.empty
|
|
12
12
|
|
|
13
13
|
extend ClassMethods
|
|
14
14
|
end
|
|
@@ -21,19 +21,19 @@ module Action
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
23
|
def _add_message(kind, message:, **kwargs, &block)
|
|
24
|
-
raise
|
|
25
|
-
raise
|
|
24
|
+
raise Axn::UnsupportedArgument, "calling #{kind} with both :if and :unless" if kwargs.key?(:if) && kwargs.key?(:unless)
|
|
25
|
+
raise Axn::UnsupportedArgument, "Combining from: with if: or unless:" if kwargs.key?(:from) && (kwargs.key?(:if) || kwargs.key?(:unless))
|
|
26
26
|
raise ArgumentError, "Provide either a message or a block, not both" if message && block_given?
|
|
27
|
-
raise ArgumentError, "Provide a message, block, or prefix" unless message || block_given? || kwargs[:prefix]
|
|
27
|
+
raise ArgumentError, "Provide a message, block, or prefix" unless message || block_given? || kwargs[:prefix] || kwargs[:from]
|
|
28
28
|
raise ArgumentError, "from: only applies to error messages" if kwargs.key?(:from) && kind != :error
|
|
29
29
|
|
|
30
30
|
# If message is already a descriptor, use it directly
|
|
31
|
-
entry = if message.is_a?(
|
|
31
|
+
entry = if message.is_a?(Axn::Core::Flow::Handlers::Descriptors::MessageDescriptor)
|
|
32
32
|
raise ArgumentError, "Cannot pass additional configuration with prebuilt descriptor" if kwargs.any? || block_given?
|
|
33
33
|
|
|
34
34
|
message
|
|
35
35
|
else
|
|
36
|
-
|
|
36
|
+
Axn::Core::Flow::Handlers::Descriptors::MessageDescriptor.build(
|
|
37
37
|
handler: block_given? ? block : message,
|
|
38
38
|
**kwargs,
|
|
39
39
|
)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "
|
|
5
|
-
require "
|
|
3
|
+
require "axn/core/flow/messages"
|
|
4
|
+
require "axn/core/flow/callbacks"
|
|
5
|
+
require "axn/core/flow/exception_execution"
|
|
6
6
|
|
|
7
|
-
module
|
|
7
|
+
module Axn
|
|
8
8
|
module Core
|
|
9
9
|
module Flow
|
|
10
10
|
def self.included(base)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Axn
|
|
4
4
|
module Core
|
|
5
5
|
module Hooks
|
|
6
6
|
def self.included(base)
|
|
@@ -68,10 +68,15 @@ module Action
|
|
|
68
68
|
private
|
|
69
69
|
|
|
70
70
|
def _with_hooks
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
# Outer is needed in the unlikely case done! is called in around hooks
|
|
72
|
+
__respecting_early_completion do
|
|
73
|
+
_run_around_hooks do
|
|
74
|
+
__respecting_early_completion do
|
|
75
|
+
_run_before_hooks
|
|
76
|
+
yield
|
|
77
|
+
_run_after_hooks
|
|
78
|
+
end
|
|
79
|
+
end
|
|
75
80
|
end
|
|
76
81
|
end
|
|
77
82
|
|
|
@@ -118,6 +123,13 @@ module Action
|
|
|
118
123
|
def _run_hook(hook, *)
|
|
119
124
|
hook.is_a?(Symbol) ? send(hook, *) : instance_exec(*, &hook)
|
|
120
125
|
end
|
|
126
|
+
|
|
127
|
+
def __respecting_early_completion
|
|
128
|
+
yield
|
|
129
|
+
rescue Axn::Internal::EarlyCompletion => e
|
|
130
|
+
@__context.__record_early_completion(e.message)
|
|
131
|
+
raise e
|
|
132
|
+
end
|
|
121
133
|
end
|
|
122
134
|
end
|
|
123
135
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/module/delegation"
|
|
4
|
+
|
|
5
|
+
module Axn
|
|
6
|
+
module Core
|
|
7
|
+
module Logging
|
|
8
|
+
LEVELS = %i[debug info warn error fatal].freeze
|
|
9
|
+
|
|
10
|
+
def self.included(base)
|
|
11
|
+
base.class_eval do
|
|
12
|
+
extend ClassMethods
|
|
13
|
+
delegate :log, *LEVELS, to: :class
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ClassMethods
|
|
18
|
+
def log_level = Axn.config.log_level
|
|
19
|
+
|
|
20
|
+
# @param message [String] The message to log
|
|
21
|
+
# @param level [Symbol] The log level (default: log_level)
|
|
22
|
+
# @param before [String, nil] Text to prepend to the message
|
|
23
|
+
# @param after [String, nil] Text to append to the message
|
|
24
|
+
# @param prefix [String, nil] Override the default prefix (useful for class-level logging)
|
|
25
|
+
def log(message, level: log_level, before: nil, after: nil, prefix: nil)
|
|
26
|
+
resolved_prefix = prefix.nil? ? _log_prefix : prefix
|
|
27
|
+
msg = [resolved_prefix, message].compact_blank.join(" ")
|
|
28
|
+
msg = [before, msg, after].compact_blank.join if before || after
|
|
29
|
+
|
|
30
|
+
Axn.config.logger.send(level, msg)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
LEVELS.each do |level|
|
|
34
|
+
define_method(level) do |message, before: nil, after: nil, prefix: nil|
|
|
35
|
+
log(message, level:, before:, after:, prefix:)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def _log_prefix
|
|
40
|
+
names = NestingTracking._current_axn_stack.map do |axn|
|
|
41
|
+
axn.class.name.presence || "Anonymous Class"
|
|
42
|
+
end
|
|
43
|
+
"[#{names.join(' > ')}]"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Axn
|
|
4
|
+
module Core
|
|
5
|
+
module Memoization
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.class_eval do
|
|
8
|
+
extend ClassMethods
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module ClassMethods
|
|
13
|
+
def memo(method_name)
|
|
14
|
+
if _memo_wise_available?
|
|
15
|
+
_ensure_memo_wise_prepended
|
|
16
|
+
memo_wise(method_name)
|
|
17
|
+
else
|
|
18
|
+
_memo_minimal(method_name)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def _memo_wise_available?
|
|
25
|
+
defined?(MemoWise)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def _ensure_memo_wise_prepended
|
|
29
|
+
return if ancestors.include?(MemoWise)
|
|
30
|
+
|
|
31
|
+
prepend MemoWise
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def _memo_minimal(method_name)
|
|
35
|
+
method = instance_method(method_name)
|
|
36
|
+
params = method.parameters
|
|
37
|
+
has_args = params.any? { |type, _name| %i[req opt rest keyreq key keyrest].include?(type) }
|
|
38
|
+
|
|
39
|
+
if has_args
|
|
40
|
+
raise ArgumentError,
|
|
41
|
+
"Memoization of methods with arguments requires the 'memo_wise' gem. " \
|
|
42
|
+
"Please add 'memo_wise' to your Gemfile or use a method without arguments."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Wrap the method with memoization
|
|
46
|
+
Axn::Util::Memoization.define_memoized_reader_method(self, method_name) do
|
|
47
|
+
method.bind(self).call
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Axn
|
|
6
|
+
module Core
|
|
7
|
+
module Tracing
|
|
8
|
+
class << self
|
|
9
|
+
# Cache the tracer instance to avoid repeated lookups
|
|
10
|
+
# The tracer provider may cache internally, but we avoid the method call overhead
|
|
11
|
+
# We check defined?(OpenTelemetry) each time to handle cases where it's loaded lazily
|
|
12
|
+
def tracer
|
|
13
|
+
return nil unless defined?(OpenTelemetry)
|
|
14
|
+
|
|
15
|
+
# Re-fetch if the tracer provider has changed (e.g., in tests with mocks)
|
|
16
|
+
current_provider = OpenTelemetry.tracer_provider
|
|
17
|
+
return @tracer if defined?(@tracer) && defined?(@tracer_provider) && @tracer_provider == current_provider
|
|
18
|
+
|
|
19
|
+
@tracer_provider = current_provider
|
|
20
|
+
@tracer = current_provider.tracer("axn", Axn::VERSION)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def _with_tracing(&)
|
|
27
|
+
resource = self.class.name || "AnonymousClass"
|
|
28
|
+
payload = { resource:, action: self }
|
|
29
|
+
|
|
30
|
+
update_payload = proc do
|
|
31
|
+
result = self.result
|
|
32
|
+
outcome = result.outcome.to_s
|
|
33
|
+
payload[:outcome] = outcome
|
|
34
|
+
payload[:result] = result
|
|
35
|
+
payload[:elapsed_time] = result.elapsed_time
|
|
36
|
+
payload[:exception] = result.exception if result.exception
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
# Don't raise in ensure block to avoid interfering with existing exceptions
|
|
39
|
+
Axn::Internal::Logging.piping_error("updating notification payload while tracing axn.call", action: self, exception: e)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
instrument_block = proc do
|
|
43
|
+
ActiveSupport::Notifications.instrument("axn.call", payload, &)
|
|
44
|
+
ensure
|
|
45
|
+
# Update payload BEFORE instrument completes so subscribers see the changes
|
|
46
|
+
update_payload.call
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# NOTE: despite using block form, ActiveSupport explicitly only emits to subscribers when it's finished,
|
|
50
|
+
# which means it's not suitable for wrapping execution with a span and tracking child spans.
|
|
51
|
+
# We use OpenTelemetry for that, if available.
|
|
52
|
+
if defined?(OpenTelemetry)
|
|
53
|
+
Tracing.tracer.in_span("axn.call", attributes: { "axn.resource" => resource }) do |span|
|
|
54
|
+
instrument_block.call
|
|
55
|
+
ensure
|
|
56
|
+
# Update span with outcome and error status after execution
|
|
57
|
+
# This ensure runs before the span finishes, so we can still update it
|
|
58
|
+
begin
|
|
59
|
+
result = self.result
|
|
60
|
+
outcome = result.outcome.to_s
|
|
61
|
+
span.set_attribute("axn.outcome", outcome)
|
|
62
|
+
|
|
63
|
+
if %w[failure exception].include?(outcome) && result.exception
|
|
64
|
+
span.record_exception(result.exception)
|
|
65
|
+
error_message = result.exception.message || result.exception.class.name
|
|
66
|
+
span.status = OpenTelemetry::Trace::Status.error(error_message)
|
|
67
|
+
end
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
# Don't raise in ensure block to avoid interfering with existing exceptions
|
|
70
|
+
Axn::Internal::Logging.piping_error("updating OTel span while tracing axn.call", action: self, exception: e)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
instrument_block.call
|
|
75
|
+
end
|
|
76
|
+
ensure
|
|
77
|
+
begin
|
|
78
|
+
emit_metrics_proc = Axn.config.emit_metrics
|
|
79
|
+
if emit_metrics_proc
|
|
80
|
+
result = self.result
|
|
81
|
+
Axn::Util::Callable.call_with_desired_shape(emit_metrics_proc, kwargs: { resource:, result: })
|
|
82
|
+
end
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
# Don't raise in ensure block to avoid interfering with existing exceptions
|
|
85
|
+
Axn::Internal::Logging.piping_error("calling emit_metrics while tracing axn.call", action: self, exception: e)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Axn
|
|
4
|
+
module Core
|
|
5
|
+
module UseStrategy
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def use(strategy_name, **config, &block)
|
|
10
|
+
strategy = Axn::Strategies.find(strategy_name)
|
|
11
|
+
raise ArgumentError, "Strategy #{strategy_name} does not support config" if config.any? && !strategy.respond_to?(:configure)
|
|
12
|
+
|
|
13
|
+
# Allow dynamic configuration of strategy (i.e. dynamically define module before returning)
|
|
14
|
+
if strategy.respond_to?(:configure)
|
|
15
|
+
configured = strategy.configure(**config, &block)
|
|
16
|
+
raise ArgumentError, "Strategy #{strategy_name} configure method must return a module" unless configured.is_a?(Module)
|
|
17
|
+
|
|
18
|
+
strategy = configured
|
|
19
|
+
else
|
|
20
|
+
raise ArgumentError, "Strategy #{strategy_name} does not support config (define #configure method)" if config.any?
|
|
21
|
+
raise ArgumentError, "Strategy #{strategy_name} does not support blocks (define #configure method)" if block_given?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
include strategy
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Axn
|
|
4
4
|
module Validation
|
|
5
5
|
class Fields
|
|
6
6
|
include ActiveModel::Validations
|
|
@@ -20,9 +20,25 @@ module Action
|
|
|
20
20
|
@context.public_send(attr)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
def method_missing(method_name, ...)
|
|
24
|
+
# Delegate method calls to the action instance to support symbol-based validations
|
|
25
|
+
# like inclusion: { in: :valid_channels_for_number }
|
|
26
|
+
action = _action_for_validation
|
|
27
|
+
return super unless action && action.respond_to?(method_name, true) # rubocop:disable Style/SafeNavigation
|
|
28
|
+
|
|
29
|
+
action.send(method_name, ...)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
33
|
+
action = _action_for_validation
|
|
34
|
+
return super unless action
|
|
35
|
+
|
|
36
|
+
action.respond_to?(method_name, include_private) || super
|
|
37
|
+
end
|
|
38
|
+
|
|
23
39
|
def self.validate!(validations:, context:, exception_klass:)
|
|
24
40
|
validator = Class.new(self) do
|
|
25
|
-
def self.name = "
|
|
41
|
+
def self.name = "Axn::Validation::Fields::OneOff"
|
|
26
42
|
|
|
27
43
|
validations.each do |field, field_validations|
|
|
28
44
|
field_validations.each do |key, value|
|
|
@@ -35,6 +51,14 @@ module Action
|
|
|
35
51
|
|
|
36
52
|
raise exception_klass, validator.errors
|
|
37
53
|
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def _action_for_validation
|
|
58
|
+
return unless @context.respond_to?(:action, true)
|
|
59
|
+
|
|
60
|
+
@context.send(:action)
|
|
61
|
+
end
|
|
38
62
|
end
|
|
39
63
|
end
|
|
40
64
|
end
|