axn 0.1.0.pre.alpha.3 → 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/CHANGELOG.md +15 -1
- data/Rakefile +102 -2
- data/docs/.vitepress/config.mjs +12 -8
- data/docs/advanced/conventions.md +1 -1
- data/docs/advanced/mountable.md +4 -90
- data/docs/advanced/profiling.md +26 -30
- data/docs/advanced/rough.md +27 -8
- data/docs/intro/overview.md +1 -1
- data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
- data/docs/recipes/memoization.md +102 -17
- data/docs/reference/async.md +269 -0
- data/docs/reference/class.md +113 -50
- data/docs/reference/configuration.md +226 -75
- data/docs/reference/form-object.md +252 -0
- data/docs/strategies/client.md +212 -0
- data/docs/strategies/form.md +235 -0
- data/docs/usage/setup.md +2 -2
- data/docs/usage/writing.md +99 -1
- data/lib/axn/async/adapters/active_job.rb +19 -10
- data/lib/axn/async/adapters/disabled.rb +15 -0
- data/lib/axn/async/adapters/sidekiq.rb +25 -32
- 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 +121 -4
- data/lib/axn/configuration.rb +53 -13
- data/lib/axn/context.rb +1 -0
- data/lib/axn/core/automatic_logging.rb +47 -51
- data/lib/axn/core/context/facade_inspector.rb +1 -1
- data/lib/axn/core/contract.rb +73 -30
- data/lib/axn/core/contract_for_subfields.rb +1 -1
- data/lib/axn/core/contract_validation.rb +14 -9
- data/lib/axn/core/contract_validation_for_subfields.rb +14 -7
- data/lib/axn/core/default_call.rb +63 -0
- data/lib/axn/core/flow/exception_execution.rb +5 -0
- data/lib/axn/core/flow/handlers/descriptors/message_descriptor.rb +19 -7
- data/lib/axn/core/flow/handlers/invoker.rb +4 -30
- data/lib/axn/core/flow/handlers/matcher.rb +4 -14
- data/lib/axn/core/flow/messages.rb +1 -1
- data/lib/axn/core/hooks.rb +1 -0
- data/lib/axn/core/logging.rb +16 -5
- data/lib/axn/core/memoization.rb +53 -0
- data/lib/axn/core/tracing.rb +77 -4
- data/lib/axn/core/validation/validators/type_validator.rb +1 -1
- data/lib/axn/core.rb +31 -46
- 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 +22 -2
- data/lib/axn/form_object.rb +90 -0
- data/lib/axn/internal/logging.rb +5 -1
- data/lib/axn/mountable/helpers/class_builder.rb +41 -10
- data/lib/axn/mountable/helpers/namespace_manager.rb +6 -34
- data/lib/axn/mountable/inherit_profiles.rb +2 -2
- data/lib/axn/mountable/mounting_strategies/_base.rb +10 -6
- data/lib/axn/mountable/mounting_strategies/method.rb +2 -2
- data/lib/axn/mountable.rb +41 -7
- data/lib/axn/rails/generators/axn_generator.rb +19 -1
- data/lib/axn/rails/generators/templates/action.rb.erb +1 -1
- data/lib/axn/result.rb +2 -2
- data/lib/axn/strategies/form.rb +98 -0
- data/lib/axn/strategies/transaction.rb +7 -0
- 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/version.rb +1 -1
- data/lib/axn.rb +9 -0
- metadata +22 -4
- data/lib/axn/core/profiling.rb +0 -124
- data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +0 -55
|
@@ -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
|
data/lib/axn/version.rb
CHANGED
data/lib/axn.rb
CHANGED
|
@@ -14,6 +14,12 @@ require "axn/core"
|
|
|
14
14
|
|
|
15
15
|
# Utilities
|
|
16
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"
|
|
17
23
|
|
|
18
24
|
# Extensions
|
|
19
25
|
require "axn/mountable"
|
|
@@ -36,3 +42,6 @@ module Axn
|
|
|
36
42
|
end
|
|
37
43
|
end
|
|
38
44
|
end
|
|
45
|
+
|
|
46
|
+
# Load after Axn is defined since it includes Axn
|
|
47
|
+
require "axn/async/enqueue_all_orchestrator"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: axn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.pre.alpha.
|
|
4
|
+
version: 0.1.0.pre.alpha.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kali Donovan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-01-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activemodel
|
|
@@ -46,6 +46,7 @@ executables: []
|
|
|
46
46
|
extensions: []
|
|
47
47
|
extra_rdoc_files: []
|
|
48
48
|
files:
|
|
49
|
+
- ".cursor/commands/pr.md"
|
|
49
50
|
- ".cursor/rules/axn-framework-patterns.mdc"
|
|
50
51
|
- ".cursor/rules/general-coding-standards.mdc"
|
|
51
52
|
- ".cursor/rules/spec/testing-patterns.mdc"
|
|
@@ -62,6 +63,7 @@ files:
|
|
|
62
63
|
- docs/index.md
|
|
63
64
|
- docs/intro/about.md
|
|
64
65
|
- docs/intro/overview.md
|
|
66
|
+
- docs/recipes/formatting-context-for-error-tracking.md
|
|
65
67
|
- docs/recipes/memoization.md
|
|
66
68
|
- docs/recipes/rubocop-integration.md
|
|
67
69
|
- docs/recipes/testing.md
|
|
@@ -70,7 +72,10 @@ files:
|
|
|
70
72
|
- docs/reference/axn-result.md
|
|
71
73
|
- docs/reference/class.md
|
|
72
74
|
- docs/reference/configuration.md
|
|
75
|
+
- docs/reference/form-object.md
|
|
73
76
|
- docs/reference/instance.md
|
|
77
|
+
- docs/strategies/client.md
|
|
78
|
+
- docs/strategies/form.md
|
|
74
79
|
- docs/strategies/index.md
|
|
75
80
|
- docs/strategies/transaction.md
|
|
76
81
|
- docs/usage/setup.md
|
|
@@ -83,6 +88,9 @@ files:
|
|
|
83
88
|
- lib/axn/async/adapters/active_job.rb
|
|
84
89
|
- lib/axn/async/adapters/disabled.rb
|
|
85
90
|
- lib/axn/async/adapters/sidekiq.rb
|
|
91
|
+
- lib/axn/async/batch_enqueue.rb
|
|
92
|
+
- lib/axn/async/batch_enqueue/config.rb
|
|
93
|
+
- lib/axn/async/enqueue_all_orchestrator.rb
|
|
86
94
|
- lib/axn/configuration.rb
|
|
87
95
|
- lib/axn/context.rb
|
|
88
96
|
- lib/axn/core.rb
|
|
@@ -94,6 +102,7 @@ files:
|
|
|
94
102
|
- lib/axn/core/contract_for_subfields.rb
|
|
95
103
|
- lib/axn/core/contract_validation.rb
|
|
96
104
|
- lib/axn/core/contract_validation_for_subfields.rb
|
|
105
|
+
- lib/axn/core/default_call.rb
|
|
97
106
|
- lib/axn/core/field_resolvers.rb
|
|
98
107
|
- lib/axn/core/field_resolvers/extract.rb
|
|
99
108
|
- lib/axn/core/field_resolvers/model.rb
|
|
@@ -113,8 +122,8 @@ files:
|
|
|
113
122
|
- lib/axn/core/flow/messages.rb
|
|
114
123
|
- lib/axn/core/hooks.rb
|
|
115
124
|
- lib/axn/core/logging.rb
|
|
125
|
+
- lib/axn/core/memoization.rb
|
|
116
126
|
- lib/axn/core/nesting_tracking.rb
|
|
117
|
-
- lib/axn/core/profiling.rb
|
|
118
127
|
- lib/axn/core/timing.rb
|
|
119
128
|
- lib/axn/core/tracing.rb
|
|
120
129
|
- lib/axn/core/use_strategy.rb
|
|
@@ -124,7 +133,11 @@ files:
|
|
|
124
133
|
- lib/axn/core/validation/validators/type_validator.rb
|
|
125
134
|
- lib/axn/core/validation/validators/validate_validator.rb
|
|
126
135
|
- lib/axn/exceptions.rb
|
|
136
|
+
- lib/axn/extras.rb
|
|
137
|
+
- lib/axn/extras/strategies/client.rb
|
|
138
|
+
- lib/axn/extras/strategies/vernier.rb
|
|
127
139
|
- lib/axn/factory.rb
|
|
140
|
+
- lib/axn/form_object.rb
|
|
128
141
|
- lib/axn/internal/logging.rb
|
|
129
142
|
- lib/axn/internal/registry.rb
|
|
130
143
|
- lib/axn/mountable.rb
|
|
@@ -137,7 +150,6 @@ files:
|
|
|
137
150
|
- lib/axn/mountable/mounting_strategies.rb
|
|
138
151
|
- lib/axn/mountable/mounting_strategies/_base.rb
|
|
139
152
|
- lib/axn/mountable/mounting_strategies/axn.rb
|
|
140
|
-
- lib/axn/mountable/mounting_strategies/enqueue_all.rb
|
|
141
153
|
- lib/axn/mountable/mounting_strategies/method.rb
|
|
142
154
|
- lib/axn/mountable/mounting_strategies/step.rb
|
|
143
155
|
- lib/axn/rails/engine.rb
|
|
@@ -147,8 +159,14 @@ files:
|
|
|
147
159
|
- lib/axn/result.rb
|
|
148
160
|
- lib/axn/rubocop.rb
|
|
149
161
|
- lib/axn/strategies.rb
|
|
162
|
+
- lib/axn/strategies/form.rb
|
|
150
163
|
- lib/axn/strategies/transaction.rb
|
|
151
164
|
- lib/axn/testing/spec_helpers.rb
|
|
165
|
+
- lib/axn/util/callable.rb
|
|
166
|
+
- lib/axn/util/contract_error_handling.rb
|
|
167
|
+
- lib/axn/util/execution_context.rb
|
|
168
|
+
- lib/axn/util/global_id_serialization.rb
|
|
169
|
+
- lib/axn/util/logging.rb
|
|
152
170
|
- lib/axn/util/memoization.rb
|
|
153
171
|
- lib/axn/version.rb
|
|
154
172
|
- lib/rubocop/cop/axn/README.md
|
data/lib/axn/core/profiling.rb
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "axn/core/flow/handlers/invoker"
|
|
5
|
-
|
|
6
|
-
module Axn
|
|
7
|
-
module Core
|
|
8
|
-
module Profiling
|
|
9
|
-
def self.included(base)
|
|
10
|
-
base.class_eval do
|
|
11
|
-
class_attribute :_profiling_enabled, default: false
|
|
12
|
-
class_attribute :_profiling_condition, default: nil
|
|
13
|
-
class_attribute :_profiling_sample_rate, default: 0.1
|
|
14
|
-
class_attribute :_profiling_output_dir, default: nil
|
|
15
|
-
|
|
16
|
-
extend ClassMethods
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
module ClassMethods
|
|
21
|
-
# Enable profiling for this action class
|
|
22
|
-
#
|
|
23
|
-
# @param if [Proc, Symbol, #call, nil] Optional condition to determine when to profile
|
|
24
|
-
# @param sample_rate [Float] Sampling rate (0.0 to 1.0, default: 0.1)
|
|
25
|
-
# @param output_dir [String, Pathname] Output directory for profile files (default: Rails.root/tmp/profiles or tmp/profiles)
|
|
26
|
-
# @return [void]
|
|
27
|
-
def profile(if: nil, sample_rate: 0.1, output_dir: nil)
|
|
28
|
-
self._profiling_enabled = true
|
|
29
|
-
self._profiling_condition = binding.local_variable_get(:if)
|
|
30
|
-
self._profiling_sample_rate = sample_rate
|
|
31
|
-
self._profiling_output_dir = output_dir || _default_profiling_output_dir
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def _default_profiling_output_dir
|
|
37
|
-
if defined?(Rails) && Rails.respond_to?(:root)
|
|
38
|
-
Rails.root.join("tmp", "profiles")
|
|
39
|
-
else
|
|
40
|
-
Pathname.new("tmp/profiles")
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
def _with_profiling(&)
|
|
48
|
-
# Check if this specific action should be profiled
|
|
49
|
-
return yield unless _should_profile?
|
|
50
|
-
|
|
51
|
-
_profile_with_vernier(&)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def _profile_with_vernier(&)
|
|
55
|
-
_ensure_vernier_available!
|
|
56
|
-
|
|
57
|
-
class_name = self.class.name.presence || "AnonymousAction"
|
|
58
|
-
profile_name = "axn_#{class_name}_#{Time.now.to_i}"
|
|
59
|
-
|
|
60
|
-
# Ensure output directory exists (only once per instance)
|
|
61
|
-
_ensure_output_directory_exists
|
|
62
|
-
|
|
63
|
-
# Build output file path
|
|
64
|
-
output_dir = self.class._profiling_output_dir || _default_profiling_output_dir
|
|
65
|
-
output_file = File.join(output_dir, "#{profile_name}.json")
|
|
66
|
-
|
|
67
|
-
# Configure Vernier with our settings
|
|
68
|
-
collector_options = {
|
|
69
|
-
out: output_file,
|
|
70
|
-
allocation_sample_rate: (self.class._profiling_sample_rate * 1000).to_i,
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
Vernier.profile(**collector_options, &)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def _ensure_output_directory_exists
|
|
77
|
-
return if @_profiling_directory_created
|
|
78
|
-
|
|
79
|
-
output_dir = self.class._profiling_output_dir || _default_profiling_output_dir
|
|
80
|
-
FileUtils.mkdir_p(output_dir)
|
|
81
|
-
@_profiling_directory_created = true
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def _should_profile?
|
|
85
|
-
# Fast path: check if action has profiling enabled
|
|
86
|
-
return false unless self.class._profiling_enabled
|
|
87
|
-
|
|
88
|
-
# Fast path: no condition means always profile
|
|
89
|
-
return true unless self.class._profiling_condition
|
|
90
|
-
|
|
91
|
-
# Slow path: evaluate condition (only when needed)
|
|
92
|
-
Axn::Core::Flow::Handlers::Invoker.call(
|
|
93
|
-
action: self,
|
|
94
|
-
handler: self.class._profiling_condition,
|
|
95
|
-
operation: "determining if profiling should run",
|
|
96
|
-
)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def _ensure_vernier_available!
|
|
100
|
-
return if defined?(Vernier) && Vernier.is_a?(Module)
|
|
101
|
-
|
|
102
|
-
begin
|
|
103
|
-
require "vernier"
|
|
104
|
-
rescue LoadError
|
|
105
|
-
raise LoadError, <<~ERROR
|
|
106
|
-
Vernier profiler is not available. To use profiling, add 'vernier' to your Gemfile:
|
|
107
|
-
|
|
108
|
-
gem 'vernier', '~> 0.1'
|
|
109
|
-
|
|
110
|
-
Then run: bundle install
|
|
111
|
-
ERROR
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def _default_profiling_output_dir
|
|
116
|
-
if defined?(Rails) && Rails.respond_to?(:root)
|
|
117
|
-
Rails.root.join("tmp", "profiles")
|
|
118
|
-
else
|
|
119
|
-
Pathname.new("tmp/profiles")
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Axn
|
|
4
|
-
module Mountable
|
|
5
|
-
class MountingStrategies
|
|
6
|
-
module EnqueueAll
|
|
7
|
-
include Base
|
|
8
|
-
extend self # rubocop:disable Style/ModuleFunction -- module_function breaks inheritance
|
|
9
|
-
|
|
10
|
-
def default_inherit_mode = :async_only
|
|
11
|
-
|
|
12
|
-
module DSL
|
|
13
|
-
def enqueue_all_via(axn_klass = nil, inherit: MountingStrategies::EnqueueAll.default_inherit_mode, **, &)
|
|
14
|
-
# enqueue_all_via defaults to :async_only - only needs async config for batch enqueuing
|
|
15
|
-
Helpers::Mounter.mount_via_strategy(
|
|
16
|
-
target: self,
|
|
17
|
-
as: :enqueue_all,
|
|
18
|
-
name: "enqueue_all",
|
|
19
|
-
axn_klass:,
|
|
20
|
-
inherit:,
|
|
21
|
-
**,
|
|
22
|
-
&
|
|
23
|
-
)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def mount_to_target(descriptor:, target:)
|
|
28
|
-
name = descriptor.name
|
|
29
|
-
|
|
30
|
-
mount_method(target:, method_name: name) do |**kwargs|
|
|
31
|
-
axn = descriptor.mounted_axn_for(target: self)
|
|
32
|
-
axn.call!(**kwargs)
|
|
33
|
-
true # Raise or return true
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
mount_method(target:, method_name: "#{name}_async") do |**kwargs|
|
|
37
|
-
axn = descriptor.mounted_axn_for(target: self)
|
|
38
|
-
axn.call_async(**kwargs)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def mount_to_namespace(descriptor:, target:)
|
|
43
|
-
super
|
|
44
|
-
|
|
45
|
-
# Add enqueue shortcut to enqueue the *attached-to* axn without
|
|
46
|
-
# the user having to reference __axn_mounted_to__ in their own code
|
|
47
|
-
mounted_axn = descriptor.mounted_axn_for(target:)
|
|
48
|
-
mounted_axn.define_method(:enqueue) do |**kwargs|
|
|
49
|
-
__axn_mounted_to__.call_async(**kwargs)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|