axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.3
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/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 +43 -0
- data/Rakefile +12 -2
- data/docs/.vitepress/config.mjs +8 -3
- data/docs/advanced/conventions.md +2 -2
- data/docs/advanced/mountable.md +562 -0
- data/docs/advanced/profiling.md +355 -0
- data/docs/advanced/rough.md +1 -1
- data/docs/index.md +5 -3
- data/docs/intro/about.md +1 -1
- data/docs/intro/overview.md +5 -5
- data/docs/recipes/memoization.md +2 -2
- 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 +160 -0
- data/docs/reference/axn-result.md +107 -0
- data/docs/reference/class.md +123 -25
- data/docs/reference/configuration.md +191 -10
- data/docs/reference/instance.md +14 -29
- data/docs/strategies/index.md +21 -21
- data/docs/strategies/transaction.md +1 -1
- data/docs/usage/setup.md +14 -0
- data/docs/usage/steps.md +7 -7
- data/docs/usage/using.md +23 -12
- data/docs/usage/writing.md +92 -11
- data/lib/axn/async/adapters/active_job.rb +65 -0
- data/lib/axn/async/adapters/disabled.rb +26 -0
- data/lib/axn/async/adapters/sidekiq.rb +74 -0
- data/lib/axn/async/adapters.rb +26 -0
- data/lib/axn/async.rb +61 -0
- data/lib/{action → axn}/configuration.rb +21 -3
- data/lib/{action → axn}/context.rb +21 -4
- data/lib/{action → axn}/core/automatic_logging.rb +6 -6
- data/lib/axn/core/context/facade.rb +69 -0
- data/lib/{action → axn}/core/context/facade_inspector.rb +31 -4
- data/lib/{action → axn}/core/context/internal.rb +5 -5
- data/lib/{action → axn}/core/contract.rb +41 -46
- data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
- data/lib/{action → axn}/core/contract_validation.rb +16 -6
- data/lib/axn/core/contract_validation_for_subfields.rb +158 -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 +4 -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 +6 -6
- data/lib/{action → axn}/core/flow/handlers/invoker.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/matcher.rb +5 -5
- 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 +7 -7
- data/lib/{action → axn}/core/flow.rb +4 -4
- data/lib/{action → axn}/core/hooks.rb +16 -5
- data/lib/{action → axn}/core/logging.rb +3 -3
- data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
- data/lib/axn/core/profiling.rb +124 -0
- data/lib/{action → axn}/core/timing.rb +1 -1
- data/lib/axn/core/tracing.rb +17 -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/axn/core.rb +123 -0
- data/lib/{action → axn}/exceptions.rb +12 -2
- data/lib/axn/factory.rb +102 -34
- data/lib/axn/internal/logging.rb +26 -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 +162 -0
- data/lib/axn/mountable/helpers/mounter.rb +33 -0
- data/lib/axn/mountable/helpers/namespace_manager.rb +66 -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 +83 -0
- data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
- data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +55 -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 +85 -0
- data/lib/axn/rails/engine.rb +51 -0
- data/lib/axn/rails/generators/axn_generator.rb +68 -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 +30 -11
- data/lib/{action → axn}/strategies/transaction.rb +1 -1
- data/lib/axn/strategies.rb +20 -0
- data/lib/axn/testing/spec_helpers.rb +6 -8
- data/lib/axn/util/memoization.rb +20 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +17 -16
- data/lib/rubocop/cop/axn/README.md +23 -23
- data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
- metadata +88 -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/core/context/facade.rb +0 -48
- data/lib/action/core/flow/handlers.rb +0 -20
- 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/core.rb +0 -108
- data/lib/action/enqueueable/via_sidekiq.rb +0 -76
- data/lib/action/enqueueable.rb +0 -13
- data/lib/action/strategies.rb +0 -48
- data/lib/axn/util.rb +0 -24
- data/package.json +0 -10
- data/yarn.lock +0 -1166
data/.rubocop.yml
DELETED
@@ -1,76 +0,0 @@
|
|
1
|
-
# RuboCop cops are not loaded by default in this repo
|
2
|
-
# Downstream consumers can enable them by adding:
|
3
|
-
# require:
|
4
|
-
# - axn/rubocop
|
5
|
-
|
6
|
-
AllCops:
|
7
|
-
TargetRubyVersion: 3.2
|
8
|
-
SuggestExtensions: false
|
9
|
-
NewCops: enable
|
10
|
-
|
11
|
-
Style/MultilineBlockChain:
|
12
|
-
Enabled: false
|
13
|
-
|
14
|
-
Style/StringLiterals:
|
15
|
-
Enabled: true
|
16
|
-
EnforcedStyle: double_quotes
|
17
|
-
|
18
|
-
Style/StringLiteralsInInterpolation:
|
19
|
-
Enabled: true
|
20
|
-
EnforcedStyle: double_quotes
|
21
|
-
|
22
|
-
Style/Documentation:
|
23
|
-
Enabled: false
|
24
|
-
|
25
|
-
Style/TrailingCommaInArguments:
|
26
|
-
EnforcedStyleForMultiline: comma
|
27
|
-
|
28
|
-
Style/TrailingCommaInArrayLiteral:
|
29
|
-
EnforcedStyleForMultiline: comma
|
30
|
-
|
31
|
-
Style/TrailingCommaInHashLiteral:
|
32
|
-
EnforcedStyleForMultiline: comma
|
33
|
-
|
34
|
-
Style/ClassAndModuleChildren:
|
35
|
-
Enabled: false
|
36
|
-
|
37
|
-
Style/HashSyntax:
|
38
|
-
EnforcedShorthandSyntax: always
|
39
|
-
|
40
|
-
Style/DoubleNegation:
|
41
|
-
Enabled: false
|
42
|
-
|
43
|
-
Metrics/BlockLength:
|
44
|
-
Enabled: false
|
45
|
-
|
46
|
-
Metrics/ModuleLength:
|
47
|
-
Enabled: false
|
48
|
-
|
49
|
-
Metrics/ClassLength:
|
50
|
-
Max: 150
|
51
|
-
|
52
|
-
Metrics/MethodLength:
|
53
|
-
Max: 70
|
54
|
-
|
55
|
-
Metrics/PerceivedComplexity:
|
56
|
-
Max: 20
|
57
|
-
|
58
|
-
Metrics/AbcSize:
|
59
|
-
Max: 60
|
60
|
-
|
61
|
-
Metrics/CyclomaticComplexity:
|
62
|
-
Max: 20
|
63
|
-
|
64
|
-
Lint/EmptyBlock:
|
65
|
-
Enabled: false
|
66
|
-
|
67
|
-
Naming/MethodParameterName:
|
68
|
-
AllowedNames: e, on, id, if
|
69
|
-
|
70
|
-
Metrics/ParameterLists:
|
71
|
-
Max: 9
|
72
|
-
|
73
|
-
Layout/LineLength:
|
74
|
-
Max: 160
|
75
|
-
|
76
|
-
|
data/.tool-versions
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
ruby 3.3.6
|
@@ -1,37 +0,0 @@
|
|
1
|
-
# `Action::Result`
|
2
|
-
|
3
|
-
Every `call` invocation on an Action will return an `Action::Result` instance, which provides a consistent interface:
|
4
|
-
|
5
|
-
| Method | Description |
|
6
|
-
| -- | -- |
|
7
|
-
| `ok?` | `true` if the call succeeded, `false` if not.
|
8
|
-
| `error` | User-facing error message (string), if not `ok?` (else nil)
|
9
|
-
| `success` | User-facing success message (string), if `ok?` (else nil)
|
10
|
-
| `message` | User-facing message (string), always defined (`ok? ? success : error`)
|
11
|
-
| `exception` | If not `ok?` because an exception was swallowed, will be set to the swallowed exception (note: rarely used outside development; prefer to let the library automatically handle exception handling for you)
|
12
|
-
| `outcome` | The execution outcome as a string inquirer (`success?`, `failure?`, `exception?`)
|
13
|
-
| `elapsed_time` | Execution time in milliseconds (Float)
|
14
|
-
| any `expose`d values | guaranteed to be set if `ok?` (since they have outgoing presence validations by default; any missing would have failed the action)
|
15
|
-
|
16
|
-
NOTE: `success` and `error` (and so implicitly `message`) can be configured per-action via [the `success` and `error` declarations](/reference/class#success-and-error).
|
17
|
-
|
18
|
-
### Clarification of exposed values
|
19
|
-
|
20
|
-
In addition to the core interface, your Action's Result class will have methods defined to read the values of any attributes that were explicitly exposed. For example, given this action and result:
|
21
|
-
|
22
|
-
|
23
|
-
```ruby
|
24
|
-
class Foo
|
25
|
-
include Action
|
26
|
-
|
27
|
-
exposes :bar, :baz # [!code focus]
|
28
|
-
|
29
|
-
def call
|
30
|
-
expose bar: 1, baz: 2
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
result = Foo.call # [!code focus]
|
35
|
-
```
|
36
|
-
|
37
|
-
`result` will have both `bar` and `baz` reader methods (which will return 1 and 2, respectively).
|
@@ -1,43 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Action
|
4
|
-
module Attachable
|
5
|
-
module Base
|
6
|
-
extend ActiveSupport::Concern
|
7
|
-
|
8
|
-
class_methods do
|
9
|
-
def axn_for_attachment(
|
10
|
-
attachment_type: "Action",
|
11
|
-
name: nil,
|
12
|
-
axn_klass: nil,
|
13
|
-
superclass: nil,
|
14
|
-
**kwargs,
|
15
|
-
&block
|
16
|
-
)
|
17
|
-
raise ArgumentError, "#{attachment_type} name must be a string or symbol" unless name.is_a?(String) || name.is_a?(Symbol)
|
18
|
-
raise ArgumentError, "#{attachment_type} '#{name}' must be given an existing action class or a block" if axn_klass.nil? && !block_given?
|
19
|
-
|
20
|
-
if axn_klass && block_given?
|
21
|
-
raise ArgumentError,
|
22
|
-
"#{attachment_type} '#{name}' was given both an existing action class and a block - only one is allowed"
|
23
|
-
end
|
24
|
-
|
25
|
-
if axn_klass
|
26
|
-
unless axn_klass.respond_to?(:<) && axn_klass < Action
|
27
|
-
raise ArgumentError,
|
28
|
-
"#{attachment_type} '#{name}' was given an already-existing class #{axn_klass.name} that does NOT inherit from Action as expected"
|
29
|
-
end
|
30
|
-
|
31
|
-
if kwargs.present?
|
32
|
-
raise ArgumentError, "#{attachment_type} '#{name}' was given an existing action class and also keyword arguments - only one is allowed"
|
33
|
-
end
|
34
|
-
|
35
|
-
return axn_klass
|
36
|
-
end
|
37
|
-
|
38
|
-
Axn::Factory.build(superclass: superclass || self, name:, **kwargs, &block)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
@@ -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
|
@@ -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,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/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
|
data/lib/action/core.rb
DELETED
@@ -1,108 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "action/context"
|
4
|
-
|
5
|
-
require "action/strategies"
|
6
|
-
require "action/core/hooks"
|
7
|
-
require "action/core/logging"
|
8
|
-
require "action/core/flow"
|
9
|
-
require "action/core/automatic_logging"
|
10
|
-
require "action/core/use_strategy"
|
11
|
-
require "action/core/timing"
|
12
|
-
require "action/core/tracing"
|
13
|
-
require "action/core/nesting_tracking"
|
14
|
-
|
15
|
-
# CONSIDER: make class names match file paths?
|
16
|
-
require "action/core/validation/validators/model_validator"
|
17
|
-
require "action/core/validation/validators/type_validator"
|
18
|
-
require "action/core/validation/validators/validate_validator"
|
19
|
-
|
20
|
-
require "action/core/contract_validation"
|
21
|
-
require "action/core/contract"
|
22
|
-
require "action/core/contract_for_subfields"
|
23
|
-
|
24
|
-
module Action
|
25
|
-
module Core
|
26
|
-
def self.included(base)
|
27
|
-
base.class_eval do
|
28
|
-
extend ClassMethods
|
29
|
-
include Core::Hooks
|
30
|
-
include Core::Logging
|
31
|
-
include Core::AutomaticLogging
|
32
|
-
include Core::Tracing
|
33
|
-
include Core::Timing
|
34
|
-
|
35
|
-
include Core::Flow
|
36
|
-
|
37
|
-
include Core::ContractValidation
|
38
|
-
include Core::Contract
|
39
|
-
include Core::ContractForSubfields
|
40
|
-
include Core::NestingTracking
|
41
|
-
|
42
|
-
include Core::UseStrategy
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
module ClassMethods
|
47
|
-
def call(**)
|
48
|
-
new(**).tap(&:_run).result
|
49
|
-
end
|
50
|
-
|
51
|
-
def call!(**)
|
52
|
-
result = call(**)
|
53
|
-
return result if result.ok?
|
54
|
-
|
55
|
-
# When we're nested, we want to raise a failure that includes the source action to support
|
56
|
-
# the error message generation's `from` filter
|
57
|
-
raise Action::Failure.new(result.error, source: result.__action__), cause: result.exception if _nested_in_another_axn?
|
58
|
-
|
59
|
-
raise result.exception
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def initialize(**)
|
64
|
-
@__context = Action::Context.new(**)
|
65
|
-
end
|
66
|
-
|
67
|
-
# Main entry point for action execution
|
68
|
-
def _run
|
69
|
-
_tracking_nesting(self) do
|
70
|
-
_with_tracing do
|
71
|
-
_with_logging do
|
72
|
-
_with_timing do
|
73
|
-
_with_exception_handling do # Exceptions stop here; outer wrappers access result status (and must not introduce another exception layer)
|
74
|
-
_with_contract do # Library internals -- any failures (e.g. contract violations) *should* fail the Action::Result
|
75
|
-
_with_hooks do # User hooks -- any failures here *should* fail the Action::Result
|
76
|
-
call
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
ensure
|
85
|
-
_emit_metrics
|
86
|
-
end
|
87
|
-
|
88
|
-
# User-defined action logic - override this method in your action classes
|
89
|
-
def call; end
|
90
|
-
|
91
|
-
def fail!(message = nil)
|
92
|
-
raise Action::Failure, message
|
93
|
-
end
|
94
|
-
|
95
|
-
private
|
96
|
-
|
97
|
-
def _emit_metrics
|
98
|
-
return unless Action.config.emit_metrics
|
99
|
-
|
100
|
-
Action.config.emit_metrics.call(
|
101
|
-
self.class.name || "AnonymousClass",
|
102
|
-
result,
|
103
|
-
)
|
104
|
-
rescue StandardError => e
|
105
|
-
Axn::Util.piping_error("running metrics hook", action: self, exception: e)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
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
|