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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Axn
|
|
4
|
+
module Util
|
|
5
|
+
module Callable
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
# Calls a callable with only the positional and keyword arguments it expects.
|
|
9
|
+
# If the callable accepts **kwargs (keyrest), passes all provided kwargs.
|
|
10
|
+
# If the callable accepts *args (rest), passes all provided positional args.
|
|
11
|
+
#
|
|
12
|
+
# @param callable [Proc, Method, #call] A callable object
|
|
13
|
+
# @param args [Array] An array of positional arguments to potentially pass
|
|
14
|
+
# @param kwargs [Hash] A hash of keyword arguments to potentially pass
|
|
15
|
+
# @return The return value of calling the callable
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# proc = ->(resource:, result:) { }
|
|
19
|
+
# Callable.call_with_desired_shape(proc, kwargs: { resource: "Action", result: result, extra: "ignored" })
|
|
20
|
+
# # Calls proc with only resource: and result:
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# proc = ->(a, b, c:) { }
|
|
24
|
+
# Callable.call_with_desired_shape(proc, args: [1, 2, 3, 4], kwargs: { c: 5, d: 6 })
|
|
25
|
+
# # Calls proc with args [1, 2, 3] and kwargs { c: 5 }
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# proc = ->(**kwargs) { }
|
|
29
|
+
# Callable.call_with_desired_shape(proc, kwargs: { resource: "Action", result: result })
|
|
30
|
+
# # Calls proc with all kwargs
|
|
31
|
+
# Calls a callable with only the positional and keyword arguments it expects.
|
|
32
|
+
def call_with_desired_shape(callable, args: [], kwargs: {})
|
|
33
|
+
filtered_args, filtered_kwargs = only_requested_params(callable, args:, kwargs:)
|
|
34
|
+
callable.call(*filtered_args, **filtered_kwargs)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns filtered args and kwargs for a callable without calling it.
|
|
38
|
+
# Useful when you need to execute the callable in a specific context (e.g., via instance_exec).
|
|
39
|
+
#
|
|
40
|
+
# @param callable [Proc, Method, #parameters] A callable object
|
|
41
|
+
# @param args [Array] An array of positional arguments to potentially pass
|
|
42
|
+
# @param kwargs [Hash] A hash of keyword arguments to potentially pass
|
|
43
|
+
# @return [Array<Array, Hash>] A tuple of [filtered_args, filtered_kwargs]
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# proc = ->(resource:, result:) { }
|
|
47
|
+
# args, kwargs = Callable.only_requested_params(proc, kwargs: { resource: "Action", result: result, extra: "ignored" })
|
|
48
|
+
# # => [[], { resource: "Action", result: result }]
|
|
49
|
+
# action.instance_exec(*args, **kwargs, &proc)
|
|
50
|
+
def only_requested_params(callable, args: [], kwargs: {})
|
|
51
|
+
return [args, kwargs] unless callable.respond_to?(:parameters)
|
|
52
|
+
|
|
53
|
+
params = callable.parameters
|
|
54
|
+
|
|
55
|
+
# Determine which positional arguments to pass
|
|
56
|
+
filtered_args = filter_positional_args(params, args)
|
|
57
|
+
|
|
58
|
+
# Determine which keyword arguments to pass
|
|
59
|
+
filtered_kwargs = filter_kwargs(params, kwargs)
|
|
60
|
+
|
|
61
|
+
[filtered_args, filtered_kwargs]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns filtered args and kwargs for a callable when passing an exception.
|
|
65
|
+
# The exception will be passed as either a positional argument or keyword argument,
|
|
66
|
+
# depending on what the callable expects.
|
|
67
|
+
#
|
|
68
|
+
# @param callable [Proc, Method, #parameters] A callable object
|
|
69
|
+
# @param exception [Exception, nil] The exception to potentially pass
|
|
70
|
+
# @return [Array<Array, Hash>] A tuple of [filtered_args, filtered_kwargs]
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# proc = ->(exception:) { }
|
|
74
|
+
# args, kwargs = Callable.only_requested_params_for_exception(proc, exception)
|
|
75
|
+
# # => [[], { exception: exception }]
|
|
76
|
+
# action.instance_exec(*args, **kwargs, &proc)
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# proc = ->(exception) { }
|
|
80
|
+
# args, kwargs = Callable.only_requested_params_for_exception(proc, exception)
|
|
81
|
+
# # => [[exception], {}]
|
|
82
|
+
# action.instance_exec(*args, **kwargs, &proc)
|
|
83
|
+
def only_requested_params_for_exception(callable, exception)
|
|
84
|
+
return [[], {}] unless exception
|
|
85
|
+
|
|
86
|
+
args = [exception]
|
|
87
|
+
kwargs = { exception: }
|
|
88
|
+
only_requested_params(callable, args:, kwargs:)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def filter_positional_args(params, args)
|
|
94
|
+
return args if args.empty?
|
|
95
|
+
|
|
96
|
+
required_count = params.count { |type, _name| type == :req }
|
|
97
|
+
optional_count = params.count { |type, _name| type == :opt }
|
|
98
|
+
has_rest = params.any? { |type, _name| type == :rest }
|
|
99
|
+
|
|
100
|
+
# If it accepts *args (rest), pass all provided args
|
|
101
|
+
return args if has_rest
|
|
102
|
+
|
|
103
|
+
# Otherwise, pass up to (required + optional) args
|
|
104
|
+
max_args = required_count + optional_count
|
|
105
|
+
args.first(max_args)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def filter_kwargs(params, kwargs)
|
|
109
|
+
return kwargs if kwargs.empty?
|
|
110
|
+
|
|
111
|
+
accepts_keyrest = params.any? { |type, _name| type == :keyrest }
|
|
112
|
+
return kwargs if accepts_keyrest
|
|
113
|
+
|
|
114
|
+
# Only pass explicitly expected keyword arguments
|
|
115
|
+
expected_keywords = params.select { |type, _name| %i[key keyreq].include?(type) }.map { |_type, name| name }
|
|
116
|
+
kwargs.select { |key, _value| expected_keywords.include?(key) }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Axn
|
|
4
|
+
module Util
|
|
5
|
+
module ContractErrorHandling
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Executes a block, allowing fail! and done! to propagate normally,
|
|
9
|
+
# but wrapping other StandardErrors in the specified exception class.
|
|
10
|
+
#
|
|
11
|
+
# @param exception_class [Class] The exception class to wrap errors in
|
|
12
|
+
# @param message [String, Proc] Error message or proc that takes (field_identifier, error)
|
|
13
|
+
# @param field_identifier [String] Identifier for the field (for error messages)
|
|
14
|
+
# @yield The block to execute
|
|
15
|
+
# @raise [Axn::Failure] Re-raised if raised in block
|
|
16
|
+
# @raise [Axn::Internal::EarlyCompletion] Re-raised if raised in block
|
|
17
|
+
# @raise [exception_class] Wrapped exception for other StandardErrors
|
|
18
|
+
def with_contract_error_handling(exception_class:, message:, field_identifier:)
|
|
19
|
+
yield
|
|
20
|
+
rescue Axn::Failure, Axn::Internal::EarlyCompletion => e
|
|
21
|
+
raise e # Re-raise control flow exceptions without wrapping
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
error_message = if message.is_a?(Proc)
|
|
24
|
+
message.call(field_identifier, e)
|
|
25
|
+
else
|
|
26
|
+
format(message, field_identifier, e.message)
|
|
27
|
+
end
|
|
28
|
+
raise exception_class, error_message, cause: e
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Axn
|
|
4
|
+
module Util
|
|
5
|
+
module ExecutionContext
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Determines if code is currently running within a background job context.
|
|
9
|
+
# Checks all registered async adapters to see if any report running in background.
|
|
10
|
+
#
|
|
11
|
+
# @return [Boolean] true if running in a background job, false otherwise
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# if Axn::Util::ExecutionContext.background?
|
|
15
|
+
# # Code is running in Sidekiq or ActiveJob
|
|
16
|
+
# end
|
|
17
|
+
def background?
|
|
18
|
+
Axn::Async::Adapters.all.values.any? do |adapter|
|
|
19
|
+
adapter.respond_to?(:_running_in_background?) && adapter._running_in_background?
|
|
20
|
+
end
|
|
21
|
+
rescue StandardError
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Determines if code is currently running in an interactive console (IRB, Pry, Rails console).
|
|
26
|
+
# Used to skip visual separators in console output since the prompt already provides separation.
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] true if running in a console, false otherwise
|
|
29
|
+
def console?
|
|
30
|
+
defined?(Rails::Console) || defined?(IRB) || defined?(Pry)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Axn
|
|
4
|
+
module Util
|
|
5
|
+
# Utilities for serializing/deserializing objects with GlobalID support.
|
|
6
|
+
# Used by async adapters to convert ActiveRecord objects to GlobalID strings
|
|
7
|
+
# for job serialization, and back to objects when the job runs.
|
|
8
|
+
module GlobalIdSerialization
|
|
9
|
+
GLOBAL_ID_SUFFIX = "_as_global_id"
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# Serialize a hash for background job processing:
|
|
13
|
+
# - Convert GlobalID-able objects (e.g., ActiveRecord models) to GlobalID strings
|
|
14
|
+
# - Stringify keys for JSON compatibility
|
|
15
|
+
#
|
|
16
|
+
# @param params [Hash] The parameters to serialize
|
|
17
|
+
# @return [Hash] Serialized hash with string keys and GlobalID strings
|
|
18
|
+
def serialize(params)
|
|
19
|
+
return {} if params.nil? || params.empty?
|
|
20
|
+
|
|
21
|
+
params.each_with_object({}) do |(key, value), hash|
|
|
22
|
+
string_key = key.to_s
|
|
23
|
+
if value.respond_to?(:to_global_id)
|
|
24
|
+
hash["#{string_key}#{GLOBAL_ID_SUFFIX}"] = value.to_global_id.to_s
|
|
25
|
+
else
|
|
26
|
+
hash[string_key] = value
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Deserialize a hash from background job processing:
|
|
32
|
+
# - Convert GlobalID strings back to objects
|
|
33
|
+
# - Symbolize keys for use with kwargs
|
|
34
|
+
#
|
|
35
|
+
# @param params [Hash] The serialized parameters
|
|
36
|
+
# @return [Hash] Deserialized hash with symbol keys and resolved objects
|
|
37
|
+
def deserialize(params)
|
|
38
|
+
return {} if params.nil? || params.empty?
|
|
39
|
+
|
|
40
|
+
params.each_with_object({}) do |(key, value), hash|
|
|
41
|
+
if key.end_with?(GLOBAL_ID_SUFFIX)
|
|
42
|
+
original_key = key.delete_suffix(GLOBAL_ID_SUFFIX).to_sym
|
|
43
|
+
hash[original_key] = GlobalID::Locator.locate(value)
|
|
44
|
+
else
|
|
45
|
+
hash[key.to_sym] = value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Axn
|
|
4
|
+
module Util
|
|
5
|
+
module Logging
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
MAX_CONTEXT_LENGTH = 150
|
|
9
|
+
TRUNCATION_SUFFIX = "…<truncated>…"
|
|
10
|
+
|
|
11
|
+
# Logs a message at the specified level with error handling
|
|
12
|
+
# @param action_class [Class] The action class to log from
|
|
13
|
+
# @param level [Symbol] The log level (e.g., :info, :warn)
|
|
14
|
+
# @param message_parts [Array<String>] Parts of the message to join
|
|
15
|
+
# @param error_context [String] Context for error reporting if logging fails
|
|
16
|
+
# @param join_string [String] String to join message parts with
|
|
17
|
+
# @param before [String, nil] Text to prepend to the message
|
|
18
|
+
# @param after [String, nil] Text to append to the message
|
|
19
|
+
# @param prefix [String, nil] Override the default log prefix (useful for class-level logging)
|
|
20
|
+
# @param context_direction [Symbol, nil] Direction for context logging (:inbound or :outbound)
|
|
21
|
+
# @param context_instance [Object, nil] Action instance for instance-level context_for_logging
|
|
22
|
+
# @param context_data [Hash, nil] Raw data for class-level context_for_logging
|
|
23
|
+
def log_at_level(
|
|
24
|
+
action_class,
|
|
25
|
+
level:,
|
|
26
|
+
message_parts:,
|
|
27
|
+
error_context:,
|
|
28
|
+
join_string: " ",
|
|
29
|
+
before: nil,
|
|
30
|
+
after: nil,
|
|
31
|
+
prefix: nil,
|
|
32
|
+
context_direction: nil,
|
|
33
|
+
context_instance: nil,
|
|
34
|
+
context_data: nil
|
|
35
|
+
)
|
|
36
|
+
return unless level
|
|
37
|
+
|
|
38
|
+
# Prepare and format context if needed
|
|
39
|
+
context_str = if context_instance && context_direction
|
|
40
|
+
# Instance-level context_for_logging
|
|
41
|
+
data = context_instance.context_for_logging(context_direction)
|
|
42
|
+
format_context(data)
|
|
43
|
+
elsif context_data && context_direction
|
|
44
|
+
# Class-level context_for_logging
|
|
45
|
+
data = action_class.context_for_logging(data: context_data, direction: context_direction)
|
|
46
|
+
format_context(data)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Add context to message parts if present
|
|
50
|
+
full_message_parts = context_str ? message_parts + [context_str] : message_parts
|
|
51
|
+
message = full_message_parts.compact.join(join_string)
|
|
52
|
+
|
|
53
|
+
action_class.public_send(level, message, before:, after:, prefix:)
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
Axn::Internal::Logging.piping_error(error_context, action: action_class, exception: e)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Formats context data for logging, with truncation if needed
|
|
61
|
+
def format_context(data)
|
|
62
|
+
return unless data.present?
|
|
63
|
+
|
|
64
|
+
formatted = format_object(data)
|
|
65
|
+
return formatted if formatted.length <= MAX_CONTEXT_LENGTH
|
|
66
|
+
|
|
67
|
+
formatted[0, MAX_CONTEXT_LENGTH - TRUNCATION_SUFFIX.length] + TRUNCATION_SUFFIX
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Formats an object for logging, handling special cases for ActiveRecord and ActionController::Parameters
|
|
71
|
+
def format_object(data)
|
|
72
|
+
case data
|
|
73
|
+
when Hash
|
|
74
|
+
# NOTE: slightly more manual in order to avoid quotes around ActiveRecord objects' <Class#id> formatting
|
|
75
|
+
"{#{data.map { |k, v| "#{k}: #{format_object(v)}" }.join(', ')}}"
|
|
76
|
+
when Array
|
|
77
|
+
data.map { |v| format_object(v) }
|
|
78
|
+
else
|
|
79
|
+
return data.to_unsafe_h if defined?(ActionController::Parameters) && data.is_a?(ActionController::Parameters)
|
|
80
|
+
return "<#{data.class.name}##{data.to_param.presence || 'unpersisted'}>" if defined?(ActiveRecord::Base) && data.is_a?(ActiveRecord::Base)
|
|
81
|
+
|
|
82
|
+
data.inspect
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
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,47 @@
|
|
|
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"
|
|
17
|
+
require "axn/util/callable"
|
|
18
|
+
require "axn/util/logging"
|
|
19
|
+
require "axn/util/execution_context"
|
|
20
|
+
require "axn/util/contract_error_handling"
|
|
21
|
+
require "axn/util/global_id_serialization"
|
|
22
|
+
require "axn/form_object"
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
require "
|
|
24
|
+
# Extensions
|
|
25
|
+
require "axn/mountable"
|
|
26
|
+
require "axn/async"
|
|
17
27
|
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
# Rails integration (if in Rails context)
|
|
29
|
+
require "axn/rails/engine" if defined?(Rails) && Rails.const_defined?(:Engine)
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
module Action
|
|
31
|
+
module Axn
|
|
25
32
|
def self.included(base)
|
|
26
33
|
base.class_eval do
|
|
27
34
|
include Core
|
|
28
35
|
|
|
29
36
|
# --- Extensions ---
|
|
30
|
-
include
|
|
31
|
-
include
|
|
37
|
+
include Mountable
|
|
38
|
+
include Async
|
|
32
39
|
|
|
33
40
|
# Allow additional automatic includes to be configured
|
|
34
|
-
Array(
|
|
41
|
+
Array(Axn.config.additional_includes).each { |mod| include mod }
|
|
35
42
|
end
|
|
36
43
|
end
|
|
37
44
|
end
|
|
45
|
+
|
|
46
|
+
# Load after Axn is defined since it includes Axn
|
|
47
|
+
require "axn/async/enqueue_all_orchestrator"
|
|
@@ -4,11 +4,11 @@ This directory contains custom RuboCop cops specifically designed for the Axn li
|
|
|
4
4
|
|
|
5
5
|
## Axn/UncheckedResult
|
|
6
6
|
|
|
7
|
-
This cop enforces proper result handling when calling
|
|
7
|
+
This cop enforces proper result handling when calling Axns. It can be configured to check nested calls, non-nested calls, or both.
|
|
8
8
|
|
|
9
9
|
### Why This Rule Exists
|
|
10
10
|
|
|
11
|
-
When
|
|
11
|
+
When Axns are nested, proper error handling becomes crucial. Without proper result checking, failures in nested Axns can be silently ignored, leading to:
|
|
12
12
|
|
|
13
13
|
- Silent failures that are hard to debug
|
|
14
14
|
- Inconsistent error handling patterns
|
|
@@ -21,8 +21,8 @@ The cop supports flexible configuration to match your team's needs:
|
|
|
21
21
|
```yaml
|
|
22
22
|
Axn/UncheckedResult:
|
|
23
23
|
Enabled: true
|
|
24
|
-
CheckNested: true # Check nested
|
|
25
|
-
CheckNonNested: true # Check non-nested
|
|
24
|
+
CheckNested: true # Check nested Axn calls (default: true)
|
|
25
|
+
CheckNonNested: true # Check non-nested Axn calls (default: true)
|
|
26
26
|
Severity: warning # or error, if you want to enforce it strictly
|
|
27
27
|
```
|
|
28
28
|
|
|
@@ -33,21 +33,21 @@ Axn/UncheckedResult:
|
|
|
33
33
|
CheckNested: true
|
|
34
34
|
CheckNonNested: true
|
|
35
35
|
```
|
|
36
|
-
Checks all
|
|
36
|
+
Checks all Axn calls regardless of nesting.
|
|
37
37
|
|
|
38
38
|
2. **Nested Only**:
|
|
39
39
|
```yaml
|
|
40
40
|
CheckNested: true
|
|
41
41
|
CheckNonNested: false
|
|
42
42
|
```
|
|
43
|
-
Only checks
|
|
43
|
+
Only checks Axn calls from within other Axns.
|
|
44
44
|
|
|
45
45
|
3. **Non-Nested Only**:
|
|
46
46
|
```yaml
|
|
47
47
|
CheckNested: false
|
|
48
48
|
CheckNonNested: true
|
|
49
49
|
```
|
|
50
|
-
Only checks top-level
|
|
50
|
+
Only checks top-level Axn calls.
|
|
51
51
|
|
|
52
52
|
4. **Disabled**:
|
|
53
53
|
```yaml
|
|
@@ -62,7 +62,7 @@ Axn/UncheckedResult:
|
|
|
62
62
|
|
|
63
63
|
```ruby
|
|
64
64
|
class OuterAction
|
|
65
|
-
include
|
|
65
|
+
include Axn
|
|
66
66
|
def call
|
|
67
67
|
InnerAction.call(param: "value") # Missing result check
|
|
68
68
|
# This will always continue even if InnerAction fails
|
|
@@ -74,7 +74,7 @@ end
|
|
|
74
74
|
|
|
75
75
|
```ruby
|
|
76
76
|
class OuterAction
|
|
77
|
-
include
|
|
77
|
+
include Axn
|
|
78
78
|
def call
|
|
79
79
|
InnerAction.call!(param: "value") # Using call! ensures exceptions bubble up
|
|
80
80
|
end
|
|
@@ -85,7 +85,7 @@ end
|
|
|
85
85
|
|
|
86
86
|
```ruby
|
|
87
87
|
class OuterAction
|
|
88
|
-
include
|
|
88
|
+
include Axn
|
|
89
89
|
def call
|
|
90
90
|
result = InnerAction.call(param: "value")
|
|
91
91
|
return result unless result.ok?
|
|
@@ -98,7 +98,7 @@ end
|
|
|
98
98
|
|
|
99
99
|
```ruby
|
|
100
100
|
class OuterAction
|
|
101
|
-
include
|
|
101
|
+
include Axn
|
|
102
102
|
def call
|
|
103
103
|
result = InnerAction.call(param: "value")
|
|
104
104
|
if result.failed?
|
|
@@ -113,7 +113,7 @@ end
|
|
|
113
113
|
|
|
114
114
|
```ruby
|
|
115
115
|
class OuterAction
|
|
116
|
-
include
|
|
116
|
+
include Axn
|
|
117
117
|
def call
|
|
118
118
|
result = InnerAction.call(param: "value")
|
|
119
119
|
if result.error
|
|
@@ -128,7 +128,7 @@ end
|
|
|
128
128
|
|
|
129
129
|
```ruby
|
|
130
130
|
class OuterAction
|
|
131
|
-
include
|
|
131
|
+
include Axn
|
|
132
132
|
def call
|
|
133
133
|
result = InnerAction.call(param: "value")
|
|
134
134
|
result # Result is returned, so it's properly handled
|
|
@@ -140,7 +140,7 @@ end
|
|
|
140
140
|
|
|
141
141
|
```ruby
|
|
142
142
|
class OuterAction
|
|
143
|
-
include
|
|
143
|
+
include Axn
|
|
144
144
|
exposes :nested_result
|
|
145
145
|
def call
|
|
146
146
|
result = InnerAction.call(param: "value")
|
|
@@ -153,7 +153,7 @@ end
|
|
|
153
153
|
|
|
154
154
|
```ruby
|
|
155
155
|
class OuterAction
|
|
156
|
-
include
|
|
156
|
+
include Axn
|
|
157
157
|
def call
|
|
158
158
|
result = InnerAction.call(param: "value")
|
|
159
159
|
process_result(result) # Result is used, so it's properly handled
|
|
@@ -165,19 +165,19 @@ end
|
|
|
165
165
|
|
|
166
166
|
The cop analyzes your code to determine if you're:
|
|
167
167
|
|
|
168
|
-
1. **Inside an
|
|
168
|
+
1. **Inside an Axn class** - Classes that `include Axn`
|
|
169
169
|
2. **Inside the `call` method** - Only the main execution method
|
|
170
|
-
3. **Calling another
|
|
170
|
+
3. **Calling another Axn** - Using `.call` on Axn classes
|
|
171
171
|
4. **Properly handling the result** - One of the acceptable patterns above
|
|
172
172
|
|
|
173
173
|
### What the Cop Ignores
|
|
174
174
|
|
|
175
175
|
The cop will NOT report offenses for:
|
|
176
176
|
|
|
177
|
-
-
|
|
178
|
-
-
|
|
179
|
-
-
|
|
180
|
-
-
|
|
177
|
+
- Axn calls outside of Axn classes
|
|
178
|
+
- Axn calls in methods other than `call`
|
|
179
|
+
- Axn calls that use `call!` (bang method)
|
|
180
|
+
- Axn calls where the result is properly handled
|
|
181
181
|
|
|
182
182
|
### Configuration
|
|
183
183
|
|
|
@@ -189,8 +189,8 @@ require:
|
|
|
189
189
|
|
|
190
190
|
Axn/UncheckedResult:
|
|
191
191
|
Enabled: true
|
|
192
|
-
CheckNested: true # Check nested
|
|
193
|
-
CheckNonNested: true # Check non-nested
|
|
192
|
+
CheckNested: true # Check nested Axn calls
|
|
193
|
+
CheckNonNested: true # Check non-nested Axn calls
|
|
194
194
|
Severity: warning # or error, if you want to enforce it strictly
|
|
195
195
|
```
|
|
196
196
|
|