axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.cursor/commands/pr.md +36 -0
- data/.cursor/rules/axn-framework-patterns.mdc +43 -0
- data/.cursor/rules/general-coding-standards.mdc +27 -0
- data/.cursor/rules/spec/testing-patterns.mdc +40 -0
- data/CHANGELOG.md +57 -0
- data/Rakefile +114 -4
- data/docs/.vitepress/config.mjs +19 -10
- data/docs/advanced/conventions.md +3 -3
- data/docs/advanced/mountable.md +476 -0
- data/docs/advanced/profiling.md +351 -0
- data/docs/advanced/rough.md +27 -8
- data/docs/index.md +5 -3
- data/docs/intro/about.md +1 -1
- data/docs/intro/overview.md +6 -6
- data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
- data/docs/recipes/memoization.md +103 -18
- data/docs/recipes/rubocop-integration.md +38 -284
- data/docs/recipes/testing.md +14 -14
- data/docs/recipes/validating-user-input.md +1 -1
- data/docs/reference/async.md +429 -0
- data/docs/reference/axn-result.md +107 -0
- data/docs/reference/class.md +225 -64
- data/docs/reference/configuration.md +366 -34
- data/docs/reference/form-object.md +252 -0
- data/docs/reference/instance.md +14 -29
- data/docs/strategies/client.md +212 -0
- data/docs/strategies/form.md +235 -0
- data/docs/strategies/index.md +21 -21
- data/docs/strategies/transaction.md +1 -1
- data/docs/usage/setup.md +16 -2
- data/docs/usage/steps.md +7 -7
- data/docs/usage/using.md +23 -12
- data/docs/usage/writing.md +191 -12
- data/lib/axn/async/adapters/active_job.rb +74 -0
- data/lib/axn/async/adapters/disabled.rb +41 -0
- data/lib/axn/async/adapters/sidekiq.rb +67 -0
- data/lib/axn/async/adapters.rb +26 -0
- data/lib/axn/async/batch_enqueue/config.rb +38 -0
- data/lib/axn/async/batch_enqueue.rb +99 -0
- data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
- data/lib/axn/async.rb +178 -0
- data/lib/axn/configuration.rb +113 -0
- data/lib/{action → axn}/context.rb +22 -4
- data/lib/axn/core/automatic_logging.rb +89 -0
- data/lib/axn/core/context/facade.rb +69 -0
- data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
- data/lib/{action → axn}/core/context/internal.rb +5 -5
- data/lib/{action → axn}/core/contract.rb +111 -73
- data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
- data/lib/{action → axn}/core/contract_validation.rb +27 -12
- data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
- data/lib/axn/core/default_call.rb +63 -0
- data/lib/axn/core/field_resolvers/extract.rb +32 -0
- data/lib/axn/core/field_resolvers/model.rb +63 -0
- data/lib/axn/core/field_resolvers.rb +24 -0
- data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
- data/lib/{action → axn}/core/flow/exception_execution.rb +9 -13
- data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +23 -11
- data/lib/axn/core/flow/handlers/invoker.rb +47 -0
- data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
- data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
- data/lib/axn/core/flow/handlers.rb +20 -0
- data/lib/{action → axn}/core/flow/messages.rb +8 -8
- data/lib/{action → axn}/core/flow.rb +4 -4
- data/lib/{action → axn}/core/hooks.rb +17 -5
- data/lib/axn/core/logging.rb +48 -0
- data/lib/axn/core/memoization.rb +53 -0
- data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
- data/lib/{action → axn}/core/timing.rb +1 -1
- data/lib/axn/core/tracing.rb +90 -0
- data/lib/axn/core/use_strategy.rb +29 -0
- data/lib/{action → axn}/core/validation/fields.rb +26 -2
- data/lib/{action → axn}/core/validation/subfields.rb +14 -12
- data/lib/axn/core/validation/validators/model_validator.rb +36 -0
- data/lib/axn/core/validation/validators/type_validator.rb +80 -0
- data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
- data/lib/{action → axn}/core.rb +55 -55
- data/lib/{action → axn}/exceptions.rb +12 -2
- data/lib/axn/extras/strategies/client.rb +150 -0
- data/lib/axn/extras/strategies/vernier.rb +121 -0
- data/lib/axn/extras.rb +4 -0
- data/lib/axn/factory.rb +122 -34
- data/lib/axn/form_object.rb +90 -0
- data/lib/axn/internal/logging.rb +30 -0
- data/lib/axn/internal/registry.rb +87 -0
- data/lib/axn/mountable/descriptor.rb +76 -0
- data/lib/axn/mountable/helpers/class_builder.rb +193 -0
- data/lib/axn/mountable/helpers/mounter.rb +33 -0
- data/lib/axn/mountable/helpers/namespace_manager.rb +38 -0
- data/lib/axn/mountable/helpers/validator.rb +112 -0
- data/lib/axn/mountable/inherit_profiles.rb +72 -0
- data/lib/axn/mountable/mounting_strategies/_base.rb +87 -0
- data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
- data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
- data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
- data/lib/axn/mountable/mounting_strategies.rb +32 -0
- data/lib/axn/mountable.rb +119 -0
- data/lib/axn/rails/engine.rb +51 -0
- data/lib/axn/rails/generators/axn_generator.rb +86 -0
- data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
- data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
- data/lib/{action → axn}/result.rb +32 -13
- data/lib/axn/strategies/form.rb +98 -0
- data/lib/axn/strategies/transaction.rb +26 -0
- data/lib/axn/strategies.rb +20 -0
- data/lib/axn/testing/spec_helpers.rb +6 -8
- data/lib/axn/util/callable.rb +120 -0
- data/lib/axn/util/contract_error_handling.rb +32 -0
- data/lib/axn/util/execution_context.rb +34 -0
- data/lib/axn/util/global_id_serialization.rb +52 -0
- data/lib/axn/util/logging.rb +87 -0
- data/lib/axn/util/memoization.rb +20 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +26 -16
- data/lib/rubocop/cop/axn/README.md +23 -23
- data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
- metadata +106 -64
- data/.rspec +0 -3
- data/.rubocop.yml +0 -76
- data/.tool-versions +0 -1
- data/docs/reference/action-result.md +0 -37
- data/lib/action/attachable/base.rb +0 -43
- data/lib/action/attachable/steps.rb +0 -63
- data/lib/action/attachable/subactions.rb +0 -70
- data/lib/action/attachable.rb +0 -17
- data/lib/action/configuration.rb +0 -55
- data/lib/action/core/automatic_logging.rb +0 -93
- data/lib/action/core/context/facade.rb +0 -48
- data/lib/action/core/flow/handlers/invoker.rb +0 -73
- data/lib/action/core/flow/handlers.rb +0 -20
- data/lib/action/core/logging.rb +0 -37
- data/lib/action/core/tracing.rb +0 -17
- data/lib/action/core/use_strategy.rb +0 -30
- data/lib/action/core/validation/validators/model_validator.rb +0 -34
- data/lib/action/core/validation/validators/type_validator.rb +0 -30
- data/lib/action/enqueueable/via_sidekiq.rb +0 -76
- data/lib/action/enqueueable.rb +0 -13
- data/lib/action/strategies/transaction.rb +0 -19
- data/lib/action/strategies.rb +0 -48
- data/lib/axn/util.rb +0 -24
- data/package.json +0 -10
- data/yarn.lock +0 -1166
|
@@ -1,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
|
data/lib/action/attachable.rb
DELETED
|
@@ -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
|
data/lib/action/configuration.rb
DELETED
|
@@ -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"
|
data/lib/action/core/logging.rb
DELETED
|
@@ -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
|
data/lib/action/core/tracing.rb
DELETED
|
@@ -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
|