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.
Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/commands/pr.md +36 -0
  3. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  4. data/.cursor/rules/general-coding-standards.mdc +27 -0
  5. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  6. data/CHANGELOG.md +57 -0
  7. data/Rakefile +114 -4
  8. data/docs/.vitepress/config.mjs +19 -10
  9. data/docs/advanced/conventions.md +3 -3
  10. data/docs/advanced/mountable.md +476 -0
  11. data/docs/advanced/profiling.md +351 -0
  12. data/docs/advanced/rough.md +27 -8
  13. data/docs/index.md +5 -3
  14. data/docs/intro/about.md +1 -1
  15. data/docs/intro/overview.md +6 -6
  16. data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
  17. data/docs/recipes/memoization.md +103 -18
  18. data/docs/recipes/rubocop-integration.md +38 -284
  19. data/docs/recipes/testing.md +14 -14
  20. data/docs/recipes/validating-user-input.md +1 -1
  21. data/docs/reference/async.md +429 -0
  22. data/docs/reference/axn-result.md +107 -0
  23. data/docs/reference/class.md +225 -64
  24. data/docs/reference/configuration.md +366 -34
  25. data/docs/reference/form-object.md +252 -0
  26. data/docs/reference/instance.md +14 -29
  27. data/docs/strategies/client.md +212 -0
  28. data/docs/strategies/form.md +235 -0
  29. data/docs/strategies/index.md +21 -21
  30. data/docs/strategies/transaction.md +1 -1
  31. data/docs/usage/setup.md +16 -2
  32. data/docs/usage/steps.md +7 -7
  33. data/docs/usage/using.md +23 -12
  34. data/docs/usage/writing.md +191 -12
  35. data/lib/axn/async/adapters/active_job.rb +74 -0
  36. data/lib/axn/async/adapters/disabled.rb +41 -0
  37. data/lib/axn/async/adapters/sidekiq.rb +67 -0
  38. data/lib/axn/async/adapters.rb +26 -0
  39. data/lib/axn/async/batch_enqueue/config.rb +38 -0
  40. data/lib/axn/async/batch_enqueue.rb +99 -0
  41. data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
  42. data/lib/axn/async.rb +178 -0
  43. data/lib/axn/configuration.rb +113 -0
  44. data/lib/{action → axn}/context.rb +22 -4
  45. data/lib/axn/core/automatic_logging.rb +89 -0
  46. data/lib/axn/core/context/facade.rb +69 -0
  47. data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
  48. data/lib/{action → axn}/core/context/internal.rb +5 -5
  49. data/lib/{action → axn}/core/contract.rb +111 -73
  50. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  51. data/lib/{action → axn}/core/contract_validation.rb +27 -12
  52. data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
  53. data/lib/axn/core/default_call.rb +63 -0
  54. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  55. data/lib/axn/core/field_resolvers/model.rb +63 -0
  56. data/lib/axn/core/field_resolvers.rb +24 -0
  57. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  58. data/lib/{action → axn}/core/flow/exception_execution.rb +9 -13
  59. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  60. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  61. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +23 -11
  62. data/lib/axn/core/flow/handlers/invoker.rb +47 -0
  63. data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
  64. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  65. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  66. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  67. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  68. data/lib/axn/core/flow/handlers.rb +20 -0
  69. data/lib/{action → axn}/core/flow/messages.rb +8 -8
  70. data/lib/{action → axn}/core/flow.rb +4 -4
  71. data/lib/{action → axn}/core/hooks.rb +17 -5
  72. data/lib/axn/core/logging.rb +48 -0
  73. data/lib/axn/core/memoization.rb +53 -0
  74. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  75. data/lib/{action → axn}/core/timing.rb +1 -1
  76. data/lib/axn/core/tracing.rb +90 -0
  77. data/lib/axn/core/use_strategy.rb +29 -0
  78. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  79. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  80. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  81. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  82. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  83. data/lib/{action → axn}/core.rb +55 -55
  84. data/lib/{action → axn}/exceptions.rb +12 -2
  85. data/lib/axn/extras/strategies/client.rb +150 -0
  86. data/lib/axn/extras/strategies/vernier.rb +121 -0
  87. data/lib/axn/extras.rb +4 -0
  88. data/lib/axn/factory.rb +122 -34
  89. data/lib/axn/form_object.rb +90 -0
  90. data/lib/axn/internal/logging.rb +30 -0
  91. data/lib/axn/internal/registry.rb +87 -0
  92. data/lib/axn/mountable/descriptor.rb +76 -0
  93. data/lib/axn/mountable/helpers/class_builder.rb +193 -0
  94. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  95. data/lib/axn/mountable/helpers/namespace_manager.rb +38 -0
  96. data/lib/axn/mountable/helpers/validator.rb +112 -0
  97. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  98. data/lib/axn/mountable/mounting_strategies/_base.rb +87 -0
  99. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  100. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  101. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  102. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  103. data/lib/axn/mountable.rb +119 -0
  104. data/lib/axn/rails/engine.rb +51 -0
  105. data/lib/axn/rails/generators/axn_generator.rb +86 -0
  106. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  107. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  108. data/lib/{action → axn}/result.rb +32 -13
  109. data/lib/axn/strategies/form.rb +98 -0
  110. data/lib/axn/strategies/transaction.rb +26 -0
  111. data/lib/axn/strategies.rb +20 -0
  112. data/lib/axn/testing/spec_helpers.rb +6 -8
  113. data/lib/axn/util/callable.rb +120 -0
  114. data/lib/axn/util/contract_error_handling.rb +32 -0
  115. data/lib/axn/util/execution_context.rb +34 -0
  116. data/lib/axn/util/global_id_serialization.rb +52 -0
  117. data/lib/axn/util/logging.rb +87 -0
  118. data/lib/axn/util/memoization.rb +20 -0
  119. data/lib/axn/version.rb +1 -1
  120. data/lib/axn.rb +26 -16
  121. data/lib/rubocop/cop/axn/README.md +23 -23
  122. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  123. metadata +106 -64
  124. data/.rspec +0 -3
  125. data/.rubocop.yml +0 -76
  126. data/.tool-versions +0 -1
  127. data/docs/reference/action-result.md +0 -37
  128. data/lib/action/attachable/base.rb +0 -43
  129. data/lib/action/attachable/steps.rb +0 -63
  130. data/lib/action/attachable/subactions.rb +0 -70
  131. data/lib/action/attachable.rb +0 -17
  132. data/lib/action/configuration.rb +0 -55
  133. data/lib/action/core/automatic_logging.rb +0 -93
  134. data/lib/action/core/context/facade.rb +0 -48
  135. data/lib/action/core/flow/handlers/invoker.rb +0 -73
  136. data/lib/action/core/flow/handlers.rb +0 -20
  137. data/lib/action/core/logging.rb +0 -37
  138. data/lib/action/core/tracing.rb +0 -17
  139. data/lib/action/core/use_strategy.rb +0 -30
  140. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  141. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  142. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  143. data/lib/action/enqueueable.rb +0 -13
  144. data/lib/action/strategies/transaction.rb +0 -19
  145. data/lib/action/strategies.rb +0 -48
  146. data/lib/axn/util.rb +0 -24
  147. data/package.json +0 -10
  148. data/yarn.lock +0 -1166
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Attachable
5
- module Steps
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- class_attribute :_axn_steps, default: []
10
- end
11
-
12
- class_methods do
13
- def steps(*steps)
14
- Array(steps).compact.each do |step|
15
- raise ArgumentError, "Step #{step} must include Action module" if step.is_a?(Class) && !step.included_modules.include?(Action) && !step < Action
16
-
17
- step("Step #{_axn_steps.length + 1}", step)
18
- end
19
- end
20
-
21
- def step(name, axn_klass = nil, error_prefix: nil, **kwargs, &block)
22
- axn_klass = axn_for_attachment(
23
- name:,
24
- axn_klass:,
25
- attachment_type: "Step",
26
- superclass: Object, # NOTE: steps skip inheriting from the wrapping class (to avoid duplicate field expectations/exposures)
27
- **kwargs,
28
- &block
29
- )
30
-
31
- # Add the step to the list of steps
32
- _axn_steps << axn_klass
33
-
34
- # Set up error handling for steps without explicit labels
35
- error_prefix ||= "#{name}: "
36
- error from: axn_klass do |e|
37
- "#{error_prefix}#{e.message}"
38
- end
39
- end
40
- end
41
-
42
- # Execute steps automatically when the action is called
43
- def call
44
- _axn_steps.each do |axn|
45
- _merge_step_exposures!(axn.call!(**_merged_context_data))
46
- end
47
- end
48
-
49
- private
50
-
51
- def _merged_context_data
52
- @__context.__combined_data
53
- end
54
-
55
- # Each step can expect the data exposed from the previous steps
56
- def _merge_step_exposures!(step_result)
57
- step_result.declared_fields.each do |field|
58
- @__context.exposed_data[field] = step_result.public_send(field)
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Attachable
5
- module Subactions
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- class_attribute :_axnable_methods, default: {}
10
- class_attribute :_axns, default: {}
11
- end
12
-
13
- class_methods do
14
- def axnable_method(name, axn_klass = nil, **action_kwargs, &block)
15
- raise ArgumentError, "Unable to attach Axn -- '#{name}' is already taken" if respond_to?(name)
16
-
17
- self._axnable_methods = _axnable_methods.merge(name => { axn_klass:, action_kwargs:, block: })
18
-
19
- action_kwargs[:expose_return_as] ||= :value unless axn_klass
20
- axn_klass = axn_for_attachment(name:, axn_klass:, **action_kwargs, &block)
21
-
22
- define_singleton_method("#{name}_axn") do |**kwargs|
23
- axn_klass.call(**kwargs)
24
- end
25
-
26
- define_singleton_method("#{name}!") do |**kwargs|
27
- result = axn_klass.call!(**kwargs)
28
- result.public_send(action_kwargs[:expose_return_as])
29
- end
30
- end
31
-
32
- def axn(name, axn_klass = nil, **action_kwargs, &block)
33
- raise ArgumentError, "Unable to attach Axn -- '#{name}' is already taken" if respond_to?(name)
34
-
35
- self._axns = _axns.merge(name => { axn_klass:, action_kwargs:, block: })
36
-
37
- axn_klass = axn_for_attachment(name:, axn_klass:, **action_kwargs, &block)
38
-
39
- define_singleton_method(name) do |**kwargs|
40
- axn_klass.call(**kwargs)
41
- end
42
-
43
- # TODO: do we also need an instance-level version that auto-creates the appropriate `error from:` to prefix with the name?
44
-
45
- define_singleton_method("#{name}!") do |**kwargs|
46
- axn_klass.call!(**kwargs)
47
- end
48
-
49
- self._axns = _axns.merge(name => axn_klass)
50
- end
51
-
52
- def inherited(subclass)
53
- super
54
-
55
- return unless subclass.name.present? # TODO: not sure why..
56
-
57
- # Need to redefine the axnable methods on the subclass to ensure they properly reference the subclass's
58
- # helper method definitions and not the superclass's.
59
- _axnable_methods.each do |name, config|
60
- subclass.axnable_method(name, config[:axn_klass], **config[:action_kwargs], &config[:block])
61
- end
62
-
63
- _axns.each do |name, config|
64
- subclass.axn(name, config[:axn_klass], **config[:action_kwargs], &config[:block])
65
- end
66
- end
67
- end
68
- end
69
- end
70
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action/attachable/base"
4
- require "action/attachable/steps"
5
- require "action/attachable/subactions"
6
-
7
- module Action
8
- module Attachable
9
- extend ActiveSupport::Concern
10
-
11
- included do
12
- include Base
13
- include Steps
14
- include Subactions
15
- end
16
- end
17
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- class Configuration
5
- attr_accessor :wrap_with_trace, :emit_metrics
6
- attr_writer :logger, :env, :on_exception, :additional_includes, :log_level
7
-
8
- def log_level = @log_level ||= :info
9
-
10
- def additional_includes = @additional_includes ||= []
11
-
12
- def on_exception(e, action:, context: {})
13
- msg = "Handled exception (#{e.class.name}): #{e.message}"
14
- msg = ("#" * 10) + " #{msg} " + ("#" * 10) unless Action.config.env.production?
15
- action.log(msg)
16
-
17
- return unless @on_exception
18
-
19
- # Only pass the kwargs that the given block expects
20
- kwargs = @on_exception.parameters.select { |type, _name| %i[key keyreq].include?(type) }.map(&:last)
21
- kwarg_hash = {}
22
- kwarg_hash[:action] = action if kwargs.include?(:action)
23
- kwarg_hash[:context] = context if kwargs.include?(:context)
24
- if kwarg_hash.any?
25
- @on_exception.call(e, **kwarg_hash)
26
- else
27
- @on_exception.call(e)
28
- end
29
- end
30
-
31
- def logger
32
- @logger ||= begin
33
- Rails.logger
34
- rescue NameError
35
- Logger.new($stdout).tap do |l|
36
- l.level = Logger::INFO
37
- end
38
- end
39
- end
40
-
41
- def env
42
- @env ||= ENV["RACK_ENV"].presence || ENV["RAILS_ENV"].presence || "development"
43
- ActiveSupport::StringInquirer.new(@env)
44
- end
45
- end
46
-
47
- class << self
48
- def config = @config ||= Configuration.new
49
-
50
- def configure
51
- self.config ||= Configuration.new
52
- yield(config) if block_given?
53
- end
54
- end
55
- end
@@ -1,93 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Core
5
- module AutomaticLogging
6
- def self.included(base)
7
- base.class_eval do
8
- extend ClassMethods
9
- include InstanceMethods
10
-
11
- # Single class_attribute - nil means disabled, any level means enabled
12
- class_attribute :auto_log_level, default: Action.config.log_level
13
- end
14
- end
15
-
16
- module ClassMethods
17
- def auto_log(level)
18
- self.auto_log_level = level.presence
19
- end
20
- end
21
-
22
- module InstanceMethods
23
- private
24
-
25
- def _with_logging
26
- _log_before if self.class.auto_log_level
27
- yield
28
- ensure
29
- _log_after if self.class.auto_log_level
30
- end
31
-
32
- def _log_before
33
- level = self.class.auto_log_level
34
- return unless level
35
-
36
- self.class.public_send(
37
- level,
38
- [
39
- "About to execute",
40
- _log_context(:inbound),
41
- ].compact.join(" with: "),
42
- before: Action.config.env.production? ? nil : "\n------\n",
43
- )
44
- rescue StandardError => e
45
- Axn::Util.piping_error("logging before hook", action: self, exception: e)
46
- end
47
-
48
- def _log_after
49
- level = self.class.auto_log_level
50
- return unless level
51
-
52
- self.class.public_send(
53
- level,
54
- [
55
- "Execution completed (with outcome: #{result.outcome}) in #{result.elapsed_time} milliseconds",
56
- _log_context(:outbound),
57
- ].compact.join(". Set: "),
58
- after: Action.config.env.production? ? nil : "\n------\n",
59
- )
60
- rescue StandardError => e
61
- Axn::Util.piping_error("logging after hook", action: self, exception: e)
62
- end
63
-
64
- def _log_context(direction)
65
- data = context_for_logging(direction)
66
- return unless data.present?
67
-
68
- max_length = 150
69
- suffix = "…<truncated>…"
70
-
71
- _log_object(data).tap do |str|
72
- return str[0, max_length - suffix.length] + suffix if str.length > max_length
73
- end
74
- end
75
-
76
- def _log_object(data)
77
- case data
78
- when Hash
79
- # NOTE: slightly more manual in order to avoid quotes around ActiveRecord objects' <Class#id> formatting
80
- "{#{data.map { |k, v| "#{k}: #{_log_object(v)}" }.join(", ")}}"
81
- when Array
82
- data.map { |v| _log_object(v) }
83
- else
84
- return data.to_unsafe_h if defined?(ActionController::Parameters) && data.is_a?(ActionController::Parameters)
85
- return "<#{data.class.name}##{data.to_param.presence || "unpersisted"}>" if defined?(ActiveRecord::Base) && data.is_a?(ActiveRecord::Base)
86
-
87
- data.inspect
88
- end
89
- end
90
- end
91
- end
92
- end
93
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/parameter_filter"
4
-
5
- module Action
6
- class ContextFacade
7
- def initialize(action:, context:, declared_fields:, implicitly_allowed_fields: nil)
8
- if self.class.name == "Action::ContextFacade" # rubocop:disable Style/ClassEqualityComparison
9
- raise "Action::ContextFacade is an abstract class and should not be instantiated directly"
10
- end
11
-
12
- @context = context
13
- @action = action
14
- @declared_fields = declared_fields
15
-
16
- (@declared_fields + Array(implicitly_allowed_fields)).each do |field|
17
- singleton_class.define_method(field) do
18
- _context_data_source[field]
19
- end
20
- end
21
- end
22
-
23
- attr_reader :declared_fields
24
-
25
- def inspect = ContextFacadeInspector.new(facade: self, action:, context:).call
26
-
27
- def fail!(...)
28
- raise Action::ContractViolation::MethodNotAllowed, "Call fail! directly rather than on the context"
29
- end
30
-
31
- private
32
-
33
- attr_reader :action, :context
34
-
35
- def action_name = @action.class.name.presence || "The action"
36
-
37
- def _context_data_source = raise NotImplementedError
38
-
39
- def _msg_resolver(event_type, exception:)
40
- Action::Core::Flow::Handlers::Resolvers::MessageResolver.new(
41
- action._messages_registry,
42
- event_type,
43
- action:,
44
- exception:,
45
- )
46
- end
47
- end
48
- end
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
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::Util.piping_error(operation, action:, exception: e)
18
- end
19
-
20
- # Shared introspection helpers
21
- def accepts_exception_keyword?(callable_or_method)
22
- return false unless callable_or_method.respond_to?(:parameters)
23
-
24
- params = callable_or_method.parameters
25
- params.any? { |type, name| %i[keyreq key].include?(type) && name == :exception } ||
26
- params.any? { |type, _| type == :keyrest }
27
- end
28
-
29
- def accepts_positional_exception?(callable_or_method)
30
- return false unless callable_or_method.respond_to?(:arity)
31
-
32
- arity = callable_or_method.arity
33
- arity == 1 || arity.negative?
34
- end
35
-
36
- private
37
-
38
- def symbol?(value) = value.is_a?(Symbol)
39
-
40
- def callable?(value) = value.respond_to?(:arity)
41
-
42
- def call_symbol_handler(action:, symbol:, exception: nil)
43
- unless action.respond_to?(symbol, true)
44
- action.warn("Ignoring apparently-invalid symbol #{symbol.inspect} -- action does not respond to method")
45
- return nil
46
- end
47
-
48
- method = action.method(symbol)
49
- if exception && accepts_exception_keyword?(method)
50
- action.send(symbol, exception:)
51
- elsif exception && accepts_positional_exception?(method)
52
- action.send(symbol, exception)
53
- else
54
- action.send(symbol)
55
- end
56
- end
57
-
58
- def call_callable_handler(action:, callable:, exception: nil)
59
- if exception && accepts_exception_keyword?(callable)
60
- action.instance_exec(exception:, &callable)
61
- elsif exception && accepts_positional_exception?(callable)
62
- action.instance_exec(exception, &callable)
63
- else
64
- action.instance_exec(&callable)
65
- end
66
- end
67
-
68
- def literal_value(value) = value
69
- end
70
- end
71
- end
72
- end
73
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Core
5
- module Flow
6
- module Handlers
7
- end
8
- end
9
- end
10
- end
11
-
12
- require "action/core/flow/handlers/base_descriptor"
13
- require "action/core/flow/handlers/matcher"
14
- require "action/core/flow/handlers/resolvers/base_resolver"
15
- require "action/core/flow/handlers/descriptors/message_descriptor"
16
- require "action/core/flow/handlers/descriptors/callback_descriptor"
17
- require "action/core/flow/handlers/invoker"
18
- require "action/core/flow/handlers/resolvers/callback_resolver"
19
- require "action/core/flow/handlers/registry"
20
- require "action/core/flow/handlers/resolvers/message_resolver"
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/core_ext/module/delegation"
4
-
5
- module Action
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 = Action.config.log_level
19
-
20
- def log(message, level: log_level, before: nil, after: nil)
21
- msg = [_log_prefix, message].compact_blank.join(" ")
22
- msg = [before, msg, after].compact_blank.join if before || after
23
-
24
- Action.config.logger.send(level, msg)
25
- end
26
-
27
- LEVELS.each do |level|
28
- define_method(level) do |message, before: nil, after: nil|
29
- log(message, level:, before:, after:)
30
- end
31
- end
32
-
33
- def _log_prefix = "[#{name.presence || "Anonymous Class"}]"
34
- end
35
- end
36
- end
37
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Core
5
- module Tracing
6
- private
7
-
8
- def _with_tracing(&)
9
- return yield unless Action.config.wrap_with_trace
10
-
11
- Action.config.wrap_with_trace.call(self.class.name || "AnonymousClass", &)
12
- rescue StandardError => e
13
- Axn::Util.piping_error("running trace hook", action: self, exception: e)
14
- end
15
- end
16
- end
17
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Core
5
- module UseStrategy
6
- extend ActiveSupport::Concern
7
-
8
- class_methods do
9
- def use(strategy_name, **config, &block)
10
- strategy = Action::Strategies.all[strategy_name.to_sym]
11
- raise StrategyNotFound, "Strategy #{strategy_name} not found" if strategy.blank?
12
- raise ArgumentError, "Strategy #{strategy_name} does not support config" if config.any? && !strategy.respond_to?(:setup)
13
-
14
- # Allow dynamic setup of strategy (i.e. dynamically define module before returning)
15
- if strategy.respond_to?(:setup)
16
- configured = strategy.setup(**config, &block)
17
- raise ArgumentError, "Strategy #{strategy_name} setup method must return a module" unless configured.is_a?(Module)
18
-
19
- strategy = configured
20
- else
21
- raise ArgumentError, "Strategy #{strategy_name} does not support config (define #setup method)" if config.any?
22
- raise ArgumentError, "Strategy #{strategy_name} does not support blocks (define #setup method)" if block_given?
23
- end
24
-
25
- include strategy
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_model"
4
-
5
- module Action
6
- module Validators
7
- class ModelValidator < ActiveModel::EachValidator
8
- def self.model_for(field:, klass: nil)
9
- return klass if defined?(ActiveRecord::Base) && klass.is_a?(ActiveRecord::Base)
10
-
11
- field.to_s.delete_suffix("_id").classify.constantize
12
- end
13
-
14
- def self.instance_for(field:, klass:, id:)
15
- klass = model_for(field:, klass:)
16
- return unless klass.respond_to?(:find_by)
17
-
18
- klass.find_by(id:)
19
- end
20
-
21
- def validate_each(record, attribute, id)
22
- klass = self.class.model_for(field: attribute, klass: options[:with])
23
- instance = self.class.instance_for(field: attribute, klass:, id:)
24
- return if instance.present?
25
-
26
- msg = id.blank? ? "not found (given a blank ID)" : "not found for class #{klass.name} and ID #{id}"
27
- record.errors.add(attribute, msg)
28
- rescue StandardError => e
29
- Axn::Util.piping_error("applying model validation on field '#{attribute}'", exception: e)
30
- record.errors.add(attribute, "error raised while trying to find a valid #{klass.name}")
31
- end
32
- end
33
- end
34
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_model"
4
-
5
- module Action
6
- module Validators
7
- class TypeValidator < ActiveModel::EachValidator
8
- def validate_each(record, attribute, value)
9
- # NOTE: the last one (:value) might be my fault from the make-it-a-hash fallback in #parse_field_configs
10
- types = options[:in].presence || Array(options[:with]).presence || Array(options[:value]).presence
11
-
12
- return if value.blank? && !types.include?(:boolean) # Handled with a separate default presence validator
13
-
14
- msg = types.size == 1 ? "is not a #{types.first}" : "is not one of #{types.join(", ")}"
15
- record.errors.add attribute, (options[:message] || msg) unless types.any? do |type|
16
- if type == :boolean
17
- [true, false].include?(value)
18
- elsif type == :uuid
19
- value.is_a?(String) && value.match?(/\A[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}\z/i)
20
- else
21
- # NOTE: allow mocks to pass type validation by default (much easier testing ergonomics)
22
- next true if Action.config.env.test? && value.class.name&.start_with?("RSpec::Mocks::")
23
-
24
- value.is_a?(type)
25
- end
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,76 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Action
4
- module Enqueueable
5
- module ViaSidekiq
6
- def self.included(base)
7
- base.class_eval do
8
- begin
9
- require "sidekiq"
10
- include Sidekiq::Job
11
- rescue LoadError
12
- puts "Sidekiq not available -- skipping Enqueueable"
13
- return
14
- end
15
-
16
- define_method(:perform) do |*args|
17
- context = self.class._params_from_global_id(args.first)
18
- bang = args.size > 1 ? args.last : false
19
-
20
- if bang
21
- self.class.call!(**context)
22
- else
23
- self.class.call(**context)
24
- end
25
- end
26
-
27
- def self.enqueue(context = {})
28
- perform_async(_process_context_to_sidekiq_args(context))
29
- end
30
-
31
- def self.enqueue!(context = {})
32
- perform_async(_process_context_to_sidekiq_args(context), true)
33
- end
34
-
35
- def self.queue_options(opts)
36
- opts = opts.transform_keys(&:to_s)
37
- self.sidekiq_options_hash = get_sidekiq_options.merge(opts)
38
- end
39
-
40
- private
41
-
42
- def self._process_context_to_sidekiq_args(context)
43
- client = Sidekiq::Client.new
44
-
45
- _params_to_global_id(context).tap do |args|
46
- if client.send(:json_unsafe?, args).present?
47
- raise ArgumentError,
48
- "Cannot pass non-JSON-serializable objects to Sidekiq. Make sure all expected arguments are serializable (or respond to to_global_id)."
49
- end
50
- end
51
- end
52
-
53
- def self._params_to_global_id(context)
54
- context.stringify_keys.each_with_object({}) do |(key, value), hash|
55
- if value.respond_to?(:to_global_id)
56
- hash["#{key}_as_global_id"] = value.to_global_id.to_s
57
- else
58
- hash[key] = value
59
- end
60
- end
61
- end
62
-
63
- def self._params_from_global_id(params)
64
- params.each_with_object({}) do |(key, value), hash|
65
- if key.end_with?("_as_global_id")
66
- hash[key.delete_suffix("_as_global_id")] = GlobalID::Locator.locate(value)
67
- else
68
- hash[key] = value
69
- end
70
- end.symbolize_keys
71
- end
72
- end
73
- end
74
- end
75
- end
76
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action/enqueueable/via_sidekiq"
4
-
5
- module Action
6
- module Enqueueable
7
- extend ActiveSupport::Concern
8
-
9
- included do
10
- include ViaSidekiq
11
- end
12
- end
13
- end