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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/commands/pr.md +36 -0
  3. data/CHANGELOG.md +15 -1
  4. data/Rakefile +102 -2
  5. data/docs/.vitepress/config.mjs +12 -8
  6. data/docs/advanced/conventions.md +1 -1
  7. data/docs/advanced/mountable.md +4 -90
  8. data/docs/advanced/profiling.md +26 -30
  9. data/docs/advanced/rough.md +27 -8
  10. data/docs/intro/overview.md +1 -1
  11. data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
  12. data/docs/recipes/memoization.md +102 -17
  13. data/docs/reference/async.md +269 -0
  14. data/docs/reference/class.md +113 -50
  15. data/docs/reference/configuration.md +226 -75
  16. data/docs/reference/form-object.md +252 -0
  17. data/docs/strategies/client.md +212 -0
  18. data/docs/strategies/form.md +235 -0
  19. data/docs/usage/setup.md +2 -2
  20. data/docs/usage/writing.md +99 -1
  21. data/lib/axn/async/adapters/active_job.rb +19 -10
  22. data/lib/axn/async/adapters/disabled.rb +15 -0
  23. data/lib/axn/async/adapters/sidekiq.rb +25 -32
  24. data/lib/axn/async/batch_enqueue/config.rb +38 -0
  25. data/lib/axn/async/batch_enqueue.rb +99 -0
  26. data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
  27. data/lib/axn/async.rb +121 -4
  28. data/lib/axn/configuration.rb +53 -13
  29. data/lib/axn/context.rb +1 -0
  30. data/lib/axn/core/automatic_logging.rb +47 -51
  31. data/lib/axn/core/context/facade_inspector.rb +1 -1
  32. data/lib/axn/core/contract.rb +73 -30
  33. data/lib/axn/core/contract_for_subfields.rb +1 -1
  34. data/lib/axn/core/contract_validation.rb +14 -9
  35. data/lib/axn/core/contract_validation_for_subfields.rb +14 -7
  36. data/lib/axn/core/default_call.rb +63 -0
  37. data/lib/axn/core/flow/exception_execution.rb +5 -0
  38. data/lib/axn/core/flow/handlers/descriptors/message_descriptor.rb +19 -7
  39. data/lib/axn/core/flow/handlers/invoker.rb +4 -30
  40. data/lib/axn/core/flow/handlers/matcher.rb +4 -14
  41. data/lib/axn/core/flow/messages.rb +1 -1
  42. data/lib/axn/core/hooks.rb +1 -0
  43. data/lib/axn/core/logging.rb +16 -5
  44. data/lib/axn/core/memoization.rb +53 -0
  45. data/lib/axn/core/tracing.rb +77 -4
  46. data/lib/axn/core/validation/validators/type_validator.rb +1 -1
  47. data/lib/axn/core.rb +31 -46
  48. data/lib/axn/extras/strategies/client.rb +150 -0
  49. data/lib/axn/extras/strategies/vernier.rb +121 -0
  50. data/lib/axn/extras.rb +4 -0
  51. data/lib/axn/factory.rb +22 -2
  52. data/lib/axn/form_object.rb +90 -0
  53. data/lib/axn/internal/logging.rb +5 -1
  54. data/lib/axn/mountable/helpers/class_builder.rb +41 -10
  55. data/lib/axn/mountable/helpers/namespace_manager.rb +6 -34
  56. data/lib/axn/mountable/inherit_profiles.rb +2 -2
  57. data/lib/axn/mountable/mounting_strategies/_base.rb +10 -6
  58. data/lib/axn/mountable/mounting_strategies/method.rb +2 -2
  59. data/lib/axn/mountable.rb +41 -7
  60. data/lib/axn/rails/generators/axn_generator.rb +19 -1
  61. data/lib/axn/rails/generators/templates/action.rb.erb +1 -1
  62. data/lib/axn/result.rb +2 -2
  63. data/lib/axn/strategies/form.rb +98 -0
  64. data/lib/axn/strategies/transaction.rb +7 -0
  65. data/lib/axn/util/callable.rb +120 -0
  66. data/lib/axn/util/contract_error_handling.rb +32 -0
  67. data/lib/axn/util/execution_context.rb +34 -0
  68. data/lib/axn/util/global_id_serialization.rb +52 -0
  69. data/lib/axn/util/logging.rb +87 -0
  70. data/lib/axn/version.rb +1 -1
  71. data/lib/axn.rb +9 -0
  72. metadata +22 -4
  73. data/lib/axn/core/profiling.rb +0 -124
  74. 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Axn
4
- VERSION = "0.1.0-alpha.3"
4
+ VERSION = "0.1.0-alpha.4"
5
5
  end
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.3
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: 2025-10-15 00:00:00.000000000 Z
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
@@ -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